Using .NET with Caché eXTreme
Using the Globals API
[Back] [Next]
   
Server:docs1
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

Caché multidimensional storage provides a very fast, flexible storage model that can be used to implement a wide variety of data structures. Caché uses multidimensional storage to structure data as objects or SQL tables, but many other structures are equally possible. The Globals API allows direct access to global arrays (the basis of the multidimensional storage model), allowing you to implement your own data structures.

The Globals API is optimized for extremely rapid storage and retrieval of a few elementary datatypes — int, long, double, byte[], String, and ValueList (a serialized list) and their associated System types. It can also be used in the same process with the eXTreme Event Persistence (XEP) API, which provides rapid access to data stored in Caché objects.
The Globals API runs in the same process as the Caché instance, rather than communicating via TCP/IP like the standard .NET binding. The Caché server and the .NET application must be on the same machine, but the application can still access data on remote machines if necessary.
For a quick overview of the multidimensional storage model, see Introduction to Global Arrays later in this chapter. See Using Caché Globals for a detailed discussion of their structure and usage in ObjectScript applications. This chapter covers the following topics:
Note:
Globals Terminology
In other parts of the Caché documentation, global arrays are frequently referred to as simply globals. This book uses the longer term global array to avoid any confusion with the Globals API name.
Introduction to the Globals API
This section introduces the basic concepts behind the Globals API. The following topics are discussed:
Introduction to Global Arrays
A Caché global array, like all sparse arrays, is a tree structure rather than a sequentially numbered list. The basic concept behind sparse arrays can be illustrated by analogy to the naming convention for .NET namespaces. Each class is uniquely identified by a qualified name made up of a series of identifiers. For example, consider a namespace containing only two classes, main.foo.SubFoo and main.bar.UnderBar. The five identifiers that make up these qualified names could be viewed as elements of a sparse array:
   main -->|--> foo --> SubFoo
           |--> bar --> UnderBar
Classes could be created that use the other possible qualified names (for example, main.foo or main.bar), but no resources are wasted if those classes do not exist.
Like .NET classes, the elements of a Caché global array are uniquely identified by a namespace consisting of an arbitrary number of identifiers. All namespaces in the global array structure are referred to as nodes. A node must either contain data, have child nodes, or both. A node that has child nodes but does not contain data is called a valueless node.
The root identifier (like main in the .NET namespace) is referred to as the global name. All other identifiers in the namespace are called subscripts. The complete namespace of the node (global name plus subscripts) is the node address.
In Caché notation, a node address is symbolized by a circumflex (^) followed by the global name and a comma-delimited subscript list in parentheses. For example, given a global array using the same identifiers as the .NET namespace example, the nodes in the array would be represented as follows:
   (valueless root node)   ^main
   (valueless node)           ^main("foo")
   (node with value)             ^main("foo","SubFoo") = <value>
   (valueless node)           ^main("bar")
   (node with value)             ^main("bar","UnderBar") = <value>
The root node of the array is simply ^main, with no subscript list. The two nodes containing data are ^main("foo","SubFoo") and ^main("bar","UnderBar"). Nodes ^main, ^main("foo") and ^main("bar") are all valueless.
All descendants of a given node are referred to as subnodes of that node. For example, ^main("foo","SubFoo") is a subnode of ^main("foo"), and all four subscripted nodes are subnodes of ^main.
The term node level refers to the number of subscripts in the subscript list. For example, ^main("bar","UnderBar") is a level 2 node. The address has a level 1 subscript ("bar"), and a level 2 subscript ("UnderBar").
In this example, all of the subscripts are strings, but numeric values can also be used. The Globals API allows subscripts to be specified using .NET datatypes string, int, long, or double, all of which can be used in the same subscript list. For example, ^myNode(1,"two",3.14) is a valid node address.
See the section on Global Array Naming Conventions for a list of naming rules. For more detailed information on the structure of global arrays, see Logical Structure of Globals in Using Caché Globals, and Arrays in the Caché ObjectScript Tutorial.
Simple Applications to Create and Fetch Nodes
This section describes two very simple applications that use the Globals API to create and access nodes in a global array:
It is assumed that these applications have exclusive use of the system, and run in two consecutive processes.
The CreateNodes Program
In CreateNodes, a new Globals connection object is created and connected to the User namespace on the Caché server. A new global array with two nodes is added to the Caché database, the connection is closed, and the program terminates.
The CreateNodes Program: Opening a Connection and Creating Nodes
using System;
using InterSystems.Globals;

public class CreateNodes {
  public static void Main(String[] args) {
    Connection myConn = ConnectionContext.GetConnection();
    try {
      myConn.Connect("User", "_SYSTEM", "SYS");
      NodeReference nodeRef = myConn.CreateNodeReference("myGlobal");
      // Create a new global array with two nodes
      nodeRef.Set("Hello world", "sub1");   // create subnode ^myGlobal("sub1")
      nodeRef.Set("value2");   // store value in root node ^myGlobal
      nodeRef.Close();
      myConn.Close();
    }
    catch (GlobalsException e) { Console.WriteLine(e.Message); }
  } // end Main()
} // end class CreateNodes
In this example, Globals API methods perform the following actions:
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on instances of Connection and NodeReference before they go out of scope or are reused. Failing to close them can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
All of these methods are discussed in detail later in this chapter. See Creating a Connection for information on opening, testing, and closing an eXTreme connection. See Building a Global Array for details about using a NodeReference object to create a node or change its value. See Addressing a Subnode of the Target for information on specifying the subscripts of a node reference.
The FetchNodes Program
This example assumes that FetchNodes runs immediately after the CreateNodes process terminates. FetchNodes creates a new eXTreme connection that accesses the same namespace as CreateNodes. A new node reference object is used to fetch the values of root node ^myGlobal and subnode ^myGlobal("sub1") from the database, demonstrating that the global array has persisted between processes. The global array is then deleted, the connection is closed, and the program terminates.
The FetchNodes Program: Fetching the Values of Existing Nodes
using System;
using InterSystems.Globals;

class FetchNodes {
  public static void Main(String[] args) {
    Connection myConn = ConnectionContext.GetConnection();
    try {
      myConn.Connect("User", "_SYSTEM", "SYS");
      NodeReference nodeRef = myConn.CreateNodeReference("myGlobal");
      // Read both existing nodes
      Console.WriteLine("Value of ^myGlobal is " + nodeRef.GetString());
      Console.WriteLine("Value of ^myGlobal(\"sub1\") is " + nodeRef.GetString("sub1"));
      nodeRef.Kill();   // delete entire array
      nodeRef.Close();
      myConn.Close();
    }
    catch (GlobalsException e) { Console.WriteLine(e.Message); }
  } // end Main()
} // end class FetchNodes
In this example, Globals API methods perform the following actions:
All of these methods are discussed in detail later in this chapter. See Fetching Values from the Database for information on GetString() and other methods that fetch node values of various datatypes. See Addressing a Subnode of the Target for information on specifying the subscripts of a node reference. See Creating and Deleting Nodes for information on deleting persistent nodes.
Creating a Connection
This section describes the Connection class, which encapsulates a reference to the underlying eXTreme Caché database connection.
Note:
It is important to understand that only one eXTreme connection can exist in a process, and all connected objects reference that connection. For example, an xep.EventPersister object and a globals.Connection object would share the same underlying connection (see Using the Globals API with Other eXTreme APIs).
The ConnectionContext.GetConnection() method is used to return a new Connection object. If no connection exists in the current process, Connection.Connect() is called to establish the connection. If a connection already exists, the new Connection object will be connected automatically when it is created, and an attempt to call to Connect() will throw an “already connected” exception.
The following methods of Connection are used to connect, test the connection, and destroy the connection:
The following example creates two Connection objects and shows that they both reference the same underlying connection.
Creating a Connection
  Connection myConn1 = ConnectionContext.GetConnection();
  Connection myConn2 = ConnectionContext.GetConnection();
  myConn1.Connect("User", "_SYSTEM", "SYS");
  Console.WriteLine("myConn1 is " + myConn1.IsConnected()
                   + "; myConn2 is " + myConn2.IsConnected());
// Prints: myConn1 is true; myConn2 is true
  myConn2.Close();
  Console.WriteLine("myConn1 is " + myConn1.IsConnected()
                   + "; myConn2 is " + myConn2.IsConnected());
// Prints: myConn1 is false; myConn2 is false
  myConn1.Close();  // release native resources
This example assumes that no other connections already exist elsewhere in this process.
Connection object myConn1 is created by a call to ConnectionContext.GetConnection(). Connection object myConn2 is defined in the same way, but since a connection already exists, myConn2 will reference the same connection as myConn1. The second call to GetConnection() is equivalent to Connection myConn2 = myConn1;.
The myConn1 object is used to call Connect(), establishing a connection to database namespace User. Each connection object makes a call to IsConnected(). Both calls return true, proving that both objects can now access the database, even though only one of them called Connect().
The myConn2 object calls Close(), which closes the underlying connection and releases associated resources. This must be done to prevent memory leaks, since memory allocated by the underlying native code cannot be released by .NET garbage collection.
Both objects call IsConnected() again to demonstrate that they are both disconnected now, even though Close() has only been called once.
Even though the connection has been closed, Close() is also called on myConn1 to release any remaining native code resources it may be holding.
Building a Global Array
This section describes the methods of the NodeReference class that allow it to specify a node address in a global array and add, change, or delete the node value. The following topics are discussed:
This section deals primarily with storing data in various global array structures. See Finding Nodes in a Global Array and Fetching Values from the Database for a detailed discussion of data retrieval methods and techniques.
Note:
Target Address and Target Node
A node address consists of a global name plus a list of zero or more subscripts. A NodeReference object contains global name and subscript list properties that store a reference to one possible address within the named global array. The address stored in these properties is referred to as the target address. If the database contains a node at the target address, it is referred to as the target node. See Changing the Target Address for information on how to change the contents of the subscript list, and Addressing a Subnode of the Target for a description of how to specify a subnode address under the target address.
See Introduction to Global Arrays for a quick overview of the terms and concepts discussed here.
Introduction to Node References
The NodeReference class encapsulates a reference to a node in a global array. It provides methods to specify the address of the node, and to add, change, or delete the corresponding persistent node in the database. The following method of Connection creates an instance of NodeReference:
Nothing is added or changed in the database when an instance of NodeReference is created. The database is affected only when you explicitly call a NodeReference method such as Set(), which takes a value argument and assigns the value to a node in the database. If there is no node at the specified address, a new node is created. If the global array does not yet exist, it is created when the node is added. The following example creates an instance of NodeReference and uses Set() to create two nodes in global array ^myGlobal:
Creating Nodes with NodeReference
By default, the Set() method acts on the target address, but the value argument can be followed by any number of optional subscript arguments that specify a subnode of the target node (see Addressing a Subnode of the Target for details). The following example assigns values to two nodes, and then changes one of them:
  nodeRef = myConn.CreateNodeReference("myGlobal"); // target is root node ^myGlobal
  nodeRef.Set("blue",3);   // create node ^myGlobal(3) = "blue"
  nodeRef.Set(123);        // set value of root node ^myGlobal to 123
  nodeRef.Set("red", 3);   // change value of ^myGlobal(3) to "red"
The call to CreateNodeReference() creates an instance of NodeReference named nodeRef, and specifies "myGlobal" as the value of the global name property. The target address of nodeRef is root node ^myGlobal, but nothing has been stored in the database yet. If ^myGlobal does not already exist in the database, it will not be created until the first call to Set() adds a new persistent node.
The first call to Set() specifies value blue and subscript 3, so the value is assigned to subnode ^myGlobal(3). The database now contains persistent global array ^myGlobal, which has two nodes: ^myGlobal(3) and valueless root node ^myGlobal.
The second call to Set() specifies only value 123 with no subscripts, so it assigns a value to target node ^myGlobal.
The final call specifies subnode ^myGlobal(3) again, changing its value to red.
The Set() method is one of four NodeReference methods that can change the contents of the database. See Creating and Deleting Nodes for a detailed description of these methods.
Addressing a Subnode of the Target
By default, NodeReference methods act on the target node (described in the previous section, Introduction to Node References). Although the target address can be changed (see Changing the Target Address), it is frequently easier to access subnodes under the target node by specifying the extra subscripts in the method call. For example, if the target address of NodeReference nodeRef is ^myGlobal("Main"), the following method call sets the value of subnode ^myGlobal("Main","A","1"):
   nodeRef.Set("This is a new value!","A","1");   // Assign string value to ^myGlobal("Main","A","1")
The method call generates a subnode address by adding the subscript arguments to a copy of the subscript list stored in nodeRef, but the actual target address of nodeRef never changes.
The following example sets the values of several different subnodes without changing the target address:
Addressing a Subnode of the Target
  nodeRef = myConn.CreateNodeReference("myGlobal");
  nodeRef.AppendSubscript("A");       // Target address is ^myGlobal("A")
  nodeRef.Set("myvalue3",3,"y","z");  // sets ^myGlobal("A",3,"y","z") = "myvalue3"
  nodeRef.Set("myvalue2",2);          // sets ^myGlobal("A",2) = "myvalue2"
  nodeRef.Set("myvalue0");            // sets target address ^myGlobal("A") = "myvalue0"
The call to CreateNodeReference() creates nodeRef with global name "myGlobal", and sets the target address to root node ^myGlobal.
The call to AppendSubscript() adds subscript "A", changing the target address to level 1 node ^myGlobal("A") (see Changing the Target Address for details).
The first call to Set() specifies subscript arguments 3, "y", and "z", and the target address is ^myGlobal("A"), so a new node with value "myvalue3" is created at subnode address ^myGlobal("A",3,"y","z").
The second call to Set() specifies subscript 2 and creates a new node at subnode address ^myGlobal("A",2).
The final call to Set() does not specify a subnode address, so it assigns the value to target node ^myGlobal("A").
In addition to Set() and Increment(), subscript arguments can be used by the following NodeReference methods:
Creating and Deleting Nodes
The Globals API contains numerous methods to connect to a Caché database and read data from the global arrays stored there, but the NodeReference class contains the only four methods that can actually make changes in the database: Set(), Increment(), Kill(), and KillNode(). This section describes how these methods are used.
Creating and Changing Nodes with Set() and Increment()
The following NodeReference methods can be used to create a persistent node with a specified value, or to change the value of an existing node:
Both methods can take optional subscript arguments to access a subnode of the target node (see Addressing a Subnode of the Target). The following example uses both Set() and Increment() to create and change node values:
Creating and Deleting Nodes: Setting and incrementing node values
// Target address of nodeRef is root node ^myGlobal
  int count = 0;
  nodeRef.Set(count, "A");     // create node ^myGlobal("A") = 0
  nodeRef.Set(++count, "A");   // change value of ^myGlobal("A") to 1.
  nodeRef.Set(("value=" + (++count)), "A");   // change value to string "value=2"

// create ^myGlobal("B")=-2, then decrement existing value two times
  for (int loop = 0; loop < 3; loop++) {
    nodeRef.Increment(-2,"B");   // final value of ^myGlobal("B") is -6.
  }
The first call to Set() creates a new node at subnode address ^myGlobal("A") and sets the value of the node to count. The second call changes the value of the subnode, replacing it with the incremented value of count. The third call assigns String value "value=2" to the subnode. Unlike Increment(), the Set() method can assign values of any supported datatype, including byte[], String, or ValueList.
In the for loop, Increment() is called three times. The first call creates new subnode ^myGlobal("B") with value -2. The next two calls each change the existing value by -2, resulting in a final value of -6. The Increment() method can both create and increment a node with a numeric value. Unlike Set(), it uses a thread-safe atomic operation that never locks the node.
Deleting Nodes with Kill() and KillNode()
A node is deleted from the database when it is valueless and has no subnodes with values. When the last value is deleted from a global array, the entire global array is deleted. The following methods of NodeReference are used to delete one or more node values from the database:
Both Kill() and KillNode() can take optional subscript arguments to access a subnode of the target node (see Addressing a Subnode of the Target), as demonstrated in the following examples.
These examples assume that root node ^myGlobal is the target address of nodeRef, and that the global array initially contains the following nodes:
   ^myGlobal = 0
   ^myGlobal("A") = 0
   ^myGlobal("A",1) = 0
   ^myGlobal("B") = 0
   ^myGlobal("B",1) = 0
   ^myGlobal("B",2) = 0
Creating and Deleting Nodes: Using KillNode() to delete only one specified node value
  nodeRef.KillNode("A");   // only delete value of ^myGlobal("A")
The call to KillNode() deletes only the value of specified subnode ^myGlobal("A"). It is now valueless, but it is still part of the global array because its child node ^myGlobal("A",1) still has a value.
Creating and Deleting Nodes: Using Kill() to delete a group of nodes or an entire global array
  nodeRef.Kill("B");   // kill ^myGlobal("B") and all of its subnodes
    // Now only these nodes are left:
    //   ^myGlobal = 0
    //   ^myGlobal("A")  <valueless node>
    //   ^myGlobal("A",1) = 0
  nodeRef.Kill();   // kill all nodes in global array ^myGlobal
The first call to Kill() deletes the values of specified subnode ^myGlobal("B") and both nodes under it. All of these nodes are deleted from the global array (unlike ^myGlobal("A") in the KillNode() example) because they no longer have values or subnodes with values. The three remaining nodes in the global array are unaffected.
No subscript argument is specified in the second call to kill(), so it deletes the values of target node ^myGlobal and all of its subnodes. This means that the entire global array is deleted from the database because it no longer contains any nodes with values.
Changing the Target Address
It is often convenient to access a node by specifying a subnode address in the method call (see Addressing a Subnode of the Target). However, a hard-coded list of subscript arguments quickly becomes difficult to manage when working with large numbers of nodes (as demonstrated later in Finding Nodes in a Global Array). In such cases, it is more practical to change the target address stored in the node reference. This section describes methods that control the subscript list property of a NodeReference instance, allowing it to target any possible address in the global array. (Methods also exist to control the global name property, as described later in Accessing Multiple Global Arrays).
Note:
Subscript Lists and Node Levels
The term node level refers to the number of subscripts in the subscript list. For example, ^myGlobal("a","b","c") is a “level 3 node,” which is just another way of saying “a node with three subscripts.”
The following methods are used to manage the subscript list:
The following example constructs a node address in NodeReference object nodeRef, and then calls Set() to store a value at the target address, creating global array ^myGlobal in the Caché database. The example then resets the target address to the root node and deletes the global array from the database:
Changing the Target Address: Adding and deleting subscripts
// Add subscripts and create a node
  nodeRef = myConn.CreateNodeReference("myGlobal"); // target address is ^myGlobal
  nodeRef.AppendSubscript("A");     // target address is  ^myGlobal("A")
  nodeRef.AppendSubscript(1);   // target address is  ^myGlobal("A",1)
  nodeRef.Set("hay");   // create persistent level 2 node ^myGlobal("A",1) = "hay"

// Re-initialize nodeRef and ^myGlobal
  nodeRef.SetSubscriptCount(0);   // set target address to root node ^myGlobal
  nodeRef.Kill();   // delete ^myGlobal from database
The call to CreateNodeReference() specifies "myGlobal" as the global name property in nodeRef, so the initial target address is root node ^myGlobal.
Each call to AppendSubscript() adds a subscript to the target address, changing it to ^myGlobal("A",1). No changes have been made to the database yet.
The call to Set() creates a persistent node at target address ^myGlobal("A",1).
The call to SetSubscriptCount() specifies 0 as the node level, removing all subscripts from the subscript list. The target address is now root node ^myGlobal, so the call to Kill() deletes the entire global array from the database. Both nodeRef and the database are now in the same state as when nodeRef was created.
Both SetSubscriptCount() and SetSubscript() will throw exceptions if given an invalid level argument. To avoid such errors, you can call the GetSubscriptCount() method to get the current node level. This method allows you to set the current level relative to another level, as demonstrated in the following example:
Changing the Target Address: Controlling node level and changing subscript values
// Set target to ^myGlobal(1,2,"3A") and add a node
  int count = 0;
  nodeRef.AppendSubscript(1);
  nodeRef.AppendSubscript(2);
  nodeRef.AppendSubscript("3A");
  nodeRef.Set(++count);   // ^myGlobal(1,2,"3A") = 1

// Set target to ^myGlobal(1,2,"3B") and add two subnodes
  nodeRef.SetSubscript(nodeRef.GetSubscriptCount(),"3B");
  nodeRef.Set(++count,"x");   // ^myGlobal(1,2,"3B","x") = 2
  nodeRef.Set(++count,"y");   // ^myGlobal(1,2,"3B","y") = 3

//  Assign values to nodes at levels 2, 1, and 0, in that order
  while (nodeRef.GetSubscriptCount()>0) {
    nodeRef.SetSubscriptCount(nodeRef.GetSubscriptCount()-1);
    nodeRef.Set(++count);
  }
The calls to AppendSubscript() add three subscripts to the target address, changing it to ^myGlobal(1,2,"3A"). This changes the subscript list in nodeRef, but no changes have been made to the database yet.
The first call to Set() specifies value 1 with no subscript argument, so it creates a node at target address ^myGlobal(1,2,"3A"). Global array ^myGlobal is added to the database when this persistent node is created.
The GetSubscriptCount() method returns the current node level (level 3, since the target address has three subscripts), and SetSubscript() changes the value of the level 3 subscript. This changes the target address from ^myGlobal(1,2,"3A") to ^myGlobal(1,2,"3B").
The next two calls to Set() specify subscript arguments "x" and "y", so new nodes are created at subnode addresses ^myGlobal(1,2,"3B","x") and ^myGlobal(1,2,"3B","x").
In the while loop, the following actions are performed:
The loop terminates when the current node level is 0, indicating that nodeRef is back to the starting target address, root node ^myGlobal.
This example produces the following global array
^myGlobal = 6
  ^myGlobal(1) = 5
    ^myGlobal(1,2) = 4
      ^myGlobal(1,2,"3A") = 1
      ^myGlobal(1,2,"3B") = <valueless>
        ^myGlobal(1,2,"3B","x") = 2
        ^myGlobal(1,2,"3B","y") = 3
Printing and Copying the Target Address
This section describes methods that read subscripts from the subscript list of a NodeReference object. These methods can be used to print the current target address or duplicate a node reference.
The following methods of NodeReference return the global name or an item in the subscript list:
Each subscript method takes a subscriptPosition argument that specifies the node level of the subscript to be read. For example, if the target address of nodeRef is ^mySubscripts(101,9876543210L,3.14,"string value"), the following code will create an exact copy in newNode:
    NodeReference newNode = myConn.CreateNodeReference(nodeRef.GetName());  // ^mySubscripts
    newNode.AppendSubscript(nodeRef.GetIntSubscript(1));     // 101
    newNode.AppendSubscript(nodeRef.GetLongSubscript(2));    // 9876543210L
    newNode.AppendSubscript(nodeRef.GetDoubleSubscript(3));  // 3.14
    newNode.AppendSubscript(nodeRef.GetStringSubscript(4));  // "string value"
This code will throw GlobalsException if the target address has less than four subscripts, or if one of the subscripts is of an incompatible datatype.
Note:
Creating a duplicate node reference
In some cases, it may be useful to retain an accurate copy of a target address before performing an operation that may change it. It is important to remember that the typed subscript list exists only in memory, as a property of the NodeReference object. In the Caché database, all subscripts are normalized as strings and the original datatype distinctions are lost. When retrieving node addresses from the database (as described later in Finding Nodes in a Global Array), there is no simple way to recover the original datatype of the subscript.
The previous example works only if given the correct number of subscripts and the correct datatype for each subscript. The following example uses GetSubscriptCount() and GetObjectSubscript() to obtain this information, and can print any given target address by reading the subscript list:
Printing and Copying the Target Address: Printing subscripts of any datatype
  Object sub = null;
  try {
    Console.Write("^" + nodeRef.GetName() +"(");
    for (int ii=1; ii<=nodeRef.GetSubscriptCount(); ii++) {
      sub = nodeRef.GetObjectSubscript(ii);
      if (sub is int) Console.Write(sub);
      else if (sub is long) Console.Write(sub + "L");
      else if (sub is Double) Console.Write(sub);
      else if (sub is String) Console.Write("\"" + sub + "\"");
      else Console.Write("[invalid class]");
      if (ii < nodeRef.GetSubscriptCount()) Console.Write(", ");
    }
    Console.WriteLine(")");
  }
  catch (GlobalsException e) { Console.WriteLine(e.Message); }
If the target address is the one copied in the previous example, the following line is printed:
  ^mySubscripts(101, 9876543210L, 3.14, "string value")
The GetName() method is called to read and print the global name.
The GetSubscriptCount() method specifies the number of iterations in the for loop.
In the loop, the GetObjectSubscript() method is called for each subscript, and the is operator is used to determine the subscript datatype. Using this information, each subscript is printed in an appropriate format: quotes are placed around a String, and an L is added after a long.
Finding Nodes in a Global Array
The Globals API provides ways to iterate over part or all of a global array. The following topics describe the various Globals iteration methods:
Iterating Over a Set of Child Nodes
Child nodes are sets of subnodes immediately under the same parent node. Any child of the current target node can be addressed by adding only one subscript to the target address. All child nodes under the same parent are sibling nodes of each other. For example, the following global array has five sibling nodes under parent node ^myNames("people"):
  ^myNames                               (valueless root node)
     ^myNames("people")                  (valueless level 1 node)
        ^myNames("people","Anna") = 2
        ^myNames("people","Julia") = 4
        ^myNames("people","Misha") = 5
        ^myNames("people","Ruri") = 3
        ^myNames("people","Vlad") = 1
The NodeReference class provides the following methods for iterating over a set of sibling nodes:
Each method starts at a specified address, finds the first sibling node in collation order, and returns its highest level subscript (or "" if no node is found). See Collation Order in Multilevel Global Arrays for details about how Caché determines collation order.
Setting the Starting Address
By default, the search will start at the current target address. To find a child of the target node, you must specify a subscript argument (see Addressing a Subnode of the Target) so the search will start at an address on the same level as the child nodes.
For example, if the target address is ^myNames("people"), a call to nextSubscript("Misha") will start the search at subnode address ^myNames("people","Misha"), and return "Ruri" as the highest level subscript of the next node in collation order. A call to previousSubscript("Misha") will return "Julia". The starting address does not have to be an existing node. For example, a call to nextSubscript("M") would return "Misha".
To find the first or last node in collation order, specify an empty string ("") as the subscript argument. For example, the following code returns the highest level subscript of the first and last siblings under target node ^myNames("people"):
// Target node is ^myNames("people")
   String firstChild = nodeRef.NextSubscript("");   // returns string "Anna"
   String lastChild = nodeRef.PreviousSubscript("");   // returns string "Vlad"
Finding the Other Subscripts
Once the first subscript has been recovered, the process can be repeated to return subscripts for all further sibling nodes in the same direction. The following example prints the node value and subscript for each child node under ^myNames("people"):
Iterating Over a Set of Child Nodes: Finding all sibling nodes under ^myNames("people")
// Set the node reference to target address ^myNames("people")
  nodeRef.SetSubscriptCount(0);
  nodeRef.AppendSubscript("people");

// Read child nodes in ascending order until NextSubscript() returns ""
  Console.Write("Ascend from first node:");
  String subscr = nodeRef.NextSubscript("");  // start before first node
  while (!subscr.Equals("")) {
    Console.Write(" \"" + subscr + "\"=" + nodeRef.GetInt(subscr));
    subscr = nodeRef.NextSubscript(subscr);
  }
This code prints the following line:
  Ascend from first node: "Anna"=2 "Julia"=4 "Misha"=5 "Ruri"=3 "Vlad"=1
The call to CreateNodeReference() creates a node reference for ^myNames, and the first call to AppendSubscript() sets the target address to ^myNames("people").
The call to NextSubscript() specifies an empty string (""), and returns the subscript of the first sibling node in ascending order. The value of subscr is now "Anna".
Each iteration of the while loop performs the following actions:
The loop will terminate when NextSubscript() returns an empty string, indicating that there are no more sibling nodes in ascending order. (For clarity, this example uses !subscr.Equals("") as the terminating condition. You may prefer an alternative such as !(subscr.Length==0)).
This example is extremely simple, and would fail in several situations. What if we don’t want to start with the first or last node? What if the code attempts to fetch a value from a valueless node? What if the global array has data on more than one level? The following sections describe how to deal with these situations.
Testing for Node Values
The next example will search a slightly more complex set of subnodes, using ^myNames("dogs") as the target node:
  ^myNames                               (valueless root node)
     ^myNames("dogs")                    (valueless level 1 node)
        ^myNames("dogs","Balto") = 6
        ^myNames("dogs","Hachiko") = 8
        ^myNames("dogs","Lassie")        (valueless level 2 node)
           ^myNames("dogs","Lassie","Timmy") = 10
        ^myNames("dogs","Whitefang") = 7
Parent node ^myNames("dogs") has five subnodes, but only four of them are child nodes. In addition to the four level 2 subnodes, there is also a level 3 subnode, ^myNames("dogs","Lassie","Timmy"). The search will not find "Timmy" because this subnode is the child of "Lassie" (not "dogs"), and therefore is not a sibling of the others.
Although node ^myNames("dogs","Lassie") has a child node, it does not have a value. If the example attempted to call getInt("Lassie"), an exception would be thrown. To avoid this problem, the following method is called to test for values:
The following example searches for children of ^myNames("dogs") in descending order, starting at subnode address ^myNames("dogs","W").
Testing for Values: descending from "W"
  nodeRef.SetSubscriptCount(0);
  nodeRef.AppendSubscript("dogs");
  String subscr = nodeRef.PreviousSubscript("W");
  Console.Write("Start at \"W\" and descend:");
  while (!subscr.Equals("")) {
    Console.Write(" \"" + subscr + "\"");
    if (nodeRef.HasData(subscr)) Console.Write("=" + nodeRef.getInt(subscr));
    subscr = nodeRef.PreviousSubscript(subscr);
  }
This code prints the following line:
Start at "W" and descend: "Lassie" "Hachiko"=8 "Balto"=6
The calls to SetSubscriptCount() and AppendSubscript() set the target address to ^myNames("dogs").
The first call to PreviousSubscript() specifies ^myNames("dogs","W") as the starting address, and returns "Lassie". which is the first sibling node that has a level 2 subscript lower than "W" in collation order.
In the while loop, each level 2 subscript lower than "W" is returned and printed in descending order. If HasData() is true, the node value is also printed. The loop terminates when PreviousSubscript() returns an empty string.
Finding Subnodes on All Levels
In the previous example, the search misses many of the nodes in global array ^myNames because the scope of the search is restricted in various ways:
The problem in both cases is that PreviousSubscript() and NextSubscript() only find nodes that are under the same parent and on the same level as the starting address. You must specify a different starting address for each group of sibling nodes.
A node can be addressed either by making it the target node or by specifying extra subscripts in a method call (as described in Changing the Target Address and Addressing a Subnode of the Target). For example, if you start at target address ^myNames("dogs"), there are two ways to do a search that will find ^myNames("dogs","Lassie","Timmy"):
  1. Specify two subscript arguments:
       String nextNode = nodeRef.NextSubscript("Lassie","");   // returns "Timmy"
  2. Change the target address of the node reference:
       nodeRef.AppendSubscript("Lassie");   // Target address is now ^myNames("dogs","Lassie")
       String nextNode = nodeRef.NextSubscript("");   // returns "Timmy"
       nodeRef.SetSubscriptCount(nodeRef.GetSubscriptCount()-1);   // go back to ^myNames("dogs")
These two searches produce identical results because both of them start at subnode address ^myNames("dogs","Lassie",""). The first example is simpler in this case, but it is not necessarily the best choice. To reach all node levels of a global array, you would have to hard-code a new search for each lower node level, with one extra subscript argument per level.
The second example demonstrates a technique that can be used on any node level to reach the next lower level. On each level, the following NodeReference method can be used to determine if the search has found a node that has subnodes:
The NodeFinder() method listed in the following example iterates over all nodes in global array ^myNames, making recursive calls to process all levels. It processes each set of child nodes just like the previous examples. Whenever HasSubnodes() indicates that a child node has descendants, NodeFinder() is called again with the child node as the target address of nodeRef.
Finding Subnodes on All Levels: The NodeFinder() method
  try {
    nodeRef.SetSubscriptCount(0);
    Console.WriteLine("Find all nodes under ^myNames");
    NodeFinder(nodeRef,"^myNames(");
  }
  catch (GlobalsException e) { Console.WriteLine(e.Message); }
The initialization code sets the target address of nodeRef to root node ^myNames and calls NodeFinder() for the first time. The string "^myNames(" will begin each printed node address. A full implementation would replace this code with a wrapper function that accepts any starting address and prints the starting node (see Printing and Copying the Target Address).
  public static void NodeFinder(NodeReference aNodeRef, String aPrefix) throws GlobalsException {
    String sub = aNodeRef.NextSubscript("");
    while (!sub.Equals("")) { 
      Console.Write(aPrefix + sub + ")");
      if (aNodeRef.HasData(sub)) Console.WriteLine(" = " + aNodeRef.GetInt(sub));
      else Console.WriteLine("   (valueless node)");
      if (aNodeRef.HasSubnodes(sub)) {
        aNodeRef.AppendSubscript(sub);   // go down to next level
        NodeFinder(aNodeRef,aPrefix + sub + ",");
        aNodeRef.SetSubscriptCount(aNodeRef.GetSubscriptCount()-1); // come back up
      }
      sub = aNodeRef.NextSubscript(sub);
    }
  }
The first call to NextSubscript() passes an empty string as the subscript argument, and returns "dogs", which is the first level 1 subscript under the root node.
Each iteration of the while loop performs the following actions:
The loop terminates when NextSubscript() returns an empty string.
On the first iteration of the loop, root node ^myNames is the parent node, and the current subnode is ^myNames("dogs"), so the line "^myNames("dogs") (valueless node)" is printed. Since this node has subnodes, a second instance of NodeFinder() is called with ^myNames("dogs") as the new target address.
The second instance prints all children of ^myNames("dogs"). It also calls a third instance of NodeFinder(), which prints level 3 subnode ^myNames("dogs","Lassie","Timmy") and returns. Once all subnodes of ^myNames("dogs") have been printed and the second instance returns, the process is repeated for ^myNames("people") and its subnodes.
This code prints the following lines:
Find all nodes under ^myNames
^myNames(dogs)   (valueless node)
^myNames(dogs,Balto) = 6
^myNames(dogs,Hachiko) = 8
^myNames(dogs,Lassie)   (valueless node)
^myNames(dogs,Lassie,Timmy) = 10
^myNames(dogs,Whitefang) = 7
^myNames(people)   (valueless node)
^myNames(people,Anna) = 2
^myNames(people,Julia) = 4
^myNames(people,Misha) = 5
^myNames(people,Ruri) = 3
^myNames(people,Vlad) = 1
This output is similar to what you would see in the Management Portal by going to [System Explorer] > [Globals], selecting the User namespace from the list on the left, and clicking the View link for ^myNames.
Fetching Values from the Database
The Globals API supports node values of datatype int, long, double, String, byte[], or Valuelist. For simplicity, examples in previous sections of this chapter have always used int or String node values, but the NodeReference class also provides datatype-specific methods for fetching the other supported datatypes. In addition, the GetObject() method fetches node values as Object variables that can be cast to a specific datatype. This section describes how to fetch the full range of supported datatypes from the database. The following topics are discussed:
Also see Using Serialized Value Lists for a complete discussion of the GetList() method, which fetches a node value as a ValueList. This class is a serialized list that can store any supported datatype, including a nested ValueList.
Fetching Values of Known Datatype
The following NodeReference methods assume that the node value is numeric, and attempt to convert it to an appropriate .NET variable.
The numeric fetch methods will throw UndefinedException if the target node is valueless or does not exist. All of them can take optional subscript arguments to access a subnode of the target node (see Addressing a Subnode of the Target).
In the Caché database, String, byte[], and ValueList objects are all stored as strings, and no information about the original datatype is preserved. The following NodeReference methods fetch String data and return it in the desired format:
The string fetch methods assume that the node value is non-numeric, and attempt to convert it appropriately. They return null if the target node is valueless or does not exist. These methods do not perform any type checking, and will not usually throw an exception if the node value is of the wrong datatype.
Important:
Fetch methods do not check for incompatible datatypes
These methods are optimized for speed, and never perform type checking. Your application should never depend on an exception being thrown if one of these methods attempts to fetch a value of the wrong datatype. Although an exception may be thrown, it is more likely that the method will return an inaccurate or meaningless value.
Given an int value, all numeric methods return meaningful values. The GetInt() and GetLong() methods cannot be applied to double values with reliable results, and may throw an exception for some double values.
Fetching Node Values: Using GetInt(), GetLong(), and GetDouble()
  nodeRef.Set(23);
  int intVal = nodeRef.GetInt();   // returns 23
  long longVal = nodeRef.GetLong();   // returns 23
  double doubleVal = nodeRef.GetDouble();   // returns 23.0

  nodeRef.Set(10.1234);
  doubleVal = nodeRef.GetDouble();   // returns 10.1234
// Do not use GetInt() or GetLong() to fetch double values!
Binary data can be stored as an array of byte and fetched with GetBytes(). The following example creates a byte array and prints it out as a list of unsigned integer values. For convenience, this example uses byte values that correspond to characters.
Fetching Node Values: Using GetBytes()
  byte[] testBytes = {(byte)'C',(byte)'a',(byte)'c',(byte)'h',(byte)'é'};
  nodeRef.Set(testBytes);
  Console.Write("Bytes fetched as byte[]:");
  byte[] outBytes = nodeRef.GetBytes();
  for (int ii = 0; ii < outBytes.Length; ii++) {
    Console.Write(" " + ((int)outBytes[ii] & 0xff));  // get byte as unsigned int
  }
  Console.WriteLine();
  // prints: Bytes fetched as byte[]: 67 97 99 104 233
By default, byte values greater than 127 are interpreted as negative numbers. This example masks each byte with 0xff to produce an unsigned integer for printing.
Fetching Values of Unknown Datatype
If a node has a value of datatype int, long, double, or String, the node value can be returned as an Object and cast to the appropriate datatype. The following method of NodeReference returns Object values:
Note:
Applications must know the original string datatype
Since String, byte[], and ValueList objects are all stored as strings in the database, all three datatypes will be interpreted as instances of String by the instanceOf operator. String objects returned by GetObject() can not be cast to the correct datatype unless the calling application knows the original datatype.
The following example creates nodes with values of int, long, double, and String datatypes, fetches each of them with GetObject(), and casts each returned Object to the appropriate datatype. In this example, both Set() and GetObject() use subscript arguments to specify the subnode that will be accessed.
Fetching Node Values: Using GetObject()
  nodeRef.Set(13, 1);              // ^MyGlobal(1) = 13
  nodeRef.Set(-4555666777L, 2);    // ^MyGlobal(2) = -4555666777L
  nodeRef.Set(1.234, 3);           // ^MyGlobal(3) = 1.234
  nodeRef.Set("hello", 4);         // ^MyGlobal(4) = "hello"

  for (int subnode = 1; subnode <= 4; subnode++) {
    Console.Write("node ^myGlobal(" + subnode + ") is type ");
    Object obj = nodeRef.GetObject(subnode);
    if (obj is String) Console.WriteLine("String: \"" + obj + "\"");
    else if (obj is int) Console.WriteLine("int: " + obj);
    else if (obj is long) Console.WriteLine("long: " + obj + "L");
    else if (obj is double) Console.WriteLine("double: " + obj);
    else Console.WriteLine("invalid class!"); 
  }
The calls to Set() produce four new nodes of various datatypes.
The GetObject() method returns each node value as an Object, and the is operator is used to determine the original datatype.
This example produces the following output:
  node ^myGlobal(1) is type int: 13
  node ^myGlobal(2) is type long: -4555666777L
  node ^myGlobal(3) is type double: 1.234
  node ^myGlobal(4) is type String: "hello"
Using Serialized Value Lists
The ValueList class encapsulates a serialized list that can store an arbitrary number of elements. Each element can be any one of the supported datatypes: int, long, double, String, byte[], or nested ValueList. In the database, a ValueList of any size is stored in a single node, encoded as a binary string that uses the Caché $LIST format. This format is transparent to .NET applications, but a node containing a ValueList can be interpreted as a $LIST by Caché ObjectScript or any Caché language binding that supports $LIST format.
This section discusses the following topics:
Creating, Storing, and Fetching a ValueList
Instances of ValueList are created and fetched by the following methods:
The following ValueList methods are used to populate and destroy a instance:
The Append() and Clear() methods are the only ways to change the contents of a list. There are no methods to delete a single element, insert a new element in the middle of a list, or change the value of an existing element.
In the following example, ValueList instance myList is created and populated with elements of datatype int, long, double, String, and byte[]. The example then saves the list to the database.
Creating and Storing a ValueList
// Create a list, append elements of different types, and store it in ^myListNode
  ValueList myList = myConn.CreateList();
  byte[] testBytes = {(byte)'H',(byte)'i',(byte)'!'};
  myList.Append(13, -4555666777L,  1.234, "hello", testBytes);
  nodeRef = myConn.CreateNodeReference("myListNode");
  nodeRef.Set(myList);
The Connection.CreateList() method creates an empty instance of ValueList named myNewList.
The call to Append() adds five elements of different types, and then Set() stores the list in node ^myListNode.
The following example clears myList (created in the previous example) and fills it with new data. It then fetches the previously stored list from node ^myListNode, and appends the new ValueList as a nested element in the old list.
Fetching a ValueList and Storing a Nested Valuelist
// Delete all elements from myList and re-use it to create a different list
  myList.Clear();
  myList.Append("world", 7, 3.1, 9876543210L);

// Fetch the old list, then store the new list as a nested element in the old list
  ValueList myLongerList = nodeRef.GetList();   // fetch previously created list
  myLongerList.Append(myList);   // append the new list object as a single element 
  nodeRef.Set(myLongerList);
  myList.Close();
  myLongerList.Close();
The call to Clear() deletes all existing elements from the myList instance so it can be re-used, and the next call to Append() fills the empty list with four new elements.
The NodeReference.GetList() method fetches the list previously stored in ^myListNode and assigns it to a new instance of ValueList named myLongerList. The myList object is appended as the sixth element in myLongerList, and the updated list is again stored in persistent node ^myListNode.
After this update process is completed, both instances of ValueList are destroyed by calling their Close() methods.
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on instances of ValueList before they go out of scope or are reused. Failing to close them can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
Reading ValueList Elements
The ValueList class provides methods that return an element value of known datatype as a variable of the desired datatype.
Reading Numeric Elements
The following datatype-specific methods are used to read numeric elements from the ValueList object:
All of the numeric methods return 0 if the list element is null.
Reading Non-numeric Elements
The following datatype-specific methods are used to read non-numeric elements from the ValueList object.
All three non-numeric types (String, byte[], and ValueList) are stored as strings in the database, so no exception will be thrown if GetNextString() or GetNextBytes() is applied to the wrong non-numeric datatype. GetNextList() throws GlobalsException if the element is not a valid ValueList. All of these methods return null if the list element is null.
Reading ValueList Elements of Known Datatype
In the following example, the list created in the previous example (see Creating, Storing, and Fetching a ValueList) is fetched from node ^myListNode, and the first five elements in the list are read and printed. Those five elements are known to be int, long, double, String, and byte[], in that order, so data-specific methods can be used to read them.
Reading ValueList Elements: reading int, long, double, String, and byte[]
// Fetch the list from the database
  nodeRef = myConn.CreateNodeReference("myListNode");
  ValueList outList = nodeRef.GetList();   // fetch list from node ^myListNode

// Read and print out each element in the list
  Console.WriteLine("   int value: " + outList.GetNextInt());
  Console.WriteLine("  long value: " + outList.GetNextLong());
  Console.WriteLine("double value: " + outList.GetNextDouble());
  Console.WriteLine("String value: \"" + outList.GetNextString() + "\"");
  byte[] listBytes = outList.GetNextBytes();
  Console.WriteLine("byte[] value: {" + listBytes[0]+","+listBytes[1]+","+listBytes[2]+"}");
The NodeReference GetList() method is used to fetch the ValueList from node ^myListNode.
The type-specific methods GetNextInt(), GetNextLong(), GetNextDouble(), GetNextString(), and GetNextBytes() each read the current list element as a specific type, and then advance the cursor to the next element in the list.
This example produces the following output:
     int value: 13
    long value: -4555666777
  double value: 1.234
  String value: "hello"
  byte[] value: {72,105,33}
Controlling the List Cursor
When a ValueList is created, it contains a list cursor that points to the first element of the list. Each call to one of the read methods returns the current element and advances the cursor to the next element. After the last element has been read, the cursor will not point to a valid element until a new element is appended or the ResetToFirst() method is called.
The following methods can also be used to control the cursor position:
Reading a Nested ValueList
At the end of the previous example, the cursor points to the final element in the list, which is a nested ValueList. The next example will use GetNextList() to return the nested list, the cursor will be moved to the final element of the new list, and the final element will be printed:
Reading ValueList Elements: reading a nested ValueList
// Reuse the ValueList object to get the nested ValueList
  outList.GetNextList(outList);   // re-use list object to store the nested list
  outList.SkipNext(outList.Length-1);  // move cursor to last element in the new list
  Console.WriteLine("\nNested ValueList: last element is long value " + outList.GetNextLong());
The GetNextList() method is called to get the nested list. Since the main list is no longer needed, the outList object is reused by specifying it as the place to store the nested list. The existing contents of outList are replaced by the nested list.
The code uses SkipNext() and Length to move the cursor to the last element of the new list, and prints the value of the element, producing the following output:
  Nested ValueList: last element is long value 9876543210
Reading ValueList Elements of Unknown Datatype
The following methods return one or more list elements as objects:
By default, both of these methods return each object as an instance of int, long, double, or String. They will return strings as instances of byte[] if the optional returnBytes parameter is set to true.
The following example gets each element of the list as an Object, uses is to determine the datatype, and prints the type and value of the element.
Reading ValueList Elements: reading elements of unknown datatype
  Console.WriteLine("Value in nested list:");
  outList.ResetToFirst();
  for (int ii = 0; ii < outList.Length; ii++) {
    Object item = outList.GetNextObject();
    if (item is String) 
      Console.WriteLine(" String: \"" + item + "\"");
    else if (item is int) 
      Console.WriteLine("    int: " + item);
    else if (item is long) 
      Console.WriteLine("   long: " + item + "L");
    else if (item is double) 
      Console.WriteLine(" double: " + item);
  }
The ResetToFirst() method moves the cursor to the first item in the list.
The GetNextObject() method returns the current item as an Object, and advances the cursor to the next item.
This example generates the following printout:
  Value in nested list:
   String: "world"
      int: 7
   double: 3.1
     long: 9876543210L
Accessing Multiple Global Arrays
Most of the examples in this chapter act on only one global array in a single namespace. In those examples, the namespace is specified when the connection is created and the global name is defined when the node reference is created. This section describes methods that allow your application to specify the global array that will be accessed by a node reference, browse a list of all global array names in a given namespace, and change the namespace accessed by the connection. The following topics are discussed:
Accessing Global Names
The following NodeReference methods provide a way to access the global name property of the node reference:
Changes in the global name property do not affect the subscript list property. The node reference continues to use the current list of subscripts until the list is changed (as described in Changing the Target Address). The following example uses a single instance of NodeReference to create two global arrays. After the first array is created, the subscript list of node reference object nodeRef contains subscript "a". Although the global name and the node value are changed when the second array is created, the subscript list is never modified:
Accessing Global Names: Creating a new array with the existing subscripts
  nodeRef = myConn.CreateNodeReference("myGlobal");
  nodeRef.AppendSubscript("a");
  nodeRef.Set("new node");  // create ^myGlobal("a")
  nodeRef.SetName("mySecondGlobal");
  nodeRef.Set("second node");  // create ^mySecondGlobal("a")
Node reference object NodeRef points to root node ^myGlobal when it is created. A subscript is added, and Set() is called to create a new global array containing persistent node ^myGlobal("a").
The call to SetName() changes the array name to ^mySecondGlobal, but does not alter the subscript list of NodeRef. The next call to Set() therefore creates a second global array containing persistent node ^mySecondGlobal("a").
It is sometimes useful to create multiple instances of NodeReference that access different parts of the same global array. In the following example, the GetName() method is used to specify the same global name for two different instances:
Accessing Global Names: Using two node reference objects to access the same global array
  NodeReference firstNodeRef = myConn.CreateNodeReference("myGlobal");
  NodeReference secondNodeRef = myConn.CreateNodeReference(firstNodeRef.GetName());
  firstNodeRef.Set("level zero");  // create ^myGlobal
  firstNodeRef.AppendSubscript("a");
  firstNodeRef.Set("level one");  // create ^myGlobal("a")
  Console.WriteLine("firstNodeRef=\"" + firstNodeRef.GetString()
              + "\", secondNodeRef=\"" + secondNodeRef.GetString() + "\"");
  // Prints: firstNodeRef="level one", secondNodeRef="level zero"
The first call to CreateNodeReference() assigns global name "myGlobal" to firstNodeRef. When the secondNodeRef object is created, GetName() is used to assign the same global name as firstNodeRef.
The firstNodeRef object is used to create a global array containing two persistent nodes. After the second node is created, firstNodeRef points to ^myGlobal("a"). Since the address of secondNodeRef has never been changed, it still points to root node ^myGlobal.
Using a Directory of Global Arrays
This section describes how use methods of the GlobalsDirectory class to browse a list of all global array names in a given namespace. Instances of GlobalsDirectory are created by Connection.CreateGlobalsDirectory(). GlobalsException will be thrown if there is no connection to the database when CreateGlobalsDirectory() is called.
The following GlobalsDirectory methods are discussed here:
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on instances of GlobalsDirectory before they go out of scope or are reused. Failing to close them can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
Both NextGlobalName() and PreviousGlobalName() can take an optional String argument that specifies where the search is to start. If no string is specified, the search starts from the beginning of the directory list or from the most recently found global name. Both methods return an empty string when no more global array names are found. This iteration technique is identical to the one previously described for the methods used to iterate over the nodes of a global array (see Testing for Values).
Browsing a Directory List
This section provides two examples using GlobalsDirectory. Both examples scan the directory list and print any global names that start with "myArray". The first example creates some test data and reads the contents of the namespace in descending order. The second example adds and deletes some global arrays in the same namespace, and then reads the contents of the changed namespace in ascending order. The examples produce the following combined output:
  List in descending order after creating ^myArray1 and ^myArray2
    ^myArray2
    ^myArray1

  List in ascending order after killing ^myArray1, adding ^myArray3, and refreshing
    ^myArray2
    ^myArray3
Note:
The Management Portal can display the current list of global arrays in a namespace, and the value of each node in an array. Go to [System Explorer] > [Globals] and select a namespace from the list displayed on the left.
The following example reads the entire directory list in descending collation order, printing all global names that start with "myArray".
Using a Directory of Global Arrays: Creating the list and browsing in descending order
  nodeRef.SetName("myArray1");
  nodeRef.Set("");
  nodeRef.SetName("myArray2");
  nodeRef.Set("");
  Console.WriteLine("List in descending order after creating ^myArray1 and ^myArray2");
  GlobalsDirectory globDir = myConn.CreateGlobalsDirectory();

  String globname = "n"; // Start above 'myArray' and descend.
  while (!globname.Equals("")) {
    if (globname.StartsWith("myArray")) Console.WriteLine("  ^" + globname);
    globname = globDir.PreviousGlobalName();
  };
Two global arrays, ^myArray1 and ^myArray2, are created in the current namespace. The NodeReference.SetName() method (described previously in Accessing Global Names) specifies each global name before Set() creates the new node.
The Connection.CreateGlobalsDirectory() method is used to create GlobalsDirectory object globDir, which starts with the cursor positioned just before the first possible item in collation order.
The first call to PreviousGlobalName() positions the cursor to "n" (greater than "myArray" in collation order). In the loop, each call to PreviousGlobalName() returns the next global name in descending order. The standard .NET StartsWith() method is used to ensure that only global names starting with "myArray" will be printed.
After the previous example has finished processing, the following example changes the contents of the User namespace before reading it again. It deletes ^myArray1, creates ^myArray3, refreshes the directory list, and prints names starting with "myArray" in ascending order from the beginning of the list.
Using a Directory of Global Arrays: Refreshing the list and browsing in ascending order
  nodeRef.SetName("myArray1");
  nodeRef.Kill();
  nodeRef.SetName("myArray3");
  nodeRef.Set("");
  globDir.Refresh();
  Console.WriteLine("\nList in ascending order after killing " +
                     "^myArray1, adding ^myArray3, and refreshing");

  globname = globDir.NextGlobalName(""); // Start before first name and ascend.
  while (!globname.Equals("")) {
    if (globname.StartsWith("myArray")) Console.WriteLine("  ^" + globname);
    globname = globDir.NextGlobalName();
  };
  globDir.Close();
Global array ^myArray1 is deleted, ^myArray3 is created, and Refresh() is called to update the directory list.
The first call to NextGlobalName() sets the cursor before the first name in collation order by specifying an empty string (""). In the loop, each call to NextGlobalName() returns the next global name in ascending order. The two global names starting with "myArray" will be printed.
The call to Close() destroys globDir and releases all associated resources.
Accessing Namespaces
A Caché namespace is a logical view of a physical database. Each installed Caché instance contains definitions of several namespaces (see Configuring Namespaces in the Caché System Administration Guide). The Globals API can access data in any available namespace of the Caché instance specified by the GLOBALS_HOME environment variable (see Required Environment Variables and References). A database on another machine can be accessed if a namespace has been mapped to that database. In such cases, access speed will by limited by the physical connection to the database (via ECP rather than direct access to local hardware).
The following Connection methods are used to read and change the namespace reference of a connection object:
The following example assumes that Connection object myConn already exists, and may or may not be connected to the database. The code tests the connection and ensures that it connects to the User namespace.
Accessing Namespaces: Testing a connection to a namespace
  if(!myConn.IsConnected()) myConn.Connect("User","_SYSTEM", "SYS");
  else if (!myConn.GetNamespace().Equals("User")) {
    Console.Write("Switching connection from \"" + myConn.GetNamespace());
    myConn.SetNamespace("User");
    Console.WriteLine("\" to \"" + myConn.GetNamespace() + "\".");
       // Prints: Switching connection from "[current namespace]" to "User".
  }
The call to IsConnected() checks to see if myConn is connected to a database. If not, Connect() connects to the database associated with the User namespace. Otherwise, GetNamespace() returns the current namespace name. If necessary, SetNamespace() is called to switch to User.
Note:
Since this call acts on the underlying eXTreme connection, every connected object in this process now accesses the User namespace (this includes eXTreme XEP EventPersister objects running in the same process, as described in Using the Globals API with Other eXTreme APIs). See Creating a Connection for a discussion of how the underlying eXTreme connection works.
Transactions and Locking
The following topics are discussed in this section:
Controlling Transactions
The Connection class provides the following methods to control transactions:
The following example starts three levels of nested transaction, setting the value of a different node in each transaction level. All three nodes are printed to prove that they have values. The example then rolls back the second and third levels and commits the first level. All three nodes are printed again to prove that only the first node still has a value.
Controlling Transactions: Using three levels of nested transaction
  NodeReference nodeRef = myConn.CreateNodeReference("myGlobal");
  myConn.StartTransaction();
  nodeRef.Set("firstValue", myConn.TransactionLevel());
  // TransactionLevel() is 1 and ^myGlobal(1) = "firstValue"

  myConn.StartTransaction();
  nodeRef.Set("secondValue", myConn.TransactionLevel());
  // TransactionLevel() is 2 and ^myGlobal(2) = "secondValue"

  myConn.StartTransaction();
  nodeRef.Set("thirdValue", myConn.TransactionLevel());
  // TransactionLevel() is 3 and ^myGlobal(3) = "thirdValue"

  Console.WriteLine("Node values before rollback and commit:");
  for (int ii=1;ii<4;ii++) {
    Console.Write("^myGlobal(" + ii + ") = ");
    if (nodeRef.HasData(ii)) Console.WriteLine(nodeRef.GetString(ii)); 
    else Console.WriteLine("<valueless>");
  }
// prints: Node values before rollback and commit:
//         ^myGlobal(1) = firstValue
//         ^myGlobal(2) = secondValue
//         ^myGlobal(3) = thirdValue

  myConn.Rollback(2);  // roll back 2 levels to TransactionLevel 1
  myConn.Commit();  // TransactionLevel() after commit will be 0
  Console.WriteLine("Node values after the transaction is committed:");
  for (int ii=1;ii<4;ii++) {
    Console.Write("^myGlobal(" + ii + ") = ");
    if (nodeRef.HasData(ii)) Console.WriteLine(nodeRef.GetString(ii)); 
    else Console.WriteLine("<valueless>");
  }
// prints: Node values after the transaction is committed:
//         ^myGlobal(1) = firstValue
//         ^myGlobal(2) = <valueless>
//         ^myGlobal(3) = <valueless>
Acquiring and Releasing Locks
The following methods of NodeReference are used to acquire and release locks. Both methods take a lockType argument to specify whether the lock is shared or exclusive:
The following argument values can be used:
The following example will lock and release node ^myGlobal("a",1). The target address in nodeRef is ^myGlobal, so both methods use subscript arguments to specify the subnode.
Acquiring and Releasing Locks: Locking and releasing a node
  nodeRef.AcquireLock(NodeReference.SHARED_LOCK, NodeReference.LOCK_INCREMENTALLY,"a",1);
  // perform transaction on locked node here...
  nodeRef.ReleaseLock(NodeReference.SHARED_LOCK, NodeReference.RELEASE_IMMEDIATELY,"a",1);
Note:
You can use the Management Portal to examine locks. Go to [System Operation] > [Locks] to see a list of the locked items on your system.
Controlling the Timeout Interval
A NodeReference object maintains an internal timeout setting that specifies how long the AcquireLock() method should wait to acquire a lock before timing out and throwing LockException. The default interval is 10 seconds.
The following argument values can be used:
The following example reads the timeout interval, and sets it to 5 seconds if it is less than 5.
Acquiring and Releasing Locks: Controlling the timeout interval
  if (nodeRef.GetOption(NodeReference.LOCK_TIMEOUT)<5) {
    nodeRef.SetOption(NodeReference.LOCK_TIMEOUT, 5);
  }
Using Locks in a Transaction
This section demonstrates incremental locking within a transaction, using the methods previously described (see Controlling Transactions and Acquiring and Releasing Locks). You can see a list of the locked items on your system by opening the Management Portal and going to [System Operation] > [Locks]. The calls to ReadLine() in the following code will pause execution so that you can look at the list whenever it changes.
The following Connection methods will release all currently held locks:
The following examples demonstrate the various lock and release methods.
Using Locks in a Transaction: Using incremental locking
  NodeReference nodeRef1 = myConn.CreateNodeReference("nodeRef1");
  nodeRef1.AppendSubscript("my-node");
  NodeReference nodeRef2 = myConn.CreateNodeReference("nodeRef2");
  nodeRef2.AppendSubscript("shared-node");
  try {
    myConn.StartTransaction();
    // lock ^nodeRef1("my-node") exclusively
    nodeRef1.AcquireLock(NodeReference.EXCLUSIVE_LOCK, NodeReference.LOCK_INCREMENTALLY);
    // lock ^nodeRef2 shared
    nodeRef2.AcquireLock(NodeReference.SHARED_LOCK, NodeReference.LOCK_INCREMENTALLY);
    Console.WriteLine("Exclusive lock on ^nodeRef1(\"my-node\") and shared lock on ^nodeRef2");
    Console.WriteLine("Press return to release locks individually");
    Console.ReadLine(); // Wait for user to press Return

    // release ^nodeRef1("my-node") after transaction
    nodeRef1.ReleaseLock(NodeReference.EXCLUSIVE_LOCK, NodeReference.RELEASE_AT_TRANSACTION_END);
    // release ^nodeRef2 immediately
    nodeRef2.ReleaseLock(NodeReference.SHARED_LOCK, NodeReference.RELEASE_IMMEDIATELY);
    Console.WriteLine("Press return to commit transaction");
    Console.ReadLine();
    myConn.Commit();
  }
  catch (LockException e) { Console.WriteLine(e.Message); }
Using Locks in a Transaction: Using non-incremental locking
  // lock ^nodeRef1("my-node") non-incremental
  nodeRef1.AcquireLock(NodeReference.EXCLUSIVE_LOCK, NodeReference.LOCK_NON_INCREMENTALLY);
  Console.WriteLine("Exclusive lock on ^nodeRef1(\"my-node\"), return to lock ^lockRef2 non-incrementally");
  Console.ReadLine();

  // lock ^nodeRef2 shared non-incremental
  nodeRef2.AcquireLock(NodeReference.SHARED_LOCK, NodeReference.LOCK_NON_INCREMENTALLY);
  Console.WriteLine("Verify that only ^nodeRef2 is now locked, then press return");
  Console.ReadLine();
Using Locks in a Transaction: Using ReleaseAllLocks() to release all incremental locks
  // lock ^nodeRef1("my-node") shared incremental
  nodeRef1.AcquireLock(NodeReference.SHARED_LOCK, NodeReference.LOCK_INCREMENTALLY);

  // lock ^nodeRef2 exclusive incremental
  nodeRef2.AcquireLock(NodeReference.EXCLUSIVE_LOCK, NodeReference.LOCK_INCREMENTALLY);
  Console.WriteLine("Two locks are held (one with lock count 2), return to release both locks");
  Console.ReadLine();

  myConn.ReleaseAllLocks();
  Console.WriteLine("Verify both locks have been released");
  Console.ReadLine();
Calling Caché Methods
The following Connection methods call Caché class methods:
The following Connection methods call Caché functions and procedures (see User-defined Code in Using Caché ObjectScript):
Globals API Requirements and Conventions
The following topics are discussed in this section:
Global Array Naming Conventions
This section describes how the standard Caché global naming conventions apply to the Globals API. See Logical Structure of Globals in Using Caché Globals for details about how the same conventions are enforced in Caché ObjectScript.
Global names must obey the following rules:
Subscript identifiers must obey the following rules:
Using the Globals API with Other eXTreme APIs
The XEP API may be used in the same applications as the Globals API, allowing object access and SQL to be part of the same transaction context. There are some common installation and configuration requirements for all eXTreme APIs (see Installation and Configuration).
These APIs use the same underlying connection, but each one has its own methods for connecting to and disconnecting from Caché. The following rules apply when using more than one of these in the same application:
Collation Order in Multilevel Global Arrays
The nodes in a global array do not have to be added in a sequential or hierarchical order, but since a global array is essentially a tree structure, a hierarchy is automatically generated whenever persistent nodes are added. For example, a global array might contain three persistent nodes: ^myGlobal("a",”23”), ^myGlobal("a"," x"), and ^myGlobal("b"). The subscript lists are arbitrary, and the nodes can be added to the array in any order. However, the pointer structure of the array will also contain two valueless nodes: root node ^myGlobal, and level 1 node ^myGlobal("a"), which is the parent of the first two persistent nodes and the sibling of the third. These nodes create the following hierarchy:
   ^myGlobal                            (valueless root node)
      ^myGlobal("a")                    (valueless level 1 node)
         ^myGlobal("a","23") = value
         ^myGlobal("a"," x") = value   (second subscript contains a space character!)
      ^myGlobal("b") = value
As demonstrated in this example, the hierarchy is based on node level and on the collation sequence of the subscripts at each level.
In the physical database, valueless nodes ^myGlobal and ^myGlobal("a") are created automatically when a value is assigned to one of their child nodes. The valueless nodes exist only as part of the pointer structure, and no extra space is allocated for data. Although any attempt to read a value from them would throw an exception, they can still be used by the Globals iteration methods to traverse the array.
Child nodes under the same parent are organized by their subscripts in collation order, with numbers first, followed by alphabetic strings. For example, the following list of level 1 nodes is in collation order:
Collation Order in Caché Databases
   ^myGlobal("-23.1")  (subscript is a string containing a numeric value)
   ^myGlobal("99")     (subscript is a string containing only digits)

   ^myGlobal(" ")      (subscript is a space character)
   ^myGlobal(" 22")    (subscript starts with a space)
   ^myGlobal("0123")   (subscript contains a leading zero)
   ^myGlobal("11 ")    (subscript contains a trailing space)
   ^myGlobal("23-2")  (subscript is a numeric expression, not a value)
The first two subscripts are numeric values, and therefore come before all alphabetic subscripts. A space character (" ") is the first alphabetic subscript in this list. For more information on collation order, see Sorting Data within Globals in Using Caché Globals.
Considerations for Safe Threading
Separate instances of Globals API classes can be used concurrently in multiple threads, in a thread-safe manner. Individual instances of Globals API classes are not thread safe, in the sense that some of their methods modify their state in a way that persists between method calls, so that the same sequence of method calls in one thread may have different results, depending on whether another thread calls methods of the same instance during the course of the sequence. For example, a call to NodeReference.AppendSubscript() in another thread changes which node is referenced by the NodeReference instance, which could cause a call to NodeReference.Set() to set the value of a different node than the one intended.
The single Connection instance may be used safely in multiple threads with these caveats: