Article
Robert Cemper · Apr 25, 2020 2m read

Semi-Persistent Classes and Tables

If you define a Persistent Class / Table the class compiler generates for you an appropriate Storage definition.
A different option is to define a SQL mapping for an already existing Global storage.  This has been excellently
explained already in a different series of articles.   The Art of Mapping Globals to Classes 1 of 3

Once your storage map is defined it might be extended by the class compiler but the fundamental
storage parameters will not change.
This does not mean that you can't change it manually yourself.

My article on The adopted Bitmap includes such a case. And in combination with some 
Static WHERE Conditions as described earlier you make it available also to SQL.

The typical definition of your storage globals may look like this example:

<DataLocation>^User.PersonD</DataLocation>
<IdLocation>^User.PersonD</IdLocation>
<IndexLocation>^User.PersonI</IndexLocation>
<StreamLocation>^User.PersonI</StreamLocation
>

Now we change this definition into a dynamic one

<DataLocation>@%MyData</DataLocation>
<IdLocation>@%MyId</IdLocation>
<IndexLocation>@%MyIndex</IndexLocation>
<StreamLocation>@%MyStream</StreamLocation>

and we add some comfort

Parameter MANAGEDEXTENT As INTEGER = 0;

ClassMethod SetStorage(ref As %String) As %Integer [ SqlName = SetStorage, SqlProc ]
{
    set %MyData=ref_"D"
      , %MyId=%MyData
      , %MyIndex=ref_"I"
      , %MyStream=ref_"S"
    quit $$$OK
}

For object access we direct our storage and fill it

write ##class(Person).SetStorage("^mtemp.Person")
write ##class(Person).Populate(5)

and in SQL:

SELECT  from Person where SetStorage('^mtemp.Person')=1

It works with PPG  (^||Person)
across namespace  (^|"USER"|Person)
also subscripted as used for The adopted Bitmap   with another variant of ClassMethod SetStorage() 

For object access (without SQL) it even works for local variables.
It's not the intended use but it demostrates how much flexibility is in this feature. 

But take care. It will NOT work with Sharding. But that should  not be surprising.

 

90
1 7 214 1

Replies

So this means, that when you create a Person class, while it is in memory, you can define in which global should it be stored, regardless of the namespace?
This way you can dynamically choose where you want to save it. That sounds cool!

BINGO!
You got my point. Of course, you should be careful with the storage not to mix it up. Therefore the class method to keep all related globals in sync.

I don't want to discourage creativity, but this approach feels very risky to me. When you issue any SQL against this table before setting the % variable, it's going to cause ugly errors or unpredictable behaviour. I also wouldn't bet my money on this working in all possible parallel query execution scenarios (likely some, likely not all).

FWIW, within InterSystems development, we typically call % variables that survive between method calls a leak rather than single-rivet-keeping-your-skyscraper-together :-). Again, apologies for putting it a little strong here, but I think most use cases asking for flexibility can be addressed with more robust solutions, such as temporary tables, class inheritance (with the NoExtent keyword), etc. 

I fully agree with your concerns. Especially related to parallel processing and sharding.
And modern code and design will never need this.
But there are millions of lines of old code out in the field that require these dirty tricks to survive.
And YES! You have to examine very carefully what you do.

It's a little bit like mountaineering:
Most take the cable car, some climb with ropes and a lot of fancy equipment.  While a few free climbers use nothing than their body.
You have to understand the risks and to take your decision.

Interesting @Robert Cemper  !

I wrote a similar code the last year in order to have storage compatible Caché\healthshare and Iris.

We have a legacy persistent class mapped on ^CacheMsg global, my solution : 

<SQLMap name="CacheMsg">
<Data name="msg">
<Delimiter>"^"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>@($s($zv'["IRIS":"^CacheMsg",1:"^IRIS.Msg"))@</Global>

It works very well, but perhaps a little bit slow due to indirection usage.

I'll keep this code until a complete migration to Iris and then It will be removed.

an Excellent Reference.  you got the point.
for speed: If don't want to run a benchmark indirection isn't bad 
as in this case: functionality counts
yes smiley

Thank you for your feedback @Robert Cemper 
I didn't run a benchmark, because in my case it's a deprecated class without intensive usage.

It's good to know the indirection performance is not bad. smiley