Как исправить ошибку «Значение поля не соответствует установленному отбору» при записи регистра сведений в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с механизмами платформы «1С:Предприятие» разработчики часто сталкиваются с необходимостью корректировки или обновления существующих записей в независимых регистрах сведений. Одной из наиболее популярных ошибок, возникающих при решении этой задачи, является системное исключение: «Ошибка при вызове метода контекста (Записать): Запись не верна! Значение поля не соответствует установленному отбору...».

В этой статье мы подробно проанализируем ситуацию, выясним глубинную причину возникновения этой ошибки, разберем по шагам типичные промахи начинающих программистов и рассмотрим три надежных и методологически верных способа обновления записей в регистре сведений.

Почему возникает ошибка и в чем кроется причина?

Для того чтобы понять природу этой ошибки, необходимо обратиться к внутренним механизмам работы платформы 1С с объектом типа РегистрСведенийНаборЗаписей. Набор записей представляет собой коллекцию строк, которая считывается из базы данных и записывается обратно по определенным правилам фильтрации.

Главное архитектурное правило платформы звучит так: если у объекта НаборЗаписей установлен отбор по какому-либо измерению, то абсолютно все записи, добавляемые, изменяемые или находящиеся в этом наборе в момент вызова метода Записать(), обязаны иметь значения этого измерения, строго совпадающие со значениями, указанными в отборе.

Проанализируем ситуацию, в которой оказался автор темы на форуме:

  1. Он установил отбор набора записей по старому статусу: Набор.Отбор.Статус.Установить(СтатусСтарый) (где статус равен значению «Ошибка обработки»).
  2. В цикле обхода записей он попытался изменить этот статус на новый: НоваяЗапись.Статус = Статус_EDI (где новое значение равно «Требует обработки»).
  3. При вызове метода Набор.Записать() платформа проверила строки набора. Обнаружив, что в поле Статус у записей находится значение «Требует обработки», в то время как отбор набора жестко зафиксирован на значении «Ошибка обработки», система заблокировала транзакцию и вызвала ошибку соответствия отбору.

Кроме того, в коде автора содержалась еще одна критическая ошибка: вызов метода Записать() выполнялся на каждой итерации цикла (внутри обхода коллекции). Запись набора данных в цикле приводит к колоссальной нагрузке на СУБД, порождает избыточные транзакции и может стать причиной взаимных блокировок в многопользовательской среде.

Ниже мы подробно разберем три качественных решения этой задачи, которые гарантируют целостность данных и корректность выполнения операции.

---

Способ 1. Набор записей без отбора по изменяемому измерению (Самый простой и элегантный)

Если объем данных по конкретному ключевому объекту (например, по конкретному контрагенту) относительно невелик и не исчисляется десятками тысяч строк, мы можем отказаться от установки отбора по изменяемому измерению. Вместо этого мы установим отбор только по тем измерениям, которые останутся неизменными (в нашем случае — это измерение Контрагент).

Рассмотрим подробнее данный алгоритм:

  1. Создаем набор записей регистра.
  2. Устанавливаем отбор только по измерению Контрагент.
  3. Считываем данные из базы с помощью метода Прочитать(). В набор попадут все записи по этому контрагенту с любыми статусами.
  4. Программно обходим полученный набор в цикле. Если статус записи совпадает со старым значением («Ошибка обработки»), мы напрямую перезаписываем его новым значением («Требует обработки»).
  5. Выносим метод Записать() за пределы цикла и вызываем его один раз для всего набора.

Посмотрим на пример программного кода, реализующего данный подход:


&НаСервере
Процедура ОбновитьСтатусыНаСервере()
    
    СтатусСтарый = Перечисления.СТ_EDIСтатусыСообщений.ОшибкаОбработки;
    СтатусНовый  = Перечисления.СТ_EDIСтатусыСообщений.ТребуетОбработки;
    
    // Создаем объект набора записей регистра
    Набор = РегистрыСведений.СТ_EDIОчередьИсходящихСообщений.СоздатьНаборЗаписей();
    
    // Устанавливаем отбор ТОЛЬКО по контрагенту. По полю "Статус" отбор НЕ ставим!
    Набор.Отбор.Контрагент.Установить(Контрагент);
    Набор.Прочитать();
    
    Модифицирован = Ложь;
    
    // Обходим прочитанные записи в оперативной памяти
    Для Каждого Запись Из Набор Цикл
        Если Запись.Статус = СтатусСтарый Тогда
            Запись.Статус = СтатусНовый;
            Запись.СрокОтправки = ТекущаяДатаСеанса();
            Модифицирован = Истина;
        КонецЕсли;
    КонецЦикла;
    
    // Выполняем запись один раз и только при наличии изменений
    Если Модифицирован Тогда
        Набор.Записать();
    КонецЕсли;
    
КонецПроцедуры

Преимущества этого способа: предельно простой и лаконичный код, отсутствие необходимости использовать временные таблицы значений, гарантированное обновление данных в рамках одного обращения к СУБД.

Ограничения: способ не подходит для высоконагруженных регистров, где у одного контрагента могут быть миллионы записей с другими статусами, так как метод Прочитать() загрузит их все в оперативную память сервера приложений.

---

Способ 2. Двухэтапная схема обновления (Удаление старых -> Запись новых)

Если регистр содержит огромный объем данных, и считывать записи с «лишними» статусами нецелесообразно, необходимо использовать классическую схему с разделением операций удаления и записи в рамках единой транзакции.

Давайте разберем по шагам, как работает этот метод:

  1. Мы создаем первый набор записей с отбором по старому статусу и контрагенту, после чего считываем его.
  2. Выгружаем данные прочитанного набора во временную таблицу значений с помощью метода Выгрузить(). Теперь старые данные сохранены в памяти.
  3. Очищаем первый набор записей методом Очистить() и записываем пустой набор в базу данных. В этот момент старые записи со статусом «Ошибка обработки» физически удаляются из регистра.
  4. Создаем второй набор записей с отбором по новому статусу и тому же контрагенту. Наполняем его строками из нашей таблицы значений, принудительно подставляя новый статус, соответствующий новому отбору.
  5. Записываем второй набор в базу данных.

Важное предупреждение: проведение таких манипуляций обязательно должно быть обернуто в транзакцию! Если между удалением старых записей и созданием новых произойдет аварийное завершение сеанса или сбой СУБД, ваши данные будут безвозвратно утеряны. Использование транзакции гарантирует, что база данных либо примет все изменения целиком, либо вернется к исходному состоянию.

Проанализируем программный код реализации двухэтапной схемы:


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

---

Способ 3. Использование Менеджера Записи (Для точечных изменений)

В сообщениях на форуме упоминалось использование объекта РегистрСведенийМенеджерЗаписи. Этот способ идеально подходит, если вам нужно изменить всего одну или несколько конкретных записей, и вы точно знаете значения всех ключевых полей (измерений) для этих записей.

Однако здесь программистов подстерегает опасная ловушка: нельзя просто прочитать менеджер записи, поменять в нем ключевое поле и нажать «Записать». Если вы так сделаете, система запишет новую строку с измененным ключевым полем, но при этом старая строка с прежним значением ключевого поля так и останется лежать в базе данных нетронутой!

Для корректного изменения измерения с помощью менеджера записи необходимо придерживаться следующего алгоритма:

  1. Инициализировать менеджер записи и заполнить его ключевые поля старыми значениями.
  2. Прочитать запись из базы данных с помощью метода Прочитать().
  3. Если запись найдена, удалить ее из базы данных методом Удалить().
  4. Изменить ключевое поле (в нашем случае Статус) на новое значение.
  5. Вызвать метод Записать() для добавления записи с новым ключом.

Пример программного кода для точечного изменения записи через менеджер:


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

---

Резюме и правила безопасной работы с регистрами

Для того чтобы избежать подобных ошибок в вашей будущей практике разработки на платформе «1С:Предприятие», всегда следуйте своду простых правил:

← На главную