В процессе поддержки и развития конфигураций на платформе 1С:Предприятие разработчики регулярно сталкиваются с необходимостью модификации структуры метаданных. Одной из классических задач является добавление нового ресурса в существующий регистр накопления с последующим заполнением его исторических данных. Например, когда в регистре уже есть ресурсы Количество и Цена, но бизнес-логика потребовала добавить ресурс Сумма, значение которого должно быть рассчитано как произведение первых двух показателей.
На первый взгляд задача кажется тривиальной, однако при работе с большими объемами данных (сотни тысяч или миллионы записей) обычный перебор записей «в лоб» может привести к катастрофическим последствиям: от полной очистки движений документов до критического зависания СУБД и блокировки работы пользователей. В этой статье мы подробно разберем правильный технический алгоритм массового обновления движений, проанализируем архитектурные нюансы оптимизации производительности и рассмотрим лучшие практики управления транзакциями.
Давайте разберем по шагам, с какими трудностями сталкиваются начинающие разработчики при попытке отредактировать записи регистра накопления.
Первая и самая распространенная ошибка — попытка установить отбор по периоду при записи набора. В регистрах накопления, которые всегда подчинены регистратору (документу), платформа 1С жестко блокирует возможность произвольной записи набора с отбором по дате или иным измерениям, кроме самого регистратора. Прямая запись набора возможна только в пределах конкретного документа-регистратора. Таким образом, отбор по свойству Регистратор является технически обязательным.
Вторая критическая ошибка заключается в непонимании принципа работы объекта РегистрНакопленияНаборЗаписей. Если программно создать набор записей, установить отбор по регистратору и сразу вызвать метод Записать(), платформа полностью очистит все существующие движения этого документа по данному регистру! Выясним причину: при записи «пустого» (не прочитанного из базы) набора система считает, что движений больше нет, и затирает их в СУБД. Чтобы этого не произошло, перед модификацией данных критически важно считать существующие движения из базы данных с помощью метода Прочитать().
Выясним, как выглядит правильный, безопасный и методологически верный алгоритм массового изменения записей регистра накопления. Процесс состоит из двух ключевых этапов: получение списка документов-регистраторов и их пообъектная обработка.
Рассмотрим подробнее программный код, реализующий эту задачу. Сначала мы должны выбрать все уникальные документы, которые сделали записи в целевой регистр за интересующий нас период. Для этого мы напишем и выполним запрос к виртуальной или физической таблице регистра:
// Шаг 1. Получение списка уникальных регистраторов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ИмяРегистра.Регистратор КАК Регистратор
|ИЗ
| РегистрНакопления.ИмяРегистра КАК ИмяРегистра
|ГДЕ
| ИмяРегистра.Период МЕЖДУ &НачалоПериода И &КонецПериода";
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
После того как список регистраторов успешно получен, мы переходим ко второму шагу — пообъектному обходу записей. Для каждого документа мы создаем индивидуальный набор записей, устанавливаем жесткий отбор по регистратору, обязательно считываем данные, производим расчет нового ресурса и записываем результат обратно в базу данных.
// Шаг 2. Пообъектный обход и безопасное изменение данных
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Создаем набор записей для конкретного регистратора
НаборЗаписей = РегистрыНакопления.ИмяРегистра.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(ВыборкаДетальныеЗаписи.Регистратор);
// КРИТИЧЕСКИ ВАЖНО: Читаем существующие движения из базы данных!
НаборЗаписей.Прочитать();
Если НаборЗаписей.Количество() = 0 Тогда
Продолжить; // Если движений нет, пропускаем обработку
КонецЕсли;
Модифицирован = Ложь;
// Перебираем записи набора и выполняем расчет
Для Каждого Запись Из НаборЗаписей Цикл
// Рассчитываем сумму на основе количества и цены
НоваяСумма = Запись.Количество * Запись.Цена;
// Проверяем, изменилось ли значение, чтобы избежать лишней перезаписи
Если Запись.Сумма <> НоваяСумма Тогда
Запись.Сумма = НоваяСумма;
Модифицирован = Истина;
КонецЕсли;
КонецЦикла;
// Записываем набор обратно в базу данных только если были изменения
Если Модифицирован Тогда
НаборЗаписей.Записать();
КонецЕсли;
КонецЦикла;
Обратим внимание на использование флага Модифицирован. Это позволяет существенно снизить нагрузку на СУБД, так как мы не будем выполнять физическую перезапись таблиц базы данных для тех документов, чьи движения не претерпели изменений или уже были корректно рассчитаны ранее.
Если изменяемый регистр накопления имеет вид «Остатки», то простая последовательная перезапись сотен тысяч документов по приведенному выше алгоритму неизбежно приведет к сильному замедлению работы системы. Проблема кроется в механизме расчета итогов платформы 1С.
При каждой записи набора данных платформа автоматически пытается на лету пересчитать таблицы итогов (как текущих итогов, так и итогов за прошлые периоды/месяцы). На больших базах данных СУБД тратит колоссальные ресурсы на постоянное обновление индексов и агрегированных таблиц при каждом вызове метода Записать().
Проанализируем классическое решение этой проблемы, которое является стандартом разработки в среде 1С. Перед выполнением массовой обработки необходимо программно отключить использование итогов для целевого регистра, а после завершения всех манипуляций — включить их обратно. В этом случае пересчет итогов выполнится платформой однократно и максимально оптимально.
Разберем по шагам, как это реализовать программно:
// 1. Отключаем использование итогов по регистру для ускорения записи
РегистрыНакопления.ИмяРегистра.УстановитьИспользованиеИтогов(Ложь);
Попытка
// Выполняем наш основной алгоритм массовой перезаписи наборов данных
// ... (код обхода регистраторов и изменения записей) ...
Исключение
// В случае ошибки обязательно включаем итоги обратно, чтобы не нарушить целостность базы
РегистрыНакопления.ИмяРегистра.УстановитьИспользованиеИтогов(Истина);
ВызватьИсключение; // Транслируем ошибку дальше
КонецПопытки;
// 2. После успешного завершения обработки включаем итоги обратно
РегистрыНакопления.ИмяРегистра.УстановитьИспользованиеИтогов(Истина);
Такой подход позволяет сократить время выполнения обработки в десятки раз, особенно на крупных информационных базах с миллионами исторических движений.
При работе с крупными базами данных разработчики часто совершают еще одну серьезную ошибку — выполняют всю пакетную обработку в рамках одной гигантской транзакции или, наоборот, не используют транзакции там, где это необходимо для ускорения дисковых операций записи СУБД.
Если поместить изменение миллионов строк в одну транзакцию, это гарантированно приведет к следующим проблемам:
С другой стороны, запись каждого набора в отдельной транзакции (без явного открытия транзакции в коде 1С) работает медленно, так как СУБД вынуждена выполнять физический сброс данных на диск для каждой отдельной записи. Решением является пакетная группировка транзакций. Мы объединяем обработку документов в небольшие порции (пакеты по 500–1000 штук) и фиксируем их одной транзакцией.
Посмотрим на пример реализации пакетной записи с контролем использования оперативной памяти:
РазмерПорции = 1000;
Счетчик = 0;
НачатьТранзакцию();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// ... Код обработки набора записей регистратора ...
Счетчик = Счетчик + 1;
// Каждые 1000 документов фиксируем транзакцию и открываем новую
Если Счетчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
// Принудительно очищаем кэш и даем передышку серверу
#Если Сервер Тогда
// При необходимости вызываем методы очистки или сборщик мусора
#КонецЕсли
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
// Фиксируем оставшийся хвост транзакции
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
В ходе обсуждения на форуме участники затронули важнейший архитектурный аспект разработки в системе 1С. Автор исходного сообщения упомянул, что умножает ресурсы Количество и Цена, чтобы получить Сумму. Встал закономерный вопрос: почему вообще цена оказалась в ресурсах регистра накопления?
С точки зрения методологии разработки на платформе 1С, ресурсы регистра накопления обязательно должны обладать свойством аддитивности. Это означает, что показатели, хранящиеся в ресурсах, должно быть логично складывать между собой для получения итогов за период или на определенную дату. Суммировать цены по разным номенклатурным позициям или за разные периоды абсолютно бессмысленно — полученная сумма цен не имеет никакого прикладного значения.
Как проектировать структуру данных правильно?
Количество, Сумма, НДС, Вес).ЦеныНоменклатуры (если требуется отслеживать историю изменения цен во времени).Если в отчете или запросе вам требуется узнать среднюю цену за период, правильным решением будет деление агрегированного ресурса Сумма на ресурс Количество непосредственно в самом запросе или схеме компоновки данных (СКД), но никак не суммирование или усреднение ресурса Цена.
Существует еще один продвинутый метод обновления данных, который часто используют опытные разработчики. Представьте ситуацию, когда формула расчета новой суммы чрезвычайно сложная, зависит от множества внешних факторов, скидок, курсов валют и прописана глубоко в модуле проведения документа. Дублировать эту сложнейшую логику внутри внешней обработки — это огромный риск допустить ошибку и получить расхождение данных.
В таких случаях применяется метод «проведения по одному регистру». Вместо того чтобы вручную пересчитывать ресурсы в обработке, разработчик программно инициирует штатное проведение документов. Однако, чтобы не затронуть другие важные регистры (например, чтобы случайно не поплыла себестоимость партионного учета или взаиморасчеты в закрытых периодах), в коде проведения временно переопределяют или отключают запись во все регистры, кроме нашего целевого. Документ делает движения только по нужному регистру накопления, используя при этом все свои штатные и выверенные временем алгоритмы конфигурации.