Недавно возникла необходимость сравнить скорость записи/чтения данных из СУБД Intersystems Caché, используя разные виды доступа – прямой к глобалам, объектный и реляционный. С объектным и реляционным доступом все понятно, а вот с прямым (он же direct access) пришлось разбираться. Для тех, кому, как и мне, с первого взгляда документация не дала полного понимания процесса, и предназначена эта статья. Для примера буду делать консольное приложение в лучших традициях процедурного программирования.
Мини экскурс в историю… Когда я первый раз познакомилась с Caché, уже был Caché eXTreme для Java, но еще не было подобной технологии для .NET. И вот в версии 2012.2 она наконец-то появилась. Так что если у вас более старая версия, можете даже не пытаться пробовать что-то делать. Кроме того, для работы вам понадобится Visual Studio и .Net Framework как минимум версии 2.0. Итак, если у вас это все есть, то можно приступать к дальнейшим настройкам среды.
Настройка переменных окружения
Специально для чистоты эксперимента, поставила на «чистую» машину в первый раз Caché. Необходимые пути для работы с Caché eXTreme инсталлятор не прописал в переменные среды. Надо это делать вручную.
- В переменной GLOBALS_HOME должен быть полный путь к директории, куда вы ставили СУБД. В моем случае это C:InterSystemsCache
Установка переменной GLOBALS_HOMEПримечаниеВ отличие от установки СУБД Caché, при установке СУБД GlobalsDB этот путь прописывается автоматически! Поэтому, если вы поставили Caché, написали свою программу, настроили систему, а потом по какой-то причине поставили GlobalsDB, вас ожидает большой сюрприз: работать ничего естественно не будет! Надо будет вручную перепрописывать пути.
- В переменной PATH должен быть полный путь к директории Bin. В моем случае это C:InterSystemsCacheBin
ПримечаниеОпять же, если у вас стоит несколько версий Caché на одной машине, в работе Caché eXTreme будет использована та, которая встречается в этой переменной первой. Бывают «приятные» ситуации, когда в GLOBALS_HOME прописан один путь, а в PATH – другой. В какую из БД будут писаться изменения в этом случае я не проверяла. Но ситуация веселая: вроде все отработало без ошибок, а данных нет.ПримечаниеПосле того, как вы установили нужные переменные окружения, мало перегрузить сервер БД, надо перегрузить Windows (проверено) или выйти и зайти в пользователя (не проверено).
Ссылки на библиотеки
После того, как переменные среды благополучно настроены, перейдем к настройке проекта. В него для работы надо добавить ссылки на две библиотеки:
- InterSystems.CacheExtreme.dll
- InterSystems.Data.CacheClient.dll
Обе библиотеки можно найти в папке C:InterSystemsCachedevdotnetbinv2.0.50727
Естественно, в соответствующий модуль в раздел using надо добавить:
using InterSystems.Globals;
using InterSystems.Data.CacheClient;
Структура данных
Поскольку Caché eXTreme дает возможность записывать в качестве индексов (они же сабскрипты/subscripts) четыре типа данных: int, double, string, long, а в качестве значений шесть типов данных: int, double, long, string, bytes[], ValueList, то я постаралась придумать такую структуру для глобала, в котором будет как можно больше типов. Я взяла данные о банковских транзакциях по картам в качестве предметной области.
set ^CardInfo(111111111111) = "Сидоров Петр Витальевич"
set ^CardInfo(111111111111, "Приват банк") = 14360570
set ^CardInfo(111111111111, "Приват банк", 29244825509100) = 28741.35
set ^CardInfo(111111111111, "Приват банк", 29244825509100, 2145632596588547) = "Сидоров ПВ/1965/Sidorov Petr"
set ^CardInfo(111111111111, "Приват банк", 29244825509100, 2145632596588547, 1) = $lb(0, 26032009100100, "Сидоров Петр Витальевич", 500.26, "Перевод на счет в другом банке")
set ^CardInfo(111111111111, "Приват банк", 29244825509100, 2145632596588547, 2) = $lb(0, 26118962412531, "Иванов Иван Иванович", 115.54, "Плата за доставку")
set ^CardInfo(111111111111, "УкрСиббанк") = 19807750
set ^CardInfo(111111111111, "УкрСиббанк", 26032009100100) = 65241.24
set ^CardInfo(111111111111, "УкрСиббанк", 26032009100100, 6541963285249512) = "СидоровП | 1965 | SidorovP"
set ^CardInfo(111111111111, "УкрСиббанк", 26032009100100, 6541963285249512, 1) = $lb(1, 29244825509100, "Сидоров Петр Витальевич", 500.26, "Перевод на счет в другом банке")
set ^CardInfo(111111111111, "УкрСиббанк", 26032009100100, 6541963285249512, 2) = $lb(0, 26008962495545, "Сидоров Петр Витальевич", 1015.10, "Перевод на счет в другом банке")
set ^CardInfo(111111111111, "ПУМБ") = 14282829
set ^CardInfo(111111111111, "ПУМБ", 26008962495545) = 126.32
set ^CardInfo(111111111111, "ПУМБ", 26008962495545, 4567098712347654) = "СидоровПетр 1965 SidorovPetr"
set ^CardInfo(111111111111, "ПУМБ", 26008962495545, 4567098712347654, 1) = $lb(0, 29244825509100, "Иванов Иван Иванович", 115.54, "Плата за доставку")
set ^CardInfo(111111111111, "ПУМБ", 26008962495545, 4567098712347654, 2) = $lb(1, 26032009100100, "Сидоров Петр Витальевич", 1015.54, "Перевод на счет в другом банке")
Таким образом у нашего пользователя «Сидорова Петра Витальевича» открыто три карточных счета в разных банках, к каждому из которых привязана одна карта.
При прочтении документации я выделила для себя три способа заполнения данных. И на каждом из этих счетов я покажу один из них.
Подключение к БД
Итак, перед тем, как пытаться что-то куда-то записать или прочитать, надо сначала подключиться к БД. В отличие от объектного/реляционного доступа, при использовании Caché eXTreme не производится TCP/IP подключение – приложение работает в том же потоке, что и база данных. Поэтому сервер Caché и само приложение должны стоять на одной машине. Если необходимо получить доступ к данным на другом сервере, можно использовать Caché ECP. И вот тут вступают в силу ограничения, упомянутые в разделе «Настройка переменных окружения», потому что именно по этим настройкам система «понимает» куда ей ломиться подключаться.
Также следует отметить, что в процессе существует исключительно одно соединение и все объекты C# используют именно это одно соединение. Чтобы его получить используется метод ConnectionContext.GetConnection(). Чтобы проверить открыто ли соединение, или нет, используется метод IsConnected(). Чтобы соединение открыть используется метод Connect(), чтобы закрыть — Close().
class Program
{
static Connection Connect() {
//получаем соединение
Connection myConn = ConnectionContext.GetConnection();
//проверяем открыто ли соединение
if (!myConn.IsConnected())
{
Console.WriteLine("Подключение к БД");
//если соединение не открыто, то подключаемся
myConn.Connect("User", "_SYSTEM", "SYS");
}
if (myConn.IsConnected())
{
Console.WriteLine("Подключение к БД выполнено успешно");
//если подключение произошло успешно, то возвращаем открытое соединение
return myConn;
}
else { return null; }
}
static void Disconnect(Connection myConn) {
//если соединение открыто, то его надо закрыть и освободить ресурсы
if (myConn.IsConnected())
myConn.Close();
}
static void Main(string[] args)
{
try
{
Connection myConn1 = Connect();
//ToDo: создание глобала и чтение его значений
Disconnect(myConn1);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
}
Первый вариант записи данных – добавление индексов
Как уже упоминалось, я для себя поняла три подхода к построению глобалов. Первый из них – поэтапное создание дерева с помощью добавления индексов (углубления от корня).
Для того чтобы начать строить глобал, надо зафиксировать его корень. В нашем случае – это значение CardInfo. Выполняется эта операция с помощью метода CreateNodeReference():
NodeReference nodeRef = myConn1.CreateNodeReference("CardInfo");
После того, как мы зафиксировали указатель на корень дерева, можно приступать к его построению. В переменной nodeRef всегда будет храниться указатель на тот узел дерева, который сейчас является активным.
Для добавления нового индекса к переменной используется метод AppendSubscript(), в который можно передать значение типа double, int, long или string. Для вставки значения в узел глобала используется метод Set(). Он может принимать значения типа byte[], double, int, long, string, ValueList в качестве первого параметра. Второй параметр рассмотрим дальше, сейчас он пока не нужен. Если с типами byte[], double, int, long, string все ясно, это стандартные массив байтов, вещественное, целочисленное и длинное целочисленное значение и строка, то последний тип данных – это специальный тип данных для работы с системными списками Caché, которые создаются в COS с помощью функции $ListBuild (она же $lb).
node.AppendSubscript("111111111111");
node.Set("Сидоров Петр Витальевич");
node.AppendSubscript("Приват банк");
node.Set(14360570);
node.AppendSubscript(29244825509100);
node.Set(28741.35);
node.AppendSubscript(2145632596588547);
string slip = "Сидоров ПВ/1965/Sidorov Petr";
byte[] bytes = System.Text.Encoding.GetEncoding(1251).GetBytes(slip);
node.Set(bytes);
node.AppendSubscript(1);
ValueList myList = myConn.CreateList();
myList.Append(0, 26032009100100, "Сидоров Петр Витальевич", 500.26, "Перевод на счет в другом банке");
node.Set(myList);
myList.Close();
С каждым «шагом» указатель на текущий узел будет перемещаться на только что добавленный индекс.
Однако у нас есть еще одна транзакция. Чтобы ее добавить таким же методом, надо подняться на уровень выше. Для того чтобы это сделать (и вообще перепрыгнуть на любой другой уровень) используется метод SetSubscriptCount(), в который передается количество индексов узла (номер уровня) куда необходимо совершить переход.
node.SetSubscriptCount(4);
node.AppendSubscript(2);
myList = myConn.CreateList();
myList.Append(0, 26118962412531, "Иванов Иван Иванович", 115.54, "Плата за доставку");
node.Set(myList);
myList.Close();
static void CreateFirstBranch(NodeReference node, Connection myConn)
{
//добавляем 1 индекс - ИНН держателя карт
node.AppendSubscript("111111111111");
//добавляем значение узла и сохраняем данные в БД - ФИО держателя карт
node.Set("Сидоров Петр Витальевич");
//добавляем 2 индекс - название банка
node.AppendSubscript("Приват банк");
//добавляем значение узла и сохраняем данные в БД - ОКПО банка
node.Set(14360570);
//добавляем 3 индекс - номер счета
node.AppendSubscript(29244825509100);
//добавляем значение узла и сохраняем данные в БД - остаток на счету
node.Set(28741.35);
//добавляем 4 индекс - номер карты
node.AppendSubscript(2145632596588547);
//добавляем значение узла и сохраняем данные в БД - SLIP-информация карты в виде массива байтов
string slip = "Сидоров ПВ/1965/Sidorov Petr";
byte[] bytes = System.Text.Encoding.GetEncoding(1251).GetBytes(slip);
node.Set(bytes);
//добавляем 5 индекс - номер транзакции по карте
node.AppendSubscript(1);
//создаем новый список
ValueList myList = myConn.CreateList();
//в него передаем через запятую значения, как в $lb: признак дебета/кредита, номер счета кредита/дебета, имя получателя/отправителя, сумма, назначение
myList.Append(0, 26032009100100, "Сидоров Петр Витальевич", 500.26, "Перевод на счет в другом банке");
//присваиваем указатель на список значению узла
node.Set(myList);
//закрываем список
myList.Close();
//перемещаемся на уровень с 4 индексами
node.SetSubscriptCount(4);
//добавляем 5 индекс - номер транзакции по карте
node.AppendSubscript(2);
//создаем новый список
myList = myConn.CreateList();
//в него передаем через запятую значения, как в $lb: признак дебета/кредита, номер счета кредита/дебета, имя получателя/отправителя, сумма, назначение
myList.Append(0, 26118962412531, "Иванов Иван Иванович", 115.54, "Плата за доставку");
//присваиваем указатель на список значению узла
node.Set(myList);
//закрываем список
myList.Close();
Console.WriteLine("Создана информация о счете в Приват банке");
}
Второй вариант записи данных – явное задание значений
В предыдущем разделе упоминалось, что метод Set() может принимать два параметра. Если передается только один – то значение вставляется в текущий узел глобала. Второй параметр предназначен для перечня индексов относительно текущего узла (на который хранится ссылка в переменной типа NodeReference), куда надо вставить значение.
Таким образом, для добавления данных о счете в следующем банке надо сначала вернуться на уровень одного индекса. Для этого снова используется метод SetSubscriptCount() с параметром 1. И дальше, используя метод Set() просто добавляем индексы.
static void CreateSecondBranch(NodeReference node, Connection myConn)
{
//перемещаемся на уровень с 1 индексом
node.SetSubscriptCount(1);
//устанавливаем значение и создаем индекс на 2 уровне - название банка и его ОКПО
node.Set(19807750, "УкрСиббанк");
//устанавливаем значение и создаем индекс на 3 уровне - номер счета и остаток на нем, при этом передаем название банка в качестве первого индекса после ИНН держателя карты
node.Set(65241.24, "УкрСиббанк", 26032009100100);
//создаем массив байтов со SLIP-информацией карты
string slip = "СидоровП | 1965 | SidorovP";
byte[] bytes = System.Text.Encoding.GetEncoding(1251).GetBytes(slip);
//устанавливаем значение и создаем индекс на 4 уровне - SLIP-информация и номер карты, не забываем все предыдущие индексы
node.Set(bytes, "УкрСиббанк", 26032009100100, 6541963285249512);
//создаем список с данными о платеже
ValueList myList = myConn.CreateList();
myList.Append(1, 29244825509100, "Сидоров Петр Витальевич", 500.26, "Перевод на счет в другом банке");
//устанавливаем значение и создаем индекс на 5 уровне - данные о платеже и номер транзакции по карте
node.Set(myList, "УкрСиббанк", 26032009100100, 6541963285249512, 1);
myList.Close();
//создаем список с данными о платеже
myList = myConn.CreateList();
myList.Append(0, 26008962495545, "Сидоров Петр Витальевич", 1015.10, "Перевод на счет в другом банке");
//устанавливаем значение и создаем индекс на 5 уровне - данные о платеже и номер транзакции по карте
//в этом случае не надо менять уровень текущего индекса, поскольку мы до сих пор находимся на уровне 1 индекса и относительно его добавляем новые значения
node.Set(myList, "УкрСиббанк", 26032009100100, 6541963285249512, 2);
myList.Close();
Console.WriteLine("Создана информация о счете в УкрСиббанке");
}
Можно заметить, что этот подход больше всего напоминает код COS для создания глобалов. Точно также мы каждый раз записываем все индексы относительно какого-то базового и добавляем его значение для сохранения в БД.
Третий вариант записи данных – явное задание количества индексов для создания значения на этом уровне иерархии
Последний подход к созданию узлов глобала – явное задание того уровня, на котором вы хотите добавить новое значение. При этом в отличие от предыдущего подхода, указатель перемещается по дереву.
Чтобы указать на какой уровень дерева надо переместиться, используется метод SetSubscript(), в который передается требуемое количество индексов и значение нового индекса, который надо добавить на этом уровне. Для вставки значений снова-таки используется метод Set(), в который передается только один параметр – значение узла.
static void CreateThirdBranch(NodeReference node, Connection myConn)
{
//указываем, что будем создавать новый индекс на 2 уровне - название банка
node.SetSubscript(2, "ПУМБ");
//создаем и сохраняем значение этого узла - ОКПО банка
node.Set(14282829);
//указываем, что будем создавать новый индекс на 3 уровне - номер счета
node.SetSubscript(3, 26008962495545);
//создаем и сохраняем значение этого узла - остаток на счету
node.Set(126.32);
//указываем, что будем создавать новый индекс на 4 уровне - номер карточки
node.SetSubscript(4, 4567098712347654);
//создаем массив байтов со SLIP-информацией карты
string slip = "СидоровПетр 1965 SidorovPetr";
byte[] bytes = System.Text.Encoding.GetEncoding(1251).GetBytes(slip);
//создаем и сохраняем значение этого узла - SLIP-информация
node.Set(bytes);
//указываем, что будем создавать новый индекс на 5 уровне - название банка
node.SetSubscript(5, 1);
//создаем список с данными о платеже
ValueList myList = myConn.CreateList();
myList.Append(0, 29244825509100, "Иванов Иван Иванович", 115.54, "Плата за доставку");
//создаем и сохраняем значение этого узла - данные о платеже
node.Set(myList);
myList.Close();
//указываем, что будем создавать новый индекс на уровне - название банка
node.SetSubscript(5, 2);
//создаем список с данными о платеже
myList = myConn.CreateList();
myList.Append(1, 26032009100100, "Сидоров Петр Витальевич", 1015.54, "Перевод на счет в другом банке");
//создаем и сохраняем значение этого узла - данные о платеже
node.Set(myList);
myList.Close();
Console.WriteLine("Создана информация о счете в ПУМБе");
}
Чтобы вначале каждого запуска программы существующий глобал удалялся, можно вызвать метод Kill().
static void Main(string[] args)
{
try
{
Connection myConn1 = Connect();
NodeReference nodeRef = myConn1.CreateNodeReference("CardInfo");
nodeRef.Kill();
CreateFirstBranch(nodeRef, myConn1);
CreateSecondBranch(nodeRef, myConn1);
CreateThirdBranch(nodeRef, myConn1);
nodeRef.Close();
//ToDo: чтение значений глобала
Disconnect(myConn1);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
Чтение данных
После того, как мы сохранили данные в БД, возможно, нам их надо прочитать и как-то обработать на стороне клиента. Поскольку глобалы похожи на деревья и мы в примере не пропускали индексы, то можем спокойно рекурсивно обойти весь глобал и вывести его значения.
Для обхода будем использовать уже знакомые методы SetSubscript() и AppendSubscript(). Кроме них будем использовать методы:
- NextSubscript() – возвращает значение следующего индекса на это же уровне, похож на функции $Next и $Order в COS.
- GetSubscriptCount() – возвращает количество индексов на текущем уровне дерева.
- HasData() – проверяет, есть ли значение в этом узле, т.е. существует ли он.
- HasSubnodes() – проверяет, есть ли у текущего индекса подиндексы.
static void ReadData(NodeReference node)
{
try
{
//опускаемся на уровень ниже
node.AppendSubscript("");
//находим первый индекс на этом уровне
string subscr = node.NextSubscript();
//проверяем существует ли он
while (!subscr.Equals(""))
{
//пока идекс существует, добавляем в дерево на клиенте значение индекса из БД
node.SetSubscript(node.GetSubscriptCount(), subscr);
//проверяем есть ли в текщем узле данные
if (node.HasData())
{
//выводим значение текущего индекса
Console.WriteLine(" ".PadLeft(node.GetSubscriptCount() * 4, '-') + subscr);
//ToDo: вывести значения узлов
}
//проверяем есть ли подиндексы у этого индекса, т.е. углубляемся в дерево дальше
if (node.HasSubnodes())
{
//если есть, то рекурсивно вызываем эту же функцию и передаем в нее указатель на текущий узел и индекс
ReadData(node);
}
//получаем значение следующего индекса на этом уровне
subscr = node.NextSubscript();
}
}
catch (GlobalsException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
//если возникла ошибка, то возвращемся на уровень выше
node.SetSubscriptCount(node.GetSubscriptCount() - 1);
}
}
которая вызывается из главной программы:
nodeRef = myConn1.CreateNodeReference("CardInfo");
Console.WriteLine("Данные из БД:");
ReadData(nodeRef);
nodeRef.Close();
Видим, что все наши индексы красиво отобразились. Теперь к ним надо добавить сами значения узлов вместо ToDo.
Учитывая то, что все данные хранятся в Caché в виде строк, то в программе необходимо будет, во-первых, помнить на каком уровне какие данные находятся и, во-вторых, преобразовывать типы данных. Для получения значений узлов глобала предназначены функции GetInt(), GetDouble(), GetLong(), GetString(), GetBytes(), GetList() и GetObject(). Они возвращают значение типа string и сразу же преобразовывают его в тип int, double, longInt, string, bytes[], ValueList соответственно. Последняя функция GetObject() возвращает объект, тип которого можно проверить и преобразовать в значение нужного типа.
Как уже отмечалось, все данные возвращаются в виде строки. Если по факту тип данных числовой, то система сможет это определить. А вот списки, массивы байт и сами строки система всегда определяет как строки. Поэтому при обработке таких данных надо учитывать на каком уровне какой тип данных хранится. В связи с чем, не стоит на одном и том же уровне хранить данные разных типов, иначе ничего хорошего не получится. Более того, система не возвращает ошибку при попытке вывода списка, используя методы работы со строками. Вместо этого будет выведен красивый (но непонятный) текст, и это в лучшем случае:
Это так отображается список с данными о платежах без преобразования.
static void GetData(NodeReference node)
{
Object value = node.GetObject();
if (value is string)
{
if (node.GetSubscriptCount() == 1)
{
Console.WriteLine(value.ToString());
}
else if (node.GetSubscriptCount() == 5) {
ValueList outList = node.GetList();
outList.ResetToFirst();
for (int i = 0; i < outList.Length-1; i++)
{
Console.Write(outList.GetNextObject()+", ");
}
Console.WriteLine(outList.GetNextObject());
outList.Close();
}
else if (node.GetSubscriptCount() == 4)
{
string tempString = Encoding.GetEncoding(1251).GetString(node.GetBytes());
Console.WriteLine(tempString);
}
}
else if (value is double)
{
Console.WriteLine(value.ToString());
}
else if (value is int)
{
Console.WriteLine(value.ToString());
}
}
Можно заметить, что некоторые данные отличаются в представлении от тех, которые были заполнены с помощью COS. Это вещественные числа и массивы байтов. При заполнении через Caché eXTreme появилась явное указание на тип данных — $double() для вещественных значений.
Таким образом, мы записали в глобал информацию и потом успешно ее смогли прочитать.
Проект доступен по ссылке.
Официальная документация по Caché eXTreme: Using .NET with Caché eXTreme. В ней вы можете найти еще больше методов для работы с индексами и узлами глобалов и областями имен.
Ссылки на статьи на Хабре:
GlobalsDB — универсальная NoSQL база данных. Часть 1
GlobalsDB — универсальная NoSQL база данных. Часть 2
Часть I. InterSystems GlobalsDB .Net — разведка боем с заглядыванием под капот
Комментарии и вопросы приветствуются!
Автор: Gra-ach