Article
· 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.

Discussion (7)0
Log in or sign up to continue

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.