Обход документов в 1С 8.3: от Выборки до фоновой обработки
Содержание
Перебрать все документы в 1С 8.3 можно тремя способами: объектом Выборка, запросом по ссылкам и обходом коллекции Метаданные.Документы. Какой выбрать — зависит от того, что вы делаете: читаете пару реквизитов, вытягиваете объект целиком или массово перезаписываете.
Ошибка почти всегда одна: берут Выборку там, где хватило бы запроса, грузят весь объект ради одного поля и заворачивают обработку в одну гигантскую транзакцию. На тысяче документов это незаметно, на полумиллионе — база встаёт. Ниже разберём каждый метод с рабочим кодом, сравним по скорости и покажем, как обрабатывать большие объёмы без простоя пользователей.
Весь код выполняется в серверном контексте: на клиенте Выбрать() и ПолучитьОбъект() недоступны.
Построчное чтение объектом Выборка
Выборка — самый простой перебор. Менеджер документа возвращает объект, который читается по одной записи:
Выборка = Документы.РеализацияТоваровУслуг.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Номер + " от " + Формат(Выборка.Дата, "ДЛФ=D"));
КонецЦикла;
Метод Выбрать() принимает интервал дат, отбор и порядок:
Отбор = Новый Структура("Организация", ТекущаяОрганизация);
Выборка = Документы.РеализацияТоваровУслуг.Выбрать(
НачалоГода, КонецГода, Отбор, "Дата Возр");
Поля вроде Выборка.Номер читаются сразу, а вот табличные части и весь документ по требованию:
Пока Выборка.Следующий() Цикл
Объект = Выборка.ПолучитьОбъект();
Для Каждого Строка Из Объект.Товары Цикл
// работа с табличной частью
КонецЦикла;
КонецЦикла;
Выборка не грузит всю таблицу в память — она читает записи порциями по мере вызова Следующий(). Поэтому по памяти она безопаснее, чем Запрос.Выполнить().Выгрузить(), но каждый ПолучитьОбъект() — это отдельное чтение объекта с сервера со всеми табличными частями.
Когда уместно: нужны почти все реквизиты документа или его табличные части, а объём умеренный. Для «пробежать и прочитать два поля у сотен тысяч документов» это медленно.
Важный момент про табличные части: их тоже можно читать запросом, не поднимая объект. Каждая таблица документа доступна как отдельная таблица запроса — Документ.РеализацияТоваровУслуг.Товары. Если нужны строки товаров по многим документам, запрос к этой таблице на порядок быстрее, чем ПолучитьОбъект() в цикле:
ВЫБРАТЬ
Строки.Ссылка КАК Документ,
Строки.Номенклатура КАК Номенклатура,
Строки.Количество КАК Количество
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Строки
ГДЕ
Строки.Ссылка.Проведен
Строки.Ссылка здесь — ссылка на документ-владелец, через точку доступны его реквизиты. Поднимать объект нужно только тогда, когда вы собираетесь строки менять и записывать.
Быстрый перебор запросом и обработка порциями
Запрос читает только те поля, которые вы назвали. Это главное преимущество по скорости: платформа не поднимает объект целиком.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка,
| Док.Дата КАК Дата,
| Док.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Проведен
|УПОРЯДОЧИТЬ ПО
| Док.Ссылка";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Выборка.Сумма уже прочитана — объект не поднимался
КонецЦикла;
Если документы надо не только прочитать, но и изменить, берите из запроса Ссылка и поднимайте объект только под запись. Не грузите всю базу ссылок в одну таблицу — на больших объёмах это съест память сервера. Правильный приём — порционное чтение «от последней ссылки»:
ПоследняяСсылка = Документы.РеализацияТоваровУслуг.ПустаяСсылка();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Ссылка > &ПоследняяСсылка
|УПОРЯДОЧИТЬ ПО
| Док.Ссылка";
Пока Истина Цикл
Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
Таблица = Запрос.Выполнить().Выгрузить();
Если Таблица.Количество() = 0 Тогда
Прервать;
КонецЕсли;
Для Каждого Строка Из Таблица Цикл
Объект = Строка.Ссылка.ПолучитьОбъект();
// изменение и запись объекта
КонецЦикла;
ПоследняяСсылка = Таблица[Таблица.Количество() - 1].Ссылка;
КонецЦикла;
Такой «курсор по ссылке» устойчив: даже если документы добавляются или помечаются на удаление во время обработки, вы не пропустите и не задвоите строки. Порцию в 500–1000 документов сервер держит легко.
Обычная пагинация через ВЫБРАТЬ ПЕРВЫЕ … УПОРЯДОЧИТЬ ПО Дата плоха для записи: при изменении данных смещаются границы и строки теряются. Упорядочивайте и фильтруйте строго по Ссылка.
Как пройти документы всех видов разом
Если задача — «все документы базы», а не один вид, перебирать каждый тип руками не нужно. Коллекция Метаданные.Документы знает все виды документов конфигурации:
Для Каждого ОписаниеДокумента Из Метаданные.Документы Цикл
МенеджерДокумента = Документы[ОписаниеДокумента.Имя];
Выборка = МенеджерДокумента.Выбрать();
Пока Выборка.Следующий() Цикл
// обработка документа любого вида
КонецЦикла;
КонецЦикла;
Документы[ОписаниеДокумента.Имя] — обращение к менеджеру по строковому имени вида. Это универсальный способ: добавится новый документ в конфигурацию — цикл подхватит его сам, без правки кода.
У такого обхода есть нюанс: у разных видов разный набор реквизитов. Если внутри нужен конкретный реквизит, проверяйте его наличие через метаданные:
Если ОписаниеДокумента.Реквизиты.Найти("Организация") <> Неопределено Тогда
// у этого вида есть реквизит Организация
КонецЕсли;
Когда видов десятки, а объёмы большие, тот же порционный приём из прошлого раздела применяйте внутри цикла по видам — для каждого вида свой «курсор по ссылке».
Что быстрее: методы под разные задачи
Единого «самого быстрого» способа нет — скорость зависит от того, что вы читаете и меняете.
| Метод | Что читает | Скорость | Когда брать |
|---|---|---|---|
Выборка без ПолучитьОбъект | реквизиты шапки по одной записи | средняя | нужны поля шапки, объём умеренный |
Выборка + ПолучитьОбъект | весь объект с табличными частями | низкая | нужны табличные части или запись |
| Запрос по полям | только названные поля | высокая | чтение отчётных данных, отборы, группировки |
Запрос по ссылкам порциями + ПолучитьОбъект | ссылки порциями, объект под запись | оптимум для массовой записи | перепроведение, пересчёт, правка реквизитов |
Ориентир для выбора:
Два общих правила скорости: не поднимайте объект ради поля, которое отдаёт запрос, и никогда не ставьте запрос внутрь цикла перебора — выносите его за цикл или объединяйте во временную таблицу.
Отдельный случай — агрегация без записи: суммы, количества, сверка по документам. Здесь не нужен ни ПолучитьОбъект, ни порционный курсор. Группируйте прямо в запросе:
ВЫБРАТЬ
Док.Организация КАК Организация,
КОЛИЧЕСТВО(Док.Ссылка) КАК Количество,
СУММА(Док.СуммаДокумента) КАК Сумма
ИЗ
Документ.РеализацияТоваровУслуг КАК Док
ГДЕ
Док.Проведен
СГРУППИРОВАТЬ ПО
Док.Организация
Один запрос с группировкой всегда быстрее, чем ручной перебор со сложением в переменную: платформа считает это на стороне СУБД.
Транзакции и блокировки при массовой записи
Пока вы только читаете, транзакции не нужны. Как только начинаете писать — Записать() или Записать(РежимЗаписиДокумента.Проведение) — появляются блокировки регистров, и способ обёртки в транзакцию решает всё.
Главная ошибка — одна транзакция на весь прогон:
// так НЕ надо на больших объёмах
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
Объект = Выборка.ПолучитьОбъект();
Объект.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();
Такая транзакция держит блокировки до самого конца, разрастается до эскалации (блокируются целые таблицы) и намертво встаёт поперёк работы пользователей. Правильно — короткая транзакция на документ или на небольшую порцию:
Для Каждого Строка Из Таблица Цикл
НачатьТранзакцию();
Попытка
Объект = Строка.Ссылка.ПолучитьОбъект();
Объект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Обработка документов",
УровеньЖурналаРегистрации.Ошибка, , Строка.Ссылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
Попытка обязательна: без неё первый же непроведённый документ (нет остатков, закрыт период) обрушит весь прогон. С обработкой исключения битый документ логируется, а остальные обрабатываются дальше.
Проведение в рабочей базе формирует движения по регистрам. Перед массовым перепроведением сделайте резервную копию и запускайте обработку в нерабочее время или монопольно — иначе конкурентные блокировки с пользователями неизбежны.
Если нужен контроль над конкуренцией явно, применяйте управляемые блокировки (БлокировкаДанных) на нужные данные, но держите их в границах короткой транзакции порции, а не всего цикла.
Порядок в блоке Исключение строгий: сначала ОтменитьТранзакцию(), и только потом логирование или любой другой код. Если вызвать запись в журнал до отмены, незакрытая транзакция может помешать и самому логированию. И не глушите исключение молча — без записи в журнал регистрации вы потом не узнаете, какие именно документы не обработались и почему.
Фоновая обработка больших объёмов
Десятки тысяч документов в клиентском сеансе обрабатывать нельзя: сеанс повиснет, а при разрыве связи работа оборвётся на середине. Выносите это в фоновое задание на сервере:
ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(НачалоПериода);
ПараметрыЗадания.Добавить(КонецПериода);
ФоновыеЗадания.Выполнить(
"ОбработкаДокументовСервер.ПеребратьИОбработать",
ПараметрыЗадания, , "Массовая обработка документов");
Внутри метода фонового задания — тот же порционный цикл «по ссылке» с короткими транзакциями. Чтобы задание было возобновляемым, сохраняйте последнюю обработанную ссылку (в константу или регистр сведений ): при повторном запуске обработка продолжится с места обрыва, а не с начала.
Для прогресса с показом пользователю в конфигурациях на БСП есть ДлительныеОперации.ВыполнитьВФоне — она запускает тот же серверный метод, но возвращает на форму идентификатор, по которому видно процент выполнения.
Когда объёмы по-настоящему велики, обработку делят на несколько параллельных заданий. Делите не по видам документов (их размеры разные — нагрузка перекосится), а по равным диапазонам ссылок: каждому заданию свой отрезок ГДЕ Ссылка > &От И Ссылка <= &До. Так работа ложится ровно, а общий прогон укладывается в ночное окно. Но не запускайте больше заданий, чем свободных рабочих процессов на сервере, — лишние встанут в очередь и выигрыша не дадут.
Отдельно про перепроведение: Записать(РежимЗаписиДокумента.Проведение) заново формирует движения по регистрам, а простая Записать() — только перезаписывает сам документ, не трогая проводки. Если задача пересчитать итоги
— нужен именно режим проведения; если поправить реквизит шапки — хватит обычной записи, и это в разы легче для базы.
Самый безопасный сценарий для сотен тысяч документов — регламентное задание ночью: пользователей нет, блокировки никому не мешают, а порционные транзакции не дают разрастись нагрузке.
Частые ошибки при переборе документов
- 🔁 Запрос внутри цикла перебора. На каждый документ — отдельное обращение к базе. Выносите запрос за цикл или собирайте данные во временную таблицу одним запросом.
- 📦
ПолучитьОбъект()ради одного поля. Подъём всего объекта с табличными частями там, где хватило бы запроса по реквизиту. Читайте полями. - 🧱 Одна транзакция на весь прогон. Ведёт к эскалации блокировок и зависанию базы. Транзакция — на документ или порцию.
- 🧭 Пагинация по дате или номеру при записи. Границы смещаются, строки теряются. Для устойчивого курсора упорядочивайте по
Ссылка. - 🧨 Отсутствие
Попыткавокруг записи. Один непроведённый документ обрушивает весь цикл. Логируйте ошибку и идите дальше. - 💾 Выгрузка всех ссылок в одну таблицу. На больших базах память сервера не выдержит. Читайте порциями по 500–1000.
Как перебрать только проведённые документы одного вида?
ГДЕ Док.Проведен. Для перебора объектом Выборка отфильтровать проведённость напрямую нельзя — там отбор идёт по реквизитам, поэтому для «только проведённые» удобнее запрос.Почему `Выборка` с `ПолучитьОбъект` работает медленно?
ПолучитьОбъект() поднимает с сервера весь документ вместе с табличными частями. Если вам нужны только поля шапки, замените на запрос по этим полям — объект поднимать не придётся, и скорость вырастет в разы.Можно ли запускать перебор документов на клиенте?
Выбрать(), ПолучитьОбъект() и выполнение запроса доступны только в серверном контексте (&НаСервере, общий серверный модуль, фоновое задание). С клиента вызывайте серверную процедуру.Как показать прогресс при обработке десятков тысяч документов?
ДлительныеОперации.ВыполнитьВФоне (БСП) и опрашивайте состояние задания на форме, либо считайте обработанные порции и пишите процент. Сам цикл при этом должен быть порционным и возобновляемым.