Как правильно добавить значения в Таблицу Значений в 1С 7.7 при выполнении условий?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

Приветствуем вас, уважаемые коллеги! Сегодня мы детально разберем классическую задачу из практики разработчика на платформе 1С:Предприятие 7.7. Очень часто в процессе работы возникает необходимость обойти справочник (например, справочник детей сотрудников), проверить соответствие элементов определенным условиям (возраст, степень родства) и перенести подходящие записи во вспомогательную или результирующую Таблицу Значений (ТЗ).

В этой статье мы подробно проанализируем код, предложенный автором на форуме, выявим в нем критические логические ошибки, проблемы с производительностью и предложим несколько вариантов качественного решения этой задачи, включая самый быстрый способ с использованием встроенных запросов.

Анализ исходного кода и выявление узких мест

Давайте внимательно посмотрим на исходный фрагмент кода, который вызвал затруднения у автора темы:


Пока спрдети.ПолучитьЭлемент()=1  Цикл
    возраст = (РабочаяДата() - спрдети.ТекущийЭлемент().ФизЛицо.ДатаРождения)/365;
    если (возраст > 18) и ((Строка( ТЗ.ПолучитьЗначение(стр,2))="Сын")или( Строка(ТЗ.ПолучитьЗначение(стр,2))="Дочь"))  тогда 
    Если (( Строка(ТЗ.ПолучитьЗначение(стр,2))="Сын")или(Строка( ТЗ.ПолучитьЗначение(стр,2))="Дочь")) тогда
    ТЗ2.НоваяСтрока();
    //ТЗ2.ФИО= кл.ФизЛицо;
    //ТЗ2.РодственнаяСвязь=кл.ФизЛицо.Родств;
    
    ТЗ2.ФИО=спрдети.ТекущийЭлемент().ФизЛицо;    
    ТЗ2.РодственнаяСвязь=спрдети.ТекущийЭлемент().Принадлежность;
КонецЕсли;
КонецЕсли;
КонецЦикла

На первый взгляд, логика понятна, но при детальном рассмотрении мы обнаруживаем целый ряд критических проблем:

  1. Неточный расчет возраста (проблема високосных лет). Деление разницы дат в днях на 365 дает серьезную погрешность. За 18 лет жизни человека проходит от 4 до 5 високосных лет (в которых по 366 дней). Из-за этого алгоритм может посчитать ребенка совершеннолетним на несколько дней раньше или, наоборот, позже его реального дня рождения.
  2. Низкая производительность из-за использования метода ТекущийЭлемент(). В цикле обхода справочника (ПолучитьЭлемент() = 1) переменная спрдети уже указывает на текущую запись. Постоянное обращение к методу ТекущийЭлемент() заставляет систему генерировать лишние ссылки в памяти, что сильно замедляет работу кода.
  3. Проблема обращения «через точку». Конструкция вида спрдети.ТекущийЭлемент().ФизЛицо.ДатаРождения заставляет платформу делать множественные последовательные запросы к базе данных. Сначала происходит обращение к справочнику детей, затем к связанному справочнику Физических Лиц. В условиях больших баз данных это приводит к катастрофическому падению скорости работы («проблема N+1 запросов»).
  4. Неправильное сравнение типов (Строка vs Перечисление). Если родственная связь в вашей конфигурации реализована через объект метаданных Перечисление, то попытка преобразовать её в строку через Строка() для сравнения является грубой ошибкой. Строковое представление зависит от синонимов и языковых настроек, что делает такой код крайне ненадежным. Сравнивать значения нужно напрямую с элементами перечисления.
  5. Неопределенный контекст переменных стр и кл. Автор использует переменную стр в методе ТЗ.ПолучитьЗначение(стр, 2), но её происхождение внутри цикла неясно. Если она статична, то на каждой итерации цикла по детям проверяется одна и та же строка исходной таблицы ТЗ, что лишает алгоритм всякого смысла. Закомментированные строки с объектом кл также указывают на потерю контекста данных при переносе логики.
  6. Избыточное дублирование условий. В коде присутствует вложенная конструкция Если, которая полностью дублирует проверку условий внешнего оператора Если. Это усложняет чтение кода и тратит процессорное время.

Давайте разберем по шагам, как исправить эти недочеты и написать профессиональный, быстродействующий код.

Шаг 1. Точный расчет возраста в 1С 7.7

Для того чтобы гарантированно и без погрешностей определить возраст физического лица, мы должны сравнивать отдельно год, месяц и день рождения с текущей (или рабочей) датой. Рассмотрим правильный алгоритм:


ДатРожд = спрдети.ФизЛицо.ДатаРождения;
ТекДата = РабочаяДата();
Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);

// Если в текущем году день рождения еще не наступил, уменьшаем возраст на 1
Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или 
     ((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
    Возраст = Возраст - 1;
КонецЕсли;

Такой подход обеспечивает 100% точность расчетов вне зависимости от количества високосных лет, прошедших с момента рождения ребенка.

Шаг 2. Оптимизация работы со справочником и исключение ТекущийЭлемент()

Выясним причину, почему код автора работает медленно. Метод ТекущийЭлемент() возвращает ссылку на текущий элемент справочника, на котором спозиционирован выбор. Однако при обходе справочника циклом Пока спрдети.ПолучитьЭлемент() = 1 Цикл сам объект спрдети уже ведет себя как ссылка на текущий элемент при чтении его реквизитов. Следовательно, мы можем обращаться к реквизитам напрямую: спрдети.ФизЛицо вместо спрдети.ТекущийЭлемент().ФизЛицо.

Также важно избавиться от двойного разыменования в цикле. Перед началом проверок лучше выгрузить нужные реквизиты в локальные переменные:


ФизЛицоРебенка = спрдети.ФизЛицо;
ДатРожд = ФизЛицоРебенка.ДатаРождения;
Родство = спрдети.Принадлежность; // Если это реквизит справочника детей

Шаг 3. Правильная работа с перечислениями

Проанализируем ситуацию со сравнением родственной связи. Если реквизит Принадлежность (или колонка в ТЗ) хранит значение перечисления, мы должны использовать прямое сравнение:


Если (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь) Тогда
    // Выполняем действия
КонецЕсли;

Это исключает ошибки, связанные с изменением синонимов объектов метаданных или локализацией платформы.

Шаг 4. Оптимальное сопоставление данных через поиск в ТЗ

Чтобы решить проблему с неопределенной переменной стр, нам необходимо найти строку в исходной Таблице Значений ТЗ, которая соответствует текущему ребенку. Для этого воспользуемся эффективным методом НайтиЗначение():


стр = 0;
// Ищем строку в исходной ТЗ, где во второй колонке (или по имени колонки) хранится нужный объект
Если ТЗ.НайтиЗначение(ФизЛицоРебенка, стр, "ФизЛицо") = 1 Тогда
    // Строка найдена, переменная "стр" теперь содержит номер этой строки
    СвязьИзТЗ = ТЗ.ПолучитьЗначение(стр, "РодственнаяСвязь");
КонецЕсли;

Полное готовое решение №1: Оптимизированный цикл

Давайте объединим все исправления в единый логичный и быстродействующий программный код:


спрдети = СоздатьОбъект("Справочник.Дети");
спрдети.ВыбратьЭлементы();

Пока спрдети.ПолучитьЭлемент() = 1 Цикл
    // 1. Получаем ссылки на объекты без использования ТекущийЭлемент()
    ТекущееФизЛицо = спрдети.ФизЛицо;
    ДатРожд = ТекущееФизЛицо.ДатаРождения;
    Родство = спрдети.Принадлежность; // Предположим, это перечисление
    
    // 2. Точный расчет возраста
    ТекДата = РабочаяДата();
    Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);
    Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или 
         ((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
        Возраст = Возраст - 1;
    КонецЕсли;
    
    // 3. Проверяем условие возраста совершеннолетия
    Если Возраст > 18 Тогда
        // 4. Проверяем родственную связь (сравнение с перечислением напрямую)
        Если (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь) Тогда
            
            // 5. Заполняем результирующую Таблицу Значений ТЗ2
            ТЗ2.НоваяСтрока();
            ТЗ2.ФИО = ТекущееФизЛицо;    
            ТЗ2.РодственнаяСвязь = Родство;
            
        КонецЕсли;
    КонецЕсли;
КонецЦикла;

Полное готовое решение №2: Максимальное ускорение через механизм Запросов 1С

Если справочник детей содержит тысячи записей, то даже оптимизированный перебор элементов в цикле может выполняться долго из-за большого количества обращений к базе данных за реквизитами каждого физического лица. В системе 1С 7.7 для решения таких задач наиболее правильным и быстрым подходом является использование объекта Запрос.

Запрос выполнит всю выборку и фильтрацию данных на уровне СУБД (или локального движка БД) за один проход и вернет готовый результат. Рассмотрим пример реализации:


Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = 
"//{{ЗАПРОС(ВыборкаДетей)
|Период с РабочаяДата() по РабочаяДата();
|Ребенок = Справочник.Дети.ТекущийЭлемент;
|ФизЛицо = Справочник.Дети.ФизЛицо;
|ДатаРождения = Справочник.Дети.ФизЛицо.ДатаРождения;
|Родство = Справочник.Дети.Принадлежность;
|Группировка Ребенок;
|Условие (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь);
|"//}}ЗАПРОС
;

Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
    Предупреждение("Ошибка при выполнении запроса!");
    Возврат;
КонецЕсли;

// Обходим результаты запроса и переносим в ТЗ2 только тех, кто старше 18 лет
Пока Запрос.Группировка(1) = 1 Цикл
    ДатРожд = Запрос.ДатаРождения;
    ТекДата = РабочаяДата();
    
    // Точный расчет возраста
    Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);
    Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или 
         ((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
        Возраст = Возраст - 1;
    КонецЕсли;
    
    Если Возраст > 18 Тогда
        ТЗ2.НоваяСтрока();
        ТЗ2.ФИО = Запрос.ФизЛицо;
        ТЗ2.РодственнаяСвязь = Запрос.Родство;
    КонецЕсли;
КонецЦикла;

Используя этот подход, мы сокращаем количество обращений к базе данных до одного-единственного запроса. Скорость выполнения операции при этом возрастает в десятки раз, особенно при работе в клиент-серверном варианте (на SQL-версии 1С 7.7).

← На главную