При работе с регистрами сведений в платформе 1С:Предприятие разработчики часто сталкиваются с неочевидным поведением объектов РегистрСведенийМенеджерЗаписи и РегистрСведенийНаборЗаписей. Ошибки, связанные с чтением, поиском и перезаписью данных, могут приводить как к ошибкам времени выполнения (Runtime Errors), так и к незаметному дублированию или потере информации в базе данных. В этой статье мы подробно разберем, почему метод Прочитать() может возвращать ложь, почему нельзя использовать метод Выбран() у набора записей, и как правильно и быстро обновлять данные в регистрах.
Рассмотрим ситуацию, описанную в первом сообщении темы. Разработчик пытается прочитать запись регистра сведений "Штрихкоды", заполнив только два измерения — Владелец и ЕдиницаИзмерения, после чего метод Прочитать() не находит запись (свойство Выбран() возвращает Ложь), хотя визуально в режиме "1С:Предприятие" или через консоль запросов запись с такими значениями существует.
Давайте проанализируем, как работает объект РегистрСведенийМенеджерЗаписи под капотом платформы. Этот объект предназначен для работы с одной конкретной записью регистра сведений. Ключ этой записи формируется строго на основании значений всех измерений регистра. Если регистр сведений имеет три измерения (например, Владелец, ЕдиницаИзмерения и ХарактеристикаНоменклатуры), а разработчик заполнил только два из них перед вызовом метода Прочитать(), система делает следующее:
Справочники.ХарактеристикиНоменклатуры.ПустаяСсылка()), для чисел — 0, для строк — пустая строка.ХарактеристикаНоменклатуры пустое, СУБД возвращает пустой результат. Платформа фиксирует, что запись не найдена, и метод Выбран() возвращает значение Ложь.Таким образом, чтобы успешно использовать метод Прочитать() у объекта РегистрСведенийМенеджерЗаписи, вам необходимо знать и явно присвоить значения абсолютно всем измерениям, существующим в данном регистре. В противном случае платформа будет искать запись с пустыми значениями в пропущенных измерениях.
В попытках решить проблему разработчики часто переходят на использование объекта РегистрСведенийНаборЗаписей, однако совершают другую грубую ошибку, аналогичную коду из Сообщения 15:
НовыйШК = РегистрыСведений.Штрихкоды.СоздатьНаборЗаписей();
НовыйШК.Отбор.Владелец.Установить(СтрокаДанных.Номенклатура);
НовыйШК.Отбор.ЕдиницаИзмерения.Установить(СтрНайденнаяЕдиница.ЕдиницаВБазе);
НовыйШК.Прочитать();
Если НовыйШК.Выбран Тогда // Ошибка времени выполнения!
// Какая-то логика...
КонецЕсли;
Этот код вызовет ошибку времени выполнения (Runtime Error): "Метод объекта не обнаружен (Выбран)".
Дело в том, что объект РегистрСведенийНаборЗаписей представляет собой коллекцию записей. У него принципиально отсутствует метод или свойство Выбран(). Этот метод есть только у РегистрСведенийМенеджерЗаписи и у некоторых других объектов (например, у выборки справочника или документа).
Для того чтобы проверить, содержит ли прочитанный набор записей какие-либо данные, необходимо использовать проверку количества элементов в коллекции:
Если НовыйШК.Количество() > 0 Тогда
// Записи по заданному отбору существуют в базе данных
КонецЕсли;
Разберем еще одно частое недоразумение, с которым столкнулся автор темы. В Сообщении 15 он жалуется, что при использовании набора записей существующая запись в регистре не замещается, а создается новая. Посмотрим на алгоритм, который привел к этой ошибке:
Прочитать(). Набор заполняется существующей из базы данных записью (допустим, в ней была заполнена пустая характеристика).Добавить(). Платформа добавляет в коллекцию набора вторую строку. Разработчик заполняет эту новую строку значениями, в том числе указывая конкретную характеристику.Записать(Истина) с включенным режимом замещения.В этот момент платформа отрабатывает штатно: она полностью удаляет из базы данных все записи, которые соответствовали установленному отбору (по номенклатуре и единице измерения). А затем записывает в базу все строки, которые находятся в объекте набора записей в оперативной памяти. Поскольку перед этим разработчик прочитал старую запись и добавил новую, в наборе оказалось две строки. Обе эти строки и записываются в базу данных. В результате мы получаем "размножение" записей вместо обновления.
Если ваша задача — изменить существующую запись, у вас есть два основных пути решения проблемы.
Если вам обязательно нужно прочитать существующие записи, проанализировать их и изменить только определенные ресурсы или реквизиты, не меняя ключевые поля (измерения), нужно обходить прочитанный набор в цикле и редактировать существующие строки, а не добавлять новые через метод Добавить(). Рассмотрим пример:
НовыйШК = РегистрыСведений.Штрихкоды.СоздатьНаборЗаписей();
НовыйШК.Отбор.Владелец.Установить(СтрокаДанных.Номенклатура);
НовыйШК.Отбор.ЕдиницаИзмерения.Установить(СтрНайденнаяЕдиница.ЕдиницаВБазе);
НовыйШК.Прочитать();
Если НовыйШК.Количество() > 0 Тогда
// Модифицируем существующие записи
Для Каждого ТекущаяЗапись Из НовыйШК Цикл
ТекущаяЗапись.Штрихкод = СтрНайденнаяЕдиница.Штрихкод;
ТекущаяЗапись.ТипШтрихкода = ОпределитьТипШтрихкода(СтрНайденнаяЕдиница.Штрихкод);
КонецЦикла;
Иначе
// Если записей не было, можем добавить новую строку
НоваяЗапись = НовыйШК.Добавить();
НоваяЗапись.Владелец = СтрокаДанных.Номенклатура;
НоваяЗапись.ЕдиницаИзмерения = СтрНайденнаяЕдиница.ЕдиницаВБазе;
НоваяЗапись.Штрихкод = СтрНайденнаяЕдиница.Штрихкод;
НоваяЗапись.ТипШтрихкода = ОпределитьТипШтрихкода(СтрНайденнаяЕдиница.Штрихкод);
КонецЕсли;
НовыйШК.Записать(); // Метод Записать() по умолчанию работает в режиме замещения (Истина)
Если вам не важно, что было записано в регистре ранее, и вы хотите гарантированно перезаписать данные по определенному отбору новыми значениями, то выполнять метод Прочитать() вообще не нужно! Это сэкономит время работы СУБД и избавит код от лишней нагрузки. Достаточно установить отбор, добавить новую запись и записать набор:
НовыйШК = РегистрыСведений.Штрихкоды.СоздатьНаборЗаписей();
НовыйШК.Отбор.Владелец.Установить(СтрокаДанных.Номенклатура);
НовыйШК.Отбор.ЕдиницаИзмерения.Установить(СтрНайденнаяЕдиница.ЕдиницаВБазе);
// Метод Прочитать() НЕ вызываем! Коллекция набора пуста.
НоваяЗапись = НовыйШК.Добавить();
НоваяЗапись.Владелец = СтрокаДанных.Номенклатура;
НоваяЗапись.ЕдиницаИзмерения = СтрНайденнаяЕдиница.ЕдиницаВБазе;
НоваяЗапись.ХарактеристикаНоменклатуры = СтрокаДанных.ХарактеристикаНоменклатуры;
НоваяЗапись.Штрихкод = СтрНайденнаяЕдиница.Штрихкод;
НоваяЗапись.ТипШтрихкода = ОпределитьТипШтрихкода(СтрНайденнаяЕдиница.Штрихкод);
НоваяЗапись.Качество = Справочники.Качество.Новый;
// При вызове Записать() платформа сотрет в базе все штрихкоды этой номенклатуры
// для данной единицы измерения, и запишет только одну нашу новую строчку.
НовыйШК.Записать(Истина);
Давайте проанализируем архитектурные особенности работы платформы "под капотом", которые напрямую влияют на производительность вашей системы, особенно при пакетной обработке данных (например, загрузке прайс-листов или штрихкодов из файлов Excel). Если вы получаете такие файлы от поставщиков регулярно, для их подготовки к импорту удобно использовать программу автоматического сбора и обработки прайс-листов с электронной почты или FTP.
Объект РегистрСведенийМенеджерЗаписи является высокоуровневой "оберткой". Когда вы вызываете метод Записать() у Менеджера записи, платформа внутри себя выполняет целый ряд неявных ресурсоемких операций:
Если вы выполняете обработку файлов в цикле и на каждой итерации вызываете МенеджерЗаписи.Записать(), база данных будет подвергаться огромной избыточной нагрузке. Каждая итерация — это отдельная транзакция записи, создание временных объектов и обращения к СУБД.
Золотое правило стандартов разработки 1С:ИТС:
Прочитать() у объектов менеджеров или наборов записей исключительно ради "чтения и проверки" является классическим антипаттерном разработки, так как запросы работают в разы быстрее и не блокируют излишне таблицы базы данных.РегистрСведенийНаборЗаписей. Вы можете скомпоновать данные в пакеты или записывать наборы порциями, что значительно увеличит скорость выполнения операции загрузки.РегистрСведенийМенеджерЗаписи рекомендуется использовать только в интерактивных сценариях (например, когда пользователь вручную редактирует одну строку в форме) или когда логика приложения требует гарантированной атомарной работы с одной конкретной записью, ключ которой полностью известен заранее.Еще один важный технический нюанс касается изменения ключевых полей (измерений). Представьте, что вам нужно перенести штрихкод с одной единицы измерения на другую для той же номенклатуры.
Если вы попытаетесь сделать это через МенеджерЗаписи следующим образом:
// Попытка изменить измерение через Менеджер записи
Менеджер = РегистрыСведений.Штрихкоды.СоздатьМенеджерЗаписи();
Менеджер.Владелец = ТекущаяНоменклатура;
Менеджер.ЕдиницаИзмерения = СтараяЕдиница;
Менеджер.Прочитать();
Если Менеджер.Выбран() Тогда
Менеджер.ЕдиницаИзмерения = НоваяЕдиница; // Меняем измерение ключа
Менеджер.Записать();
КонецЕсли;
В результате работы этого кода старая запись с измерением СтараяЕдиница останется в базе данных нетронутой, а в регистре появится новая дублирующая запись с измерением НоваяЕдиница. Так происходит потому, что при изменении поля измерения меняется сам ключ записи, и метод Записать() отрабатывает как создание новой записи.
Чтобы правильно изменить ключевое поле с помощью Менеджера записи, вам придется выполнить транзакционное удаление старой записи и последующее создание новой:
Менеджер = РегистрыСведений.Штрихкоды.СоздатьМенеджерЗаписи();
Менеджер.Владелец = ТекущаяНоменклатура;
Менеджер.ЕдиницаИзмерения = СтараяЕдиница;
Менеджер.Прочитать();
Если Менеджер.Выбран() Тогда
НачатьТранзакцию();
Попытка
// Удаляем старую запись
Менеджер.Удалить();
// Меняем ключевое поле и создаем новую запись
Менеджер.ЕдиницаИзмерения = НоваяЕдиница;
Менеджер.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогОшибки(ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
При использовании объекта РегистрСведенийНаборЗаписей с установленным отбором по старым измерениям эта задача решается гораздо лаконичнее, так как механизм замещения автоматически очистит старые данные по отбору при записи нового набора.