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