Как реализовать кэширование данных с составным ключом в 1С 8.3?

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

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

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

Почему сложные объекты не работают как ключи в объекте Соответствие

Посмотрим на частую ошибку: попытку использовать коллекцию Структура или ХранилищеЗначения в качестве ключа для объекта Соответствие.

Проанализируем тестовый пример, демонстрирующий данную проблему:


СоответствиеКэш = Новый Соответствие;
МойУИД = Новый УникальныйИдентификатор;

ХЗ = Новый ХранилищеЗначения(МойУИД);
СтруктураКлюча = Новый Структура;
СтруктураКлюча.Вставить("Номер", 5);
СтруктураКлюча.Вставить("ХЗ", ХЗ);

СоответствиеКэш.Вставить(СтруктураКлюча, "Значение для 5");

// Пытаемся получить значение по аналогичной структуре с теми же данными
ПоисковаяСтруктура = Новый Структура;
ПоисковаяСтруктура.Вставить("Номер", 5);
ПоисковаяСтруктура.Вставить("ХЗ", Новый ХранилищеЗначения(МойУИД));

Значение = СоответствиеКэш.Получить(ПоисковаяСтруктура);
// Результат: Неопределено

Выясним причину такого поведения платформы 1С:

  1. Сравнение по значению в коллекции Соответствие выполняется только для примитивных типов (числа, строки, даты, булево), ссылок базы данных и значений типа УникальныйИдентификатор.
  2. Сравнение по ссылке в памяти применяется для всех сложных и мутабельных объектов (таких как Структура, Массив, ТаблицаЗначений, ХранилищеЗначения, НастройкиКомпоновкиДанных). Даже если две структуры содержат абсолютно идентичные ключи и значения, они являются разными объектами в памяти, поэтому поиск по такому ключу вернет Неопределено.
  3. Модули с повторным использованием возвращаемых значений работают по точно такому же принципу: параметры функций сравниваются по ссылке в памяти для сложных типов. Передача вновь созданного экземпляра структуры или отбора СКД приведет к повторному расчету функции в обход кэша.

Решение 1: Хеширование составного ключа в MD5

Для создания универсального строкового ключа или ключа типа УникальныйИдентификатор преобразуем составные параметры в детерминированную строку или бинарные данные и получим их хеш-сумму.

Разберем функцию формирования хеша на основе объекта ХешированиеДанных:


// Возвращает хэш-сумму значения, сравнение которой может заменить сравнение самих значений.
//
// Параметры:
//  Значение - Произвольный - Любое значение.
//
// Возвращаемое значение:
//  УникальныйИдентификатор - Уникальный идентификатор, соответствующий MD5-хэшу значения.
//
Функция ХэшЗначения(Значение) Экспорт
    
    Хэширование = Новый ХешированиеДанных(ХешФункция.MD5);
    Хэширование.Добавить(ЗначениеВСтрокуВнутр(Значение));
    
    Хэш = НРег(СтрЗаменить(Строка(Хэширование.ХешСумма), " ", ""));
    
    Возврат Новый УникальныйИдентификатор(
        Сред(Хэш, 1, 8) + "-" + 
        Сред(Хэш, 9, 4) + "-" + 
        Сред(Хэш, 13, 4) + "-" + 
        Сред(Хэш, 17, 4) + "-" + 
        Сред(Хэш, 21, 12)
    );
    
КонецФункции

Посмотрим на практическое применение хеширования для кэша:


СоответствиеКэш = Новый Соответствие;

// Формируем ключ для записи в кэш
КлючПараметров = Новый ФиксированныйМассив(Новый Массив);
// Заполняем массив строго в определенном порядке
МассивКлюча = Новый Массив;
МассивКлюча.Добавить(ПоказательСсылка);
МассивКлюча.Добавить(ДатаНачала);
МассивКлюча.Добавить(ДатаОкончания);
КлючФиксированный = Новый ФиксированныйМассив(МассивКлюча);

КлючКэша = ХэшЗначения(КлючФиксированный);
СоответствиеКэш.Вставить(КлючКэша, РассчитанныйОборот);

// При повторном расчете получаем результат по вычисленному хешу
ЗначениеИзКэша = СоответствиеКэш.Получить(КлючКэша);

Важное замечание: сериализация через ЗначениеВСтрокуВнутр() для обычной Структуры или ТаблицыЗначений может зависеть от порядка добавления свойств и колонок. Поэтому для составного ключа рекомендуется использовать ФиксированныйМассив или ФиксированнаяСтруктура.

Решение 2: Формирование канонической строки-ключа

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

Разберем по шагам создание канонического ключа:

  1. Получаем строковый идентификатор ссылки: Строка(ПоказательСсылка.УникальныйИдентификатор()) или уникальный код.
  2. Преобразуем даты в канонический строковый формат фиксированной длины: Формат(Дата, "ДФ=yyyyMMddHHmmss").
  3. Нормализуем отборы СКД: обходим коллекцию КомпоновщикНастроек.Настройки.Отбор.Элементы, отбираем только активные (Применение = Истина) и конкатенируем их значащие свойства (ЛевоеЗначение, ВидСравнения, ПравоеЗначение). Это исключает влияние динамически генерируемых платформой внутренних идентификаторов СКД.
  4. Объединяем параметры в единую строку через уникальный разделитель.

Функция СформироватьКлючКэша(Показатель, ДатаНач, ДатаКон, ХешОтбора) Экспорт

    Разделитель = "|";
    
    СтрокаКлюча = Строка(Показатель.УникальныйИдентификатор()) + Разделитель
        + Формат(ДатаНач, "ДФ=yyyyMMddHHmmss") + Разделитель
        + Формат(ДатаКон, "ДФ=yyyyMMddHHmmss") + Разделитель
        + ХешОтбора;
        
    Возврат СтрокаКлюча;

КонецФункции

Рекомендации по управлению кэшем в памяти

При использовании сеансового кэширования важно учитывать следующие особенности:

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