При доработке форм списков в 1С программисты часто сталкиваются с ошибкой "Обнаружено дублирование ключевых значений в колонках: 'Ссылка'". Обычно она возникает после изменения стандартного запроса динамического списка, например, при добавлении в него новых таблиц через соединения для вывода дополнительной информации.
Эта ошибка — не сбой платформы, а сигнал о нарушении основного правила работы динамических списков: для каждой записи основной таблицы в результате запроса должна быть ровно одна строка. Давайте подробно разберем, почему возникает эта проблема и какие существуют способы ее решения.
Рассмотрим ситуацию из практики. Программист изменяет форму списка документов Отбор (размещение) товаров в конфигурации "Управление торговлей 11.5". Задача — вывести в списке номер и клиента из связанного документа Заказ клиента. Для этого он программно изменяет текст запроса динамического списка, добавляя соединения таблиц.
Исходный запрос был примерно таким:
ВЫБРАТЬ
ДокументОтборРазмещениеТоваров.Ссылка,
ДокументОтборРазмещениеТоваров.Номер,
ДокументОтборРазмещениеТоваров.Дата,
// ... другие поля документа
ИЗ
Документ.ОтборРазмещениеТоваров КАК ДокументОтборРазмещениеТоваров
После доработки запрос стал выглядеть так:
ВЫБРАТЬ
ДокументОтборРазмещениеТоваровПереопределяемый.Ссылка КАК Ссылка,
// ... другие поля документа
ЗаказКлиента.Номер КАК НомерЗаказа,
ЗаказКлиента.Партнер КАК КлиентЗаказа
ИЗ
Документ.ОтборРазмещениеТоваров КАК ДокументОтборРазмещениеТоваровПереопределяемый
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходныйОрдерНаТовары.ТоварыПоРаспоряжениям КАК РасходныйОрдерНаТоварыТоварыПоРаспоряжениям
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента КАК ЗаказКлиента
ПО РасходныйОрдерНаТоварыТоварыПоРаспоряжениям.Распоряжение = ЗаказКлиента.Ссылка
ПО ДокументОтборРазмещениеТоваровПереопределяемый.Распоряжение = РасходныйОрдерНаТоварыТоварыПоРаспоряжениям.Ссылка
При открытии формы с таким запросом система выдает ошибку о дублировании ключевых значений. Проанализируем причину. Проблема кроется в логике связей между документами:
Отбор (размещение) товаров создается на основании одного документа Расходный ордер на товары.Расходный ордер на товары может быть сформирован для отгрузки товаров по нескольким Заказам клиента.В итоге образуется связь "один ко многим". Если один документ отбора связан с расходным ордером, который, в свою очередь, относится к двум заказам клиентов, то в результате выполнения запроса с ВНУТРЕННЕЕ СОЕДИНЕНИЕ мы получим две строки с абсолютно одинаковым значением в поле Ссылка документа отбора. А платформа 1С требует, чтобы значение в ключевом поле (Ссылка) было уникальным для каждой строки динамического списка. В этом и заключается корень проблемы.
Теперь рассмотрим, как это можно исправить.
Этот метод является наиболее гибким и правильным с точки зрения архитектуры платформы. Его суть в том, чтобы не усложнять основной запрос динамического списка, а получать дополнительные данные "на лету" для тех строк, которые сейчас отображаются пользователю.
Порядок действий:
ПриПолученииДанныхНаСервере.Пример кода для обработчика ПриПолученииДанныхНаСервере:
&НаСервере
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, ОформленияСтрок)
// 1. Собираем массив ссылок на документы, которые сейчас отображаются
МассивСсылок = Новый Массив;
Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
МассивСсылок.Добавить(ОформлениеСтроки.ДанныеСтроки.Ссылка);
КонецЦикла;
Если МассивСсылок.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// 2. Одним запросом получаем все необходимые данные
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОтборРазмещениеТоваров.Ссылка КАК Ссылка,
| ЗаказыКлиента.Номер КАК НомерЗаказа,
| ЗаказыКлиента.Партнер КАК КлиентЗаказа,
| ЗаказыКлиента.Дата КАК ДатаЗаказа
|ИЗ
| Документ.ОтборРазмещениеТоваров КАК ОтборРазмещениеТоваров
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходныйОрдерНаТовары.ТоварыПоРаспоряжениям КАК ТоварыПоРаспоряжениям
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента КАК ЗаказыКлиента
| ПО ТоварыПоРаспоряжениям.Распоряжение = ЗаказыКлиента.Ссылка
| ПО ОтборРазмещениеТоваров.Распоряжение = ТоварыПоРаспоряжениям.Ссылка
|ГДЕ
| ОтборРазмещениеТоваров.Ссылка В (&МассивСсылок)";
Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// 3. Сопоставляем полученные данные с оформляемыми строками
Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
// Для отображения нескольких заказов можно собрать их в одну строку
НайденныеСтроки = РезультатЗапроса.НайтиСтроки(Новый Структура("Ссылка", ОформлениеСтроки.ДанныеСтроки.Ссылка));
ТекстовоеПредставлениеЗаказов = "";
Для Каждого НайденнаяСтрока Из НайденныеСтроки Цикл
ТекстовоеПредставлениеЗаказов = ТекстовоеПредставлениеЗаказов
+ ?(ПустаяСтрока(ТекстовоеПредставлениеЗаказов), "", ", ")
+ "№" + НайденнаяСтрока.НомерЗаказа + " (" + Строка(НайденнаяСтрока.КлиентЗаказа) + ")";
КонецЦикла;
// Устанавливаем значения в ячейки новых колонок
Ячейки = ОформлениеСтроки.Ячейки;
Ячейки.НомерЗаказа.УстановитьТекст(ТекстовоеПредставлениеЗаказов);
// Если нужно заполнить разные колонки, можно сделать так:
// Ячейки.НомерЗаказа.УстановитьТекст(НайденныеСтроки[0].НомерЗаказа);
// Ячейки.КлиентЗаказа.УстановитьТекст(НайденныеСтроки[0].КлиентЗаказа);
КонецЦикла;
КонецПроцедуры
Этот подход эффективен, так как выполняется всего один дополнительный запрос на всю порцию отображаемых данных, а не по запросу на каждую строку.
Этот способ подходит, если вам достаточно отобразить только одно из множества возможных значений. Например, номер самого первого или последнего заказа, или их общее количество.
Суть метода:
ЛЕВОЕ СОЕДИНЕНИЕ, чтобы не потерять документы отбора, у которых нет связанных заказов.Документ.ОтборРазмещениеТоваров).МАКСИМУМ(), МИНИМУМ(), КОЛИЧЕСТВО(РАЗЛИЧНЫЕ).Пример измененного запроса:
ВЫБРАТЬ
ДокументОтборРазмещениеТоваровПереопределяемый.Ссылка,
ДокументОтборРазмещениеТоваровПереопределяемый.Номер,
ДокументОтборРазмещениеТоваровПереопределяемый.Дата,
// ... другие поля основной таблицы
МАКСИМУМ(ЗаказКлиента.Номер) КАК НомерПоследнегоЗаказа,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ЗаказКлиента.Ссылка) КАК КоличествоЗаказов
ИЗ
Документ.ОтборРазмещениеТоваров КАК ДокументОтборРазмещениеТоваровПереопределяемый
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходныйОрдерНаТовары.ТоварыПоРаспоряжениям КАК РасходныйОрдерНаТоварыТоварыПоРаспоряжениям
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента КАК ЗаказКлиента
ПО РасходныйОрдерНаТоварыТоварыПоРаспоряжениям.Распоряжение = ЗаказКлиента.Ссылка
ПО ДокументОтборРазмещениеТоваровПереопределяемый.Распоряжение = РасходныйОрдерНаТоварыТоварыПоРаспоряжениям.Ссылка
СГРУППИРОВАТЬ ПО
ДокументОтборРазмещениеТоваровПереопределяемый.Ссылка,
ДокументОтборРазмещениеТоваровПереопределяемый.Номер,
ДокументОтборРазмещениеТоваровПереопределяемый.Дата
// ... все остальные поля основной таблицы
Важный момент: при использовании группировки или ключевого слова РАЗЛИЧНЫЕ может возникнуть ошибка, если в запросе есть поля неограниченной длины (например, Комментарий). В этом случае их нужно привести к строке ограниченной длины с помощью функции ВЫРАЗИТЬ. Например: ВЫРАЗИТЬ(ДокументОтборРазмещениеТоваровПереопределяемый.Комментарий КАК СТРОКА(1000)).
Иногда сама постановка задачи не подходит для реализации в форме списка. Если пользователю действительно нужно видеть все связанные заказы и как-то с ними работать, возможно, стоит изменить сам интерфейс.
В заключение, ошибка дублирования ключевых значений — это не баг, а следствие логики работы динамических списков. Перед тем как изменять запрос, всегда анализируйте связи между таблицами. Если вы видите связь "один ко многим", будьте готовы к тому, что простое соединение не сработает, и выбирайте один из описанных выше подходов, который лучше всего соответствует вашей задаче.