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