Overriding the property setter and getter methods

Primary tabs

I want to override the Get and Set methods of a class property.  The class maps to a pre-existing global. The property is defined like so:

Property Invalid As %Library.Boolean;

with the property mapping to a node like ^GLOBAL(Code,"INVALID")=1
Code is a property in the same class.

The value can be 0 or 1 or the node might not exist. When it doesn't exist I want the value of the SQL field to come out as 0 (false).

I'm trying to do it so that it will be self-contained, so that nothing that references the class or uses  SQL to access/update the table also won't need to be changed and the field needs to be updatable by SQL as well as retrievable.

I've tried using the Retrieval code of the storage map, but then updating the field from SQL doesn't work. And I've also tried to override the Set and Get methods as follows:

Method InvalidGet() As %Library.Boolean
{
  //I've tried both of the below lines
  I +$G(^GLOBAL(..Code,"INVALID")) Quit 1
  I +$G(^GLOBAL(i%Code,"INVALID")) Quit 1
  Quit 0
}

Method InvalidSet(value As %Library.Boolean) As %Status
{
    Set i%Invalid=value
    quit $$$OK
}


but it doesn't seem to be even running the code. I've cleared cached queries to make sure it's picking up the changes and I'm still getting blank fields when doing an SQL on the table.

The documentation page I've looked at is entitled Using and Overriding property methods.

Anyone got any ideas how to get it to work or why it isn't working?

thanks

Wendy

  • + 2
  • 0
  • 895
  • 10
  • 2

Answers

Getter and Setter are object related concepts, SQL does not use them. You can, however  specify SqlComputeCode  for SELECT access to a property. This example stores and retrieves Invalid property value from ^Utils.GlobalPropP global.

Class Utils.GlobalProp Extends %Persistent
{

Parameter InvalidGLVN = "^Utils.GlobalPropP";

Property Invalid As %String [ SqlComputeCode = {set {*} = ##class(Utils.GlobalProp).InvalidStatic()}, SqlComputed, Transient ];

Method InvalidGet() As %String
{
    Return ..InvalidStatic()
}

ClassMethod InvalidStatic() As %String
{
    Return $Get(@..#InvalidGLVN)
}

Method InvalidSet(val As %String) As %Status
{
    Set @..#InvalidGLVN = val
    Return $$$OK
}

/// Do ##class(Utils.GlobalProp).Test()
ClassMethod Test()
{
    Do ..%KillExtent()
    Set obj = ..%New()
    Write "Invalid old: " _ obj.Invalid,!
    Set obj.Invalid = $Random(100)
    Write "Invalid new: " _ obj.Invalid,!
    Do obj.%Save()
    Kill obj
    &sql(SELECT Invalid INTO :invalid FROM Utils.GlobalProp WHERE Id = 1)
    Write "SQLCODE: " _ SQLCODE,!
    Write "Invalid sql: " _ invalid,!
}
}

Code on GitHub.

You do not need to override the propGet method here as this will be generated automatically to call the sqlcomputecode if that is defined.

I removed InvalidGet method and object access to the property stopped working.

Class Utils.GlobalProp Extends %Persistent
{

Parameter InvalidGLVN = "^Utils.GlobalPropP";

Property Invalid As %String [ SqlComputeCode = {set {*} = ##class(Utils.GlobalProp).InvalidStatic()}, SqlComputed, Transient ];

ClassMethod InvalidStatic() As %String
{
    Return $Get(@..#InvalidGLVN)
}

Method InvalidSet(val As %String) As %Status
{
    Set @..#InvalidGLVN = val
    Return $$$OK
}

/// Do ##class(Utils.GlobalProp).Test()
ClassMethod Test()
{
    Do ..%KillExtent()
    Set obj = ..%New()
    Write "Invalid old: " _ obj.Invalid,!
    Set obj.Invalid = $Random(100)
    Write "Invalid new: " _ obj.Invalid,!
    Do obj.%Save()
    Kill obj
    &sql(SELECT Invalid INTO :invalid FROM Utils.GlobalProp WHERE Id = 1)
    Write "SQLCODE: " _ SQLCODE,!
    Write "Invalid sql: " _ invalid,!
}

Outputs:

Invalid old:
Invalid new:
SQLCODE: 0
Invalid sql: 65

Related Int code:

zInvalidCompute(%id)
    New %tException,%val set %val = ""
    try {
    set %val = ##class(Utils.GlobalProp).InvalidStatic()
    } catch %tException { throw %tException }
    Quit %val
zInvalidGet() public {
    If i%Invalid = "" { Set ..Invalid=..InvalidCompute($listget(i%"%%OID")) } Quit i%Invalid }
zInvalidSQLCompute()
    // Compute code for field Invalid
 set %d(2) = ##class(Utils.GlobalProp).InvalidStatic()
 QUIT

    Do $System.Status.DisplayError(tStatus)
        Write !
    }
    Quit
}
}

Firstly thanks for your replies.

I've tried to incorporate your ideas into our code, but I find it's still not working.

But then I didn't really understand the use of Parameter InvalidGLVN = "^Utils.GlobalPropP";

I couldn't see that you're tell it where the "INVALID" node is set, so I've tried to create a self-contained class for my example:


 

Now with the Storage definition for Invalid (highlighted) that seems to override the compute code and the SQL works but it shows blanks on the rows where it's not set.

But when I remove the storage definition for Invalid and try to query the table with SQL it just falls over with

  [SQLCODE: <-400>:<Fatal error occurred>]

  [%msg: <Unexpected error occurred: <UNDEFINED>%0Go+1^%sqlcq.USER.cls2.1 *i%%CursorDatad(12)>]

Class Wendy.LTCodes Extends %Persistent [ StorageStrategy = LTCStorage ]
{

Property Code As %String;

Property Description As %String;

Property Invalid As %Library.Boolean [ SqlComputeCode = { set {*} = ##class(Wendy.LTCodes).GetInvalid({Code})}, SqlComputed, SqlComputeOnChange = Code ];

Index CodeIndex On Code [ IdKey, PrimaryKey, Unique ];

ClassMethod GetInvalid(WSCode) As %Boolean
{
    Quit +$G(^LTCODES(WSCode,"INVALID"))
}

Method InvalidGet() As %Boolean
{
    Quit ..GetInvalid(i%Code)
}

Method InvalidSet(value As %Boolean) As %Status
{
    Set ^LTCODES(i%Code,"INVALID")=value
    quit $$$OK
}

/// Do ##class(Wendy.LTCodes).SetData()
ClassMethod SetData()
{
    S ^LTCODES("N001")="ANYOLD DESC"
    S ^LTCODES("N001","INVALID")=1
    S ^LTCODES("N002")="C5 REPEAT 1"
    S ^LTCODES("N111")="SPECIMEN COMMENT"
    S ^LTCODES("N200")="MSUD SCREEN|MSUD"
    S ^LTCODES("N500")="Sickle Cell Screen"
}

Storage LTCStorage
{
<SQLMap name="LTCMap">
<Data name="Description">
<Delimiter>"|"</Delimiter>
<Piece>1</Piece>
</Data>
<Data name="Invalid">
<Node>"INVALID"</Node>
</Data>
<Global>^LTCODES</Global>
<Subscript name="1">
<Expression>{Code}</Expression>
</Subscript>
<Type>data</Type>
</SQLMap>
<StreamLocation>^Wendy.LTCodesS</StreamLocation>
<Type>%CacheSQLStorage</Type>
}

}

So is there a way I can get the above to work?

I imported your example, executed:

Do ##class(Wendy.LTCodes).SetData()

Then executed this sql:

SELECT
ID, Code, Description, Invalid
FROM Wendy.LTCodes

and received the following results:

IDCodeDescription  Invalid
N001N001ANYOLD DESC1
N002N002C5 REPEAT 1 
N111N111SPECIMEN COMMENT 
N200N200MSUD SCREEN 
N500N500Sickle Cell Screen 

seems to be working.

 

But then I didn't really understand the use of Parameter InvalidGLVN = "^Utils.GlobalPropP";

it's for use with indirection.  Example:

set ^Utils.GlobalPropP = 123
set glvn = "^Utils.GlobalPropP"
write glvn
> ^Utils.GlobalPropP
write @glvn
> 123

It doesn't work in the sense that it's not producing either 0 or 1 in the invalid column. It has blanks where it's not set.

These changes fixed it:

  • Removed highlited part of storage definition
  • Made invalid property Transient
  • Added 0 default to $Get
Class Wendy.LTCodes Extends %Persistent [ StorageStrategy = LTCStorage ]
{

Property Code As %String;

Property Description As %String;

Property Invalid As %Library.Boolean [ SqlComputeCode = { set {*} = ##class(Wendy.LTCodes).GetInvalid({Code})}, SqlComputed, Transient ];

Index CodeIndex On Code [ IdKey, PrimaryKey, Unique ];

ClassMethod GetInvalid(WSCode) As %Boolean
{
    Quit $G(^LTCODES(WSCode,"INVALID"),0)
}

Method InvalidGet() As %Boolean
{
    Quit ..GetInvalid(i%Code)
}

Method InvalidSet(value As %Boolean) As %Status
{
    Set ^LTCODES(i%Code,"INVALID")=value
    quit $$$OK
}

/// Do ##class(Wendy.LTCodes).SetData()
ClassMethod SetData()
{
    kill ^LTCODES
    S ^LTCODES("N001")="ANYOLD DESC"
    S ^LTCODES("N001","INVALID")=1
    S ^LTCODES("N002")="C5 REPEAT 1"
    S ^LTCODES("N111")="SPECIMEN COMMENT"
    S ^LTCODES("N200")="MSUD SCREEN|MSUD"
    S ^LTCODES("N500")="Sickle Cell Screen"
}

Storage LTCStorage
{
<SQLMap name="LTCMap">
<Data name="Description">
<Delimiter>"|"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>^LTCODES</Global>
<Subscript name="1">
<Expression>{Code}</Expression>
</Subscript>
<Type>data</Type>
</SQLMap>
<StreamLocation>^Wendy.LTCodesS</StreamLocation>
<Type>%CacheSQLStorage</Type>
}

}

Yes, the 1's and 0's come out then, but the field isn't updatable from SQL.  Presumably because you've now said it's transcient?

Update Wendy.LTCodes set Invalid=1 where ID = 'N002'

doesn't do anything

Okay, then you need to use your original storage definition, but when selecting invalid, select it in a NVL function:

SELECT
ID, Code, Description, NVL(Invalid, 0)
FROM Wendy.LTCodes

It should return 0 instead of empty string.

In the original post I said I'm trying to do the change without needing to change any existing SQL.  If I can't fix it within Cache then I will likely have to pass the problem over to the front end (VB client)  support, but thanks again for trying.

You have two cases, you change storage definition or you can use calculated value. But in case with calculated value, you have two options. It's an object's access and sql access, and such getter it is just only for object's access. For SQL you should define SqlComputeCode. Something like this.

Property Invalid As %Library.Boolean [Calculated, SqlComputed, SqlComputeCode = { s {*}={+$G(^GLOBAL({Code},"INVALID"))} }];

more details in documentation

Comments

Anyone else got any ideas on getting this working so Invalid field always displays 0 or 1, but is also updateable from SQL?  Example class as follows:

Class Wendy.LTCodes Extends %Persistent [ StorageStrategy = LTCStorage ]
{

Property Code As %String;

Property Description As %String;

Property Invalid As %Library.Boolean [ SqlComputeCode = { set {*} = ##class(Wendy.LTCodes).GetInvalid({Code})}, SqlComputed, SqlComputeOnChange = Code ];

Index CodeIndex On Code [ IdKey, PrimaryKey, Unique ];

ClassMethod GetInvalid(WSCode) As %Boolean
{
    Quit +$G(^LTCODES(WSCode,"INVALID"))
}

Method InvalidGet() As %Boolean
{
    Quit ..GetInvalid(i%Code)
}

Method InvalidSet(value As %Boolean) As %Status
{
    Set ^LTCODES(i%Code,"INVALID")=value
    quit $$$OK
}

/// Do ##class(Wendy.LTCodes).SetData()
ClassMethod SetData()
{
    S ^LTCODES("N001")="ANYOLD DESC"
    S ^LTCODES("N001","INVALID")=1
    S ^LTCODES("N002")="C5 REPEAT 1"
    S ^LTCODES("N111")="SPECIMEN COMMENT"
    S ^LTCODES("N200")="MSUD SCREEN|MSUD"
    S ^LTCODES("N500")="Sickle Cell Screen"
}

Storage LTCStorage
{
<SQLMap name="LTCMap">
<Data name="Description">
<Delimiter>"|"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>^LTCODES</Global>
<Subscript name="1">
<Expression>{Code}</Expression>
</Subscript>
<Type>data</Type>
</SQLMap>
<StreamLocation>^Wendy.LTCodesS</StreamLocation>
<Type>%CacheSQLStorage</Type>
}

}