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

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

При проектировании учетных систем в среде «1С:Предприятие 8» (особенно в сферах ЖКХ, аренды коммерческой недвижимости или обслуживания оборудования) разработчики часто сталкиваются с задачей организации связей между объектами разного уровня. Классический пример: у нас есть Договор, к которому привязано несколько Помещений (квартир), а в каждом помещении находится определенный состав Оборудования. При этом состав оборудования со временем меняется, и нам необходимо хранить всю историю этих изменений.

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

Архитектурный тупик: почему нельзя использовать подчиненные справочники или табличные части?

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

  1. Использовать табличную часть в справочнике договоров. Мы можем создать табличную часть «Квартиры» в справочнике ДоговорыКонтрагентов, а внутри нее — еще одну подчиненную таблицу с оборудованием. Однако в 1С нет штатного механизма «вложенных табличных частей». Кроме того, если у нас изменится договор (например, будет заключено дополнительное соглашение или договор перезаключат на нового контрагента), нам придется заново переносить весь список квартир и оборудования, что приведет к дублированию данных.
  2. Использовать подчиненные справочники. Сделать справочник Квартиры подчиненным справочнику ДоговорыКонтрагентов, а справочник Оборудование подчиненным Квартирам. Это архитектурная ошибка. Квартиры (помещения) существуют физически и независимо от того, заключен ли на них договор в данный момент и кто является контрагентом. Если договор закроется, квартира никуда не исчезнет. Привязав квартиру к конкретному договору через отношение подчинения, мы лишим себя возможности переиспользовать этот объект недвижимости в других договорах.

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

Рассмотрим оптимальную структуру метаданных, которую мы будем создавать.

Шаг 1. Проектирование структуры хранения данных (БД)

Для создания гибкой и расширяемой системы нам понадобятся три независимых справочника и один периодический регистр сведений.

1. Справочник «ОбъектыНедвижимости» (или «Квартиры»)
Это самостоятельный справочник. Он не подчинен договорам. В реальных системах его полезно делать иерархическим (например: Здание — Подъезд — Помещение).

2. Справочник «Номенклатура» (или «Оборудование»)
Общий классификатор оборудования, приборов учета, мебели или других материальных ценностей, которые могут быть установлены в квартирах.

3. Справочник «ДоговорыКонтрагентов»
Стандартный справочник платформы, связывающий клиента и наши обязательства.

4. Регистр сведений «СоставОборудованияПомещений»
Это ключевой элемент нашей архитектуры. Именно здесь будет храниться информация о том, какое оборудование, в каком помещении и в рамках какого договора находится в данный момент. Чтобы хранить историю изменений, мы сделаем этот регистр периодическим.

Настроим свойства регистра сведений:

Определим структуру измерений и ресурсов регистра:

Шаг 2. Разработка управляемого интерфейса (две связанные таблицы)

Теперь перейдем к реализации визуальной части. Нам нужно создать форму (например, форму договора или специальное рабочее место), на которой будут расположены две таблицы. В левой таблице отображается список квартир, связанных с договором. В правой — состав оборудования в выбранной слева квартире.

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

Создадим на управляемой форме два реквизита:

  1. СписокКвартир (тип: ДинамическийСписок)
  2. СписокОборудования (тип: ДинамическийСписок)

Настроим запрос для левого списка (СписокКвартир). Нам нужно выбрать уникальные квартиры, которые фигурируют в договоре. Предположим, что список квартир по договору хранится в табличной части самого договора ДоговорыКонтрагентов.Квартиры:


ВЫБРАТЬ
    ТабличнаяЧастьКвартиры.Ссылка КАК Договор,
    ТабличнаяЧастьКвартиры.Квартира КАК Квартира
ИЗ
    Справочник.ДоговорыКонтрагентов.Квартиры КАК ТабличнаяЧастьКвартиры
ГДЕ
    ТабличнаяЧастьКвартиры.Ссылка = &ПараметрСсылкаДоговора

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


ВЫБРАТЬ
    СоставОборудованияСрезПоследних.Оборудование КАК Оборудование,
    СоставОборудованияСрезПоследних.Количество КАК Количество,
    СоставОборудованияСрезПоследних.Регистратор КАК ДокументУстановки,
    СоставОборудованияСрезПоследних.Период КАК ДатаУстановки
ИЗ
    РегистрСведений.СоставОборудованияПомещений.СрезПоследних(&ПериодСреза, Помещение = &ПараметрТекущееПомещение) КАК СоставОборудованияСрезПоследних

Шаг 3. Программирование динамической связи таблиц

Для того чтобы при активизации строки в левой таблице (квартиры) данные в правой таблице (оборудование) обновлялись мгновенно и без зависаний интерфейса, необходимо правильно настроить событие ПриАктивизацииСтроки левой таблицы.

Типичная ошибка начинающих разработчиков — выполнять тяжелые контекстные вызовы сервера при каждом перемещении курсора. Мы реализуем оптимизированную схему работы через изменение параметров динамического списка прямо на клиенте.

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


&НаКлиенте
Перем ПредыдущаяКвартира;

Разберем код обработчика события ПриАктивизацииСтроки для левой таблицы формы:


&НаКлиенте
Процедура СписокКвартирПриАктивизацииСтроки(Элемент)
    
    ТекущиеДанные = Элементы.СписокКвартир.ТекущиеДанные;
    
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Проверяем, изменился ли действительно выбор квартиры
    Если ПредыдущаяКвартира = ТекущиеДанные.Квартира Тогда
        Возврат; 
    КонецЕсли;
    
    ПредыдущаяКвартира = ТекущиеДанные.Квартира;
    
    // Передаем параметр в динамический список оборудования на клиенте без серверного вызова формы
    СписокОборудования.Параметры.УстановитьЗначениеПараметра("ПараметрТекущееПомещение", ТекущиеДанные.Квартира);
    СписокОборудования.Параметры.УстановитьЗначениеПараметра("ПериодСреза", ТекущаяДата());
    
КонецПроцедуры

Использование метода УстановитьЗначениеПараметра на клиенте заставляет платформу самостоятельно и оптимизированно отправить запрос к СУБД в фоновом режиме, обновляя только правую таблицу.

Шаг 4. Оптимизация интерфейса с помощью обработчика ожидания

Если пользователь начнет быстро перемещаться по списку квартир с помощью стрелок на клавиатуре («вверх» / «вниз»), это может породить лавинообразный поток запросов к базе данных, что приведет к кратковременному зависанию интерфейса.

Для предотвращения этой проблемы применим паттерн «дребезг» (debounce) с помощью подключения обработчика ожидания. Суть метода заключается в том, что мы не обновляем правый список мгновенно, а ждем короткую паузу (например, 0.3 секунды). Если за это время пользователь перешел на другую строку, таймер сбрасывается и запускается заново. Обновление произойдет только тогда, когда пользователь остановится на конкретной строке.

Модифицируем код нашей формы:


&НаКлиенте
Процедура СписокКвартирПриАктивизацииСтроки(Элемент)
    
    // Сбрасываем предыдущий таймер ожидания, если он был запущен
    ОтключитьОбработчикОжидания("Подключаемый_ОбновитьСписокОборудования");
    
    // Запускаем новый таймер на 0.3 секунды
    ПодключитьОбработчикОжидания("Подключаемый_ОбновитьСписокОборудования", 0.3, Истина);
    
КонецПроцедуры

&НаКлиенте
Процедура Подключаемый_ОбновитьСписокОборудования()
    
    ТекущиеДанные = Элементы.СписокКвартир.ТекущиеДанные;
    
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    Если ПредыдущаяКвартира = ТекущиеДанные.Квартира Тогда
        Возврат;
    КонецЕсли;
    
    ПредыдущаяКвартира = ТекущиеДанные.Квартира;
    
    СписокОборудования.Параметры.УстановитьЗначениеПараметра("ПараметрТекущееПомещение", ТекущиеДанные.Квартира);
    СписокОборудования.Параметры.УстановитьЗначениеПараметра("ПериодСреза", ТекущаяДата());
    
КонецПроцедуры

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

Шаг 5. Реализация вывода истории изменений состава оборудования

По условию задачи нам обязательно нужно хранить и отображать историю изменений состава оборудования. Для этого добавим на форму флаг (тумблер) ПоказатьИсториюИзменений (тип: Булево).

Если флаг выключен — мы показываем только то оборудование, которое актуально на текущий момент (работает запрос к таблице СрезПоследних). Если флаг включен — мы должны показать пользователю полную хронологическую ленту событий: когда оборудование поступило, когда было заменено, списано или перемещено в другое помещение.

Для реализации этой возможности мы будем динамически изменять текст запроса правого динамического списка при изменении положения флага.

Разберем код обработчика изменения флага на форме:


&НаСервере
Процедура ПереключитьОтображениеИсторииНаСервере()
    
    Если ПоказатьИсториюИзменений Тогда
        
        // Переключаем запрос динамического списка на физическую таблицу регистра сведений для вывода всей истории
        СписокОборудования.ТекстЗапроса = 
            "ВЫБРАТЬ
            |	СоставОборудования.Период КАК Период,
            |	СоставОборудования.Оборудование КАК Оборудование,
            |	СоставОборудования.Количество КАК Количество,
            |	СоставОборудования.Регистратор КАК ДокументУстановки,
            |	СоставОборудования.Состояние КАК Состояние
            |ИЗ
            |	РегистрСведений.СоставОборудованияПомещений КАК СоставОборудования
            |ГДЕ
            |	СоставОборудования.Помещение = &ПараметрТекущееПомещение
            |УПОРЯДОЧИТЬ ПО
            |	Период УБЫВ";
            
        // Делаем колонку "Период" и "Состояние" видимыми на форме
        Элементы.СписокОборудованияПериод.Видимость = Истина;
        Элементы.СписокОборудованияСостояние.Видимость = Истина;
        
    Иначе
        
        // Возвращаем стандартный запрос среза последних
        СписокОборудования.ТекстЗапроса = 
            "ВЫБРАТЬ
            |	СоставОборудованияСрезПоследних.Оборудование КАК Оборудование,
            |	СоставОборудованияСрезПоследних.Количество КАК Количество,
            |	СоставОборудованияСрезПоследних.Регистратор КАК ДокументУстановки,
            |	СоставОборудованияСрезПоследних.Период КАК ДатаУстановки,
            |	СоставОборудованияСрезПоследних.Состояние КАК Состояние
            |ИЗ
            |	РегистрСведений.СоставОборудованияПомещений.СрезПоследних(&ПериодСреза, Помещение = &ПараметрТекущееПомещение) КАК СоставОборудованияСрезПоследних";
            
        // Скрываем колонку периода, так как она дублирует дату установки
        Элементы.СписокОборудованияПериод.Видимость = Ложь;
        
    КонецЕсли;

КонецПроцедуры

&НаКлиенте
Процедура ПоказатьИсториюИзмененийПриИзменении(Элемент)
    
    // Вызываем серверный метод для изменения структуры запроса динамического списка
    ПереключитьОтображениеИсторииНаСервере();
    
    // Принудительно обновляем данные списка на клиенте
    Элементы.СписокОборудования.Обновить();
    
КонецПроцедуры

Таким образом, пользователь получает мощный и интуитивно понятный инструмент управления оборудованием. В обычном режиме он видит только то, что сейчас физически стоит в квартире. Но стоит ему нажать одну кнопку — и перед ним разворачивается детальный лог всей истории обслуживания объекта.

Заключение

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

← На главную