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