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