При интеграции 1С с внешними системами (сайт, CRM, производственная база на MS SQL) одной из самых частых задач становится загрузка документов. Основная сложность заключается в том, что в SQL данные хранятся в «плоских» связанных таблицах, а в 1С документ представляет собой объектную сущность с иерархией: одна шапка и множество строк в табличных частях. В этой статье мы подробно разберем, как правильно организовать этот процесс, избегая типичных ошибок производительности.
Это наиболее современный и «нативный» способ для платформы 1С 8.3. Если внешняя база подключена через ВнешниеИсточникиДанных, мы можем обращаться к ней обычным запросом 1С. Рассмотрим ситуацию, когда у нас есть таблица заголовков и таблица строк, связанных по полю ID.
Основная проблема простого соединения (LEFT JOIN) заключается в том, что данные шапки дублируются для каждой строки табличной части. Чтобы не писать «тупые» алгоритмы сравнения ID текущей строки с предыдущей, воспользуемся предложением ИТОГИ ПО.
Разберем по шагам, как это работает в коде:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВнешнийЗаголовок.ID КАК Идентификатор,
| ВнешнийЗаголовок.Дата КАК ДатаДок,
| ВнешнийЗаголовок.Контрагент КАК КонтрагентНаименование,
| ВнешниеСтроки.Номенклатура КАК НоменклатураНаименование,
| ВнешниеСтроки.Количество КАК Количество,
| ВнешниеСтроки.Цена КАК Цена
|ИЗ
| ВнешнийИсточникДанных.НашаБаза.ТаблицаЗаголовков КАК ВнешнийЗаголовок
| ЛЕВОЕ СОЕДИНЕНИЕ ВнешнийИсточникДанных.НашаБаза.ТаблицаСтрок КАК ВнешниеСтроки
| ПО ВнешнийЗаголовок.ID = ВнешниеСтроки.ID_Шапки
|ИТОГИ ПО
| Идентификатор";
РезультатЗапроса = Запрос.Выполнить();
// Обходим сначала группировки (шапки документов)
ВыборкаШапка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаШапка.Следующий() Цикл
// Здесь мы создаем документ 1С один раз для каждого ID из внешней базы
НовыйДок = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
НовыйДок.Дата = ВыборкаШапка.ДатаДок;
// Заполняем остальные реквизиты шапки...
// Теперь выбираем строки, относящиеся ТОЛЬКО к этой шапке
ВыборкаСтрока = ВыборкаШапка.Выбрать();
Пока ВыборкаСтрока.Следующий() Цикл
// Проверяем, что строка ТЧ существует (на случай ЛЕВОГО СОЕДИНЕНИЯ без строк)
Если ВыборкаСтрока.НоменклатураНаименование <> NULL Тогда
НоваяСтрока = НовыйДок.Товары.Добавить();
НоваяСтрока.Количество = ВыборкаСтрока.Количество;
НоваяСтрока.Цена = ВыборкаСтрока.Цена;
// Поиск номенклатуры по наименованию или коду...
КонецЕсли;
КонецЦикла;
НовыйДок.Записать();
КонецЦикла;
Проанализируем этот подход: использование ОбходРезультатаЗапроса.ПоГруппировкам позволяет платформе самостоятельно управлять иерархией. Это избавляет от необходимости хранить переменные типа ПредыдущийID и делает код чистым и понятным.
Если данных очень много (например, тысячи документов за один раз), LEFT JOIN может начать тормозить из-за избыточности передаваемого трафика (повторение данных шапки в каждой строке). В этом случае эффективнее применить стратегию разделения.
Рассмотрим алгоритм действий:
ID в массив.ГДЕ ID_Шапки В (&МассивID).ТаблицаЗначений и индексируем её по полю ID_Шапки.Посмотрим на пример реализации поиска строк:
// Предположим, ТЗ_Строки уже заполнена данными из SQL
ТЗ_Строки.Индексы.Добавить("ID_Шапки");
Для Каждого СтрокаШапки Из ТЗ_Шапки Цикл
ДокОбъект = Документы.ЗаказПокупателя.СоздатьДокумент();
// Заполнение шапки...
// Быстрый поиск строк через НайтиСтроки по индексу
ПараметрыОтбора = Новый Структура("ID_Шапки", СтрокаШапки.ID);
НайденныеСтроки = ТЗ_Строки.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрТЧ Из НайденныеСтроки Цикл
НоваяСтрока = ДокОбъект.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрТЧ);
КонецЦикла;
ДокОбъект.Записать();
КонецЦикла;
Этот метод значительно быстрее на больших объемах, так как поиск в индексированной таблице значений происходит почти мгновенно, а объем данных, передаваемых по сети, минимален.
Если вы работаете с MS SQL Server 2016 или новее, можно заставить SQL сформировать готовую иерархию. Выясним причину, почему это удобно: 1С отлично умеет парсить JSON в структуры и массивы.
На стороне SQL запрос будет выглядеть примерно так:
SELECT HeaderID, Date, (SELECT Item, Qty FROM Lines WHERE Lines.HID = Headers.ID FOR JSON PATH) as Items FROM Headers FOR JSON AUTO
В 1С мы получим одну длинную строку. Разберем, как ее обработать:
// Получаем JSONString через ADO или ВнешнийИсточник
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(JSONString);
ДанныеИзSQL = ПрочитатьJSON(ЧтениеJSON); // Получаем массив структур
Для Каждого ДокДанные Из ДанныеИзSQL Цикл
НовыйДок = Документы.ПриходныйКассовыйОрдер.СоздатьДокумент();
НовыйДок.СуммаДокумента = ДокДанные.TotalAmount;
// ДокДанные.Items — это уже готовый массив строк
Для Каждого СтрДанные Из ДокДанные.Items Цикл
НоваяСтр = НовыйДок.МногострочнаяЧасть.Добавить();
НоваяСтр.Количество = СтрДанные.Qty;
КонецЦикла;
НовыйДок.Записать();
КонецЦикла;
При любой загрузке крайне важно не создавать дубли при повторном запуске обработки. Рассмотрим стандартный механизм контроля. Нам понадобится РегистрСведений (например, «СоответствиеОбъектовДляОбмена»), где измерениями будут ИдентификаторSQL и ТипОбъекта, а ресурсом — Ссылка1С.
Проанализируем логику проверки:
ID_SQL в регистре.ПолучитьОбъект()).СоздатьДокумент() и после записи фиксируем связь в регистре.
Важный нюанс: при работе с типом uniqueidentifier в SQL помните, что порядок байтов в GUID в 1С и в MS SQL различается. Если вы ищете документ по GUID напрямую, используйте преобразование в строку на стороне SQL или специальные функции конвертации в 1С.
Если вы используете старый метод подключения через COM-объект ADODB.Connection, забудьте про перебор записей через Recordset.MoveNext(). Это очень медленно из-за постоянных обращений между процессами.
Рассмотрим более производительный вариант:
Recordset = Connection.Execute(ТекстЗапроса);
// Выгружаем всё в двумерный массив одним вызовом
ДанныеМассивом = Recordset.GetRows();
Recordset.Close();
// Далее работаем с обычным массивом 1С, что в десятки раз быстрее
КоличествоЗаписей = ДанныеМассивом.GetUpperBound(1);
Для НомерЗаписи = 0 По КоличествоЗаписей Цикл
ЗначениеПоля = ДанныеМассивом.GetValue(ИндексКолонки, НомерЗаписи);
// Обработка...
КонецЦикла;
Подводя итог, выбор метода зависит от версии вашей платформы и объема данных. Для простых задач идеально подходит Внешний источник данных + ИТОГИ ПО, а для высоконагруженных систем — раздельные запросы с индексацией в памяти или JSON-трансформация на стороне сервера базы данных.