Перейти к содержанию

Справочники нормализации

Справочник нормализации - это источник строковых значений, доступных по ключам-идентификаторам.

Использование справочников

Справочники нормализации могут использоваться для получения (в зависимости от регистрации справочника):

  • названий карточек: роли, категории документов, валюты, контрагенты и др.;

  • названий в таблицах-перечислениях: состояния документов и др.;

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

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

Нормализация может применяться для заполнения названий в ссылках на справочник:

Important

Поскольку строковое значение колонки, заполняемое с использованием нормализации, не хранится в БД, то невозможна оптимальная сортировка по такой колонке (с использованием индекса в БД) в представлении.

Например, пусть строка с именем автора документа не хранится в таблице со ссылкой на автора в БД. Тогда в представлении с договорами, которое выводит колонку с автором договора, нельзя организовать оптимальную сортировку по этой колонке.

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

Кэширование справочников

Кэширование справочников существенно оптимизирует получение названий по ключам. Оно возможно в следующих конфигурациях (в зависимости от регистрации справочника):

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

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

  3. Комбинированный кэш: использует кэш в памяти совместно с кэшом в Redis.

    • Если значение присутствует в памяти по запрошенному ключу, то оно мгновенно возвращается.

    • Иначе значение запрашивается из Redis, и при его наличии - добавляется в кэш в памяти и возвращается.

    • Если значение также отсутствует в Redis, то оно запрашивается из БД (или другого источника данных), записывается и в кэш Redis, и в кэш в памяти, и возвращается.

Tip

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

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

Также при регистрации справочника возможно указать настройки:

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

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

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

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

    Note

    Для большинства справочников в типовом решении определяется время жизни кэша Redis в 24 часа и время жизни кэша в памяти в 12 часов.

    Эти значения могут быть переопределены изменением объекта DefaultNormalizationOptions для отражения специфики вашего проектного решения.

Справочники в типовом решении

В системе присутствует набор справочников нормализации, все из которых описаны в таблице NormalizationSources и зарегистрированы в файле расширений типового решения Tessa.Extensions.Default.Server/Normalization/Registrator.cs (открыв который, можно увидеть различные способы регистраций справочников с поясняющими комментариями).

Tip

Написав свой класс Registrator, выполняемый позже регистратора из типового решения - с указанием атрибута [Registrator(Order = 1)], - возможно изменить регистрацию любого из справочников вызовом instanceRegistry.Register, указав другие настройки кэширования и способ получения значений (из БД, представления, метаинформации и др.).

Часть справочников являются платформенными, т.е. они используются компонентами платформы, не входящими в типовое решение. Регистрация такого справочника выполняется в том же файле Registrator.cs из проекта расширений типового решения Tessa.Extensions.Default.Server.

Note

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

Далее указаны все определённые по умолчанию справочники нормализации с указанием колонок:

  1. Алиас - алиас (уникальное имя) справочника.

  2. Константа - константа с идентификатором справочника Guid, которая может использоваться в коде регистрации или при запросе значений в коде посредством API.

  3. Описание - краткое описание справочника.

  4. Платформенный - признак того, что справочник является платформенными (см. выше).

Алиас Константа Описание Платформенный
Currencies DefaultNormalizationSources.Currencies карточки “Валюта” нет
DocumentCategories DefaultNormalizationSources.DocumentCategories карточки “Категория документа” нет
KrDocStates DefaultNormalizationSources.KrDocStates перечисление “Состояние документа” нет
KrTypes DefaultNormalizationSources.KrTypes типы документов и все типы карточек, файлов, заданий и диалогов нет
Partners DefaultNormalizationSources.Partners карточки “Контрагент” нет
Roles PlatformNormalizationSources.Roles карточки ролей, кроме ролей заданий и вложенных ролей да
TaskKinds DefaultNormalizationSources.TaskKinds карточки “Вид задания” нет
Types PlatformNormalizationSources.Types типы карточек, файлов, заданий и диалогов да
Users PlatformNormalizationSources.Users сотрудники (получаемые имена могут отличаться от справочника Roles) да

Создание справочника нормализации

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

Переименование элемента справочника мгновенно актуализирует ссылки на него, поскольку имя не будет записываться в ссылке.

В демонстрационных объектах AbTest содержится тип карточки AbResolution Резолюция (тест). Далее будет показано:

  • как добавить справочник для таких карточек-резолюций;
  • как добавить ссылку Связанный документ на элемент этого справочника внутри той же карточки Резолюция (тест) (т.е. одна резолюция может ссылаться на другую);
  • как использовать справочник нормализации в представлении AbResolution Резолюции для вывода имени резолюции в колонке и в сабсете.

Справочник в схеме данных

В первую очередь необходимо описать справочник нормализации, добавив запись в служебную таблицу NormalizationSources в схеме данных.

Откройте вкладку Схема в TessaAdmin, найдите таблицу NormalizationSources (в группе System), на узле “Записи” нажмите кнопку “Добавить строку” и введите следующие значения:

Колонка Значение Описание колонки
ID 6257dc0d-8928-4969-81c0-2773ed977a63 уникальный идентификатор справочника
Name AbResolutions алиас (уникальное имя) справочника
Description AbResolution cards произвольное описание

Сохраните схему данных.

Справочник в серверных расширениях .NET

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

Ниже сначала описано, какой код необходимо добавить, а далее расположены пояснения, что этот код определяет.

  1. Откройте проект Tessa.Extensions.Server в вашей IDE.

  2. Создайте папку AbTest, в которой размещаются файлы кода, связанные с демонстрационными объектами AbTest.

    Tip

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

  3. Создайте файл AbNormalizationSources.cs, содержащий константу с идентификатором справочника, который выше был добавлен в схему данных, причём имя константы соответствует алиасу справочника AbResolutions из схемы.

using System;

namespace Tessa.Extensions.Server.AbTest { /// <summary> /// Справочники нормализации в решении <c>AbTest</c>. /// </summary> public static class AbNormalizationSources { /// <summary> /// Резолюции. /// </summary> public static readonly Guid AbResolutions = // 6257DC0D-8928-4969-81C0-2773ED977A63 new(0x6257dc0d, 0x8928, 0x4969, 0x81, 0xc0, 0x27, 0x73, 0xed, 0x97, 0x7a, 0x63); } }

  1. Создайте файл Registrator.cs, в котором располагается код с регистрацией справочника в служебных объектах.

using Tessa.Cards.Normalization; using Tessa.Extensions.Default.Server.Normalization; using Tessa.Extensions.Default.Shared.AbTest; using Tessa.Normalization; using Tessa.Platform; using Sources = Tessa.Extensions.Server.AbTest.AbNormalizationSources;

namespace Tessa.Extensions.Server.AbTest { [Registrator] public sealed class Registrator : RegistratorBase { public override void FinalizeRegistration() { if (this.UnityContainer.TryResolve<INormalizationDescriptorRegistry>() is not { } descriptorRegistry || this.UnityContainer.TryResolve<INormalizationInstanceRegistry>() is not { } instanceRegistry || this.UnityContainer.TryResolve<INormalizationInvalidatorRegistry>() is not { } invalidatorRegistry) { return; }

var options = this.UnityContainer.TryResolve<DefaultNormalizationOptions>(); var redisExpiry = options?.RedisExpiry; var inMemoryExpiry = options?.InMemoryExpiry;

// AbResolutions descriptorRegistry.Register(new() { ID = Sources.AbResolutions, Name = nameof(Sources.AbResolutions), KeyType = NormalizationKeyType.Guid }); instanceRegistry.Register(Sources.AbResolutions, (factory, descriptor) => factory.Create(new InMemoryNormalizationOptions(descriptor) { Source = factory.Create(new RedisNormalizationOptions(descriptor) { Source = factory.Create(new DatabaseNormalizationOptions(descriptor) { TableName = "AbResolutions", KeyColumnName = "ID", ValueColumnName = "Name", PartialOnly = true }), Expiry = redisExpiry }), Expiry = inMemoryExpiry })); invalidatorRegistry.Register( new GlobalSourceNormalizationInvalidator([Sources.AbResolutions]), new CardNormalizationTrigger([AbCardTypes.AbResolutionTypeID]) { ValueSectionName = "AbResolutions", ValueFieldName = "Name" }); } } }


Далее рассмотрим содержимое этого файла:

  1. В объекте INormalizationDescriptorRegistry определяется дескриптор, содержащий идентификатор ID, алиас Name и тип ключа KeyType справочника. Идентификатор и алиас совпадают с указанными значениями в схеме данных, а типом ключа является Guid - уникальный идентификатор.

    Note

    Каждый справочник нормализации соотносит ключ определённого типа со строковым значением (именем элемента справочника). Тип ключа определяет, по какому уникальному значению элементы справочника различаются друг от друга. Строковые значения неуникальны, они используются для вывода названия элемента в пользовательском интерфейсе.

    Когда справочник состоит из карточек, то идентификатором элемента справочника будет идентификатор карточки. В примере - из карточек AbResolution, или для справочника нормализации сотрудников Users - из карточек PersonalRole. В этом случае укажите тип Guid.

    Также справочник может иметь целочисленный тип ключа Integer (подходит для любых целочисленных значений, т.е. Int16, Int32, Int64 и др.) - это удобно для справочников, построенных по таблице-перечислении с целочисленным ключом (например, справочник KrDocStates - по таблице KrDocState). И есть строковый тип ключа String (сравнение ключей выполняется с учётом регистра) - используйте, если ключом является уникальный код культуры, валюты и др.

  2. В объекте INormalizationInstanceRegistry задаётся, как справочник получает и кэширует значения.

    • Значения справочника AbResolutions запрашиваются из базы данных, что настраивается в объекте DatabaseNormalizationOptions: TableName - имя таблицы, KeyColumnName - имя колонки с идентификатором, ValueColumnName - имя строковой колонки со значением. PartialOnly - опциональный признак того, что для справочника не могут быть запрошены все доступные значения при вызове через API; здесь предполагается, что справочник может содержать сотни тысяч значений по карточкам, поэтому при запросе всех значений будет существенная нагрузка на СУБД.

    • Справочник имеет первый уровень кэширования в Redis: если запрошенное из справочника значение есть в Redis, то возвращается оно, иначе значение запрашивается из БД, после чего записывается в Redis. Настройки определяются объектом RedisNormalizationOptions, где Source указывает источник значений (объект для запроса из БД), а Expiry - опциональное время жизни кэша в Redis, по истечению которого очищаются все заполненные там значения. Если свойство Expiry не указано или равно null, то кэшированные в Redis значения не очищаются по истечению времени (но по-прежнему могут изменяться при инвалидации, см. ниже).

      Tip

      Рекомендуется указывать периодическую очистку для справочников, которые могут неограниченно расширяться по мере работы системы, чтобы кэш содержал только актуальные значения, запрашиваемые в последнее время в течение срока из свойства Expiry. Здесь пользователи могут создавать всё новые и новые карточки AbResolution, поэтому для справочника AbResolutions указано свойство Expiry.

    • Справочник имеет второй уровень кэширования в памяти процесса, настройки которого определяются объектом InMemoryNormalizationOptions, где Source указывает источник значений (объект для запроса из Redis), а Expiry - опциональное время жизни кэша в памяти (поведение аналогично одноимённому свойству для Redis). Двухуровневый кэш работает следующим образом: если запрошенное значение есть в памяти, то возвращается оно; иначе оно запрашивается из Redis (т.е. либо сразу возвращается из Redis, либо запрашивается из БД, записывается в Redis и возвращается), после чего значение запоминается в памяти.

      Note

      Кэш является потокобезопасным, и он корректно работает (без race condition) как при использовании из разных потоков в пределах одного процесса, так и при использовании из разных процессов (рабочих процессов веб-сервисов, плагинов Chronos и других видов сервисов).

      Tip

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

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

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

    Important

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

    • Первым параметром передаётся объект-инвалидатор, который задаёт действие при выполнении триггеров (см. ниже). Здесь указан объект GlobalSourceNormalizationInvalidator, который оповещает указанные справочники нормализации (в примере - единственный справочник Sources.AbResolutions), что их элементы с определёнными идентификаторами надо удалить (или заменить актуальным значением), причём справочники имеют кэши в памяти каждого процесса (отсюда префикс Global в имени объекта).

      Tip

      Если для справочника определён кэш InMemoryNormalizationOptions, то всегда используйте объект-инвалидатор GlobalSourceNormalizationInvalidator, чтобы кэш в памяти каждого процесса был актуализирован. Для двухуровневого кэша при наступлении события инвалидации кэш в Redis актуализируется в том же процессе, в котором наступило событие, а кэш в памяти актуализируется в каждом процессе (все процессы синхронизируются через подписку на события в Redis).

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

    • Вторым и последующими параметрами передаются объекты-триггеры, связанные с объектом-инвалидатором в первом параметре. Они обозначают событие инвалидации и новое актуальное значение для элемента справочника, если оно доступно. Здесь передаётся объект CardNormalizationTrigger, который определяет, что при изменении строкового поля Name (свойство ValueFieldName) в строковой секции AbResolutions (свойство ValueSectionName) для карточки-резолюции с типом AbCardTypes.AbResolutionTypeID наступит событие инвалидации, причём актуальное значение из этого поля будет передано во все подписанные кэши (в соответствии с объектом-инвалидатором).

      Note

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

      Также при удалении карточки значение будет удалено из кэшей.

  4. Объект DefaultNormalizationOptions содержит рекомендуемые настройки по умолчанию для времени жизни Expiry кэшей в Redis (свойство RedisExpiry) и кэшей в памяти (свойство InMemoryExpiry). Эти настройки применяются для большинства кэшей, доступных в типовом решении, и могут быть переопределены изменением объекта DefaultNormalizationOptions для отражения специфики вашего проектного решения.

    • В этом примере для добавляемого справочника указаны настройки свойств Expiry в соответствии с объектом DefaultNormalizationOptions, но в проектном решении для каждого справочника возможно указать любые настройки, которые соответствуют вашим потребностям.

Important

Все справочники нормализации, используемые в типовом решении, определены в файле Tessa.Extensions.Default.Server/Normalization/Registrator.cs. Обратитесь к этому файлу за дополнительными примерами по задействованию API для конфигурации справочников. Файл содержит комментарии, а также во всплывающих подсказках на именах объектов и свойств будет их описание.

Обратите внимание на свойство PrefetchAll, позволяющее кэшировать справочник целиком, что рекомендуется для редко изменяемых небольших справочников ограниченного размера (менее 10 тысяч элементов). Например, это используется в справочнике типов карточек и документов KrTypes, и в справочнике состояний документов KrDocStates.


После изменения проекта Tessa.Extensions.Server необходимо выполнить его компиляцию, и положить собранный файл Tessa.Extensions.Server.dll в папки веб-сервисов, сервисов Chronos и других сервисов, в которых этот файл присутствовал.

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

Ссылка на справочник в типе карточки

Здесь будет показано, как добавить колонку LinkDoc со ссылкой на справочник нормализации AbResolutions, и затем как добавить контрол “Ссылка” в тип карточки.

Откройте вкладку Схема в TessaAdmin, найдите таблицу AbResolutions (в группе AbTest), в контекстном меню на узле “Колонки” нажмите кнопку “Добавить комплексную колонку” и укажите следующие значения:

Свойство Значение
Название LinkDoc
Библиотека AbTest
Тип Reference(Typified) Null
Ссылка на таблицу AbResolutions
Справочник нормализации AbResolutions

Для выбранного справочника нормализации в скобках выводится “добавьте строковую колонку-значение”. Отметьте флагом ссылочную колонку Name.

Когда справочник нормализации корректно установлен, то в скобках выводятся имена колонок LinkDocID => LinkDocName, где:

  • LinkDocID - ключевая колонка, по значению которой выполняется поиск строки в справочнике нормализации;
  • LinkDocName - строковая колонка-значение, найденная по ключу, которая не хранится в БД и подставляется из справочника.

Note

Ключевой колонкой выбирается колонка во внешнем ключе (или которая была бы во внешнем ключе, если флаг “С внешним ключом” снят), причём тип колонки должен быть Guid, числовой тип (Int16, Int32, Int64 и др.) или строковый тип (String, AnsiString и др.). Если тип колонки не соответствует типу ключа в справочнике, то будет ошибка при использовании колонки (но не при сохранении схемы).

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

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

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


Откройте вкладку Карточки.

Для использования в карточке достаточно добавить комплексную колонку в секции типа.

Tip

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

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

  1. Выберите тип карточки AbResolution (в группе AbTest).
  2. В узле “Секции” отметьте флагом колонку LinkDoc из таблицы AbResolutions.
  3. В узле “Вкладки” добавьте контрол типа “Ссылка” с указанными свойствами.
Свойство Значение
Заголовок Связанный документ
Поля карточки AbResolutions: LinkDoc
Алиас представления AbResolutions
Алиас параметра Name
Скрывать кнопку выбора +

Tip

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

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

Сохраните тип.

Note

Нажав “Предпросмотр”, а затем кнопку “Структура карточки”, можно заметить, что в секции AbResolutions присутствуют колонки LinkDocID и LinkDocName, и они заполняются при выборе значения в ссылочном поле Связанный документ так же, как если бы справочник нормализации не был указан.

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


Теперь откройте web-клиент (обновите страницу браузера, если он уже был открыт), выберите узел Тестирование → Резолюции, а затем создайте или откройте карточку из этого представления.

Здесь видно, что поле Связанный документ - обыкновенное ссылочное поле с автодополнением, в котором можно указать ссылку на любую карточку-резолюцию.

Note

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

Однако, если переименовать карточку-резолюцию, на которую ведёт ссылка (изменить поле Название резолюции), то во всех ссылающихся на неё карточках в полях Связанный документ будут актуальные значения (после обновления этих карточек, если они были открыты).

Также в БД можно убедиться, что колонка LinkDocName отсутствует в таблице AbResolutions.

Нормализация в представлении

Поскольку колонка LinkDocName отсутствует в БД, то для вывода её в качестве колонки представления и для использования сабсета с этой колонкой также следует использовать справочник нормализации.

Откройте вкладку Представления и выберите представление AbResolutions в группе AbTest.

Добавьте колонку LinkDocID перед колонкой rn с настройками:

Свойство Значение
Alias LinkDocID
Type $AbResolutions.LinkDocID
Hidden +

Под колонкой LinkDocID добавьте колонку LinkDocName с настройками:

Свойство Значение
Alias LinkDocName
Caption Связанный документ
Type $AbResolutions.LinkDocName

Эта колонка не будет возвращаться SQL-запросом представления, вместо этого она должна заполняться из справочника нормализации. В блоке NormalizationColumnInfo укажите свойства:

Свойство Значение Описание свойства
NormalizationSource AbResolutions алиас (уникальное имя) справочника нормализации
NormalizationKeyColumn LinkDocID алиас колонки представления, значение которой используется в качестве ключа для поиска в справочнике нормализации

Note

Тип колонки, указанный в свойстве NormalizationKeyColumn, должен быть допустимым для типа ключа в выбранном справочнике нормализации.

Справочник AbResolutions имеет тип ключа Guid, поэтому тип колонки представления LinkDocID также должен иметь тип Guid (который он получает из типа колонки схемы $AbResolutions.LinkDocID).


Теперь добавьте параметр фильтрации для поиска по LinkDocID. Это будет ссылочный параметр, его настройки не зависят от задействования нормализации. Укажите свойства параметра:

Свойство Значение
Alias LinkDoc
Caption Связанный документ
Type $AbResolutions.LinkDocID
RefSection AbResolutions
HideAutoCompleteButton +
AutoCompleteInfo: Param Name
AutoCompleteInfo: View AbResolutions

Затем добавьте сабсет для группировки по связанным документам. Укажите настройки свойств:

Свойство Значение
Alias ByLinkDoc
Caption По связанному документу
CaptionColumn LinkDocName
CountColumn cnt
RefColumn LinkDocID
RefParam LinkDoc
SortingColumns Caption ASC
NormalizationSource AbResolutions

Колонка LinkDocName, указанная в свойстве CaptionColumn, не будет возвращаться в SQL запросе, а будет заполняться из справочника нормализации AbResolutions, который установлен в свойстве NormalizationSource.

Значения сабсета должны быть отсортированы по именам в колонке LinkDocName, такая сортировка не может быть выполнена в БД (без усложнения и замедления запроса). Поэтому в свойстве SortingColumns указана программная сортировка Caption ASC, выполняемая по значению колонки из свойства CaptionColumn после применения нормализации.

Tip

Также можно указать сортировку: по LocalizedCaption - локализованное значение колонки CaptionColumn после применения нормализации; по Count - количеству строк из значения колонки CountColumn; по RefColumn - идентификатору из значения колонки RefColumn.

Возможно указание нескольких колонок: например, сначала сортировка по Count по убыванию, а затем по Caption по возрастанию (для тех строк, у которых значение в CountColumn совпало).

Сортировка в свойстве SortingColumns применима и без использования нормализации.

Tip

Если сабсет вернул значение LinkDocID, равное NULL, то пользователю оно выводится как строка (пусто).

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


Далее необходимо изменить SQL-запрос представления, добавив в него:

  1. новую колонку "t"."LinkDocID" в выражении SELECT для режима выполнения без сабсета (внутри #if(Normal));
  2. колонки "t"."LinkDocID" и count(*) AS "cnt" в выражении SELECT для режима выполнения в сабсете ByLinkDoc (внутри #if(ByLinkDoc));
  3. фильтрацию по параметру LinkDoc посредством выражения #param(LinkDoc, "t"."LinkDocID");
  4. группировку по колонке "t"."LinkDocID" для режима выполнения в сабсете ByLinkDoc (внутри #if(ByLinkDoc)).

Ниже представлен код SQL-запроса для СУБД PostgreSQL с указанием номера пункта, к которому относится изменение.

Note

Для СУБД MS SQL Server изменения будут аналогичны, здесь они не приводятся.

SELECT #if(Normal) { "t"."ID" AS "DocID", "t"."Name" AS "DocName", "t"."CategoryID", "t"."StateID", "t"."DepartmentID", "t"."AuthorID", "t"."PartnerID", "t"."RefTypeID", "t"."TaskKindID", "t"."LinkDocID", -- (1) 0::int8 AS "rn" } #if(ByCategory) { "t"."CategoryID", count(*) AS "cnt" } #if(ByState) { "t"."StateID", count(*) AS "cnt" } #if(ByDepartment) { "t"."DepartmentID", count(*) AS "cnt" } #if(ByAuthor) { "t"."AuthorID" } #if(ByLinkDoc) { -- (2) "t"."LinkDocID", count(*) AS "cnt" } #if(Count) { count(*) AS "cnt" } FROM "AbResolutions" AS "t" WHERE true #param(Name, "t"."Name") #param(Category, "t"."CategoryID") #param(State, "t"."StateID") #param(Department, "t"."DepartmentID") #param(Author, "t"."AuthorID") #param(LinkDoc, "t"."LinkDocID") -- (3) #if(ByCategory) { GROUP BY "t"."CategoryID" } #if(ByState) { GROUP BY "t"."StateID" } #if(ByDepartment) { GROUP BY "t"."DepartmentID" } #if(ByAuthor) { GROUP BY "t"."AuthorID" } #if(ByLinkDoc) { -- (4) GROUP BY "t"."LinkDocID" } #if(Normal) { ORDER BY #order_by } #if(PageOffset) { OFFSET #param(PageOffset) - 1 LIMIT #eval(PageLimit.Value) }

На вкладке Отладка рассмотрим, как выглядит SQL-запрос для режима выполнения без сабсета (настройка Выбранное подмножество равна Не выбрано):

-- {"ViewAlias":"AbResolutions","Subset":null,"IsAdministrator":true,"Parameters":["PageOffset","PageLimit","UserAccessLevel"],"OrderBy":{"DocName":"asc"}} SELECT

"t"."ID" AS "DocID", "t"."Name" AS "DocName", "t"."CategoryID", "t"."StateID", "t"."DepartmentID", "t"."AuthorID", "t"."PartnerID", "t"."RefTypeID", "t"."TaskKindID", "t"."LinkDocID", 0::int8 AS "rn" FROM "AbResolutions" AS "t" WHERE true ORDER BY "t"."Name" asc OFFSET 1 - 1 LIMIT 20;

Обратите внимание, что SQL-запрос возвращает только идентификаторы колонок, а в результатах выполнения представления присутствуют колонки с именами (которые и выводятся пользователю). Причём значения колонок наполняются из справочников нормализации, установленных в метаинформации колонок.

Посмотрим на выполнение SQL-запроса в режиме сабсета ByLinkDoc (настройка Выбранное подмножество равна ByLinkDoc):

-- {"ViewAlias":"AbResolutions","Subset":"ByLinkDoc","IsAdministrator":true,"Parameters":["UserAccessLevel"]} SELECT "t"."LinkDocID", count(*) AS "cnt" FROM "AbResolutions" AS "t" WHERE true GROUP BY "t"."LinkDocID";

Этот SQL-запрос не возвращает колонку с названием связанного документа LinkDocName и не выполняет сортировку. Но в результатах присутствует эта колонка, заполненная из справочника нормализации (который установлен в метаинформации сабсета), и результаты отсортированы (также в соответствии с метаинформацией сабсета).


Сохраните представление и проверьте его работу, открыв узел Тестирование → Резолюции:

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

Как здесь видно, представление выглядит и функционирует так же, как если бы справочники нормализации не использовались. Однако, представление работает существенно быстрее без задействования колонок с именами в SQL-запросе, и оно выводит всегда актуальные названия ссылочных данных.

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

Использование справочников нормализации ограничивает возможность сортировки по строке в нормализуемой колонке (в примере это колонка Связанный документ).

Note

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

Если выводимая представлением таблица (здесь AbResolutions) небольшая (меньше тысячи строк), то в SQL-запросе при сортировке по нормализуемой колонке LinkDocName можно добавить дополнительный LEFT JOIN для получения имени в связанном справочнике:

FROM "AbResolutions" AS "t" #if(request.SortedBy("LinkDocName")) { LEFT JOIN "AbResolutions" AS "linked" ON "linked"."ID" = "t"."LinkDocID" }

Тогда в метаинформации колонки LinkDocName в настройке SortBy укажите строку linked.Name. Это сделает колонку сортируемой, и добавит LEFT JOIN только в случае, если пользователь выбрал эту сортировку.

SQL-запрос на вкладке Отладка для сортировки по колонке LinkDocName по возрастанию будет выглядеть так:

-- {"ViewAlias":"AbResolutions","Subset":null,"IsAdministrator":true,"Parameters":["PageOffset","PageLimit","UserAccessLevel"],"OrderBy":{"LinkDocName":"asc"}} SELECT "t"."ID" AS "DocID", "t"."Name" AS "DocName", "t"."CategoryID", "t"."StateID", "t"."DepartmentID", "t"."AuthorID", "t"."PartnerID", "t"."RefTypeID", "t"."TaskKindID", "t"."LinkDocID", 0::int8 AS "rn" FROM "AbResolutions" AS "t" LEFT JOIN "AbResolutions" AS "linked" ON "linked"."ID" = "t"."LinkDocID" WHERE true ORDER BY "linked"."Name" asc OFFSET 1 - 1 LIMIT 20;

Important

Как только в таблице AbResolutions из выражения FROM будет большое количество строк (больше тысячи), то такой запрос будет выполняться медленно с существенной нагрузкой на сервер СУБД, т.к. СУБД сортирует строки в памяти по всей таблице AbResolutions перед применением пейджинга.

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

Переименование карточки в ссылочном поле

Далее продемонстрировано, что происходит при переименовании карточки AbResolution, ссылка на которую присутствует в колонке LinkDoc.

Рассмотрим набор карточек-резолюций, две из которых ссылаются на карточку Корпоратив в колонке Связанный документ:

Откройте карточку Корпоратив, переименуйте её, указав в поле Название резолюции строку Корпоративное мероприятие, и сохраните карточку.

Затем посмотрим, какие значения справочника нормализации есть в кэше Redis.

С помощью Redis Explorer выберите в дереве узел

redis → instances → tessa → normalization → AbResolutions

где tessa - код сервера TESSA (из настройки ServerCode в конфигурационном файле app.json веб-сервиса web), а AbResolutions - алиас интересуемого справочника нормализации.

В колонке Name выводится значение ключа в справочнике (идентификатор карточки-резолюции), а в колонке Value - строковое значение по этому ключу.

Note

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

Также имя было обновлено в памяти всех рабочих процессов веб-сервиса, и в памяти других сервисов, связанных с этим сервером TESSA (в соответствии с кодом сервера).

Теперь в веб-клиенте переключите вкладку рабочего места Тестирование. Представление Резолюции будет обновлено, и в колонке Связанный документ будет актуальное название Корпоративное мероприятие.

Далее откройте одну из карточек, для которой в колонке Связанный документ указано название Корпоративное мероприятие:

Как видим, в поле карточки Связанный документ название также актуальное, поскольку оно было запрошено из справочника нормализации.

Использование API нормализации

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

Для примера рассмотрим задачу: требуется создать объект, возвращающий информацию по карточкам сотрудников, которая включает в себя:

  • идентификатор карточки сотрудника ID;
  • имя сотрудника Name;
  • версию карточки Version;
  • идентификатор CreatedByID и имя сотрудника CreatedByName, создавшего карточку;
  • идентификатор ModifiedByID и имя сотрудника ModifiedByName, изменившего карточку.

Имена сотрудников оптимально будет получить из справочника нормализации Users, а не запросами к БД.

Откройте проект Tessa.Extensions.Server, добавьте в него папку AbTest для файлов этого примера. В ней создайте файл UserInfo.cs, содержащий объект с запрашиваемой информацией:

using System;

namespace Tessa.Extensions.Server.AbTest { public sealed record UserInfo(Guid ID, int Version, Guid CreatedByID, Guid ModifiedByID) { public string? Name { get; set; }

public string? CreatedByName { get; set; }

public string? ModifiedByName { get; set; } } }

Создайте файл с объектом стратегии UserInfoStrategy.cs, содержащий логику получения списка объектов UserInfo:

using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Normalization; using Tessa.Platform.Data; using Tessa.Platform.Validation; using Tessa.Roles; using Tessa.Scheme;

namespace Tessa.Extensions.Server.AbTest { public sealed class UserInfoStrategy( INormalizationBatchProcessor normalizationBatchProcessor, ICardMetadata cardMetadata, IDbScope dbScope) { private readonly INormalizationBatchProcessor normalizationBatchProcessor = NotNullOrThrow(normalizationBatchProcessor);

private readonly ICardMetadata cardMetadata = NotNullOrThrow(cardMetadata);

private readonly IDbScope dbScope = NotNullOrThrow(dbScope);

public async Task<List<UserInfo>> GetUsersAsync(CancellationToken cancellationToken = default) { // запрашиваем из таблицы Instances значения колонок ID, Version, CreatedByID, ModifiedByID // для всех карточек с типом "Сотрудник" (RoleHelper.PersonalRoleTypeID) var users = new List<UserInfo>(); await using (this.dbScope.Create()) { var db = this.dbScope.Db;

db .SetCommand(this.dbScope.BuilderFactory .Select().C(null, Names.Instances_ID, Names.Instances_Version, Names.Instances_CreatedByID, Names.Instances_ModifiedByID) .From(Names.Instances).NoLock() .Where().C(Names.Instances_TypeID).Equals().V(RoleHelper.PersonalRoleTypeID) .Build()) .LogCommand();

await using var reader = await db.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { users.Add(new( ID: reader.GetGuid(0), Version: reader.GetInt32(1), CreatedByID: reader.GetGuid(2), ModifiedByID: reader.GetGuid(3))); } }

if (users.Count == 0) { return users; }

// получаем идентификатор справочника нормализации сотрудников Users из кэша метаинформации var info = await this.cardMetadata.GetNormalizationInfoAsync(cancellationToken); var usersSourceID = info.UsersSourceID;

// создаём и наполняем запрос на нормализацию - при этом запрос ещё не выполняется var request = await this.normalizationBatchProcessor.CreateRequestAsync(cancellationToken); foreach (var user in users) { // UnknownValueForPlatformSources - константа "???", если значение не найдено в справочнике, // но мы хотим что-то показать пользователю; // // можно не указывать, если null в свойствах Name, CreatedByName // и ModifiedByName является корректным значением

request.Add(usersSourceID, new(user.ID), user, static (key, value, user, _) => user.Name = value ?? NormalizationHelper.UnknownValueForPlatformSources);

request.Add(usersSourceID, new(user.CreatedByID), user, static (key, value, user, _) => user.CreatedByName = value ?? NormalizationHelper.UnknownValueForPlatformSources);

request.Add(usersSourceID, new(user.ModifiedByID), user, static (key, value, user, _) => user.ModifiedByName = value ?? NormalizationHelper.UnknownValueForPlatformSources);

// в пределах одного запроса request также возможно запрашивать // ключи (идентификаторы) из разных справочников }

// далее при вызове ProcessAsync будут выполнены запросы к справочникам нормализации, // после чего (при отсутствии ошибок) выполнятся наши делегаты, определённые выше var response = await this.normalizationBatchProcessor.ProcessAsync(request, cancellationToken); if (!response.Result.IsSuccessful) { // критическая ошибка: справочник usersSourceID не найден, отсутствует подключение // к кэширующему серверу Redis и пр.

// отсутствие ключа в справочнике не является ошибкой, в этом случае // делегат вызывается с параметром value, равным null throw new ValidationException(response.Result); }

// поскольку вызов ProcessAsync выполнил добавленные выше делегаты, // то для всех объектов users уже заполнены свойства Name, CreatedByName, ModifiedByName return users; } } }

Tip

Если по какой-то причине использование делегатов не подходит (например, установка свойства выполняется асинхронным методом, а делегат допускает только синхронный код), то:

  • добавление запрашиваемых ключей в request производится с двумя параметрами: идентификатор справочника и запрашиваемый из него ключ;
  • значение запрашивается из ответа на запрос response вызовом метода TryGet.

Указанный выше код нормализации можно переписать следующим образом:

var request = await this.normalizationBatchProcessor.CreateRequestAsync(cancellationToken); foreach (var user in users) { request.Add(usersSourceID, new(user.ID)); request.Add(usersSourceID, new(user.CreatedByID)); request.Add(usersSourceID, new(user.ModifiedByID)); }

var response = await this.normalizationBatchProcessor.ProcessAsync(request, cancellationToken); if (!response.Result.IsSuccessful) { throw new ValidationException(response.Result); }

foreach (var user in users) { user.Name = response.TryGet(usersSourceID, new(user.ID)) ?? NormalizationHelper.UnknownValueForPlatformSources;

user.CreatedByName = response.TryGet(usersSourceID, new(user.CreatedByID)) ?? NormalizationHelper.UnknownValueForPlatformSources;

user.ModifiedByName = response.TryGet(usersSourceID, new(user.ModifiedByID)) ?? NormalizationHelper.UnknownValueForPlatformSources; }

Добавьте файл Registrator.cs, в котором будет выполнена регистрация зависимости UserInfoStrategy в серверном DI-контейнере:

using Unity;

namespace Tessa.Extensions.Server.AbTest { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() => this.UnityContainer.RegisterSingleton<UserInfoStrategy>(); } }

Теперь для любого класса серверных расширений или иного объекта, регистрируемого в DI, достаточно получить объект UserInfoStrategy в конструкторе и вызвать его метод GetUsersAsync. Внутри метода будет выполнено заполнение имён сотрудников из соответствующего справочника нормализации.

Инвалидация справочника через API

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

Note

В качестве такого справочника выбран справочник сотрудников с идентификатором PlatformNormalizationSources.Users.

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


Кэши справочников нормализации бывают двух типов:

  1. глобальные - не опираются на информацию в текущем процессе (рабочем процессе веб-сервиса, плагина Chronos и пр.). Кэш расположен во внешнем ресурсе. Например, это кэш в Redis, зарегистрированный с использованием RedisNormalizationOptions;

  2. локальные - расположены в памяти текущего процесса. Например, это кэш в памяти, зарегистрированный с использованием InMemoryNormalizationOptions.

Пусть интересующий нас справочник Users имеет и глобальный кэш (кэш Redis), и локальный кэш (кэш в памяти). Либо мы не знаем, как именно он зарегистрирован.

Требуется определить объект, который выполнит сброс и глобального, и локального кэшей, независимо от того, какие кэши используются этим справочником.


Такая задача решается одним из способов:

Ниже рассмотрены оба варианта и приведены их особенности.

Инвалидация методом InvalidateGlobalAsync

Метод-расширение INormalizationService.InvalidateGlobalAsync выполняет инвалидацию для справочников с заранее известными идентификаторами.

В данном примере это справочник с идентификатором PlatformNormalizationSources.Users.

Important

Метод InvalidateGlobalAsync отправит сообщение об инвалидации через канал Redis даже в том случае, если для справочника не зарегистрированы локальные кэши (т.е. не используется кэш в памяти). Такая ситуация будет корректно обработана, но она является избыточной для таких справочников.

Рассмотрите написание триггера инвалидации для случаев, когда неизвестно, как зарегистрирован инвалидируемый справочник нормализации.

Откройте проект Tessa.Extensions.Server, добавьте в него папку AbTest для файлов этого примера. В ней создайте файл IUsersNormalizationInvalidator.cs, содержащий интерфейс для инвалидации справочника Users:

using System; using System.Collections.Generic; using System.Threading.Tasks;

namespace Tessa.Extensions.Server.AbTest { public interface IUsersNormalizationInvalidator { Task InvalidateAllAsync();

Task InvalidateAsync(IReadOnlyCollection<Guid>? ids); } }

Интерфейс содержит методы:

  • InvalidateAllAsync - сбрасывает все значения в кэшах;
  • InvalidateAsync - сбрасывает значения в кэшах для сотрудников с указанными идентификаторами.

Создайте файл UsersNormalizationInvalidator.cs с реализацией интерфейса:

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Tessa.Normalization;

namespace Tessa.Extensions.Server.AbTest { public sealed class UsersNormalizationInvalidator( INormalizationService normalizationService, INormalizationEventNotifier normalizationEventNotifier) : IUsersNormalizationInvalidator { public Task InvalidateAllAsync() => normalizationService.InvalidateGlobalAsync( normalizationEventNotifier, [PlatformNormalizationSources.Users], NormalizationTriggerResult.ShouldInvalidateAll());

public Task InvalidateAsync(IReadOnlyCollection<Guid>? ids) => normalizationService.InvalidateGlobalAsync( normalizationEventNotifier, [PlatformNormalizationSources.Users], NormalizationTriggerResult.ShouldInvalidateKeys( ids? .Select(static id => new KeyValuePair<NormalizationKey, NormalizationValue?>(new(id), null)) .ToArray())); } }

  1. Зависимости INormalizationService и INormalizationEventNotifier запрашиваются из DI. Они требуются для вызова метода InvalidateGlobalAsync.

  2. Этому методу передаётся коллекция инвалидируемых справочников, в данном случае это единственный справочник Users.

  3. Также методу передаётся объект NormalizationTriggerResult, определяющий, какие значения должны инвалидироваться.

    • Вызов NormalizationTriggerResult.ShouldInvalidateAll() указывает, что кэши сбрасывают все свои значения.
    • Вызов NormalizationTriggerResult.ShouldInvalidateKeys(...) перечисляет идентификаторы сбрасываемых значений. Вместе со сбросом возможно указать новое актуальное значение NormalizationValue для записей с определёнными идентификаторами, здесь этого не выполняется.

Создайте файл Registrator.cs, в котором объявленный объект регистрируется как синглтон по интерфейсу:

using Unity;

namespace Tessa.Extensions.Server.AbTest { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() => this.UnityContainer .RegisterSingleton<IUsersNormalizationInvalidator, UsersNormalizationInvalidator>(); } }

Теперь объект IUsersNormalizationInvalidator возможно запросить из DI-контейнера через конструктор для любого регистрируемого в Unity объекта или расширения. Например, из расширения на сохранение карточки:

using System.Threading.Tasks; using Tessa.Cards.Extensions; using Tessa.Platform.Storage;

namespace Tessa.Extensions.Server.AbTest { public sealed class SomeSettingsStoreExtension( IUsersNormalizationInvalidator usersNormalizationInvalidator) : CardStoreExtension { public override async Task AfterRequest(ICardStoreExtensionContext context) { if (context.RequestIsSuccessful && context.Info.TryGet<bool>("InvalidateAllUsers")) { await usersNormalizationInvalidator.InvalidateAllAsync(); } } } }

Note

Недостаток такого подхода в том, что методу InvalidateGlobalAsync передаётся идентификатор конкретного справочника Users.

Если будет объявлен другой справочник, который требуется инвалидировать при наступлении этого же набора событий (например, в некотором расширении SomeSettingsStoreExtension, связанном с изменением формата имён для сотрудников, и в иных расширениях, использующих IUsersNormalizationInvalidator), то необходимо будет указать идентификатор справочника в объекте UsersNormalizationInvalidator, или написать другие аналогичные расширения, вызывающие сброс такого справочника.

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

Инвалидация посредством триггеров

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

Tip

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

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

В папке AbTest из предыдущего примера добавьте объект контекста с данными по событию “имена всех сотрудников изменены”. Создайте файл AllUserNamesInvalidatedNormalizationContext.cs с простым объектом record:

namespace Tessa.Extensions.Server.AbTest { public record AllUserNamesInvalidatedNormalizationContext; }

Затем создайте файл SpecificUserNamesInvalidatedNormalizationContext.cs с данными по событию “имена указанных сотрудников изменены”, а именно, с идентификаторами таких сотрудников:

using System; using System.Collections.Generic;

namespace Tessa.Extensions.Server.AbTest { public record SpecificUserNamesInvalidatedNormalizationContext( IReadOnlyCollection<Guid>? Identifiers); }

Далее создайте файл UserNamesInvalidatedTrigger.cs. В нём определяется триггер, задача которого - в зависимости от переданного объекта контекста вернуть объект с результатом INormalizationTriggerResult:

using System.Collections.Generic; using System.Linq; using Tessa.Normalization;

namespace Tessa.Extensions.Server.AbTest { public sealed class UserNamesInvalidatedTrigger : DelegateNormalizationTrigger { private UserNamesInvalidatedTrigger() : base(ctx => ctx switch { AllUserNamesInvalidatedNormalizationContext => NormalizationTriggerResult.ShouldInvalidateAll(), SpecificUserNamesInvalidatedNormalizationContext s => NormalizationTriggerResult.ShouldInvalidateKeys( s.Identifiers? .Select(static id => new KeyValuePair<NormalizationKey, NormalizationValue?>(new(id), null)) .ToArray()), _ => null }) { }

public static UserNamesInvalidatedTrigger Instance { get; } = new(); } }

Как видно, объект возвращает результат NormalizationTriggerResult по аналогии с тем, как в предыдущем примере это выполнял объект UsersNormalizationInvalidator.

Теперь необходимо изменить объект UsersNormalizationInvalidator следующим образом:

using System; using System.Collections.Generic; using System.Threading.Tasks; using Tessa.Normalization;

namespace Tessa.Extensions.Server.AbTest { public sealed class UsersNormalizationInvalidator( INormalizationInvalidatorProvider normalizationInvalidatorProvider) : IUsersNormalizationInvalidator { public Task InvalidateAllAsync() => normalizationInvalidatorProvider.InvalidateAsync( new AllUserNamesInvalidatedNormalizationContext());

public Task InvalidateAsync(IReadOnlyCollection<Guid>? ids) => normalizationInvalidatorProvider.InvalidateAsync( new SpecificUserNamesInvalidatedNormalizationContext(ids)); } }

  • В методах класса UsersNormalizationInvalidator отправляются объекты контекстов AllUserNamesInvalidatedNormalizationContext (для сброса всех значений в кэшах) и SpecificUserNamesInvalidatedNormalizationContext (для сброса значений в кэшах для сотрудников с переданными идентификаторами).
  • Зависимость INormalizationInvalidatorProvider, полученная из DI-контейнера, выполняет распространение таких контекстов по всем триггерам.
  • Триггер UserNamesInvalidatedTrigger реагирует на контексты указанных типов, создавая результат INormalizationTriggerResult.
  • Для справочников, зарегистрированных по триггеру UserNamesInvalidatedTrigger, будет выполнена инвалидация.

Файл с интерфейсом IUsersNormalizationInvalidator.cs такой же, как в предыдущем примере.

Модифицируйте файл Registrator.cs, добавив в него метод FinalizeRegistration, в котором глобальная инвалидация GlobalSourceNormalizationInvalidator справочника Users связывается с триггером UserNamesInvalidatedTrigger:

using Tessa.Normalization; using Tessa.Platform; using Unity;

namespace Tessa.Extensions.Server.AbTest { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() => this.UnityContainer .RegisterSingleton<IUsersNormalizationInvalidator, UsersNormalizationInvalidator>();

public override void FinalizeRegistration() { if (this.UnityContainer.TryResolve<INormalizationInvalidatorRegistry>() is not { } invalidatorRegistry) { return; }

invalidatorRegistry.Register( new GlobalSourceNormalizationInvalidator([PlatformNormalizationSources.Users]), UserNamesInvalidatedTrigger.Instance); } } }

Note

Если справочник Users не имеет локальных кэшей (т.е. кэша в памяти процесса), то вместо GlobalSourceNormalizationInvalidator используйте SourceNormalizationInvalidator.

В настоящем примере дополнялась инвалидация платформенного справочника Users.

В ситуации, когда в решении создаётся справочник нормализации с объявленным для него же триггером, то вызов invalidatorRegistry.Register размещайте рядом с регистрацией этого справочника.

Tip

Параметрами метода invalidatorRegistry.Register допустимо передавать несколько триггеров в коллекции [...]: например, и CardNormalizationTrigger для инвалидации по изменению поля карточки, и UserNamesInvalidatedTrigger для инвалидации с использованием объекта IUsersNormalizationInvalidator.

Тогда будут применены оба триггера, т.е. при наступлении любого из связанных с триггерами событий будет произведена инвалидация посредством предоставленного объекта GlobalSourceNormalizationInvalidator.

Изменение регистрации справочника через API

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

Для примера рассматривается справочник нормализации Users (имена сотрудников), который зарегистрирован в расширениях типового решения в файле Tessa.Extensions.Default.Server/Normalization/Registration.cs.

Вместо изменения исходного кода типового решения рекомендуемым подходом является объявление собственного класса регистратора, который выполняется после регистратора из типового решения, и в нём производится:

  • регистрация в объекте INormalizationInstanceRegistry для изменения того, как справочник получает и кэширует значения;
  • регистрация в объекте INormalizationInvalidatorRegistry для определения того, когда выполняется инвалидация значений этого справочника (т.е. сброс кэша).

Пусть необходимо внести следующие модификации:

  1. Справочник кэшируется в памяти, но не кэшируется в Redis.
  2. Кэширование выполняется по мере запроса значений, т.е. без указания свойства PrefetchAll = true.
  3. Вычисление имени сотрудника и его инвалидация происходит по значению свойства Name в секции Roles (а не в секции PersonalRoles).

Откройте проект Tessa.Extensions.Server, добавьте в него папку AbTest для файлов этого примера. В ней создайте файл Registrator.cs, содержащий объект с регистрациями в API нормализации:

using Tessa.Cards.Normalization; using Tessa.Extensions.Default.Server.Normalization; using Tessa.Normalization; using Tessa.Platform; using Tessa.Roles;

namespace Tessa.Extensions.Server.AbTest { [Registrator(Order = 100)] public sealed class Registrator : RegistratorBase { public override void FinalizeRegistration() { if (this.UnityContainer.TryResolve<INormalizationInstanceRegistry>() is not { } instanceRegistry || this.UnityContainer.TryResolve<INormalizationInvalidatorRegistry>() is not { } invalidatorRegistry) { return; }

var options = this.UnityContainer.TryResolve<DefaultNormalizationOptions>(); var inMemoryExpiry = options?.InMemoryExpiry;

instanceRegistry.Register(PlatformNormalizationSources.Users, (factory, descriptor) => factory.Create(new InMemoryNormalizationOptions(descriptor) { Source = factory.Create(new DatabaseNormalizationOptions(descriptor) { TableName = "Roles", KeyColumnName = "ID", ValueColumnName = "Name" }), Expiry = inMemoryExpiry }));

invalidatorRegistry.Remove(static (invalidator, trigger) => invalidator.IsApplicableToSource(PlatformNormalizationSources.Users));

invalidatorRegistry.Register( new GlobalSourceNormalizationInvalidator([PlatformNormalizationSources.Users]), new CardNormalizationTrigger([RoleHelper.PersonalRoleTypeID]) { ValueSectionName = "Roles", ValueFieldName = "Name" }); } } }

Обратите внимание на следующие нюансы:

  1. В атрибуте [Registrator] указан порядковый номер Order = 100, чтобы регистрация выполнялась позже, чем объявления в регистраторе типового решения (где значение по умолчанию Order = 0).

  2. Код выполняется в методе FinalizeRegistration по аналогии с тем, как производится регистрация новых справочников.

    • Объект INormalizationDescriptorRegistry не запрашивается из DI-контейнера, поскольку Users - существующий справочник, дескриптор для которого уже был зарегистрирован.
  3. Метод instanceRegistry.Register выполняет построение и регистрацию объекта INormalizationSource для справочника с идентификатором PlatformNormalizationSources.Users:

    • из базы данных DatabaseNormalizationOptions значение справочника возвращается по данным в таблице TableName = "Roles";
    • кэш в памяти InMemoryNormalizationOptions указан без свойства PrefetchAll = true;
    • кэш в Redis RedisNormalizationOptions отсутствует.

    Note

    Любые существующие регистрации по идентификатору справочника PlatformNormalizationSources.Users, переданному в метод instanceRegistry.Register, отбрасываются. Их не требуется удалять явно вызовом метода INormalizationInstanceRegistry.Remove.

  4. Метод invalidatorRegistry.Remove выполняет удаление ранее зарегистрированных триггеров для объектов-инвалидаторов, которые производят инвалидацию справочника PlatformNormalizationSources.Users.

    • Функция-предикат в параметре получает объекты INormalizationInvalidator и INormalizationTrigger. Она должна вернуть true, если регистрация этой пары должна быть удалена.
    • Метод invalidator.IsApplicableToSource проверяет, то объект-инвалидатор производит, в числе прочего, инвалидацию справочника нормализации с указанным идентификатором.

    Important

    Если метод Remove не вызвать, то последующая регистрация invalidatorRegistry.Register добавит триггер для инвалидации справочника, но все зарегистрированные ранее (в типовом решении) триггеры продолжат быть актуальны. Это приведёт к лишним вызовам инвалидации кэша для справочника.

  5. Метод invalidatorRegistry.Register добавляет объект-инвалидатор GlobalSourceNormalizationInvalidator для глобального сброса кэша у справочника Users при изменении поля Name из секции Roles карточки с типом Сотрудник (а не из секции PersonalRoles).

Tip

При разработке модулей и универсальных решений рекомендуется не указывать более одного справочника в объектах-инвалидаторах GlobalSourceNormalizationInvalidator и SourceNormalizationInvalidator, чтобы основанные на этом модуле проектные решения могли корректно удалить регистрацию одного из справочников вызовом invalidatorRegistry.Remove, не затрагивая регистрации других справочников.

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

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

Теперь достаточно собрать проект расширений Tessa.Extensions.Server.dll, после чего заменить этот файл в соответствующих папках с расширениями: для веб-сервиса, Chronos и др.

Back to top