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