📜 ⬆️ ⬇️

Intersystems Caché: Globals API for .NET - direct access to globals from C #

Recently there was a need to compare the speed of writing / reading data from Intersystems Caché DBMS using different types of access - direct to globals, object and relational. Everything is clear with object and relational access, but we had to deal with direct (or direct access). For those who, like me, at first glance, the documentation did not give a complete understanding of the process, and this article is intended. For example, I will do a console application in the best traditions of procedural programming.

Mini excursion into history ... When I first met Caché, I already had Caché eXTreme for Java, but there was no such technology for .NET yet. And in version 2012.2 it finally appeared. So if you have an older version, you can not even try to do something. In addition, to work, you will need Visual Studio and the .Net Framework at least version 2.0. So, if you have all this, then you can proceed to further settings of the environment.

Setting Environment Variables
')
Especially for the purity of the experiment, put on a "clean" car for the first time Caché. The installer did not register the necessary paths for working with Caché eXTreme into variable environments. It must be done manually.

  1. The variable GLOBALS_HOME should have the full path to the directory where you put the DBMS. In my case, this is C: \ InterSystems \ Cache \
    Setting the GLOBALS_HOME variable


    Note
    Unlike the installation of the Caché DBMS, when installing the GlobalsDB DBMS, this path is recorded automatically! Therefore, if you set up Caché, wrote your program, set up the system, and then for some reason set up GlobalsDB, a big surprise expects you: nothing will naturally work! It will be necessary to manually re-register the path.

  2. The PATH variable must contain the full path to the Bin directory. In my case, this is C: \ InterSystems \ Cache \ Bin

    Note
    Again, if you have several versions of Caché on the same machine, the one that occurs in this variable first will be used in Caché eXTreme. There are "pleasant" situations when one way is registered in GLOBALS_HOME, and another way in PATH. In which of the database changes will be written in this case, I did not check. But the situation is funny: everything seems to have worked without errors, but there is no data.

    Note
    After you have set the necessary environment variables, it is not enough to reboot the database server, you need to reboot Windows (checked) or log out and log into the user (not verified).

Links to libraries

After the environment variables are safely configured, we proceed to setting up the project. In order to work, you need to add links to two libraries:

Both libraries can be found in the folder C: \ InterSystems \ Cache \ dev \ dotnet \ bin \ v2.0.50727
Adding references




Naturally, in the corresponding module in the section you need to add:
using InterSystems.Globals; using InterSystems.Data.CacheClient; 

Data structure

Since Caché eXTreme allows you to record four types of data as indices (they are subscripts): int, double, string, long, and six data types as values: int, double, long, string, bytes [], ValueList, then I tried to come up with a structure for the global, in which there will be as many types as possible. I took the data on bank card transactions as a subject area.
The result is a global with the following structure


It is filled with data


If you fill it in Caché Studio, it looks like this.
 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, "     ") 


Thus, our user “Sidorov Peter Vitalyevich” has three card accounts opened in different banks, each of which has one card attached.

When reading the documentation, I identified for myself three ways to fill in the data. And on each of these accounts, I will show one of them.

Connect to the database

So, before you try to write or read something somewhere, you must first connect to the database. Unlike object / relational access, when using Caché eXTreme, a TCP / IP connection is not made — the application runs in the same stream as the database. Therefore, the Caché server and the application itself must be on the same machine. If you need to access data on a different server, you can use Caché ECP . And this is where the restrictions mentioned in the section “Setting Environment Variables” come into effect, because it is for these settings that the system “understands” where to break it to connect.
Note
When working with Caché eXTreme Event Persistence (XEP), the connection can be established either via TCP / IP or in the same process as the server (as in the Globals API).

It should also be noted that in the process there is only one connection and all C # objects use this particular connection. To get it, use the ConnectionContext.GetConnection () method. To check whether the connection is open or not, the IsConnected () method is used. To open a connection, use the Connect () method to close - Close () .
As a result, the program looks like this.
 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(); } } 


Execution result


The first data entry option is adding indexes.

As already mentioned, I understood for myself three approaches to the construction of globals. The first of these is the phased creation of a tree by adding indexes (deepening from the root).

In order to start building a global one, you need to fix its root. In our case, this is the CardInfo value. This operation is performed using the CreateNodeReference () method:
 NodeReference nodeRef = myConn1.CreateNodeReference("CardInfo"); 

Once we have fixed the pointer to the root of the tree, you can begin to build it. The nodeRef variable will always store a pointer to the node in the tree that is currently active.

To add a new index to a variable, use the AppendSubscript () method, in which you can pass a double, int, long, or string value. To insert a value into the global node, use the Set () method. It can accept values ​​of the type byte [], double, int, long, string, ValueList as the first parameter. The second parameter will be considered further, now it is not needed yet. If everything is clear with byte [], double, int, long, string, it is a standard byte array, real, integer and long integer value and a string, then the last data type is a special data type for working with Caché system lists in COS using the $ ListBuild function (also known as $ lb ).

Note
In general, until the Set () method is executed, the data will not be written to the database. Those. you can build trees with empty elements and thus get sparse arrays.

Thus, we can go down to the leaves of the tree, adding indexes at every step
  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(); 


With each “step” the pointer to the current node will be moved to the newly added index.

However, we have another transaction. To add it in the same way, you need to go up a level. In order to do this (and generally jump to any other level), the SetSubscriptCount () method is used, to which the number of node indices (level number) is passed to where the transition should be made.
In our case, it will look like this.
  node.SetSubscriptCount(4); node.AppendSubscript(2); myList = myConn.CreateList(); myList.Append(0, 26118962412531, "  ", 115.54, "  "); node.Set(myList); myList.Close(); 


Thus, the CreateFirstBranch () procedure will be called in the ToDo program in the program.
 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("      "); } 


Execution result


The second version of the data record is an explicit assignment of values.

The previous section mentioned that the Set () method can take two parameters. If only one is transmitted, then the value is inserted into the current global node. The second parameter is intended for a list of indexes relative to the current node (to which the reference is stored in a variable of NodeReference type) where the value should be inserted.

Thus, to add account data in the next bank, you must first return to the level of a single index. To do this, use the SetSubscriptCount () method with parameter 1. Again, using the Set () method, we simply add indices.
In the program after the procedure CreateFirstBranch () we call the procedure CreateSecondBranch ()
 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("     "); } 


You may notice that this approach most closely resembles COS code for creating globals. In the same way, every time we write all the indices relative to some base one and add its value to be saved in the database.
Execution result


The third way to write data is to explicitly set the number of indexes to create a value at this level of hierarchy

The final approach to creating global nodes is an explicit task of the level at which you want to add a new value. At the same time, unlike the previous approach, the pointer moves through the tree.

To indicate the level of the tree that needs to be moved, the SetSubscript () method is used, to which the required number of indexes is transferred and the value of the new index to be added at this level. To insert values ​​again, the Set () method is used, in which only one parameter is passed — the value of the node.
In the program after the procedure CreateSecondBranch () we call the procedure CreateThirdBranch ()
 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("     "); } 


Execution result


In order to delete the existing global at the beginning of each program launch, you can call the Kill () method.
After all these changes, the Main () procedure of our program is as follows.
 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(); } 


Reading data

After we have saved the data in the database, we may need to read it and somehow process it on the client side. Since globals are like trees and we did not skip indexes in the example, we can safely recursively go around the whole global and output its values.

To work around, we will use the familiar methods SetSubscript () and AppendSubscript () . In addition to them, we will use the methods:


The result is a tree traversal procedure.
 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); } } 


which is called from the main program:
  nodeRef = myConn1.CreateNodeReference("CardInfo"); Console.WriteLine("  :"); ReadData(nodeRef); nodeRef.Close(); 

Execution result


We see that all our indexes are beautifully displayed. Now you need to add the node values ​​themselves instead of ToDo.

Considering that all data is stored in Caché as strings, then the program will need, first, to remember at what level what data is located and, second, to convert data types. The GetInt () , GetDouble () , GetLong () , GetString () , GetBytes () , GetList (), and GetObject () functions are used to get global node values. They return a value of type string and immediately convert it to type int, double, longInt, string, bytes [], ValueList, respectively. The last function GetObject () returns an object whose type can be checked and converted to the value of the desired type.

As already noted, all data is returned as a string. If, in fact, the data type is numeric, the system can determine this. But the system always defines lists, arrays of bytes and the lines themselves as strings. Therefore, the processing of such data must take into account at what level what type of data is stored. In this connection, it is not necessary to store data of different types at the same level, otherwise nothing good will come out. Moreover, the system does not return an error when trying to display a list using the methods of working with strings. Instead, a beautiful (but incomprehensible) text will be displayed, and this is at best:



This is how the list with payment data without conversion is displayed.
Instead of ToDo, we call the GetData () procedure, which appends the value of the corresponding global node to the string.
 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()); } } 


Execution result


In the database we can see the following information.


You may notice that some of the data is different in representation from those that were filled out using COS. These are real numbers and byte arrays. When filling through Caché eXTreme, an explicit reference to the data type appeared - $ double () for real values.

Thus, we recorded in the global information and then successfully read it.

The project is available on GitHub .

Official Caché eXTreme documentation: Using .NET with Caché eXTreme . In it, you can find even more methods for working with indexes and global nodes and namespaces.

Links to articles on Habré:
GlobalsDB is a universal NoSQL database. Part 1
GlobalsDB is a universal NoSQL database. Part 2
Part I. InterSystems GlobalsDB .Net - reconnaissance in force with peeping under the hood

Related Videos

Comments and questions are welcome!

Note
It is worth noting that the meaning of working with globals from .NET and Java is almost the same. Functions are repeated, signatures are very similar. So for those who are familiar with Caché eXTreme for Java, dealing with Caché eXTreme for .NET will not be a problem.

Source: https://habr.com/ru/post/228869/


All Articles