В процессе разработки в среде 1С программисты часто сталкиваются с классической, но довольно сложной задачей: необходимо получить историю изменений периодического регистра сведений за определенный период в виде непрерывных интервалов. Например, нам нужно вывести историю проживания граждан в квартире с указанием периодов «С... По...» и количества проживающих в каждый из этих периодов.
Простая выборка из физической таблицы регистра сведений или использование стандартного метода СрезПоследних не решают эту задачу корректно. В этой статье мы подробно разберем, почему стандартные подходы не работают, проанализируем архитектурные особенности платформы 1С и реализуем правильное, высокопроизводительное решение с использованием пакетного запроса и постобработки на языке 1С (BSL).
Давайте проанализируем типичные ошибки, которые совершают разработчики при попытке решить эту задачу «в лоб»:
Период МЕЖДУ &ДатаН И &ДатаК, мы потеряем данные по жильцам, которые были заселены до даты начала периода (например, в прошлом году) и продолжают жить в текущем периоде. Запись об их заселении физически осталась за пределами интервала, поэтому простой запрос вернет нулевое или некорректное количество человек на начало периода.СрезПоследних умеет возвращать данные только на одну конкретную точку во времени (параметр Период). Мы не можем передать туда массив дат или заставить ее динамически вычислять срез на каждую дату изменения в запросе без использования тяжелых и неоптимальных соединений.Для решения этой проблемы мы применим методику под названием «Срез последних на каждую дату изменений в интервале». Разберем этот алгоритм по шагам.
Для работы мы будем использовать периодический регистр сведений ркОбъектыРасчета со следующей структурой:
ЛицевойСчет (наша квартира), ОбъектРасчета (физическое лицо).Включен (Булево, признак активности проживания).СтатусОбъектаРасчета (ссылка на справочник со значением "Проживает").Периодичность регистра установлена в значение По позиции регистратора.
Нам потребуется написать пакетный запрос, состоящий из четырех временных таблиц. Рассмотрим подробно логику работы каждого пакета.
1. Получение контрольных дат (ВТ_Даты). Нам нужно собрать все уникальные даты, когда происходили какие-либо движения по нашему лицевому счету в заданном интервале. Также мы обязательно должны принудительно добавить в этот список дату начала нашего периода (&ДатаН), чтобы зафиксировать состояние жильцов на самый первый день отчета.
2. Получение среза последних на каждую контрольную дату (ВТ_МаксДаты). Для каждой контрольной даты из нашей первой таблицы нам нужно найти дату последней (максимальной) записи в регистре для каждого проживающего физлица (ОбъектРасчета), которая меньше или равна этой контрольной дате.
3. Подсчет количества активных жильцов (ВТ_КоличествоПоДатам). Мы соединяем полученные срезы дат с физической таблицей регистра сведений. Это позволяет нам узнать актуальное состояние флага Включен для каждого человека на каждую контрольную дату. Мы суммируем только те записи, где флаг равен Истина.
4. Построение интервалов (стыковка дат). На последнем этапе мы соединяем таблицу контрольных дат саму с собой со смещением. Каждая текущая контрольная дата стыкуется со следующей за ней по хронологии датой. Дата окончания текущего интервала будет равна «следующая дата контроля минус один день». Для самого последнего интервала датой окончания станет дата конца периода (&ДатаК).
Рассмотрим готовый текст запроса, реализующий эту логику:
// 1. Собираем все контрольные даты (точка старта + даты любых изменений по ЛС)
ВЫБРАТЬ
&ДатаН КАК ДатаКонтроля
ПОМЕСТИТЬ ВТ_Даты
ОБЪЕДИНИТЬ
ВЫБРАТЬ РАЗЛИЧНЫЕ
ркОбъектыРасчета.Период
ИЗ
РегистрСведений.ркОбъектыРасчета КАК ркОбъектыРасчета
ГДЕ
ркОбъектыРасчета.ЛицевойСчет = &ЛС
И ркОбъектыРасчета.Период МЕЖДУ &ДатаН И &ДатаК
;
////////////////////////////////////////////////////////////////////////////////
// 2. Для каждой контрольной даты находим дату последней записи по каждому жильцу
ВЫБРАТЬ
ВТ_Даты.ДатаКонтроля КАК ДатаКонтроля,
ркОбъектыРасчета.ОбъектРасчета КАК ОбъектРасчета,
МАКСИМУМ(ркОбъектыРасчета.Период) КАК МаксПериод
ПОМЕСТИТЬ ВТ_МаксДаты
ИЗ
ВТ_Даты КАК ВТ_Даты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ркОбъектыРасчета КАК ркОбъектыРасчета
ПО ркОбъектыРасчета.ЛицевойСчет = &ЛС
И ркОбъектыРасчета.Период <= ВТ_Даты.ДатаКонтроля
СГРУППИРОВАТЬ ПО
ВТ_Даты.ДатаКонтроля,
ркОбъектыРасчета.ОбъектРасчета
;
////////////////////////////////////////////////////////////////////////////////
// 3. Получаем состояние флага "Включен" на каждую дату и считаем количество проживающих
ВЫБРАТЬ
ВТ_МаксДаты.ДатаКонтроля КАК ДатаКонтроля,
СУММА(ВЫБОР КОГДА ркОбъектыРасчета.Включен ТОГДА 1 ИНАЧЕ 0 КОНЕЦ) КАК КоличествоЧеловек
ПОМЕСТИТЬ ВТ_КоличествоПоДатам
ИЗ
ВТ_МаксДаты КАК ВТ_МаксДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ркОбъектыРасчета КАК ркОбъектыРасчета
ПО ркОбъектыРасчета.ЛицевойСчет = &ЛС
И ркОбъектыРасчета.ОбъектРасчета = ВТ_МаксДаты.ОбъектРасчета
И ркОбъектыРасчета.Период = ВТ_МаксДаты.МаксПериод
ГДЕ
ркОбъектыРасчета.СтатусОбъектаРасчета = &СтатусОбъектаРасчета
СГРУППИРОВАТЬ ПО
ВТ_МаксДаты.ДатаКонтроля
;
////////////////////////////////////////////////////////////////////////////////
// 4. Строим интервалы (стыкуем текущую дату изменения со следующей)
ВЫБРАТЬ
Т1.ДатаКонтроля КАК ДатаНачала,
ЕСТЬNULL(ДОБАВИТЬКДАТЕ(МИНИМУМ(Т2.ДатаКонтроля), ДЕНЬ, -1), &ДатаК) КАК ДатаОкончания,
Т1.КоличествоЧеловек КАК КоличествоЧеловек
ИЗ
ВТ_КоличествоПоДатам КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_КоличествоПоДатам КАК Т2
ПО Т2.ДатаКонтроля > Т1.ДатаКонтроля
СГРУППИРОВАТЬ ПО
Т1.ДатаКонтроля,
Т1.КоличествоЧеловек
УПОРЯДОЧИТЬ ПО
ДатаНачала
Запрос возвращает нам детальную историю изменений по датам. Однако на практике часто возникает ситуация «филигранного» изменения состава жильцов: например, 15 мая один человек выписался, и в этот же день прописался другой человек. С точки зрения базы данных произошли изменения, но итоговое количество проживающих не изменилось (как было 2 человека, так и осталось 2).
Чтобы в печатной форме или табличной части документа не плодить дублирующиеся строки с одинаковым количеством проживающих, мы выполним «схлопывание» (свертку) соседних интервалов с одинаковыми значениями в процессе обхода выборки в коде 1С.
Проанализируем алгоритм заполнения табличной части:
// Создаем объект запроса и устанавливаем параметры
Запрос = Новый Запрос;
Запрос.Текст = "..."; // Сюда вставляем текст запроса, разработанный на Шаге 1
Запрос.УстановитьПараметр("ЛС", Объект.ЛицевойСчет);
Запрос.УстановитьПараметр("СтатусОбъектаРасчета", Справочники.ркСтатусыОбъектовРасчета.НайтиПоНаименованию("Проживает"));
Запрос.УстановитьПараметр("ДатаН", Объект.ДатаНачала);
Запрос.УстановитьПараметр("ДатаК", Объект.ДатаОкончания);
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
// Очищаем табличную часть перед заполнением
Объект.ПроживающиеИстория.Очистить();
ТекущееКоличество = -1;
ДатаНачалаИнтервала = Неопределено;
ДатаКонцаИнтервала = Неопределено;
Для Каждого Строка Из ТаблицаРезультат Цикл
Если Строка.КоличествоЧеловек <> ТекущееКоличество Тогда
// Если это не первый проход цикла, то записываем предыдущий накопленный интервал в ТЧ
Если ТекущееКоличество <> -1 Тогда
НоваяСтрока = Объект.ПроживающиеИстория.Добавить();
НоваяСтрока.КоличествоЧеловек = ТекущееКоличество;
НоваяСтрока.ПериодПроживания = СформироватьСтрокуПериода(ДатаНачалаИнтервала, ДатаКонцаИнтервала);
КонецЕсли;
// Запоминаем параметры для старта нового интервала
ТекущееКоличество = Строка.КоличествоЧеловек;
ДатаНачалаИнтервала = Строка.ДатаНачала;
КонецЕсли;
// Сдвигаем границу окончания интервала вперед
ДатаКонцаИнтервала = Строка.ДатаОкончания;
КонецЦикла;
// Не забываем записать самый последний интервал после завершения цикла
Если ТекущееКоличество <> -1 Тогда
НоваяСтрока = Объект.ПроживающиеИстория.Добавить();
НоваяСтрока.КоличествоЧеловек = ТекущееКоличество;
НоваяСтрока.ПериодПроживания = СформироватьСтрокуПериода(ДатаНачалаИнтервала, ДатаКонцаИнтервала);
КонецЕсли;
Для форматирования вывода дат добавим простую и удобную вспомогательную функцию:
&НаКлиентеНаСервереБезКонтекста
Функция СформироватьСтрокуПериода(ДатаНач, ДатаКон)
Возврат Формат(ДатаНач, "ДФ=dd.MM.yyyy") + " - " + Формат(ДатаКон, "ДФ=dd.MM.yyyy");
КонецФункции
Подводя итоги, выделим ключевые преимущества использования данного подхода в реальных проектах на платформе 1С: