При работе с независимыми непериодическими регистрами сведений в 1С (такими как типовой регистр Штрихкоды в конфигурации «Управление торговлей 10.3») разработчики часто сталкиваются с ошибками блокировки или уникальности записей при попытке их изменения. Типичный сценарий: программа сообщает, что «штрихкод уже имеет владельца», либо указывает на дублирование записей, хотя разработчик ожидает простой перезаписи существующих данных.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем разницу в поведении платформы при использовании объектов НаборЗаписей и МенеджерЗаписи, а также выработаем оптимальные и безопасные алгоритмы изменения данных в регистре.
Прежде чем писать код, важно понять природу возникающей ошибки. В платформе 1С Предприятие уникальность записей в независимом регистре сведений обеспечивается уникальностью набора его измерений. Если мы пытаемся записать строку с точно такими же измерениями, платформа просто перезапишет ресурсы и реквизиты этой строки.
Однако ошибка вида "Штрихкод: MAG99912352 уже имеет владельца..." не является системной ошибкой СУБД. Это классическая прикладная проверка, заложенная разработчиками конфигурации в модуле набора записей регистра сведений в обработчике события ПередЗаписью. Алгоритм этой проверки выглядит следующим образом:
Если мНеРазрешитьНеуникальныеШтрихкоды Тогда
Для Каждого ТекущаяЗапись Из ЭтотОбъект Цикл
Если ЗначениеЗаполнено(ТекущаяЗапись.ШтрихКод) Тогда
Запрос = Новый Запрос("ВЫБРАТЬ ПЕРВЫЕ 1
| РегШтрихкоды.Владелец КАК Владелец,
| РегШтрихкоды.Штрихкод КАК Штрихкод
|ИЗ
| РегистрСведений.Штрихкоды КАК РегШтрихкоды
|ГДЕ
| РегШтрихкоды.Штрихкод = &ШтрихКод
| И РегШтрихкоды.Владелец <> &Владелец");
Запрос.УстановитьПараметр("ШтрихКод", ТекущаяЗапись.ШтрихКод);
Запрос.УстановитьПараметр("Владелец", ТекущаяЗапись.Владелец);
РезультатЗапроса = Запрос.Выполнить;
Если Не РезультатЗапроса.Пустой() Тогда
// Вызывается исключение или Отказ = Истина
Сообщить("Штрихкод уже имеет владельца!");
Отказ = Истина;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
Когда вы пытаетесь изменить владельца существующего штрихкода или перезаписать запись стандартным методом считывания, эта проверка находит в базе данных старую запись (где владельцем еще числится предыдущий товар) и блокирует запись новой строки, считая это попыткой назначить штрихкод другому товару.
Если перед вами стоит задача изменить или создать ровно одну запись в регистре сведений, использование объекта РегистрыСведений.[ИмяРегистра].СоздатьМенеджерЗаписи() является наиболее лаконичным и правильным подходом. В отличие от набора записей, менеджер записи работает напрямую с одной строкой физической таблицы.
Рассмотрим по шагам правильный алгоритм обновления записи с использованием менеджера:
Прочитать(), чтобы заполнить объект существующими данными (если они есть).Выбран(). Если запись существует, мы просто меняем нужные ресурсы или реквизиты. Если не существует — заполняем новые.Записать().Посмотрим на пример практической реализации данного подхода:
Менеджер = РегистрыСведений.Штрихкоды.СоздатьМенеджерЗаписи();
// Обязательно заполняем все измерения для поиска существующей записи
Менеджер.Штрихкод = ВыбранноеЗначение;
// Читаем запись из базы данных
Менеджер.Прочитать();
// Вне зависимости от того, существовала ли запись, заполняем новые значения
Менеджер.Владелец = ЭтотОбъект.Ссылка;
Менеджер.Поставщик = ЭлементыФормы.МТД_Поставщики.ТекущаяСтрока.Поставщики;
Менеджер.ЕдиницаИзмерения = ЭтотОбъект.БазоваяЕдиницаИзмерения;
Менеджер.Качество = Справочники.Качество.Новый;
// Если тип штрихкода нужно получить запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Штрихкоды.ТипШтрихкода КАК ТипШтрихкода
|ИЗ
| РегистрСведений.Штрихкоды КАК Штрихкоды
|ГДЕ
| Штрихкоды.Штрихкод = &Штрихкод";
Запрос.УстановитьПараметр("Штрихкод", ВыбранноеЗначение);
Выгрузка = Запрос.Выполнить().Выгрузить();
Если Выгрузка.Количество() > 0 Тогда
Менеджер.ТипШтрихкода = Выгрузка[0].ТипШтрихкода;
Иначе
Менеджер.ТипШтрихкода = ПланыВидовХарактеристик.ТипыШтрихкодов.EAN13; // Значение по умолчанию
КонецЕсли;
Попытка
Менеджер.Записать(Истина); // Параметр Истина означает замещение существующей записи
Исключение
Сообщить("Не удалось записать штрихкод: " + ОписаниеОшибки());
КонецПопытки;
Что делать, если вам всё же необходимо использовать объект НаборЗаписей? Основная ошибка разработчиков заключается в вызове метода Прочитать() перед изменением строки. Считав данные в оперативную память, разработчик пытается изменить измерение (например, Владелец) и записать набор обратно. В этот момент транзакция наталкивается на проверку уникальности, поскольку старая запись в БД всё еще существует и указывает на прежнего владельца.
Правильный механизм замещения данных с помощью набора записей базируется на принципе: «Установить отбор — Добавить новые данные — Записать». При вызове метода Записать() платформа автоматически очищает в базе данных все записи, которые соответствуют установленному отбору, и только после этого вставляет новые строки из памяти в рамках одной транзакции.
Проанализируем этот алгоритм на практике:
Набор = РегистрыСведений.Штрихкоды.СоздатьНаборЗаписей();
// Устанавливаем жесткий отбор по штрихкоду
Набор.Отбор.Штрихкод.Установить(ВыбранноеЗначение);
// ВНИМАНИЕ: Метод Набор.Прочитать() НЕ вызываем!
// Это позволит очистить старую запись по этому штрихкоду в момент вызова Записать().
// Получаем необходимые сопутствующие данные (например, тип штрихкода) до очистки регистра
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Штрихкоды.ТипШтрихкода КАК ТипШтрихкода
|ИЗ
| РегистрСведений.Штрихкоды КАК Штрихкоды
|ГДЕ
| Штрихкоды.Штрихкод = &Штрихкод";
Запрос.УстановитьПараметр("Штрихкод", ВыбранноеЗначение);
Выгрузка = Запрос.Выполнить().Выгрузить();
ТекущийТип = Неопределено;
Если Выгрузка.Количество() > 0 Тогда
ТекущийТип = Выгрузка[0].ТипШтрихкода;
КонецЕсли;
// Добавляем новую чистую запись в набор в памяти
НоваяЗапись = Набор.Добавить();
НоваяЗапись.Штрихкод = ВыбранноеЗначение;
НоваяЗапись.Владелец = ЭтотОбъект.Ссылка;
НоваяЗапись.Поставщик = ЭлементыФормы.МТД_Поставщики.ТекущаяСтрока.Поставщики;
НоваяЗапись.ЕдиницаИзмерения = ЭтотОбъект.БазоваяЕдиницаИзмерения;
НоваяЗапись.Качество = Справочники.Качество.Новый;
НоваяЗапись.ТипШтрихкода = ?(ТекущийТип <> Неопределено, ТекущийТип, Справочники.ТипыШтрихкодов.EAN13);
// Выполняем запись. Старая запись в БД удалится по отбору, новая запишется без конфликтов
Попытка
Набор.Записать(Истина);
Исключение
Сообщить("Ошибка при записи набора: " + ОписаниеОшибки());
КонецПопытки;
В процессе обсуждения на форумах часто всплывает работоспособный, но крайне неоптимальный вариант решения проблемы (так называемый метод «двойной записи»):
Набор = РегистрыСведений.Штрихкоды.СоздатьНаборЗаписей();
Набор.Отбор.Штрихкод.Установить(ВыбранноеЗначение);
Набор.Записать(); // Первая запись — очищает регистр по отбору
НоваяЗапись = Набор.Добавить();
// Заполнение полей...
Набор.Записать(); // Вторая запись — добавляет новую строку
Выясним, почему этот код работает, но использовать его в реальных проектах крайне не рекомендуется:
При работе в многопользовательских базах данных (особенно в клиент-серверном режиме работы 1С) параллельная запись штрихкодов может приводить к транзакционным взаимоблокировкам (Deadlocks). Чтобы защитить операцию изменения регистра, настоятельно рекомендуется использовать объект БлокировкаДанных перед чтением или записью набора:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.Штрихкоды");
ЭлементБлокировки.УстановитьЗначение("Штрихкод", ВыбранноеЗначение);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Далее выполняем логику записи набора или менеджера записи...
Данный подход гарантирует, что ни один другой сеанс пользователей не сможет прочитать или изменить этот конкретный штрихкод до тех пор, пока ваша транзакция записи не завершится успешно.