Classes, tables and globals - how it all works?

When I start talking about InterSystems IRIS with more technically-minded people I always talk about how at the root of things it is a multimodel DBMS.

In my opinion that is the main advantage (on the DBMS side).

You want some sort of summary for your data? Use SQL!

Do you want to work extensively with one record? Use objects!

Want to access or set one value and you know the key? Think again. Use globals!

And the data is stored only once. You just choose the way you want to access it.

On the first overview it's a nice story - short and concise and it gets the message across, but when people really start working with InterSystems IRIS the questions start.

How are classes and tables and globals related? What are they to each other? How's data really stored?

In this article I would try to answer these questions and explain what's really going on.

Part one. Model bias.

People working with data are oft biased towards the model they work with.

Developers think in objects. For them databases and tables are boxes you interact with via Save/Load (preferably over ORM) but the underlying structure for them is objects (of course it's mainly true for developers in object-oriented languages - so most of us).

On the other hand DBAs often think of data as tables - result of working with relational DBMS. Objects are just wrappers over rows in this case.

As with InterSystems IRIS class is also a table which stores data in global, some clarification is required.

 

Part two.Example.

Let's say you created class Point:

Class try.Point Extends %Persistent [DDLAllowed]
{

Property X;

Property Y;

}

You can also create it via DDL:

CREATE Table try.Point (
    X VARCHAR(50),
    Y VARCHAR(50))

It would create the same class.

After compilation our new class would have autogenerated Storage structure which is a mapping of global to data to columns and properties:

Storage Default
{
<Data name="PointDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>X</Value>
</Value>
<Value name="3">
<Value>Y</Value>
</Value>
</Data>
<DataLocation>^try.PointD</DataLocation>
<DefaultData>PointDefaultData</DefaultData>
<IdLocation>^try.PointD</IdLocation>
<IndexLocation>^try.PointI</IndexLocation>
<StreamLocation>^try.PointS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

What is going on here?

From the bottom and up (bolded are important, ignore the rest):

  • Type: type of generated storage, in our case the default storage for persistent objects
  • StreamLocation - name of global where we store streams
  • IndexLocation - name of global for indices
  • IdLocation - name of global where we store ID autoincremental counter
  • DefaultData - name of storage XML element which maps global value to columns/properties
  • DataLocation - name of global to store data

Now our default data is PointDefaultData so let's see it.  Essentially it says that global node has this structure:

  • 1 - %%CLASSNAME
  • 2 - X
  • 3 - Y

So we might expect our global to look like this:

^try.PointD(id) = %%CLASSNAME, X, Y

But if we output our global it would be empty because we didn't add any data:

zw ^try.PointD

Let's add one object:

set p = ##class(try.Point).%New()
set p.X = 1
set p.Y = 2
write p.%Save()

And here's our global

zw ^try.PointD
^try.PointD=1
^try.PointD(1)=$lb("",1,2)

As you see our expected structure %%CLASSNAME, X, Y is set with $lb("",1,2) which corresponds to X and Y properties of our object (%%CLASSNAME is system property, ignore it).

We can also add a row via SQL:

INSERT INTO try.Point (X, Y) VALUES (3,4)

Now our global looks like this:

zw ^try.PointD
^try.PointD=2
^try.PointD(1)=$lb("",1,2)
^try.PointD(2)=$lb("",3,4)

So the data we add via objects or sql is stored in globals according to storage definitions (you can manually modify the storage definition by replacing X and Y in PointDefaultData  - check what happens to the new data!)

Now, what happens when we want to execute SQL query?

SELECT * FROM try.Point

It is translated into ObjectScript code that iterates over ^try.PointD global and populates columns based on storage definition - PointDefaultData  part of it precisely.

Now for modifications. Let's delete all the data from the table

DELETE FROM try.Point

And let's see our global after it:

zw ^try.PointD
^try.PointD=2

Note that only ID counter is left, so new object/row would have an ID=3. Also our class and table continue to exist.

But what happens on:

DROP TABLE try.Point

It would destroy the table, class and delete the global.

zw ^try.PointD

 

If you followed this example I hope you now understand how does globals, classes and tables correspond to each other.