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 : 

ClassMethod RunMe()
{
   // 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 : 

Executing ##class(chs.Fw.EnsExt.Lib.CResultSetTools).RunMe()
+----------------- 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

Answers

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.

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).