Как заполнить документ из нескольких записей регистра сведений в 1С?

Программист 1С v8.3 (Управляемые формы) Сфера услуг, туризм и социальный сектор
← На главную

При разработке механизмов заполнения документов из регистров сведений программисты часто сталкиваются со следующей проблемой: в регистре на одну и ту же ключевую дату (и по одному и тому же аналитическому разрезу, например, пациенту или контрагенту) создается несколько записей. Каждая из этих записей может содержать лишь частично заполненные реквизиты (так называемые «разреженные данные»).

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

В чем причина затирания данных?

Давайте посмотрим на классический код обхода выборки, который часто пишут начинающие разработчики:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	ОбращКИФ.ПосещениеВрач,
|	ОбращКИФ.ПосещениеМедсестра,
|	ОбращКИФ.НаблюдениеВрачаЗаПодросткомДа,
|	ОбращКИФ.НаблюдениеВрачаЗаПодросткомНет
|ИЗ
|	РегистрСведений.ОбращКИФ КАК ОбращКИФ
|ГДЕ
|	ОбращКИФ.Пациент = &Пациент
|	И ОбращКИФ.ДатаОбращения = &ДатаОбращения";

Запрос.УстановитьПараметр("ДатаОбращения", Объект.Обращение.Дата);
Запрос.УстановитьПараметр("Пациент", Объект.Пациент);

РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
	Объект.ПосищениеПервыеТриДняВрач = ВыборкаДетальныеЗаписи.ПосещениеВрач;
	Объект.ПосищениеПервыеТриДняМедсестра = ВыборкаДетальныеЗаписи.ПосещениеМедсестра;
	Объект.НаблюдениеВрачаЗаПодросткомДа = ВыборкаДетальныеЗаписи.НаблюдениеВрачаЗаПодросткомДа;
	Объект.НаблюдениеВрачаЗаПодросткомНет = ВыборкаДетальныеЗаписи.НаблюдениеВрачаЗаПодросткомНет;
КонецЦикла;

Представим ситуацию: в регистре хранятся две записи на одну дату 08.02.2018.

При первом шаге цикла Пока ВыборкаДетальныеЗаписи.Следующий() реквизиту Объект.ПосищениеПервыеТриДняВрач присваивается значение Истина. Однако на втором шаге цикла этот же реквизит перезаписывается значением из второй записи (которое является пустым или Ложь). В результате пользователь видит в документе незаполненный флаг, хотя в регистре информация присутствовала. Нам нужно научить программу «собирать» заполненные значения из всех строк и не затирать их пустыми. Сделать это можно несколькими способами.

Решение №1: Оптимальное (сворачивание данных в запросе через МАКСИМУМ)

Самый быстрый и профессиональный способ решения проблемы — это переложить задачу объединения строк на СУБД с помощью языка запросов 1С. Нам не нужно будет писать сложные условия в циклах на стороне сервера приложений. Мы сгруппируем записи по ключевым полям и применим агрегатную функцию МАКСИМУМ к ресурсам регистра.

Как агрегатная функция МАКСИМУМ ведет себя с разными типами данных?

  1. Для типа Булево (флажки): На уровне СУБД значение ИСТИНА эквивалентно единице (1), а ЛОЖЬ — нулю (0). Функция МАКСИМУМ из значений ИСТИНА и ЛОЖЬ всегда выберет ИСТИНА. Таким образом, если хотя бы в одной строке регистра стоял флажок, результатом группировки станет установленный флаг.
  2. Для строк и ссылок: Пустая ссылка или пустая строка всегда «меньше», чем заполненная строка или заполненная ссылка. Агрегатная функция МАКСИМУМ проигнорирует пустоту и выберет заполненное значение.
  3. Для чисел: Функция вернет наибольшее числовое значение из всех записей на эту дату.

Давайте перепишем наш запрос с использованием группировки:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	МАКСИМУМ(ОбращКИФ.ПосещениеВрач) КАК ПосещениеВрач,
|	МАКСИМУМ(ОбращКИФ.ПосещениеМедсестра) КАК ПосещениеМедсестра,
|	МАКСИМУМ(ОбращКИФ.НаблюдениеВрачаЗаПодросткомДа) КАК НаблюдениеВрачаЗаПодросткомДа,
|	МАКСИМУМ(ОбращКИФ.НаблюдениеВрачаЗаПодросткомНет) КАК НаблюдениеВрачаЗаПодросткомНет
|ИЗ
|	РегистрСведений.ОбращКИФ КАК ОбращКИФ
|ГДЕ
|	ОбращКИФ.Пациент = &Пациент
|	И ОбращКИФ.ДатаОбращения = &ДатаОбращения
|СГРУППИРОВАТЬ ПО
|	ОбращКИФ.Пациент,
|	ОбращКИФ.ДатаОбращения";

Запрос.УстановитьПараметр("ДатаОбращения", Объект.Обращение.Дата);
Запрос.УстановитьПараметр("Пациент", Объект.Пациент);

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

Если Выборка.Следующий() Тогда
	// Используем системную функцию для быстрого копирования значений
	ЗаполнитьЗначенияСвойств(Объект, Выборка);
КонецЕсли;

Преимущества этого способа:

Решение №2: Программное слияние данных в цикле (с обработкой Булево)

Если по каким-то причинам свернуть данные на уровне запроса невозможно (например, вам требуется сложная прикладная логика анализа каждого пустого и заполненного значения), мы можем объединять строки в цикле программно. Однако здесь разработчиков ждет опасная ловушка, связанная с логическим типом Булево.

Многие пытаются использовать стандартную функцию 1С ЗначениеЗаполнено() для всех типов данных подряд:


// Пример кода С ОШИБКОЙ!
Если ЗначениеЗаполнено(ВыборкаДетальныеЗаписи.ПосещениеВрач) Тогда
	Объект.ПосищениеПервыеТриДняВрач = ВыборкаДетальныеЗаписи.ПосещениеВрач;
КонецЕсли;

Внимание! Функция ЗначениеЗаполнено(Ложь) всегда возвращает значение Ложь. Платформа 1С считает ложное булево значение «пустым». Из-за этого условие внутри Если сработает только в том случае, если из регистра пришла Истина. Но если в документе изначально стояло значение Истина, а в следующей строке регистра пришло осознанное значение Ложь, которое нам действительно нужно записать — такое условие проигнорирует его.

Для правильного программного объединения разреженных строк в цикле необходимо разделять логику для Булево и для ссылочных/строковых типов:


Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
	
	// 1. Обработка Булево (флажков):
	// Мы взводим флаг в документе в Истина, только если в регистре встретилась Истина.
	// Если пришла Ложь, мы не затираем уже установленную ранее Истину.
	Если ВыборкаДетальныеЗаписи.ПосещениеВрач Тогда
		Объект.ПосищениеПервыеТриДняВрач = Истина;
	КонецЕсли;
	
	Если ВыборкаДетальныеЗаписи.ПосещениеМедсестра Тогда
		Объект.ПосищениеПервыеТриДняМедсестра = Истина;
	КонецЕсли;
	
	Если ВыборкаДетальныеЗаписи.НаблюдениеВрачаЗаПодросткомДа Тогда
		Объект.НаблюдениеВрачаЗаПодросткомДа = Истина;
	КонецЕсли;

	// 2. Обработка Строк, Чисел и Ссылок:
	// Здесь функция ЗначениеЗаполнено() работает корректно.
	Если ЗначениеЗаполнено(ВыборкаДетальныеЗаписи.Комментарий) Тогда
		Объект.Комментарий = ВыборкаДетальныеЗаписи.Комментарий;
	КонецЕсли;
	
КонецЦикла;

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

Решение №3: Кардинальное (Исправление архитектуры записи в регистр)

Появление нескольких «полупустых» записей на одну и ту же дату по одному пациенту указывает на несовершенство архитектуры базы данных. Наличие множества мелких записей вместо одной полноценной приводит к неоправданному росту размера таблиц базы данных и замедлению работы отчетов.

Правильным подходом является использование паттерна «Прочитать — Изменить — Записать» на этапе записи данных в регистр сведений. Когда любая обработка или документ пытается записать свои данные в регистр:

  1. Программа сначала должна попытаться прочитать уже существующую запись из регистра по этому ключу (Пациент + Дата).
  2. Если запись уже существует, мы берем ее структуру, дополняем ее новыми реквизитами из текущей обработки (не затирая старые!).
  3. Записываем обратно одну единую, полностью заполненную строку.

Для реализации этой схемы в 1С используется объект РегистрСведенийМенеджерЗаписи. Разберем пример кода записи данных:


// Создаем менеджер записи для точечной работы с одной строкой регистра
МенеджерЗаписи = РегистрыСведений.ОбращКИФ.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Пациент = Объект.Пациент;
МенеджерЗаписи.ДатаОбращения = Объект.ДатаОбращения;

// Пробуем прочитать существующие данные из базы данных
МенеджерЗаписи.Прочитать();

// Если запись уже существовала, МенеджерЗаписи заполнится старыми значениями.
// Теперь мы перезаписываем ТОЛЬКО те поля, которые пришли из текущей обработки:
Если ЗначениеЗаполнено(НовоеПосещениеВрач) Тогда
	МенеджерЗаписи.ПосещениеВрач = НовоеПосещениеВрач;
КонецЕсли;

Если ЗначениеЗаполнено(НовоеНаблюдениеВрача) Тогда
	МенеджерЗаписи.НаблюдениеВрачаЗаПодросткомДа = НовоеНаблюдениеВрача;
КонецЕсли;

// Записываем объединенную строку обратно в базу данных
МенеджерЗаписи.Записать();

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

Резюме и рекомендации

Выбор конкретного метода решения зависит от ваших возможностей по изменению конфигурации:

← На главную