Приветствуем вас, уважаемые коллеги! Сегодня мы детально разберем классическую задачу из практики разработчика на платформе 1С:Предприятие 7.7. Очень часто в процессе работы возникает необходимость обойти справочник (например, справочник детей сотрудников), проверить соответствие элементов определенным условиям (возраст, степень родства) и перенести подходящие записи во вспомогательную или результирующую Таблицу Значений (ТЗ).
В этой статье мы подробно проанализируем код, предложенный автором на форуме, выявим в нем критические логические ошибки, проблемы с производительностью и предложим несколько вариантов качественного решения этой задачи, включая самый быстрый способ с использованием встроенных запросов.
Давайте внимательно посмотрим на исходный фрагмент кода, который вызвал затруднения у автора темы:
Пока спрдети.ПолучитьЭлемент()=1 Цикл
возраст = (РабочаяДата() - спрдети.ТекущийЭлемент().ФизЛицо.ДатаРождения)/365;
если (возраст > 18) и ((Строка( ТЗ.ПолучитьЗначение(стр,2))="Сын")или( Строка(ТЗ.ПолучитьЗначение(стр,2))="Дочь")) тогда
Если (( Строка(ТЗ.ПолучитьЗначение(стр,2))="Сын")или(Строка( ТЗ.ПолучитьЗначение(стр,2))="Дочь")) тогда
ТЗ2.НоваяСтрока();
//ТЗ2.ФИО= кл.ФизЛицо;
//ТЗ2.РодственнаяСвязь=кл.ФизЛицо.Родств;
ТЗ2.ФИО=спрдети.ТекущийЭлемент().ФизЛицо;
ТЗ2.РодственнаяСвязь=спрдети.ТекущийЭлемент().Принадлежность;
КонецЕсли;
КонецЕсли;
КонецЦикла
На первый взгляд, логика понятна, но при детальном рассмотрении мы обнаруживаем целый ряд критических проблем:
365 дает серьезную погрешность. За 18 лет жизни человека проходит от 4 до 5 високосных лет (в которых по 366 дней). Из-за этого алгоритм может посчитать ребенка совершеннолетним на несколько дней раньше или, наоборот, позже его реального дня рождения.ТекущийЭлемент(). В цикле обхода справочника (ПолучитьЭлемент() = 1) переменная спрдети уже указывает на текущую запись. Постоянное обращение к методу ТекущийЭлемент() заставляет систему генерировать лишние ссылки в памяти, что сильно замедляет работу кода.спрдети.ТекущийЭлемент().ФизЛицо.ДатаРождения заставляет платформу делать множественные последовательные запросы к базе данных. Сначала происходит обращение к справочнику детей, затем к связанному справочнику Физических Лиц. В условиях больших баз данных это приводит к катастрофическому падению скорости работы («проблема N+1 запросов»).Перечисление, то попытка преобразовать её в строку через Строка() для сравнения является грубой ошибкой. Строковое представление зависит от синонимов и языковых настроек, что делает такой код крайне ненадежным. Сравнивать значения нужно напрямую с элементами перечисления.стр и кл. Автор использует переменную стр в методе ТЗ.ПолучитьЗначение(стр, 2), но её происхождение внутри цикла неясно. Если она статична, то на каждой итерации цикла по детям проверяется одна и та же строка исходной таблицы ТЗ, что лишает алгоритм всякого смысла. Закомментированные строки с объектом кл также указывают на потерю контекста данных при переносе логики.Если, которая полностью дублирует проверку условий внешнего оператора Если. Это усложняет чтение кода и тратит процессорное время.Давайте разберем по шагам, как исправить эти недочеты и написать профессиональный, быстродействующий код.
Для того чтобы гарантированно и без погрешностей определить возраст физического лица, мы должны сравнивать отдельно год, месяц и день рождения с текущей (или рабочей) датой. Рассмотрим правильный алгоритм:
ДатРожд = спрдети.ФизЛицо.ДатаРождения;
ТекДата = РабочаяДата();
Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);
// Если в текущем году день рождения еще не наступил, уменьшаем возраст на 1
Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или
((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
Возраст = Возраст - 1;
КонецЕсли;
Такой подход обеспечивает 100% точность расчетов вне зависимости от количества високосных лет, прошедших с момента рождения ребенка.
Выясним причину, почему код автора работает медленно. Метод ТекущийЭлемент() возвращает ссылку на текущий элемент справочника, на котором спозиционирован выбор. Однако при обходе справочника циклом Пока спрдети.ПолучитьЭлемент() = 1 Цикл сам объект спрдети уже ведет себя как ссылка на текущий элемент при чтении его реквизитов. Следовательно, мы можем обращаться к реквизитам напрямую: спрдети.ФизЛицо вместо спрдети.ТекущийЭлемент().ФизЛицо.
Также важно избавиться от двойного разыменования в цикле. Перед началом проверок лучше выгрузить нужные реквизиты в локальные переменные:
ФизЛицоРебенка = спрдети.ФизЛицо;
ДатРожд = ФизЛицоРебенка.ДатаРождения;
Родство = спрдети.Принадлежность; // Если это реквизит справочника детей
Проанализируем ситуацию со сравнением родственной связи. Если реквизит Принадлежность (или колонка в ТЗ) хранит значение перечисления, мы должны использовать прямое сравнение:
Если (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь) Тогда
// Выполняем действия
КонецЕсли;
Это исключает ошибки, связанные с изменением синонимов объектов метаданных или локализацией платформы.
Чтобы решить проблему с неопределенной переменной стр, нам необходимо найти строку в исходной Таблице Значений ТЗ, которая соответствует текущему ребенку. Для этого воспользуемся эффективным методом НайтиЗначение():
стр = 0;
// Ищем строку в исходной ТЗ, где во второй колонке (или по имени колонки) хранится нужный объект
Если ТЗ.НайтиЗначение(ФизЛицоРебенка, стр, "ФизЛицо") = 1 Тогда
// Строка найдена, переменная "стр" теперь содержит номер этой строки
СвязьИзТЗ = ТЗ.ПолучитьЗначение(стр, "РодственнаяСвязь");
КонецЕсли;
Давайте объединим все исправления в единый логичный и быстродействующий программный код:
спрдети = СоздатьОбъект("Справочник.Дети");
спрдети.ВыбратьЭлементы();
Пока спрдети.ПолучитьЭлемент() = 1 Цикл
// 1. Получаем ссылки на объекты без использования ТекущийЭлемент()
ТекущееФизЛицо = спрдети.ФизЛицо;
ДатРожд = ТекущееФизЛицо.ДатаРождения;
Родство = спрдети.Принадлежность; // Предположим, это перечисление
// 2. Точный расчет возраста
ТекДата = РабочаяДата();
Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);
Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или
((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
Возраст = Возраст - 1;
КонецЕсли;
// 3. Проверяем условие возраста совершеннолетия
Если Возраст > 18 Тогда
// 4. Проверяем родственную связь (сравнение с перечислением напрямую)
Если (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь) Тогда
// 5. Заполняем результирующую Таблицу Значений ТЗ2
ТЗ2.НоваяСтрока();
ТЗ2.ФИО = ТекущееФизЛицо;
ТЗ2.РодственнаяСвязь = Родство;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если справочник детей содержит тысячи записей, то даже оптимизированный перебор элементов в цикле может выполняться долго из-за большого количества обращений к базе данных за реквизитами каждого физического лица. В системе 1С 7.7 для решения таких задач наиболее правильным и быстрым подходом является использование объекта Запрос.
Запрос выполнит всю выборку и фильтрацию данных на уровне СУБД (или локального движка БД) за один проход и вернет готовый результат. Рассмотрим пример реализации:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(ВыборкаДетей)
|Период с РабочаяДата() по РабочаяДата();
|Ребенок = Справочник.Дети.ТекущийЭлемент;
|ФизЛицо = Справочник.Дети.ФизЛицо;
|ДатаРождения = Справочник.Дети.ФизЛицо.ДатаРождения;
|Родство = Справочник.Дети.Принадлежность;
|Группировка Ребенок;
|Условие (Родство = Перечисление.РодственныеСвязи.Сын) Или (Родство = Перечисление.РодственныеСвязи.Дочь);
|"//}}ЗАПРОС
;
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Предупреждение("Ошибка при выполнении запроса!");
Возврат;
КонецЕсли;
// Обходим результаты запроса и переносим в ТЗ2 только тех, кто старше 18 лет
Пока Запрос.Группировка(1) = 1 Цикл
ДатРожд = Запрос.ДатаРождения;
ТекДата = РабочаяДата();
// Точный расчет возраста
Возраст = ДатаГод(ТекДата) - ДатаГод(ДатРожд);
Если (ДатаМесяц(ТекДата) < ДатаМесяц(ДатРожд)) Или
((ДатаМесяц(ТекДата) = ДатаМесяц(ДатРожд)) И (ДатаЧисло(ТекДата) < ДатаЧисло(ДатРожд))) Тогда
Возраст = Возраст - 1;
КонецЕсли;
Если Возраст > 18 Тогда
ТЗ2.НоваяСтрока();
ТЗ2.ФИО = Запрос.ФизЛицо;
ТЗ2.РодственнаяСвязь = Запрос.Родство;
КонецЕсли;
КонецЦикла;
Используя этот подход, мы сокращаем количество обращений к базе данных до одного-единственного запроса. Скорость выполнения операции при этом возрастает в десятки раз, особенно при работе в клиент-серверном варианте (на SQL-версии 1С 7.7).