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