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