Skip to main content

Using Multidimensional Storage (Globals)

This chapter describes the various operations you can perform using multidimensional storage (global variables). It includes the following topics:

Storing Data in Globals

Storing data in global nodes is simple: you treat a global as you would any other variable. The difference is that operations on globals are automatically written to the database.

Creating Globals

There is no setup work required to create a new global; simply setting data into a global implicitly creates a new global structure. You can create a global (or a global subscript) and place data in it with a single operation, or you can create a global (or subscript) and leave it empty by setting it to the null string. In ObjectScript, these operations are done using the SET command.

The following examples define a global named Color (if one does not already exist) and associate the value “Red” with it. If a global already exists with the name Color, then these examples modify it to contain the new information.

In Caché Basic:

^Color = "Red"

In ObjectScript:

 SET ^Color = "Red"
Note:

When using direct global access within applications, develop and adhere to a naming convention to keep different parts of an application from “walking over” one another; this is similar to developing naming convention for classes, method, and other variables. Also, avoid certain global names that Caché uses; for a list of these, see the section “Global Variable Names to Avoid” in the “Rules and Guidelines for Identifiers” appendix of the Caché Programming Orientation Guide.

Storing Data in Global Nodes

To store a value within a global subscript node, simply set the value of the global node as you would any other variable array. If the specified node did not previously exist, it is created. If it did exist, its contents are replaced with the new value.

You specify a node within a global by means of an expression (referred to as a global reference). A global reference consists of the caret character (^), the global name, and (if needed) one or more subscript values. Subscripts (if present) are enclosed within parentheses “( )” and are separated by commas. Each subscript value is itself an expression: a literal value, a variable, a logical expression, or even a global reference.

Setting the value of a global node is an atomic operation: It is guaranteed to succeed and you do not need to use any locks to ensure concurrency.

The following are all valid global references:

In Caché Basic:

^Data = 2
^Data("Color") = "Red"
^Data(1,1) = 100
^Data(^Data) = 10     ' The value of ^Data is the subscript
^Data(a,b) = 50       ' The values of local variables a and b are subscripts
^Data(a + 10) = 50    

In ObjectScript:

   SET ^Data = 2
   SET ^Data("Color")="Red"
   SET ^Data(1,1)=100        /* The 2nd-level subscript (1,1) is set
                                to the value 100. No value is stored at
                                the 1st-level subscript (^Data(1)). */   
   SET ^Data(^Data)=10       /* The value of global variable ^Data 
                                is the name of the subscript. */
   SET ^Data(a,b)=50         /* The values of local variables a and b
                                are the names of the subscripts. */
   SET ^Data(a+10)=50       

If you are using ObjectScript, you can construct global references at runtime using indirection.

Storing Structured Data in Global Nodes

Each global node can contain a single string of up to 32K characters.

Data is typically stored within nodes in one of the following ways:

  • As a single string of up to 32K characters (specifically, 32K minus 1).

  • As a character-delimited string containing multiple pieces of data.

    To store a set of fields within a node using a character delimiter, simply concatenate the values together using the concatenate operator (_). The following ObjectScript examples use the # character as a delimiter:

       SET ^Data(id)=field(1)_"#"_field(2)_"#"_field(3)
    

    When the data is retrieved, you can pull the fields apart using the $PIECE function:

       SET data = $GET(^Data(id))
       FOR i=1:1:3 {
         SET field(i) = $PIECE(data,"#",i)
         }
       QUIT
  • As a $LIST-encoded string containing multiple pieces of data.

    The $LIST functions use a special length-encoding scheme that does not require you to reserve a delimiter character. (This is the default structure used by Caché objects and SQL.)

    To store a set of fields within a node use the $LISTBUILD function to construct a list:

       SET ^Data(id)=$LISTBUILD(field(1),field(2),field(3))
    

    When the data is retrieved, you can pull the fields apart using the $LIST or $LISTGET functions:

       SET data = $GET(^Data(id))
       FOR i = 1:1:3 {
           SET field(i)=$LIST(data,i)
           }
       QUIT
  • As one part of a larger set of data (such as a stream or “BLOB”).

    As individual nodes are limited to just under 32K of data, larger structures, such as streams, are implemented by storing data in a set of successive nodes:

       SET ^Data("Stream1",1) = "First part of stream...."
       SET ^Data("Stream1",2) = "Second part of stream...."
       SET ^Data("Stream1",3) = "Third part of stream...."
    

    Code that fetches the stream (such as that provided by the %GlobalCharacterStreamOpens in a new tab class) loops over the successive nodes in such a structure providing the data as a continuous string.

  • As a bitstring.

    If you are implementing a bitmap index (an index where a bit in a bitstring corresponds to a row in a table), you would set the node values of an index global to bit strings. Note that Caché uses a compression algorithm for encoding bit strings; therefore, bit strings can only be handled using the Caché $BIT functions. Refer to the Bit String Functions Overview for more details on bit strings.

  • As an empty node.

    If the data you are interested in is provided by the nodes themselves, then it is typical to set the actual subscript to a null string (""). For example, an index that associates a name with an ID value typically looks like this:

      SET ^Data("APPLE",1) = ""
      SET ^Data("ORANGE",2) = ""
      SET ^Data("BANANA",3) = ""

Deleting Global Nodes

To remove a global node, a group of subnodes, or an entire global from the database, use the ObjectScript KILL or ZKILL commands, or the Caché Basic Erase command.

The KILL command deletes all nodes (data as well as its corresponding entry in the array) at a specific global reference, including any descendant subnodes. That is, all nodes starting with the specified subscript are deleted.

For example, the ObjectScript statement:

  KILL ^Data

deletes the entire ^Data global. A subsequent reference to this global would return an <UNDEFINED> error.

The ObjectScript statement:

   KILL ^Data(100)

deletes contents of node 100 within the ^Data global. If there are descendant subnodes, such as ^Data(100,1), ^Data(100,2), and ^Data(100,1,2,3), these are deleted as well.

The ObjectScript ZKILL command deletes a specified global or global subscript node. It does not delete descendant subnodes.

Note:

Following the kill of a large global, the space once occupied by that global may not have been completely freed, since the blocks are marked free in the background by the Garbage Collector daemon. Thus, a call to the ReturnUnusedSpace method of the SYS.DatabaseOpens in a new tab class immediately after killing a large global may not return as much space as expected, since blocks occupied by that global may not have been released as yet.

You cannot use the NEW command on global variables.

Testing the Existence of a Global Node

To test if a specific global (or its descendants) contains data, use the ObjectScript $DATA function.

$DATA returns a value indicating whether or not the specified global reference exists. The possible return values are:

Status Value Meaning
0 The global variable is undefined.
1 The global variable exists and contains data, but has no descendants. Note that the null string ("") qualifies as data.
10 The global variable has descendants (contains a downward pointer to a subnode) but does not itself contain data. Any direct reference to such a variable will result in an <UNDEFINED> error. For example, if $DATA(^y) returns 10, SET x=^y will produce an <UNDEFINED> error.
11 The global variable both contains data and has descendants (contains a downward pointer to a subnode).

Retrieving the Value of a Global Node

To get the value stored within a specific global node, simply use the global reference as an expression.

Using Caché Basic:

color = ^Data("Color")   ' assign to a local variable
Print ^Data("Color")     ' use as an argument to a command
MyMethod(^Data("Color")) ' use as a function argument

Using Caché ObjectScript:

   SET color = ^Data("Color")    ; assign to a local variable
   WRITE ^Data("Color")          ; use as a command argument
   SET x=$LENGTH(^Data("Color")) ; use as a function parameter

The $GET Function

Within ObjectScript, you can also get the value of a global node using the $GET function:

   SET mydata = $GET(^Data("Color"))

This retrieves the value of the specified node (if it exists) or returns the null string ("") if the node has no value. You can use the optional second argument of $GET to return a specified default value if the node has no value.

The WRITE, ZWRITE, and ZZDUMP Commands

You can display the contents of a global or a global subnode by using the various ObjectScript display commands. The WRITE command returns the value of the specified global or subnode as a string. The ZWRITE command returns the name of the global variable and its value, and each of its descendant nodes and their values. The ZZDUMP command returns the value of the specified global or subnode in hexadecimal dump format.

Traversing Data within a Global

There are a number of ways to traverse (iterate over) data stored within a global.

The $ORDER (Next / Previous) Function

The ObjectScript $ORDER function (and its Caché Basic equivalent: Traverse) allows you to sequentially visit each node within a global.

The $ORDER function returns the value of the next subscript at a given level (subscript number). For example, suppose you have defined the following global:

 Set ^Data(1) = ""
 Set ^Data(1,1) = ""
 Set ^Data(1,2) = ""
 Set ^Data(2) = ""
 Set ^Data(2,1) = ""
 Set ^Data(2,2) = ""
 Set ^Data(5,1,2) = ""

To find the first, first-level subscript, we can use:

 SET key = $ORDER(^Data(""))

This returns the first, first-level subscript following the null string (""). (The null string is used to represent the subscript value before the first entry; as a return value it is used to indicate that there are no following subscript values.) In this example, key will now contain the value 1.

We can find the next, first-level subscript by using 1 or key in the $ORDER expression:

 SET key = $ORDER(^Data(key))

If key has an initial value of 1, then this statement will set it to 2 (as ^Data(2) is the next first-level subscript). Executing this statement again will set key to 5 as that is the next first-level subscript. Note that 5 is returned even though there is no data stored directly at ^Data(5). Executing this statement one more time will set key to the null string (""), indicating that there are no more first level subscripts.

By using additional subscripts with the $ORDER function, you can iterate over different subscript levels. $ORDER returns the next value of the last subscript in its argument list. Using the data above, the statement:

 SET key = $ORDER(^Data(1,""))

will set key to 1 as ^Data(1,1) is the next second-level subscript. Executing this statement again will set key to 2 as that is the next second-level subscript. Executing this statement one more time will set key to “” indicating that there are no more second-level subscripts under node ^Data(1).

Looping with $ORDER

The following ObjectScript code defines a simple global and then loops over all of its first-level subscripts:

 // clear ^Data in case it has data
 Kill ^Data

 // fill in ^Data with sample data
 For i = 1:1:100 {
     // Set each node to a random person's name
     Set ^Data(i) = ##class(%PopulateUtils).Name()
 }

 // loop over every node
 // Find first node
 Set key = $Order(^Data(""))

 While (key '= "") {
     // Write out contents
     Write "#", key, " ", ^Data(key),!

     // Find next node
     Set key = $Order(^Data(key))
 }

Additional $ORDER Arguments

The ObjectScript $ORDER function takes optional second and third arguments. The second argument is a direction flag indicating in which direction you wish to traverse a global. The default, 1, specifies forward traversal, while –1 specifies backward traversal.

The third argument, if present, contains a local variable name. If the node found by $ORDER contains data, the data found is written into this local variable. When you are looping over a global and you are interested in node values as well as subscript values, this operates more efficiently.

Looping Over a Global

If you know that a given global is organized using contiguous numeric subscripts, you can use a simple For loop to iterate over its values. For example, in Caché Basic:

For i = 1 To 100
    Print ^Data(i)
Next

or the equivalent in ObjectScript:

 For i = 1:1:100 {
     Write ^Data(i),!
 }

Generally, it is better to use the $ORDER function described above: it is more efficient and you do not have to worry about gaps in the data (such as a deleted node).

The $QUERY Function

If you need to visit every node and subnode within a global, moving up and down over subnodes, use the ObjectScript $QUERY function. (Alternatively you can use nested $ORDER loops).

The $QUERY function takes a global reference and returns a string containing the global reference of the next node in the global (or "" if there are no following nodes). To use the value returned by $QUERY, you must use the ObjectScript indirection operator (@).

For example, suppose you define the following global:

 Set ^Data(1) = ""
 Set ^Data(1,1) = ""
 Set ^Data(1,2) = ""
 Set ^Data(2) = ""
 Set ^Data(2,1) = ""
 Set ^Data(2,2) = ""
 Set ^Data(5,1,2) = ""

The following call to $QUERY:

 SET node = $QUERY(^Data(""))

sets node to the string “^Data(1)”, the address of the first node within the global. Then, to get the next node in the global, call $QUERY again and use the indirection operator on node:

 SET node = $QUERY(@node)

At this point, node contains the string “^Data(1,1)”.

The following example defines a set of global nodes and then walks over them using $QUERY, writing the address of each node as it does:

 Kill ^Data // make sure ^Data is empty

 // place some data into ^Data
 Set ^Data(1) = ""
 Set ^Data(1,1) = ""
 Set ^Data(1,2) = ""
 Set ^Data(2) = ""
 Set ^Data(2,1) = ""
 Set ^Data(2,2) = ""
 Set ^Data(5,1,2) = ""

 // now walk over ^Data
 // find first node
 Set node = $Query(^Data(""))
 While (node '= "") {
     Write node,!
     // get next node
     Set node = $Query(@node)
 }

Copying Data within Globals

To copy the contents of a global (entire or partial) into another global (or a local array), use the ObjectScript MERGE command.

The following example demonstrates the use of the MERGE command to copy the entire contents of the OldData global into the NewData global:

 Merge ^NewData = ^OldData

If the source argument of the MERGE command has subscripts then all data in that node and its descendants are copied. If the destination argument has subscripts, then the data is copied using the destination address as the top level node. For example, the following code:

 Merge ^NewData(1,2) = ^OldData(5,6,7)

copies all the data at and beneath ^OldData(5,6,7) into ^NewData(1,2).

Maintaining Shared Counters within Globals

A major concurrency bottleneck of large-scale transaction processing applications can be the creation of unique identifier values. For example, consider an order processing application in which each new invoice must be given a unique identifying number. The traditional approach is to maintain some sort of counter table. Every process creating a new invoice waits to acquire a lock on this counter, increments its value, and unlocks it. This can lead to heavy resource contention over this single record.

To deal with this issue, Caché provides the ObjectScript $INCREMENT function. $INCREMENT atomically increments the value of a global node (if the node has no value, it is set to 1). The atomic nature of $INCREMENT means that no locks are required; the function is guaranteed to return a new incremented value with no interference from any other process.

You can use $INCREMENT as follows. First, you must decide upon a global node in which to hold the counter. Next, whenever you need a new counter value, simply invoke $INCREMENT:

 SET counter = $INCREMENT(^MyCounter)

The default storage structure used by Caché objects and SQL uses $INCREMENT to assign unique object (row) identifier values.

Using Temporary Globals

For certain operations, you may need the power of globals without requiring persistence. For example, you may want to use a global to sort some data which you do not need to store to disk. For these operations, Caché provides temporary globals.

Temporary globals have the following characteristics:

  • Temporary globals are stored within the CACHETEMP database, which is always defined to be a local (that is, a non-network) database. All globals mapped to the CACHETEMP database are treated as temporary globals.

  • Changes to temporary globals are not written to disk. Instead the changes are maintained within the in-memory buffer pool. A large temporary global may be written to disk if there is not sufficient space for it within the buffer pool.

  • For maximum efficiency, changes to temporary globals are not logged to a journal file.

  • Temporary globals are automatically deleted whenever the Caché system is restarted. (Note: it can be a very long time before a live system is restarted; so you should not count on this for cleaning up temporary globals.)

By default, Caché defines any global whose name starts with “CacheTemp” as being a temporary global. To avoid conflict with any temporary globals that Caché itself may use, you should start your temporary global names with “CacheTempUser”.

Caché SQL uses temporary globals as scratch space for optimizing complex queries. It may also uses temporary globals as temporary indices during the execution of certain queries (for sorting, grouping, calculating aggregates, etc.)

Sorting Data within Globals

Data stored within globals is automatically sorted according to the value of the subscripts. For example, the following ObjectScript code defines a set of globals (in random order) and then iterates over them to demonstrate that the global nodes are automatically sorted by subscript:

 // Erase any existing data
 Kill ^Data
 
 // Define a set of global nodes
 Set ^Data("Cambridge") = ""
 Set ^Data("New York") = ""
 Set ^Data("Boston") = ""
 Set ^Data("London") = ""
 Set ^Data("Athens") = ""

 // Now iterate and display (in order)
 Set key = $Order(^Data(""))
 While (key '= "") {
     Write key,!
     Set key = $Order(^Data(key)) // next subscript
 }

Applications can take advantage of the automatic sorting provided by globals to perform sort operations or to maintain ordered, cross-referenced indices on certain values. Caché SQL and ObjectScript use globals to perform such tasks automatically.

Collation of Global Nodes

The order in which the nodes of a global are sorted (referred to as collation) is controlled at two levels: within the global itself and by the application using the global.

At the application level, you can control how global nodes are collated by performing data transformations on the values used as subscripts (Caché SQL and objects do this via user-specified collation functions). For example, if you wish to create a list of names that is sorted alphabetically but ignores case, then typically you use the uppercase version of the name as a subscript:

 // Erase any existing data
 Kill ^Data
 
 // Define a set of global nodes for sorting
 For name = "Cobra","jackal","zebra","AARDVark" {
     // use UPPERCASE name as subscript
     Set ^Data($ZCONVERT(name,"U")) = name
 }

 // Now iterate and display (in order)
 Set key = $Order(^Data(""))
 While (key '= "") {
     Write ^Data(key),!  // write untransformed name
     Set key = $Order(^Data(key)) // next subscript
 }

This example converts each name to uppercase (using the $ZCONVERT function) so that the subscripts are sorted without regard to case. Each node contains the untransformed value so that the original value can be displayed.

Numeric and String-Valued Subscripts

Numeric values are collated before string values; that is a value of 1 comes before a value of “a”. You need to be aware of this fact if you use both numeric and string values for a given subscript. If you are using a global for an index (that is, to sort data based on values), it is most common to either sort values as numbers (such as salaries) or strings (such as postal codes).

For numerically collated nodes, the typical solution is to coerce subscript values to numeric values using the unary + operator. For example, if you are building an index that sort id values by age, you can coerce age to always be numeric:

 Set ^Data(+age,id) = ""

If you wish to sort values as strings (such as “0022”, “0342”, “1584”) then you can coerce the subscript values to always be strings by prepending a space (“ ”) character. For example, if you are building an index that sort id values by zipcode, you can coerce zipcode to always be a string:

 Set ^Data(" "_zipcode,id) = ""

This ensures that values with leading zeroes, such as “0022” are always treated as strings.

The $SORTBEGIN and $SORTEND Functions

Typically you do not have to worry about sorting data within Caché. Whether you use SQL or direct global access, sorting is handled automatically.

There are, however, certain cases where sorting can be done more efficiently. Specifically, in cases where (1) you need to set a large number of global nodes that are in random (that is, unsorted) order and (2) the total size of the resulting global approaches a significant portion of the Caché buffer pool, then performance can be adversely affected — since many of the SET operations involve disk operations (as data does not fit in the cache). This scenario usually arises in cases involving the creation of index globals such as bulk data loads, index population, or sorting of unindexed values in temporary globals.

To handle these cases efficiently, ObjectScript provides the $SORTBEGIN and $SORTEND functions. The $SORTBEGIN function initiates a special mode for a global (or part thereof) in which data set into the global is written to a special scratch buffer and sorted in memory (or temporary disk storage). When the $SORTEND function is called at the end of the operation, the data is written to actual global storage sequentially. The overall operation is much more efficient as the actual writing is done in an order requiring far fewer disk operations.

The $SORTBEGIN function is quite easy to use; simply invoke it with the name of the global you wish to sort before beginning the sort operation and call $SORTEND when the operation is complete:

 // Erase any existing data
 Kill ^Data

 // Initiate sort mode for ^Data global
 Set ret = $SortBegin(^Data)

 // Write random data into ^Data
 For i = 1:1:10000 {
     Set ^Data($Random(1000000)) = ""
 }

 Set ret = $SortEnd(^Data)

 // ^Data is now set and sorted

 // Now iterate and display (in order)
 Set key = $Order(^Data(""))
 While (key '= "") {
     Write key,!
     Set key = $Order(^Data(key)) // next subscript
 }

The $SORTBEGIN function is designed for the special case of global creation and must be used with some care. Specifically, you must not read from the global to which you are writing while in $SORTBEGIN mode; as the data is not written, reads will be incorrect.

Caché SQL automatically uses these functions for creation of temporary index globals (such as for sorting on unindexed fields).

Using Indirection with Globals

By means of indirection, ObjectScript provides a way to create global references at runtime. This can be useful in applications where you do not know global structure or names at program compilation time.

Indirection is supported via the indirection operator, @, which de-references a string containing an expression. There are several types of indirection, based on how the @ operator is used.

The following code provides an example of name indirection in which the @ operator is used to de-reference a string containing a global reference:

 // Erase any existing data
 Kill ^Data

 // Set var to an global reference expression
 Set var = "^Data(100)"

 // Now use indirection to set ^Data(100)
 Set @var = "This data was set indirectly."

 // Now display the value directly:
 Write "Value: ",^Data(100)

You can also use subscript indirection to mix expressions (variables or literal values) within indirect statements:

 // Erase any existing data
 Kill ^Data

 // Set var to a subscript value
 Set glvn = "^Data"

 // Now use indirection to set ^Data(1) to ^Data(10)
 For i = 1:1:10 {
     Set @glvn@(i) = "This data was set indirectly."
 }

 // Now display the values directly:
 Set key = $Order(^Data(""))
 While (key '= "") {
     Write "Value ",key, ": ", ^Data(key),!
     Set key = $Order(^Data(key))
 }

Indirection is a fundamental feature of ObjectScript; it is not limited to global references. For more information, refer to the “Indirection” section in the “Operators” chapter of Using Caché ObjectScript. Indirection is less efficient than direct access, so you should use it judiciously.

Managing Transactions

Caché provides the primitive operations needed to implement full transaction processing using globals. Caché objects and SQL make use of these features automatically. If you are directly writing transactional data into globals, you can make use of these operations.

The transaction commands are TSTART, which defines the start of a transaction; TCOMMIT, which commits the current transaction; and TROLLBACK, which aborts the current transaction and undoes any changes made to globals since the start of the transaction.

For example, the following ObjectScript code defines the start of a transaction, sets a number of global nodes, and then commits or rolls back the transaction depending on the value of ok:

 TSTART

 Set ^Data(1) = "Apple"
 Set ^Data(2) = "Berry"

 If (ok) {
     TCOMMIT
 }
 Else {
     TROLLBACK
 }

The TSTART writes a transaction start marker in the Caché journal file. This defines the starting boundary of the transaction. If the variable ok is true (nonzero) in the above example, then the TCOMMIT command marks the successful end of the transaction and a transaction completion marker is written to the journal file. If ok is false (0), then the TROLLBACK command will undo every set or kill operation made since the start of the transaction. In this case, ^Data(1) and ^Data(2) are restored to their previous values.

Note that no data is written at the successful completion of a transaction. This is because all modifications to the database during a transaction are carried out as normal during the course of a transaction. Only in the case of a rollback is the data in the database affected. This implies that the transaction in this example has limited isolation; that is, other processes can see the modified global values before the transaction is committed. This is typically referred to as an uncommitted read. Whether this is good or bad depends on application requirements; in many cases this is perfectly reasonable behavior. If an application requires a higher degree of isolation, then this is accomplished by using locks. This is described in the following section.

Locks and Transactions

To create isolated transactions—that is, to prevent other processes from seeing modified data before a transaction is committed—requires the use of locks. Within ObjectScript, you can directly acquire and release locks by means of the LOCK command. Locks work by convention; for a given data structure (such as used for a persistent object) all code that requires locks uses the same logical lock reference (that is, the same address is used by the LOCK command).

Within a transaction, locks have a special behavior; any locks acquired during the course of a transaction are not released until the end of the transaction. To see why this is, consider the actions carried out by typical transaction:

  1. Start the transaction using TSTART.

  2. Acquire a lock (or locks) on the node (or nodes) you wish to modify. This is usually referred to as a “write” lock.

  3. Modify the node (or nodes).

  4. Release the lock (or locks). Because we are in a transaction, these locks are not actually released at this time.

  5. Commit the transaction using TCOMMIT. At this point, all the locks released in the previous step are actually released.

If another process wants to look at the nodes involved in this transaction and does not want to see uncommitted modifications, then it simply tests for a lock (referred to a “read” lock) before reading the data from the nodes. Because the write locks are held until the end of the transaction, the reading process does not see the data until the transaction is complete (committed or rolled back).

Most database management systems use a similar mechanism to provide transaction isolation. Caché is unique in that it makes this mechanism available to developers. This makes it possible to create custom database structure for new application types while still supporting transactions. Of course, you can simply use Caché objects or SQL to manage your data and let your transactions be managed automatically.

Nested Calls to TSTART

Caché maintains a special system variable, $TLEVEL, that tracks how many times the TSTART command has been called. $TLEVEL starts with a value of 0; each call to TSTART increments the value of $TLEVEL by 1, while each call to TCOMMIT decrements its value by 1. If a call to TCOMMIT results in setting $TLEVEL back to 0, the transaction ends (with a commit).

A call to the TROLLBACK command always terminates the current transaction and sets $TLEVEL back to 0, regardless of the value of $TLEVEL.

This behavior gives applications the ability to wrap transactions around code (such as object methods) that itself contains a transaction. For example, the %Save method, provided by persistent objects, always performs its operation as a transaction. By explicitly calling TSTART and TCOMMIT you can create a larger transaction that encompasses several object save operations:

 TSTART
 Set sc = object1.%Save()
 If ($$$ISOK(sc)) {
     // first save worked, do the second
    Set sc = object2.%Save()
 }

 If ($$$ISERR(sc)) {
     // one of the saves failed, rollback
     TROLLBACK
 }
 Else {
     // everything is ok, commit
  TCOMMIT
 }

Managing Concurrency

The operation of setting or retrieving a single global node is atomic; it is guaranteed to always succeed with consistent results. For operations on multiple nodes or for controlling transaction isolation (see the section on Lock and Transactions), Caché provides the ability to acquire and release locks.

Locks are managed by the Caché Lock Manager. Within ObjectScript, you can directly acquire and release locks by means of the LOCK command. (Caché objects and SQL automatically acquire and release locks as needed).

For details on the LOCK command, refer to the LOCK command reference page.

Checking the Most Recent Global Reference

The most recent global reference is recorded in the ObjectScript $ZREFERENCE special variable. $ZREFERENCE contains the most recent global reference, including subscripts and extended global reference, if specified. Note that $ZREFERENCE indicates neither whether the global reference succeeded, nor if the specified global exists. Caché simply records the most recently specified global reference.

Naked Global Reference

Following a subscripted global reference, Caché sets a naked indicator to that global name and subscript level. You can then make subsequent references to the same global and subscript level using a naked global reference, omitting the global name and higher level subscripts. This streamlines repeated references to the same global at the same (or lower) subscript level.

Specifying a lower subscript level in a naked reference resets the naked indicator to that subscript level. Therefore, when using naked global references, you are always working at the subscript level established by the most recent global reference.

The naked indicator value is recorded in the $ZREFERENCE special variable. The naked indicator is initialized to the null string. Attempting a naked global reference when the naked indicator is not set results in a <NAKED> error. Changing namespaces reinitializes the naked indicator. You can reinitialize the naked indicator by setting $ZREFERENCE to the null string ("").

In the following example, the subscripted global ^Produce(“fruit”,1) is specified in the first reference. Caché saves this global name and subscript in the naked indicator, so that the subsequent naked global references can omit the global name “Produce” and the higher subscript level “fruit”. When the ^(3,1) naked reference goes to a lower subscript level, this new subscript level becomes the assumption for any subsequent naked global references.

   SET ^Produce("fruit",1)="Apples"  /* Full global reference  */
   SET ^(2)="Oranges"                /* Naked global references */
   SET ^(3)="Pears"                  /* assume subscript level 2 */
   SET ^(3,1)="Bartlett pears"       /* Go to subscript level 3  */
   SET ^(2)="Anjou pears"            /* Assume subscript level 3 */
   WRITE "latest global reference is: ",$ZREFERENCE,!
   ZWRITE ^Produce
   KILL ^Produce

This example sets the following global variables: ^Produce("fruit",1), ^Produce("fruit",2), ^Produce("fruit",3), ^Produce("fruit",3,1), and ^Produce("fruit",3,2).

With few exceptions, every global reference (full or naked) sets the naked indicator. The $ZREFERENCE special variable contains the full global name and subscripts of the most recent global reference, even if this was a naked global reference. The ZWRITE command also displays the full global name and subscripts of each global, whether or not it was set using a naked reference.

Naked global references should be used with caution, because Caché sets the naked indicator in situations that are not always obvious, including the following:

  • A full global reference initially sets the naked indicator, and subsequent full global references or naked global references change the naked indicator, even when the global reference is not successful. For example attempting to WRITE the value of a nonexistent global sets the naked indicator.

  • A command postconditional that references a subscripted global sets the naked indicator, regardless of how Caché evaluates the postconditional.

  • An optional function argument that references a subscripted global may or may not set the naked indicator, depending on whether Caché evaluates all arguments. For example the second argument of $GET always sets the naked indicator, even when the default value it contains is not used. Caché evaluates arguments in left-to-right sequence, so the last argument may reset the naked indicator set by the first argument.

  • The TROLLBACK command, which rolls back a transaction, does not roll back the naked indicator to its value at the beginning of the transaction.

If a full global reference contains an extended global reference, subsequent naked global references assume the same extended global reference; you do not have to specify the extended reference as part of a naked global reference.

FeedbackOpens in a new tab