Мы рассмотрим распространенную задачу: необходимо автоматически присваивать уникальный последовательный номер новой записи в регистре сведений. При этом крайне важно гарантировать, что в условиях одновременной работы нескольких пользователей не возникнут дублирующие номера. Мы проанализируем различные подходы к решению этой проблемы, начиная от исправления некорректных методов блокировки до использования рекомендуемых платформенных механизмов.
Давайте сначала разберем, почему первоначальная попытка реализовать блокировку прямо в обработчике события формы, таком как ПередЗаписьюНаСервере, не будет работать должным образом и может привести к нежелательным последствиям или даже ошибкам.
Когда мы работаем в обработчике формы, например, ПередЗаписьюНаСервере или ПриЗаписиНаСервере, важно понимать, что платформа 1С:Предприятие уже открывает свою собственную транзакцию для сохранения данных объекта. Попытка начать новую транзакцию с помощью НачатьТранзакцию() в этом контексте может привести к непредсказуемому поведению. Платформа, выходя из процедуры, откатит вашу "вложенную" транзакцию, и все установленные в ней блокировки будут сняты. Таким образом, механизм блокировки, который мы пытались применить, просто не сработает.
Кроме того, блокировка всего регистра сведений в исключительным режиме (РежимБлокировкиДанных.Исключительный) может значительно снизить параллельность работы пользователей. Если ваш регистр активно используется, другие сеансы будут вынуждены ожидать снятия блокировки со всего ресурса, даже если им нужна только другая запись. Это может привести к таймаутам и взаимоблокировкам.
Посмотрим на пример некорректного кода:
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.РС");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// !!! ОШИБКА: НачатьТранзакцию здесь не нужна и вредна !!!
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка.Заблокировать();
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ЕСТЬNULL(МАКСИМУМ(РС.Номер), 0) КАК Номер
|ИЗ
| РегистрСведений.РС КАК РС";
ТекущийОбъект.Номер = Запрос.Выполнить().Выгрузить()[0].Номер + 1;
// Транзакция, начатая выше, будет откатана платформой после выхода из процедуры
КонецПроцедуры
Правильным местом для установки управляемых блокировок и выполнения логики, требующей транзакционности, является модуль объекта регистра сведений, а именно процедура ПередЗаписью. В этом обработчике уже действует платформенная транзакция, и все установленные нами блокировки будут корректно удерживаться до ее завершения (фиксации или отката).
Давайте разберем по шагам, как реализовать этот подход:
1. **Размещение кода:** Переносим всю логику генерации номера и установки блокировки в процедуру ПередЗаписью модуля объекта вашего регистра сведений.
2. **Создание объекта блокировки:** Мы создаем новый объект БлокировкаДанных.
3. **Добавление элемента блокировки:** Добавляем элемент блокировки для нашего регистра сведений. Важно указывать полное имя метаданного, например, "РегистрСведений.РС".
4. **Установка режима блокировки:** Устанавливаем РежимБлокировкиДанных.Исключительный. Этот режим гарантирует, что пока наш сеанс работает с регистром, другие сеансы не смогут ни читать, ни изменять его данные.
5. **Блокировка:** Вызываем метод Заблокировать() у объекта БлокировкаДанных. Поскольку мы находимся внутри платформенной транзакции, эта блокировка будет действовать до ее завершения.
6. **Получение максимального номера:** Выполняем запрос к регистру, чтобы найти текущий максимальный номер. Используем функцию ЕСТЬNULL, чтобы в случае отсутствия записей получить 0.
7. **Присвоение нового номера:** Увеличиваем полученный номер на 1 и присваиваем его полю Номер текущей записи регистра через ЭтотОбъект[0].Номер (если работаем с набором записей) или ЭтотОбъект.Номер (если это менеджер записи).
8. **Фиксация/откат:** Платформа автоматически зафиксирует транзакцию и снимет блокировку после успешного выполнения процедуры или откатит ее в случае ошибки.
Посмотрим на пример кода для модуля объекта регистра сведений:
// В модуле объекта регистра сведений "РС"
Процедура ПередЗаписью(Отказ, Замещение)
// Проверяем, что номер не заполнен и это не замещение записи,
// чтобы генерировать номер только при создании новой записи или если он пуст.
// Условие ЭтотОбъект.ЗаписьИсторииДанных.КомментарийВерсии = "МенеждерЗаписи"
// из исходного сообщения является специфичным для отладки или конкретной логики,
// в общем случае его можно опустить, если номер генерируется всегда при пустом значении.
Если ЗначениеЗаполнено(ЭтотОбъект) И Не ЗначениеЗаполнено(ЭтотОбъект[0].Номер) Тогда
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.РС"); // Блокируем весь регистр
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать(); // Блокировка устанавливается в рамках текущей транзакции
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ЕСТЬNULL(МАКСИМУМ(РС.Номер), 0) КАК Номер
|ИЗ
| РегистрСведений.РС КАК РС";
НовыйНомер = Запрос.Выполнить().Выгрузить()[0].Номер + 1;
// Присваиваем номер текущей записи в наборе
ЭтотОбъект[0].Номер = НовыйНомер;
// Если регистр непериодический и не имеет дополнительных измерений,
// то набор записей будет содержать только одну запись, которую мы изменяем.
// Если это менеджер записи, то обращение будет через ЭтотОбъект.Номер = НовыйНомер;
// Но в контексте ПередЗаписью для регистра сведений мы чаще работаем с набором записей.
// Комментарий из форума "ЭтотОбъект.Отбор.Номер.Установить(Номер);"
// не нужен здесь, так как мы меняем значение поля записи, а не отбор набора.
КонецЕсли;
КонецПроцедуры
**Важное замечание:** Несмотря на то, что это решение корректно с точки зрения транзакций и блокировок, блокировка всего регистра в исключительным режиме может стать "узким местом" в высоконагруженных системах. Рассмотрим более оптимальные подходы.
Создание отдельного справочника, который будет выступать в роли нумератора, является одним из наиболее рекомендуемых и производительных подходов. Платформа 1С:Предприятие оптимизирована для работы со справочниками и их встроенными механизмами нумерации, что обеспечивает высокую скорость и надежность.
Давайте разберем реализацию этого подхода по шагам:
1. **Создаем справочник-нумератор:**
* Создайте новый объект метаданных типа Справочник, например, с именем ПоследовательностиНомеров или Нумераторы.
* Добавьте в него реквизит, например, ПоследнийНомер, с типом Число.
* Если вам нужна нумерация для разных объектов, можете добавить измерение или реквизит, например, ТипОбъекта (строка или перечисление), чтобы хранить разные последовательности номеров в одном справочнике.
2. **Заполняем справочник:** Создайте в этом справочнике запись, которая будет хранить текущий номер для вашего регистра сведений. Например, запись с наименованием "Номер для РС".
3. **Реализуем логику в модуле объекта регистра:** В процедуре ПередЗаписью модуля объекта вашего регистра сведений мы будем обращаться к этому справочнику:
* Находим нужную запись справочника-нумератора.
* Блокируем эту запись справочника. Это намного "легче", чем блокировать весь регистр.
* Увеличиваем значение реквизита ПоследнийНомер на 1.
* Записываем измененную запись справочника.
* Присваиваем полученный номер записи регистра.
Посмотрим на пример кода:
// В модуле объекта регистра сведений "РС"
Процедура ПередЗаписью(Отказ, Замещение)
Если Не ЗначениеЗаполнено(ЭтотОбъект[0].Номер) Тогда
// 1. Находим нумератор. Предположим, у нас есть одна запись
// в справочнике "ПоследовательностиНомеров" с наименованием "Номер для РС".
Нумератор = Справочники.ПоследовательностиНомеров.НайтиПоНаименованию("Номер для РС");
Если Нумератор = Неопределено Тогда
Отказ = Истина;
Сообщить("Не найден нумератор для РС. Обратитесь к администратору.");
Возврат;
КонецЕсли;
// Получаем объект нумератора для изменения
ОбъектНумератора = Нумератор.ПолучитьОбъект();
// 2. Блокируем объект нумератора.
// Платформа сама установит блокировку на запись элемента справочника
// при вызове метода Записать(). Нам не нужно явно использовать БлокировкаДанных здесь,
// так как Записать() уже делает это в рамках своей транзакции.
// Если же мы хотим явную управляемую блокировку на чтение, то:
// Блокировка = Новый БлокировкаДанных;
// ЭлементБлокировки = Блокировка.Добавить("Справочник.ПоследовательностиНомеров");
// ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// ЭлементБлокировки.УстановитьЗначение("Ссылка", Нумератор);
// Блокировка.Заблокировать();
// 3. Увеличиваем номер
ОбъектНумератора.ПоследнийНомер = ОбъектНумератора.ПоследнийНомер + 1;
Попытка
// 4. Записываем измененный нумератор. Это действие будет заблокировано
// для других сеансов на время записи этого элемента.
ОбъектНумератора.Записать();
Исключение
Отказ = Истина;
Сообщить("Не удалось получить уникальный номер. Попробуйте еще раз.");
Возврат;
КонецПопытки;
// 5. Присваиваем новый номер записи регистра
ЭтотОбъект[0].Номер = ОбъектНумератора.ПоследнийНомер;
КонецЕсли;
КонецПроцедуры
**Преимущества этого подхода:** * **Производительность:** Платформа 1С оптимизирована для работы со справочниками, и их механизмы записи обычно более эффективны, чем ручные блокировки всего регистра. * **Изоляция блокировок:** Блокируется только *одна запись* справочника (нумератора), а не весь регистр сведений. Это значительно повышает параллельность работы пользователей. * **Гибкость:** Вы можете легко создавать разные последовательности номеров для разных типов регистров или даже для разных условий внутри одного регистра.
Еще один вариант, который может быть проще в реализации для очень простых случаев, – это использование отдельной константы для хранения последнего номера.
Разберем реализацию:
1. **Создаем константу:**
* Создайте новый объект метаданных типа Константа, например, с именем МаксимальныйНомерРС, с типом Число.
2. **Реализуем логику в модуле объекта регистра:** В процедуре ПередЗаписью модуля объекта регистра сведений:
* Читаем значение константы.
* Увеличиваем его на 1.
* Устанавливаем новое значение константы.
* Присваиваем полученный номер записи регистра.
Посмотрим на пример кода:
// В модуле объекта регистра сведений "РС"
Процедура ПередЗаписью(Отказ, Замещение)
Если Не ЗначениеЗаполнено(ЭтотОбъект[0].Номер) Тогда
// 1. Получаем текущий максимальный номер из константы
ТекущийМаксимальныйНомер = Константы.МаксимальныйНомерРС.Получить();
// 2. Увеличиваем номер
НовыйНомер = ТекущийМаксимальныйНомер + 1;
Попытка
// 3. Устанавливаем новое значение константы.
// Запись константы также происходит в транзакции и автоматически
// блокирует ее для других сеансов на время записи.
Константы.МаксимальныйНомерРС.Установить(НовыйНомер);
Исключение
Отказ = Истина;
Сообщить("Не удалось получить уникальный номер из константы. Попробуйте еще раз.");
Возврат;
КонецПопытки;
// 4. Присваиваем новый номер записи регистра
ЭтотОбъект[0].Номер = НовыйНомер;
КонецЕсли;
КонецПроцедуры
**Преимущества и недостатки:** * **Преимущества:** Простота реализации. * **Недостатки/Особенности:** * Как и в случае со справочником, платформа сама управляет блокировками при записи константы, обеспечивая уникальность. * Однако, в некоторых версиях платформы константы могут храниться в отдельных таблицах, и блокировка одной константы не обязательно блокирует другие. Но для обеспечения уникальности *этой* константы это работает. * Менее гибкий подход, чем справочник, если требуется несколько независимых последовательностей номеров.
Если вам нужна абсолютная уникальность номера, но *последовательность* не является критичной (то есть, номера могут быть произвольными, не идущими подряд), то вы можете использовать тип УникальныйИдентификатор (GUID).
Разберем реализацию:
1. **Добавляем измерение:** Добавьте в ваш регистр сведений новое измерение, например, ИдентификаторЗаписи, с типом УникальныйИдентификатор.
2. **Реализуем логику в модуле объекта регистра:** В процедуре ПередЗаписью модуля объекта вашего регистра сведений:
* Просто присвойте этому измерению новое уникальное значение с помощью функции Новый УникальныйИдентификатор().
Посмотрим на пример кода:
// В модуле объекта регистра сведений "РС"
Процедура ПередЗаписью(Отказ, Замещение)
// Предполагаем, что ИзмерениеИдентификаторЗаписи - это измерение типа УникальныйИдентификатор
Если Не ЗначениеЗаполнено(ЭтотОбъект[0].ИдентификаторЗаписи) Тогда
ЭтотОбъект[0].ИдентификаторЗаписи = Новый УникальныйИдентификатор();
КонецЕсли;
КонецПроцедуры
**Преимущества и недостатки:** * **Преимущества:** Гарантированная абсолютная уникальность. Не требует никаких ручных блокировок или сложных механизмов, так как генерация GUID не конфликтует между сеансами. * **Недостатки:** Номера не последовательные и не читаемые человеком, что может быть неприемлемо для некоторых бизнес-требований.
Независимо от выбранного подхода, всегда полезно помнить о следующих рекомендациях при работе с блокировками в 1С:Предприятии:
1. **Режим управления блокировкой:** Убедитесь, что для вашей конфигурации или для конкретных объектов метаданных (регистра сведений, справочника, константы) установлен режим управления блокировкой данных "Управляемый" или "Автоматический и управляемый". Это можно проверить в свойствах конфигурации и объектов.
2. **Минимизация длительности транзакций:** Чем короче транзакция, тем меньше время удержания блокировок и выше параллельность работы системы. Старайтесь выполнять только критически важные операции внутри транзакционных блоков.
3. **Точечные блокировки:** Вместо блокировки всего регистра или крупного объекта, всегда старайтесь блокировать только те данные, которые действительно необходимы для операции. Это основная причина, почему нумераторы (справочники, константы) предпочтительнее блокировки всего регистра.
4. **Модуль объекта — лучшее место:** Для логики, которая должна выполняться в рамках единой транзакции и требует надежной работы с блокировками (как в нашем случае генерации уникального номера), всегда используйте серверные обработчики событий объекта (например, ПередЗаписью в модуле объекта), а не обработчики форм.
5. **Видимость данных в транзакции:** Помните, что запрос, выполняемый внутри транзакции, видит только те изменения, которые были сделаны в этой же транзакции и уже "записаны" (хотя еще не зафиксированы в базе) или те данные, которые были зафиксированы ранее.
6. **Уровень изоляции СУБД:** Для повышения параллельности работы в режиме управляемых блокировок рекомендуется использовать режим Read Committed Snapshot Isolation на сервере СУБД, если это поддерживается вашей СУБД (например, Microsoft SQL Server).
Мы рассмотрели различные подходы к решению задачи получения уникального последовательного номера для записи регистра сведений. Наиболее надежным и производительным решением является использование отдельного объекта-нумератора (например, справочника или константы), который позволяет минимизировать область блокировки и повысить параллельность работы. Если же последовательность не важна, а нужна только уникальность, УникальныйИдентификатор будет самым простым и эффективным выбором. Всегда выбирайте решение, которое наилучшим образом соответствует вашим бизнес-требованиям и обеспечивает оптимальную производительность системы.