Представим задачу: дана база даных GlobalsDB или Сache с их пресловутыми глобалами; надо обеспечить средствами прямого доступа GlobalsAPI, чтобы в узлах некоего глобала типа «^n(«Japan»,«Yamato»)=$lb(«R.Keanu»,1000)» первые индексы принимали в качестве значений названия стран с количеством символов от 0 до n; вторые индексы — названия кораблей, тоже с ограничениями. А значениями по этим индексам были бы имена капитанов и количество их подчиненных, естественно, тоже с ограничениями.

Сразу сообщу, что только средствами GlobalsAPI это сделать невозможно. Cache GlobalsProxy Framework предлагает быстрое и универсальное решение подобных задач. Благодаря объединению технологии Object-Globals Mapping (OGM) с методами GlobalsAPI обеспечивается возможность обрабатывать данные глобала с помощью объектов (прокси-классов) представляющих узлы глобала подобно тому, как в ORM к реляционным базам данных — с помощью объектов представляющих таблицы.

Основные возможности Cache GlobalsProxy Framework:
  1. Средства создания описания структур данных и их ограничений (метаданных) глобала.
  2. Автоматизированная валидации данных.
  3. Генерация прокси-классов на основе метаданных.
  4. CRUD операции над данными в глобале с помощью прокси-классов.

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

Для вьезда в тему, рекомендую ознакомится с:
habrahabr.ru/company/intersystems/blog/228869 — Intersystems Cache: Globals API для .NET
habrahabr.ru/company/intersystems/blog/184882 GlobalsDB — универсальная NoSQL база данных.
habrahabr.ru/company/intersystems/blog/263791 Глобалы — мечи-кладенцы для хранения данных.

Проблематика


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

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

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

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

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

Обеспечение глобалов метаданными


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

Для описания глобала разработан вариант спецификации мета-глобала, имеющей следующий вид:
Спецификация мета-глобала:
//Под семантикой подразумевается значение/название сущности в предметной области.
^meta = $lb(<название глобала, метаинформация которого описывается>, <семантика глобала>)
^meta(«Indexes», <порядковый номер индекса = 1..N>) = $lb(<семантика индекса>, <<тип>>)

^meta(«Values», <для узла доступного по кол-ву индексов = [1; N]>)=$lb(<семантика узла>)
^meta(«Values», <для узла доступного по кол-ву индексов = [1; N], <значение № = 1..V>)
= $lb(<семантика значения>, <<тип>>)

^meta(«Structs»,<идентификатор структуры = 1..S>) = $lb(<семантика структуры>)
^meta(«Structs»,<для структуры с идентификатором = [1; S]>, <значение № = 1..C>)
= $lb(<семантика значения>, <<тип >>)

<<тип>> =
<<*строка>> = («string», <максимальная длинна>, <минимальная длинна>, <по умолчанию>)
<<*целое>> = («integer», <минимум>, <максимум>, < по умолчанию >)
<<*действительное>> = («double», < минимум >, <максимум>, < по умолчанию >)
<<**структура>> = («struct», <идентификатор структуры>)
<<список>> = («list», << тип>>, <минимум элементов>, <максимум элементов>)
<<массив байтов>> = («byte», <максимальный размер>)

* — допустимый тип индекса.
** — структура может использоваться в качестве типа индекса при условии, что в ней и во всех вложенных в нее структурах содержатся допустимые для индексов типы.

Для примера:
Создадим мета-глобал «^nMeta», описывающий глобал «^n» с данными о военно-морских силах стран:
Созданный мета-глобал
^nMeta = («n», «Navies»)
Или, «nMeta» описывает глобал «n», который хранит информацию о «Navies».
^nMeta(«Indexes»,1) = $lb(«Country»,«string»,0,255," Country ")
Или, первый индекс глобала хранит информацию о «Country», его тип " string" Аналогично:
^nMeta(«Indexes»,2) = $lb(«ShipClass»,«struct»,1)
^nMeta(«Indexes»,3) = $lb(«Name»,«string»,0,255," Name ")
^nMeta(«Structs»,1) = $lb(«Classification»)
Или, структура с идентификатором «1» называется «Classification»
^nMeta(«Structs»,1,1) = $lb(«ClassType»,«string»,0,255," ClassType")
Или, значение номер «1» структуры «1» хранит информацию о «ClassType», его тип «string» Аналогично:
^nMeta(«Structs»,1,2) = $lb(«Rank»,«integer»,0,10,0)
^nMeta(«Structs»,2) = $lb(«ContactInfo»)
^nMeta(«Structs»,2,1) = $lb(«Name»,«string»,0,255,"")
^nMeta(«Structs»,2,2) = $lb(«Phone»,«integer»,1111111,9999999,1111111)
^nMeta(«Values»,1) = $lb(«Manufacturer»)
Или в узлах доступных по кол-ву индексов «1», хранится информация о «Manufacturer»
^nMeta(«Values»,1,1) = $lb(«Charge»,«struct»,2)
Или в узлах уровня «1» значение номер «1» хранит информацию о «Charge», тип «struct» Аналогично:
^nMeta(«Values»,1,2) = $lb(«Ports»,«list»,«struct»,2, 0,1000)
^nMeta(«Values»,2) = $lb(«ShipCounter»)
^nMeta(«Values»,2,1) = $lb(«Count»,«integer»,0,100500,0)
^nMeta(«Values»,3) = $lb(«ShipInfo»)
^nMeta(«Values»,3,1) = $lb(«Captain»,«struct»,2)
^nMeta(«Values»,3,2) = $lb(«StuffCount»,«integer»,1,5000,1)
^nMeta(«Values»,3,3) = $lb(«Efficienty»,«double»,0,1,0)
Заполним глобал «^n» согласно созданному описанию:
Заполненный глобал
^n(«Japan») = $lb($lb(«Akihito»,1534598),$lb($lb(«Tokyo»,5645122),$lb(«Miaon»,645122)))
^n(«Japan»,«Battleship»,3) = $lb(1)
^n(«Japan»,«Battleship»,3,«Kawachi») = $lb($lb(«I.O. Jaiodeen»,124234),999,.7)
^n(«Japan»,«Battleship»,10) = $lb(1)
^n(«Japan»,«Battleship»,10,«Yamato») = $lb($lb(«L.A. Myolin»,142323),2350,.6)
^n(«Japan»,«Carry»,4,«Hosho») = $lb($lb(«A.M. Harusimo»,134254),548,.8)
^n(«Japan»,«Carry»,7) = $lb(1)
^n(«Japan»,«Destroyer»,5,«Hatsuharu») = $lb($lb(«W.C. Hawanio»,98034),212,.65)
^n(«Japan»,«Destroyer»,7) = $lb(1)
^n(«USA») = $lb($lb(«O.Barack»,1534598),$lb($lb(«NH2»,4568976),$lb(«MnP1»,987654)))
^n(«USA»,«Carry»,4) = $lb(1)
^n(«USA»,«Carry»,4,«Langey») = $lb($lb(«R.C. Adams»,9832723),521,.65)
^n(«USA»,«Carry»,4,«Saipan») = $lb($lb(«P.D. Strong»,5234542),1751,.75)
^n(«USA»,«Carry»,7) = $lb(1)
^n(«USA»,«Destroyer»,5) = $lb(1)
^n(«USA»,«Destroyer»,5,«Nikolas») = $lb($lb(«J.C. Denton»,5443123),168,.75)
^n(«USA»,«Destroyer»,6) = $lb(1)
^n(«USA»,«Destroyer»,6,«Farragut») = $lb($lb(«O.C. Ohara»,1233422),195,.8)
^n(«USA»,«Kruiser»,4) = $lb(1)
^n(«USA»,«Kruiser»,4,«Omaha») = $lb($lb(«P.A. Jenson»,1342344),458,.65)

Можно заметить, что с помощью данного варианта спецификации мета-глобала структура, семантики и ограничения на индексы и значения глобала задаются однозначно, будто бы предполагается, что иных структур данных глобал хранить не может, тем самым теряя гибкость хранения. Так оно в каком-то смысле и есть, но это только в рамках одного мета-глобала… Ничто не мешает создать несколько мета-глобалов для одного и того же глобала.

Object-Global Mapping


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

public class ShipInfoProxy
{
    //поля, соответствующие индексам узла
    public String Country;
    public Classification ShipClass;
    public String Name;
    
    //поля, соответствующие значениям в узле
    public ContactInfo Captain;
    public Int32 StuffCount;
    public Double Efficienty;
    ...
}
public class ShipInfoProxyKey
{
    //поля, соответствующие индексам узлов
    public String Country;
    public Classification ShipClass;
    public String Name;
    ...
}

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

Прокси-классу ShipInfoProxy, в контексте данного глобала, будет соответствовать менеджер ShipInfoProxyManager<ShipInfoProxy, ShipInfoProxyKey>, общий вид которого:

public class ProxyManager<ProxyT, ProxyKeyT>
    where ProxyT : class
    where ProxyKeyT : class
{
    public bool Validate;

    public ProxyManager(EntityMeta meta, TrueNodeReference globalRef, List<IStructManager> structsManagers = null);

    public ProxyT Get(ArrayList keys);
    public ProxyT Get(object key);
    public ProxyT Get(ProxyKeyT key);
    public List<ProxyT> GetAll();
    public void Delete(ProxyKeyT key);
    public void Save(ProxyT entity);
    public List<ProxyT> GetByKeyMask(object key);
    public List<ProxyT> GetByKeyMask (ProxyKeyT key);
}
Где, капитан очевидность, как бы намекая нам:
void Save(ProxyT entity)— сохраняет или заменяет данные в глобале.
ProxyT Get(ProxyKeyT key) — считывает данные из узла глобала по заданному ключу;
void Delete(ProxyKeyT key) — удаляет узел из глобала по заданному ключу.
public List<'ProxyT> GetAll() — считывает значения из всех узлов;
А вот это поворот — особый метод получения данных, позволяющий получать данные из узлов, не указав некоторые индексы этих узлов:
List<'n1Proxy> GetByKeyMask(ShipInfoProxyKey key) — считывает данные из всех узлов глобала, индексы которых соответствуют заданной ключом маске.

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

public class NaviesContext : CacheEXTREMEcontext
{
    public ProxyManager<ManufacturerProxy, ManufacturerProxyKey> ManufacturerManager;
    public ProxyManager<ShipCounterProxy, ShipCounterProxyKey> ShipCounterManager;
    public ProxyManager<ShipInfoProxy, ShipInfoProxyKey> ShipInfoManager;

    public WowsNiceContext(InterSystems.Globals.Connection conn) : 
                base(conn, "nMeta")
    {
        this.ManufacturerManager = new ProxyManager<ManufacturerProxy,ManufacturerProxyKey>
                           (base.entitiesMeta[typeof(ManufacturerProxy).Name], base.globalRef, base.structsManagers);
        ...
        base.structsManagers.Add(new StructManager<Classification>
                                                 (base.globalMeta.GetLocalStructs()[0], base.structsManagers));
        ...
     }
}

Cache GlobalsProxy Framework


Фреймворк Cache GlobalsProxy состоит из динамически подключаемой библиотеки CacheExtremeProxy.dll и вспомогательного приложения редактора метаданных. Что бы в полной мере его использовать, а именно редактировать данные глобала, нужно сделать, условно 3-и простых шага:

1. Создание метаданных

Можно пойти сложным путем, создать метаданные из кода, с помощью специально созданного класса GlobalMeta, а затем записать их в мета-глобал:
Создание метаданных в коде
StructValMeta structV = new StructValMeta("ContactInfo", " ContactInfo "
, new List<ValueMeta>(){
    new StringValMeta(new ArrayList(){"Name","String",0,255,""})
    ,new IntValMeta(new ArrayList(){"Phone","Integer",0,9999999,1})
});
GlobalMeta gm = new GlobalMeta("n", "Navies");
gm.AddStruct(structVal);
gm.AddKeyMeta(new StringValMeta(new ArrayList{"Country","String",0,255,""}),"Manufacturer");
gm.SetValuesMeta(1, new List<ValueMeta>(){
            new StructValMeta ("Charge", structV)
            ,new ListValMeta("Ports", structV)
        });
gm.AddKeyMeta(new StructValMeta ("ClassInfo", "Classification"
     , new List<ValueMeta>(){...}), " ClassType");
...
//сохранение метаданных в мета-глобал 
new MetaReaderWriter(dbConnection).SaveMeta(gm);

А можно пойти простым путем, создать метаданные с помощью прилагаемого вспомогательного приложения редактора метаданных:



Да… есть еще «олдскул» — путь создания мета-глобала с помощью COS.

2. Генерация контекста и прокси-классов

И снова, можно пойти относительно сложным путем — кодом. Заполнить специальный класс GlobalMeta (можно считать из предварительно созданного мета-глобала) и воспользоваться специальным классом-генератором:
Генерация контекста из кода
//Считывание мета-даних
  GlobalMeta meta = new MetaReader(conn).GetMeta("nMeta"); 
//Генерация контекста и прокси-классов
  ContextGenerator gen = new ContextGenerator(meta,"NaviesNamespace","ContextFile.cs");
  gen.GenerateCSharpCode();

А можно, все также, воспользоваться вспомогательным приложением-редактором:



3. Подключение и инициализация

Подключаем к проекту библиотеку CecheExtremeProxy.dll, добавляем сгенерированный файл с контекстом и прокси-классами. Далее, создаем соединения с базой и с его помощью инициализируем класс-контекст.

4. ???????


5. PROFIT!

Теперь метаданные доступны в процессе написания программного кода (конечно, если среда разработки поддерживает аналог InteliSense), от чего не болит голова о том, «что где и как?» хранится. Менеджеры скрывают в себе всю логику работы с глобалом и метаданным, и, в частности, автоматически проводят валидацию данных. От всего этого можно вообще почти забыть о том, что используется прямой доступ.

Примеры использования


Сперва создадим соединения с базой.
  conn = ConnectionContext.GetConnection();
  conn.Connect(_namespace, _user, _password);
Далее нужно инициализировать контекст:
//Создание и инициализация контекста прокси-классов
  NaviesContext Navies = new NaviesContext(conn);
Предположим мы хотим сохранить информацию о новой военно-морской силе. Для этого надо заполнить поля соответствующего экземпляра прокси-класса и передается его в метод сохранения соответствующего менеджера:
//Создание и инициализация экземпляра прокси-класса
  ManufacturerProxy manufacter = new ManufacturerProxy();
  manufacter.Country = "Great Britan";
  manufacter.Charge = new ContactInfo(Name: "D. Cameron", Phone: 1112233);
  manufacter.Ports = new List<ContactInfo> {
      new ContactInfo("Portsmun",2223344)
      , new ContactInfo("Dartford",3334455) 
  };
//Сохранение
  try
  {
      Navies.ManufacturerManager.Save(manufacter);
  }
  catch (Exception ex)
  {
      MessageBox.Show(ex.Message);
  }
После чего, в случае корректности данных, в глобале появляется следующая запись:
^n(«Great Britan») = $lb($lb(«D.Cameron», 1112233),$lb($lb(«Portsmun», 2223344),$lb(«Dartford», 3334455)))

В случае некорректности данных:


Для сравнения, что бы сохранить подобную запись стандартным GlobalsAPI.
И это без проверки корректности введенных данных:

NodeReference r = conn.CreateNodeReference("wowsNice");
//Инициализация переменных с данными перед сохранение базовым GlobalsAPI
  ValueList manufacturer = conn.CreateList(2);
  ValueList charge = conn.CreateList(2);
  charge.Append(new object[] { "D.Cameron", 1112233 });
  ValueList ports = conn.CreateList();
  ValueList port1 = conn.CreateList(2);
  port1.Append(new object[] { "Portsmun", 2223344 });
  ValueList port2 = conn.CreateList(2);
  port2.Append(new object[] { "Dartford", 3334455 });
  ports.Append(port1);
  ports.Append(port2);
  manufacturer.Append(charge);
  manufacturer.Append(ports);
//Сохранение c помощью базового GlobalsAPI
  r.Set(manufacturer, new object[] { "Great Britan2" });

Для доступа к данным в узлах используется экземпляр соответствующего класса-ключа:

//Считывание записи о военно-морской силе из глобала по ключу
  ManufacturerProxyKey key = new ManufacturerProxyKey("Great Britan");
  ManufacturerProxy readed = Navies.ManufacturerManager.Get(key);

//Удаление 
Navies.ManufacturerManager.Kill(key);
Предположим мы хотим получить все корабли по одному условию — они должны быть 4-го ранга. Для этого остальные поля экземпляра класса-ключа заполним пустыми значениями:

//Поиск или считывание данных по неполному ключу
 List<ShipInfoProxy> allFourRankShips;
 ShipInfoProxyKey keyMask = new ShipInfoProxyKey();
 keyMask.Country = "";
 keyMask.ShipClass = new Classification{ ClassType = "", Rank = 4 };
 keyMask.Name = "";
 allFourRankShips = Navies.ShipInfoManager.GetByKeyMask(keyMask);
Тестирование

С учетом немногочисленных недооптимизаций на сохранение 300 000 записей подобных:
^wowsTest(«kruis»,2) = $lb($lb(«3»,«4»,«5»),$lb(«5»,«4»,«3»),1000,$c(6,6,6))
^wowsTest(«kruis»,3) = $lb($lb(«4»,«5»,«6»),$lb(«6»,«5»,«4»),1000,$c(6,6,6))
у Cache GlobalsProxy Framework в среднем уходит в 1,5 больше времени по сравнению с GlobalsAPI.

Заключение


На данном этапе Cache GlobalsProxy Framework является вполне рабочим прототипом, использование которого многократно ускоряет процесс разработки программного обеспечения, в котором подразумевается использование прямого доступа к глобалам баз данных GlobalsDB и Cache.

Cache GlobalsProxy Framework полностью инкапсулирует в себе логику работы с глобалом средствами GlobalsAPI, а также логику работы с метаданными, в частности, автоматизируя валидацию.

В итоге:
  1. Обеспечивается логическая целостность данных. Теперь невозможно записать что угодно и куда угодно, так как сперва данным автоматически будет делаться валидация, и только при ее успехе они будут сохраняться.
  2. Многократно ускоряется процесс разработки ПО с использованием GlobalAPI.
Плюшки:
  1. Создание собственных типов (структур) в том числе и для индексов.
  2. Получение данных по неполному ключу.

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

Развитие


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

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

Проект в открытом доступе на github (дистрибутив GlobalsDB прилагается)

Виновны:

Гайдаржи В.И. — за соучастие в поиске темы проекта и главное — за веру в его пользу.
Витюк В.Р. — за соучастие в определении спецификации и создание редактора метаданных.
Кручок С.І. — за рецензию.
Предки — за еду и крышу над головой.
Николашин Н.Р. — за рецензию и главное — за отсутствие веры в пользу проекта.
Поделиться с друзьями
-->

Комментарии (2)


  1. ZOXEXIVO
    26.07.2016 10:59

    Издевательство какое-то


    1. SsMihand
      27.07.2016 16:47

      что имеется в виду?