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