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