В процессе разработки внешних обработок или доработки типовых конфигураций на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с задачей интерактивного удаления строк из регистров сведений. Пользователь выделяет одну или несколько строк в списке, нажимает кнопку, и система должна корректно и безвозвратно удалить эти записи из базы данных.
Решение этой задачи существенно отличается в зависимости от того, является ли регистр сведений независимым или он подчинен регистратору (ведет учет в разрезе документов). В этой статье мы подробно разберем оба сценария, проанализируем критические ошибки, которые часто допускают разработчики, и напишем оптимальный, производительный код для управляемых форм.
Прежде чем перейти к правильным алгоритмам, давайте разберем основные грабли, на которые наступают даже опытные специалисты, как это видно из обсуждения на форуме:
Очистить() у набора записей: Вызов метода НаборЗаписей.Очистить() полностью опустошает коллекцию в оперативной памяти. Если после этого записать набор в базу данных, система удалит абсолютно все движения по данному регистратору (или по текущему отбору), а не только одну выбранную пользователем строку. Это приводит к катастрофической потере данных.МенеджерЗаписи для подчиненных регистров: Платформа 1С накладывает жесткое ограничение: для регистров, подчиненных регистратору, запись и удаление возможны только через объект НаборЗаписей с обязательным установлением отбора по регистратору. Объект МенеджерЗаписи для таких регистров использовать запрещено программно.0 до N) и удалять строки по условию, то после удаления первой подходящей строки все последующие элементы смещаются на один индекс назад. В результате цикл пропускает следующую за удаленной строку. Обход коллекции всегда нужно выполнять с конца к началу.Теперь, когда мы понимаем зоны риска, рассмотрим правильные подходы к реализации функционала.
В управляемом интерфейсе, когда табличная часть формы отображает динамический список регистра сведений, свойство элемента формы Элементы.ЗаписиРегистра.ВыделенныеСтроки возвращает массив элементов специального типа — РегистрСведенийКлючЗаписи.<ИмяРегистра>.
Этот системный объект крайне полезен. Он автоматически инкапсулирует в себе значения периода (для периодических регистров) и всех измерений строки, а также ссылку на документ-регистратор. Передавая этот массив на сервер, мы избавляемся от необходимости вручную собирать сложные структуры измерений на клиенте.
Для независимого регистра сведений наиболее лаконичным и эффективным инструментом является объект МенеджерЗаписи. Мы можем перенести ключевые поля из ключа записи в менеджер одной командой благодаря функции ЗаполнитьЗначенияСвойств.
Разберем готовый шаблон кода. На клиенте мы получаем выделенные строки, проверяем их наличие и передаем массив ключей на сервер для удаления.
&НаКлиенте
Процедура УдалитьВыделенныеСтрокиНезависимого(Команда)
ВыделенныеСтроки = Элементы.ЗаписиРегистра.ВыделенныеСтроки;
Если ВыделенныеСтроки.Количество() > 0 Тогда
// Вызываем серверную процедуру без контекста формы для лучшей производительности
УдалитьСтрокиНезависимогоНаСервере(ВыделенныеСтроки);
// Обновляем динамический список на клиенте, чтобы удаленные строки исчезли с экрана
Элементы.ЗаписиРегистра.Обновить();
Иначе
ПоказатьПредупреждение(, "Не выбрано ни одной строки для удаления!");
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Процедура УдалитьСтрокиНезависимогоНаСервере(МассивКлючейЗаписей)
Для Каждого КлючЗаписи Из МассивКлючейЗаписей Цикл
// Создаем менеджер записи для конкретного регистра
МенеджерЗаписи = РегистрыСведений.ИмяРегистра.СоздатьМенеджерЗаписи();
// Копируем измерения и период из ключа в менеджер записи
ЗаполнитьЗначенияСвойств(МенеджерЗаписи, КлючЗаписи);
// Проверяем существование записи в базе данных перед удалением
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
МенеджерЗаписи.Удалить();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот алгоритм работает быстро, безопасно и не перегружает сервер контекстными вызовами.
Удаление записей из подчиненного регистра (например, такого как КадроваяИсторияСотрудников в ЗУП) требует деликатного подхода. Платформа не позволяет удалить одну строку напрямую. Нам необходимо выполнить следующие шаги:
Давайте посмотрим, как выглядит правильная программная реализация этой логики:
&НаКлиенте
Процедура УдалитьВыделенныеСтрокиПодчиненного(Команда)
ВыделенныеСтроки = Элементы.ЗаписиРегистра.ВыделенныеСтроки;
Если ВыделенныеСтроки.Количество() > 0 Тогда
УдалитьСтрокиПодчиненногоНаСервере(ВыделенныеСтроки);
Элементы.ЗаписиРегистра.Обновить();
Иначе
ПоказатьПредупреждение(, "Не выбрано ни одной строки!");
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Процедура УдалитьСтрокиПодчиненногоНаСервере(МассивКлючейЗаписей)
// Шаг 1. Группируем ключи удаляемых записей по Регистраторам
// Это спасет систему от избыточных чтений и записей СУБД, если выделено много строк одного документа
ЗаписиПоРегистраторам = Новый Соответствие;
Для Каждого КлючЗаписи Из МассивКлючейЗаписей Цикл
ТекущийРегистратор = КлючЗаписи.Регистратор;
Если ЗаписиПоРегистраторам[ТекущийРегистратор] = Неопределено Тогда
ЗаписиПоРегистраторам.Вставить(ТекущийРегистратор, Новый Массив);
КонецЕсли;
ЗаписиПоРегистраторам[ТекущийРегистратор].Добавить(КлючЗаписи);
КонецЦикла;
// Шаг 2. Обрабатываем каждый регистратор в отдельной транзакции записи набора
Для Каждого ЭлементСоответствия Из ЗаписиПоРегистраторам Цикл
Регистратор = ЭлементСоответствия.Ключ;
КлючиДляУдаления = ЭлементСоответствия.Значение;
// Создаем набор записей с жестким отбором по документу
НаборЗаписей = РегистрыСведений.КадроваяИсторияСотрудников.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Регистратор);
НаборЗаписей.Прочитать();
// Шаг 3. Обходим набор с конца по индексу во избежание сдвига элементов
Индекс = НаборЗаписей.Количество() - 1;
Пока Индекс >= 0 Цикл
СтрокаНабора = НаборЗаписей[Индекс];
// Проверяем, совпадает ли строка набора с одной из удаляемых строк
Для Каждого Ключ Из КлючиДляУдаления Цикл
// Сравниваем период и все ключевые измерения (например, Сотрудник и Организация)
Если СтрокаНабора.Период = Ключ.Период
И СтрокаНабора.Сотрудник = Ключ.Сотрудник Тогда
// Удаляем конкретную строку из коллекции
НаборЗаписей.Удалить(Индекс);
Прервать; // Переходим к анализу следующей строки набора
КонецЕсли;
КонецЦикла;
Индекс = Index - 1;
КонецЦикла;
// Шаг 4. Записываем измененный набор обратно в СУБД с замещением (Истина)
Попытка
НаборЗаписей.Записать(Истина);
Исключение
ЗаписьЖурналаРегистрации("Программное удаление записей регистра",
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение "Не удалось записать изменения по документу " + СокрЛП(Регистратор);
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Важно понимать, что прямое вмешательство в записи подчиненного регистра сведений в обход самого документа-регистратора несет серьезные риски для стабильности информационной системы:
РучнаяКорректировка с типом Булево. Если вы программно корректируете движения документа, настоятельно рекомендуется устанавливать этот реквизит в значение Истина в самом документе. Это заблокирует стандартный механизм перезаписи движений при проведении и сохранит ваши ручные правки.Используя приведенные выше паттерны программирования, мы можем гарантировать стабильную, быструю и безопасную работу механизмов удаления записей в любых прикладных решениях на платформе 1С.