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