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