Unable to store (or rather retrieve) OREF in globals - Trying to create a Singleton Class.
We are trying to create a simple class extending %RegisteredObject that could be used as a singleton. However we are not able to store it in a global to later be retrieved (by the same process but elsewhere in the code).
I resumed my issue in this small code sample :
{
// Create a simple %RegisteredObject
set obj = ##class(%ZEN.proxyObject).%New()
set obj.MyProp = 22
do ##class(%SYSTEM.OBJ).Dump(obj)
// Store it in a global
set ^MyGlobalName = obj
write "Stored : " _ obj,!!
// Retrieve it from the global
#Dim obj2 As %ZEN.proxyObject = ^MyGlobalName
write "Got : " _ obj2,!
do ##class(%SYSTEM.OBJ).Dump(obj2)
}
Which lead to the following output when executed :
E
+----------------- general information ---------------
| oref value: 1
| class name: %ZEN.proxyObject
| reference count: 1
+----------------- attribute values ------------------
| %changed = 1
| %data("MyProp") = 22
| %index = ""
Stored : 1@%ZEN.proxyObject
Got : 1@%ZEN.proxyObject
'1@%ZEN.proxyObject' is not an oref value.
I'm open to all suggestions ! Any help would be more than welcomed ! Kind regards
André-Claude Gendron
You're asking two things: how to persist an object, and how to implement a singleton.
A global on its own is not able to save an object. Something needs to map the structure of its class to a global layout of some kind. The simplest way to do this is to subclass %Persistent, rather than %RegisteredObject, then call %Save().
I notice, however, that you're using %ZEN.proxyObject, presumably to avoid defining a class/schema upfront. In that case, you may be interested in looking at the document data model (DocDM) in the 2016.2 field test.
As for implementing a singleton, it depends on the context. In general, I would look at overriding %OnNew() to load an existing object if it exists. If you want to persist its state, you'll need to consider concurrency.
A very interesting question. I decided to write a singleton, with the idea that it searches process memory for other instances of this class and returns existing OREF if found. It also stores data in global and retrives it from there on a first load:
/// Singleton pattern implementation Class Utils.Singleton Extends %Library.SystemBase { /// Global to store content Parameter Global = "^Singleton"; /// Actual object content /// It can be be anything: string or a dynamic object Property Content As %String; /// This method finds OREF for an object of a current class if it exists in a current process ClassMethod GetOref() As %Integer { // Get current classname Set Cls = $ClassName() Set OREF = $$$NULLOREF // This query returns a list of all object instances currently in memory within the current process. &sql(SELECT Count(OREF),OREF INTO :Count,:OREFTemp FROM %SYSTEM.OBJ_ObjectList() WHERE ClassName=:Cls) If Count'=0 { Set OREF = OREFTemp } Return OREF } /// If no object of this class is found in process memory /// then a new one would be returned /// with Content value taken from global /// /// If there is an object of this class in memory /// then it would be returned ClassMethod Get() As Utils.Singleton { Set OREF = ..GetOref() If OREF = $$$NULLOREF { Set Obj = ..%New() } Else { // Convert integer-oref into real OREF Set Obj = $$$objIntToOref(OREF) } Return Obj } /// Test singleton object /// Do ##class(Utils.Singleton).Test() ClassMethod Test() { Set a = ##class(Utils.Singleton).Get() Set a.Content = $Random(100) Set b = ##class(Utils.Singleton).Get() Write "object b: " _ b.Content,! Do a.Save() Kill Set c = ##class(Utils.Singleton).Get() Write "object c: " _ c.Content } /// Constructor, loads data from global Method %OnNew() As %Status { // Return:($Stack($Stack-2,"PLACE")'["Get") $$$ERROR($$$GeneralError, "Сall Get method") Set ..Content = $Get(@..#Global) Return $$$OK } /// Saves data to global Method Save() { Set @..#Global = ..Content } }
Run:
Do ##class(Utils.Singleton).Test()
I tried to disable direct instantiation with $Stack checking, but it seems to fail from a terminal. I think it would work from non-interactive code, but I had not checked. Another way would be to set some variable in Get method and check it from %OnNew() method.
Download xml from GitHub.
OMG! Looks a little bit over-designed
Here are assumptions:
- you could not implement singleton which will be working across jobs, only for this same process;
- to share instance to teh object you could emply %-named variable or process-private global.
Here is the sample using PPG, which is easy to convert to % with redefinition of CpmVar macro
https://github.com/cpmteam/CPM/blob/master/CPM/Main.cls.xml#L33
> you could not implement singleton which will be working across jobs, only for this same process;
Storing something in %/PPG would not work across jobs too. Singleton is a single-thread anyway.
> to share instance to teh object you could emply %-named variable or process-private global.
Wrote another singleton using % variable. How would I do it with PPG (any global)? When you set global to an OREF it actually sets a string: "int@class". And you could not convert it back to an OREF with $$$objIntToOref at a later date because the object would be already destroyed.
/// Another singleton Class Utils.Singleton2 Extends %SystemBase { Property Content As %String; /// Set a = ##class(Utils.Singleton2).Get() ClassMethod Get() As Utils.Singleton2 { #Define Var %Var If '$Data($$$Var) || '$IsObject($$$Var) { Set Obj = ..%New() Set $$$Var = Obj } Else { Set Obj = $$$Var } Return Obj } /// Do ##class(Utils.Singleton2).Test() ClassMethod Test() { Set a = ##class(Utils.Singleton2).Get() Set a.Content = $Random(100) Set b = ##class(Utils.Singleton2).Get() Write b.Content } }
Ok, ok, good point about PPG that it will remove reference to the object. And that simple local variable will work better, because it will not serialize OREF to string.
And the only case when it was workng for me - then the parent call is still presented in the stack, and still has reference to the originally created object. Which is exactly the case when %-named singleton would work (i.e. you open object at the top of the stack, and it's still visible in the nested calls).
So yes, we better to get rid of PPG and use %-named variables in our CPM code (pull-requests welcome)
How would you have done it? It's 35 lines of code in total, 25 if we remove persistence (because singleton does not actually has persistence, but OP seemed to need it).
Hi André-Claud,
Taking the learning from @Eduard Lebedyuk, and just replacing ^MyGlobalName with %MyGlobalName would have worked:
do ##class(MMLOGGINGPKG.Util.Singleton).RunMe()
+----------------- general information ---------------
| oref value: 55
| class name: %ZEN.proxyObject
| reference count: 1
+----------------- attribute values ------------------
| %changed = 1
| %data("MyProp") = 22
| %index = ""
+-----------------------------------------------------
Stored : 55@%ZEN.proxyObject
Got : 55@%ZEN.proxyObject
+----------------- general information ---------------
| oref value: 55
| class name: %ZEN.proxyObject
| reference count: 3
+----------------- attribute values ------------------
| %changed = 1
| %data("MyProp") = 22
| %index = ""
+-----------------------------------------------------