При разработке интерфейсов в среде «1С:Предприятие 8» программисты часто сталкиваются с необходимостью отобразить в списке документов или справочников дополнительную информацию, которая хранится во внешних источниках — например, в регистрах сведений. Типичная задача: вывести текущее состояние заявки или дату последнего изменения этого состояния в форму списка документов.
Однако попытки решить эту задачу «в лоб» через перебор строк или некорректно настроенные запросы динамического списка приводят к неприятным ошибкам: «Поле объекта недоступно для записи» или «Обнаружено дублирование ключевых значений в колонках "Ссылка"». В рамках этой статьи мы подробно разберем, почему возникают эти ошибки, проанализируем внутренние механизмы платформы 1С и рассмотрим два правильных архитектурных подхода к решению этой задачи.
---
Рассмотрим исходный код, который вызвал ошибку у автора темы:
&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
МассивКлючей = Строки.ПолучитьКлючи();
// ... Выполнение запроса к регистру сведений ...
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Для каждого Строчка Из МассивКлючей Цикл
Строчка.СостояниеЗаявки = ВыборкаДетальныеЗаписи.СостояниеЗаявки;
Строчка.ДатаСостояния = ВыборкаДетальныеЗаписи.ДатаСостояния;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Выясним причину, почему этот код вызывает критическую ошибку выполнения. Метод Строки.ПолучитьКлючи() возвращает массив ключей строк, которые в данный момент отображаются на экране пользователя. Для динамического списка документов такими ключами являются ссылки на сами документы — объекты типа ДокументСсылка.ИмяДокумента.
Важное правило объектной модели 1С: любые ссылки (СправочникСсылка, ДокументСсылка и т.д.) являются неизменяемыми объектами. Они служат лишь уникальными идентификаторами записей в базе данных. Вы не можете напрямую присвоить новое значение реквизиту ссылки (например, написать Ссылка.Реквизит = Значение). Для изменения данных в базе необходимо получить объект базы данных через метод ПолучитьОбъект(), изменить его свойства и вызвать метод Записать(). Но в контексте формы списка нам вовсе не нужно менять данные в базе — нам нужно лишь визуально отобразить их на форме.
Кроме того, обратите внимание на логическую ошибку в циклах. Конструкция вида:
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Для каждого Строчка Из МассивКлючей Цикл
// присвоение...
КонецЦикла;
КонецЦикла;
содержит вложенный цикл без какого-либо сопоставления данных. Это означает, что на каждом шаге обхода результатов запроса мы заново перезаписываем значения для всего массива строк. Это приводит к алгоритмической сложности $O(N \times M)$ и полностью ломает логику соответствия строк их реальным состояниям.
---
По совету коллег автор темы попытался перевести форму списка на использование Динамического списка с произвольным запросом. Запрос выглядел следующим образом:
ВЫБРАТЬ
ИТС_ВходящаяЗаявкаНаТОР.Ссылка КАК Ссылка,
...
ИТС_ПодпискиТОРСрезПоследних.СостояниеЗаявки КАК СостояниеЗаявкиРегистр,
ИТС_ПодпискиТОРСрезПоследних.ДатаСостояния КАК ДатаСостоянияРегистр
ИЗ
РегистрСведений.ИТС_ПодпискиТОР.СрезПоследних КАК ИТС_ПодпискиТОРСрезПоследних
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ИТС_ВходящаяЗаявкаНаТОР КАК ИТС_ВходящаяЗаявкаНаТОР
ПО ИТС_ПодпискиТОРСрезПоследних.Заявка = ИТС_ВходящаяЗаявкаНаТОР.Ссылка
Этот подход вызвал ошибку: «Обнаружено дублирование ключевых значений в колонках "Ссылка". Отображение данных в списке не возможно». Разберем по шагам, почему это произошло.
Динамический список использует поле Ссылка в качестве уникального идентификатора строки. Платформа ожидает, что каждая строка в результирующей таблице будет иметь уникальное значение в этой колонке. Если СУБД возвращает несколько строк с одинаковой ссылкой, платформа не может понять, какую именно строку отрисовывать, и аварийно завершает работу.
Дублирование строк в запросе автора произошло по двум причинам:
СрезПоследних регистра сведений, к которой левым соединением подключалась таблица документов. При такой структуре, во-первых, документы, по которым вообще нет записей в регистре, просто исчезнут из списка. Во-вторых, если в регистре содержатся неактуальные или множественные записи, структура выборки искажается.СрезПоследних возвращает уникальные записи в разрезе всех измерений регистра. Если в регистре ИТС_ПодпискиТОР кроме измерения Заявка есть другие измерения (например, Услуга, Организация или Исполнитель), то для одной и той же заявки в срезе последних может находиться несколько записей (по одной для каждой комбинации измерений). При соединении с таблицей документов одна и та же ссылка на документ продублируется столько раз, сколько уникальных строк по этой заявке вернул срез последних.---
Если мы хотим решить задачу исключительно средствами динамического списка (произвольного запроса), нам необходимо гарантировать две вещи: во-первых, главной таблицей запроса должна быть таблица документов; во-вторых, мы должны исключить возможность дублирования записей из регистра сведений.
Для этого мы можем использовать вложенный запрос с группировкой, который свернет данные регистра до уровня уникальных документов (заявок). Проанализируем следующий пример запроса:
ВЫБРАТЬ
ДокументТаблица.Ссылка КАК Ссылка,
ДокументТаблица.ПометкаУдаления КАК ПометкаУдаления,
ДокументТаблица.Номер КАК Номер,
ДокументТаблица.Дата КАК Дата,
ДокументТаблица.Проведен КАК Проведен,
// Выбираем другие необходимые реквизиты документа...
ЕСТЬNULL(ПоследниеПодписки.СостояниеЗаявки, ЗНАЧЕНИЕ(Перечисление.СостоянияЗаявок.ПустаяСсылка)) КАК СостояниеЗаявкиРегистр,
ЕСТЬNULL(ПоследниеПодписки.ДатаСостояния, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаСостоянияРегистр
ИЗ
Документ.ИТС_ВходящаяЗаявкаНаТОР КАК ДокументТаблица
// Подключаем регистр левым соединением, гарантируя отсутствие дублей
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
Срез.Заявка КАК Заявка,
// Сворачиваем данные до одной строки на одну заявку с помощью агрегатных функций
МАКСИМУМ(Срез.СостояниеЗаявки) КАК СостояниеЗаявки,
МАКСИМУМ(Срез.ДатаСостояния) КАК ДатаСостояния
ИЗ
РегистрСведений.ИТС_ПодпискиТОР.СрезПоследних КАК Срез
ГРУППИРОВАТЬ ПО
Срез.Заявка
) КАК ПоследниеПодписки
ПО ДокументТаблица.Ссылка = ПоследниеПодписки.Заявка
Преимущества данного подхода:
ЛЕВОМУ СОЕДИНЕНИЮ от таблицы документов).Недостатки: Использование вложенных запросов и группировок совместно с виртуальными таблицами среза последних в динамических списках может негативно сказаться на производительности при больших объемах данных (приводить к задержкам при прокрутке списка).
---
Если производительность списка критически важна, а сортировка и поиск по полям состояния не требуются, наилучшим решением будет использование серверного события динамического списка ПриПолученииДанныхНаСервере. Начиная с версии платформы 8.3.10, этот механизм работает чрезвычайно быстро и запрашивает данные из базы только для тех строк, которые физически отображаются на экране пользователя (около 20–45 строк).
Разберем по шагам, как реализовать этот алгоритм правильно.
Шаг 1. Создаем пустые колонки-заглушки в Динамическом списке. В запросе динамического списка мы просто объявляем пустые типизированные поля:
ВЫБРАТЬ
Док.Ссылка КАК Ссылка,
Док.Номер КАК Номер,
Док.Дата КАК Дата,
// Создаем пустые поля нужного типа
ЗНАЧЕНИЕ(Перечисление.СостоянияЗаявок.ПустаяСсылка) КАК СостояниеЗаявкиРегистр,
ДАТАВРЕМЯ(1,1,1) КАК ДатаСостоянияРегистр
ИЗ
Документ.ИТС_ВходящаяЗаявкаНаТОР КАК Док
Шаг 2. Пишем эффективный обработчик события. В модуле формы программируем событие списка, используя коллекцию Соответствие для быстрого поиска сопоставленных данных без вложенных циклов:
&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
МассивКлючей = Строки.ПолучитьКлючи();
Если МассивКлючей.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Запрос получает данные только для тех документов, которые отображены на экране
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РегистрСрез.Заявка КАК Заявка,
| РегистрСрез.СостояниеЗаявки КАК СостояниеЗаявки,
| РегистрСрез.ДатаСостояния КАК ДатаСостояния
|ИЗ
| РегистрСведений.ИТС_ПодпискиТОР.СрезПоследних(, Заявка В (&МассивКлючей)) КАК РегистрСрез";
Запрос.УстановитьПараметр("МассивКлючей", МассивКлючей);
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
// Заполняем Соответствие для быстрого поиска сопоставленных данных (сложность O(1))
СоответствиеДанных = Новый Соответствие;
Для Каждого СтрокаТаблицы Из ТаблицаДанных Цикл
СоответствиеДанных.Вставить(СтрокаТаблицы.Заявка, СтрокаТаблицы);
КонецЦикла;
// Заполняем визуальное представление строк на форме
Для Каждого КлючСтроки Из МассивКлючей Цикл
СтрокаСписка = Строки[КлючСтроки];
ДанныеРегистра = СоответствиеДанных[КлючСтроки];
Если ДанныеРегистра <> Неопределено Тогда
// Изменяем данные отображения в специальном свойстве Данные строки списка
СтрокаСписка.Данные.СостояниеЗаявкиРегистр = ДанныеРегистра.СостояниеЗаявки;
СтрокаСписка.Данные.ДатаСостоянияRegистр = ДанныеРегистра.ДатаСостояния;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Важное отличие: обратите внимание, что мы пишем значения не в сам КлючСтроки (который является ссылкой), а обращаемся к коллекции отображаемых строк через Строки[КлючСтроки] и меняем значения внутри структуры СтрокаСписка.Данные. Этот объект доступен для записи и предназначен именно для динамического дополнения отображаемых на форме данных.
---
Мы проанализировали причины ошибок при работе со списками документов в 1С и подготовили надежные варианты их обхода. При выборе конкретного решения руководствуйтесь следующими правилами:
ПриПолученииДанныхНаСервере с оптимизированным пакетным запросом и использованием Соответствия). Это обеспечит максимальное быстродействие интерфейса вашей конфигурации.