В процессе разработки на платформе 1С часто возникает задача: необходимо обработать список объектов и создать по ним документы. Однако, если обработка запускается несколько раз в день, важно не плодить дубликаты, а находить уже созданный за текущие сутки документ и обновлять в нем информацию. В этой статье мы подробно разберем, как правильно реализовать такой алгоритм, избежав типичных ошибок новичков, связанных с запросами в циклах и блокировками данных.
Рассмотрим ситуацию: у нас есть список объектов ремонта, по которым нужно создать документ Событие. Если документ за сегодня уже существует, мы должны просто дописать новую строку в текстовый реквизит СодержаниеСобытия. Основная сложность, с которой сталкиваются разработчики, — это пустая выборка запроса, даже когда данные в базе есть. Зачастую это связано с неверной передачей параметров (например, поиск по коду, который не совпадает, или передача ссылки на элемент справочника, который не соответствует условиям фильтра).
Проанализируем типичный (но не самый оптимальный) код, который часто используют для решения этой задачи:
Процедура ДействияФормыЗапланироватьОбзвон(Кнопка)
Для Каждого Стр Из СписокНаПокраску Цикл
Если Стр.Отметка Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Событие.Ссылка,
| Событие.СодержаниеСобытия
|ИЗ
| Документ.Событие КАК Событие
|ГДЕ
| Событие.Дата МЕЖДУ &НачДата И &КонДата
| И Событие.ОбъектРемонта = &ОбъектРемонта
| И Событие.ГруппаСобытия = &ГруппаСобытия
| И Событие.ПометкаУдаления = ЛОЖЬ";
Запрос.УстановитьПараметр("ГруппаСобытия", Справочники.ГруппыСобытий.НайтиПоКоду("000000043"));
Запрос.УстановитьПараметр("НачДата", НачалоДня(ТекущаяДата()));
Запрос.УстановитьПараметр("КонДата", КонецДня(ТекущаяДата()));
Запрос.УстановитьПараметр("ОбъектРемонта", Стр.Объект);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
// Документ найден, будем его менять
ОбъектДокумента = Выборка.Ссылка.ПолучитьОбъект();
ОбъектДокумента.СодержаниеСобытия = ОбъектДокумента.СодержаниеСобытия + Символы.ПС + "Новая запись";
ОбъектДокумента.Записать(РежимЗаписиДокумента.Запись);
Иначе
// Документ не найден, создаем новый
СоздатьНовоеСобытие(Стр);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Выясним причины, по которым Выборка.Количество() может быть равно нулю, даже если визуально документы в списке присутствуют:
Стр.Объект действительно содержится Ссылка на справочник, а не строка или другой тип данных.НайтиПоКоду("000000043") крайне ненадежно. Если длина кода в справочнике изменится (например, станет больше знаков с ведущими нулями), метод вернет пустую ссылку.НачалоДня и КонецДня.Событие.ПометкаУдаления = ЛОЖЬ исключит его из результатов.Выполнение запроса внутри цикла Для Каждого — это серьезная ошибка производительности. При каждой итерации программа обращается к серверу БД, что замедляет работу. Разберем, как сделать это правильно с помощью пакетной обработки.
Шаг 1: Сначала соберем все объекты ремонта в один массив.
Шаг 2: Выполним один запрос ко всем документам за указанный период, где ОбъектРемонта В (&МассивОбъектов).
Шаг 3: Выгрузим результат в Соответствие для быстрого поиска в памяти.
Посмотрим на пример реализации:
// Собираем ссылки на объекты
МассивОбъектов = Новый Массив;
Для Каждого Стр Из СписокНаПокраску Цикл
Если Стр.Отметка Тогда
МассивОбъектов.Добавить(Стр.Объект);
КонецЕсли;
КонецЦикла;
Если МассивОбъектов.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Ищем существующие документы одним запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Событие.Ссылка КАК Ссылка,
| Событие.ОбъектРемонта КАК ОбъектРемонта
|ИЗ
| Документ.Событие КАК Событие
|ГДЕ
| Событие.Дата МЕЖДУ &НачДата И &КонДата
| И Событие.ОбъектРемонта В (&МассивОбъектов)
| И Событие.ГруппаСобытия = &ГруппаСобытия
| И Событие.ПометкаУдаления = ЛОЖЬ";
Запрос.УстановитьПараметр("МассивОбъектов", МассивОбъектов);
Запрос.УстановитьПараметр("НачДата", НачалоДня(ТекущаяДата()));
Запрос.УстановитьПараметр("КонДата", КонецДня(ТекущаяДата()));
// Лучше вынести поиск группы из цикла или использовать предопределенный элемент
Запрос.УстановитьПараметр("ГруппаСобытия", Справочники.ГруппыСобытий.НайтиПоКоду("000000043"));
Результат = Запрос.Выполнить().Выгрузить();
// Создаем соответствие для быстрого поиска: Ключ - ОбъектРемонта, Значение - ДокументСсылка
ДокументыПоОбъектам = Новый Соответствие;
Для Каждого СтрокаТЗ Из Результат Цикл
ДокументыПоОбъектам.Вставить(СтрокаТЗ.ОбъектРемонта, СтрокаТЗ.Ссылка);
КонецЦикла;
Проанализируем важный технический нюанс: работа со ссылкой и объектом. Помните, что Выборка.Ссылка (или значение из таблицы запроса) — это лишь указатель на запись в базе данных. Вы не можете напрямую присвоить значение её реквизиту. Чтобы изменить данные, необходимо выполнить метод ПолучитьОбъект().
Рассмотрим процесс изменения текстового поля:
Символы.ПС (перенос строки), чтобы записи не сливались.
Для Каждого Стр Из СписокНаПокраску Цикл
Если НЕ Стр.Отметка Тогда Продолжить; КонецЕсли;
СсылкаНаДокумент = ДокументыПоОбъектам.Получить(Стр.Объект);
Если СсылкаНаДокумент <> Неопределено Тогда
// Документ найден - редактируем
ДокОбъект = СсылкаНаДокумент.ПолучитьОбъект();
НовоеСообщение = "Дозапись от " + ТекущаяДата();
// Добавляем текст с новой строки
ДокОбъект.СодержаниеСобытия = ДокОбъект.СодержаниеСобытия +
?(ПустаяСтрока(ДокОбъект.СодержаниеСобытия), "", Символы.ПС) +
НовоеСообщение;
Попытка
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
Исключение
Сообщить("Не удалось записать документ: " + ОписаниеОшибки());
КонецПопытки;
Иначе
// Документ не найден - создаем новый
СоздатьНовоеСобытие(Стр);
КонецЕсли;
КонецЦикла;
Если база данных интенсивно растет, поиск по таблице документов за каждый день может начать "подтормаживать". Профессиональным подходом считается использование вспомогательного Регистра сведений.
Разберем, как это работает:
СобытияЗаДень.ДатаСобытия (тип Дата), ОбъектРемонта.ДокументСобытие (тип ДокументСсылка.Событие).При создании нового документа мы делаем запись в этот регистр. При поиске мы обращаемся не к тяжелой таблице документов с множеством условий, а к легкому регистру по двум индексированным измерениям. Это гарантирует мгновенный поиск даже при миллионах записей.
Проанализируем ситуацию, когда два пользователя одновременно запускают одну и ту же обработку. Оба могут одновременно проверить наличие документа, получить ответ "не найден" и создать два дубликата. Это называется "состоянием гонки" (race condition).
Для решения этой проблемы при выполнении поиска и записи стоит использовать Блокировку данных. Перед выполнением запроса на проверку существования документа установите исключительную блокировку на объект ремонта или на запись в регистре сведений (если используете методику из предыдущего пункта). Это заставит второй сеанс подождать, пока первый завершит транзакцию и создаст документ.
Важные выводы:
ПолучитьОбъект().Символы.ПС для формирования многострочных текстов.НайтиПоКоду в программном коде, используйте ссылки или настройки в константах.