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