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