Un-Typical persistence

Primary tabs

This is a coding example working on Caché 2018.1.3 and IRIS 2020.2 
It will not be kept in sync with new versions 
It is also NOT serviced by InterSystems Support !

During my search for a snapshot of a persistent object,
I met a feature that I would like tho share as it could be useful in some special situations.
My trigger was to have a before- and an after-image during unit testing. A typical persistent class may have a storge definition like this

Storage Default
{
 <Data name="kDefaultData">
 +<Value name="1">
 </Value>
 </Data>
 <DataLocation>^rcc.kD</DataLocation>
 <DefaultData>kDefaultData</DefaultData>
 <IdLocation>^rcc.kD</IdLocation>
 <IndexLocation>^rcc.kI</IndexLocation>
 <StreamLocation>^rcc.kS</StreamLocation>
 <Type>%Storage.Persistent</Type>
}

Now I applied this change: 

  Parameter MANAGEDEXTENT = 0; ;extent manager dislikes this change

Storage Default
{
 <Data name="kDefaultData">
 +<Value name="1">
  </Value>
 </Data>
 <DataLocation>@(%storage_"D")</DataLocation>
 <DefaultData>kDefaultData</DefaultData>
 <IdLocation>@(%storage_"D")</IdLocation>
 <IndexLocation>@(%storage_"I")</IndexLocation>
 <StreamLocation>@(%storage_"S")</StreamLocation>
 <Type>%Storage.Persistent</Type>
}

All you have to do now to use it:

set %storage="^myGlobal"         ;; normal use with ROLLBACK
or 
set %storage="%myLocalVariable"  ;; no ROLLBACK
or
set %storage="^||myPPG"          ;; no ROLLBACK

and it works as you are used to it.
Except for ROLLBACK as there is, of course,  no Journal behind PPG or local variables 

A typical use sequence to prepare a check for changes could look similar:

set %storage="^rcc.k" 
set obj=##class(rcc.k).%OpenId(id)       ;; get original
do obj.%SetModified(1)                    ;; prepare for %Save
set %storage="^||rcc"                    ;; location of copy
set sc=obj.%Save()                        ;; write copy to temp storage
//// carry on with testing and changes and find what happened

 

 I think it's worth to share it.

Replies

This is nifty! Note, you can make the extent manager happy by using:


Class DC.Demo.SometimesPersistent Extends %Persistent
{

Property Foo As %String;

ClassMethod Demo()
{
    New %storage,%fooD,%fooI,%fooS
    Set obj = ##class(DC.Demo.SometimesPersistent).%New()
    Set obj.Foo = "bar"
    Set %storage = "%foo"
    Write !,obj.%Save()
    Kill obj
    Set obj = ..%OpenId(1)
    w ! zw obj
    zw %fooD
}

Storage Default
{
<Data name="SometimesPersistentDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Foo</Value>
</Value>
</Data>
<DataLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</DataLocation>
<DefaultData>SometimesPersistentDefaultData</DefaultData>
<IdLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</IdLocation>
<IndexLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"I")</IndexLocation>
<StreamLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"S")</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

There is much to be discovered regarding Object Persistence. The ability to specify an expression in place of a literal global name in any of the various LOCATION keywords is just one. For example, you can add a public variable to a LOCATION keyword and its value at object filing time will be used to form the global location where the filer will update the data. There is risk involved and these "features" are most likely not documented. I do not recommend using these in production systems unless you fully understand the ramifications of doing so.

That said, there are a number of features that may or may not be documented but are certainly not private. In the context of this message, two come to mind. First is the Object Journal. Override the parameter OBJJOURNAL in a persistent class and all other classes referenced by this class and filing events are journaled. The Object Journal records each filing event for classes with the OBJJOURNAL parameter set and another class, %ObjectJournalTransaction, can be used to view the versions of those objects that were filed. All this would be wonderful but for a bug that I just discovered while coming up with an example for this post. I did a bit of research and it seems this bug has been present for a very long time and never reported. That indicates nobody is aware that this feature exists. I fixed the bug temporarily for the example.

SAMPLES>set person = ##class(Sample.Person).%OpenId(10)

SAMPLES>write oldperson.Name
Uhles,Ralph W.
SAMPLES>set person.Home.State = "NY"

SAMPLES>w person.%Save()
1
SAMPLES>set person.Office.State = "FL"

SAMPLES>set person.Name = "Book, John J"

SAMPLES>write person.%Save()
1
SAMPLES>write person.Name
Book, John J
SAMPLES>set journal = ##class(%ObjectJournalRecord).IDKeyOpen(3,4)

SAMPLES>set oldperson = journal.OpenObjectVersion(.s)

SAMPLES>write oldperson.Name
Uhles,Ralph W.

The second item of interest is something that has been around for a while as well and is definitely a fully documented and supported feature. This feature, Triggers, was previously an SQL only feature but it is now (and has been for several versions) available for Object filing as well. Using a save trigger for Objects and SQL allows access to the old and new values as well as a way to detect which values have been modified. I am happy to post an example if anyone is interested.

While the ability to specify an expression as a LOCATION keyword value and the use of Object Journal are not well known or mainstream features, Triggers are very much mainstream and can be quite useful.

Thank you @Dan Pasco !
This confirms that it is not just a crazy idea ( as some of my former colleagues classified it).
I met this during testing about a decade ago to manage test data and to compare the impact of code changes. 
And - based on my history - the global was and is the ultimate truth for me of what is done on objects.
Robert