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