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