What Is the Best Object Callback for Insert Operations: %OnBeforSave(), %OnAddToSaveSet(), %OnAfterSave()

Hi!

I'm saving object instance using RestForms.

RESTFormsUI calls %Save() for me, which is great. But I want to set the property CreationDate with the current date for every new record being inserted.

So  object callback implementation seems as a reasonable option. I did the following:

Method %OnBeforeSave(insert as %Boolean) As %Status [ Private, ServerOnly = 1 ] 
{

 if insert s ..CreationDate=$H

 q $$$OK

}

And it works fine.

But Documentation says I'd better use %OnAddToSaveSet()

What are you using for the callbacks in similar cases?

  • 0
  • 0
  • 308
  • 9
  • 5

Answers

As far as I know, %OnBeforeSave is used to prevent (abort) the save operation and to customize %Save status.
You can't change any properties on that phase as they won't be reflected, that is because the payload is already queued

to be saved.

%OnAddToSaveSet is executed before before the queueing phase, which allows you to overwrite property values.

 

Unless you want to do something that deals with complex business rules, you should indeed use InitialExpression as Fabian suggested, even use ReadOnly to prevent the property's value to be edited.

Triggers and computed properties (compute triggers) are certainly better than callbacks. Now that we have unified triggers callbacks should be avoided - IMO - in favor of using triggers. However, initial expression when there is some outside influence such as a clock or other value source involved, when the property is not read-only protected, etc. makes using initialexpression a bad idea in some cases. In cases where there is no opportunity for the user to interact with a new instance prior to it being saved then initialexpression is acceptable. If there is opportunity for interaction between instantiation and save (serialization) then initialexpression can be unreliable. The %%INSERT/%%UPDATE compute triggers avoid that. 

Thanks, Dan!

That definitely makes sense.

What I like about object callbacks is the code readability. Compare:

Method %OnBeforeSave(insert as %Boolean) As %Status [ Private, ServerOnly = 1 ]

{

if insert s ..CreationDate=$H

q $$$OK }

And 

Property CreationDate As %TimeStamp [ SqlComputeCode = { set {*}=$zdt($h,3) }, SqlComputed, SqlComputeOnChange = %%INSERT ];

What if I would need to refer to other properties in

{*)=expression() 

Or the expression would be complex and if I want to debug it?

Is there a way to keep callbacks readability and have the callback methods  be fired for object and SQL access both?

Something like:

Property CreationDate As %TimeStamp [ SqlComputeCode = { set {*}=..%OnBeforeSave(1) }, SqlComputed, SqlComputeOnChange = %%INSERT ];

Yes, of course. The SQLCOMPUTECODE can invoke a classmethod but not an instance method unless you provide the instance. If you wish to pass arguments that correspond to other properties then just use curly-brace syntax to refer to them:

sqlcomputecode = { set {*} = ..MyComputeMethod({property1},{property2}) }

and the compiler will work out the details.

Also, we have proposed syntactical changes that would make this much nicer. The idea is that a class definition is composed of some set of members, members being things like properties, methods, indices, and so on. Each member type, excepting methods, can also have methods. The idea for the syntax is to embed the method definition with the member. This was just an idea and is not an active development project. But - something like this is what we envision:

property foo as %String [ sqlcomputed ] {

    method Compute(someParameter as %String) as %String {

        your code goes here, set the return value and return it

    }

}

 

Just an idea...

Hi Evgeny,

I have a JSON-RPC library that does something similar to RESTForms. My approach is to have a higher level save method where I can create / modify the object and then call its actual save method.

The problem with modifying the object inside OnBeforeSave() is that the modifications will occur after the validation has been called. The danger being that duff data could get persisted.

The OnAddToSaveSet() will trigger before the validation is called which will bubble up any validation errors correctly.

Alternatively...

Property CreationDate As %Date [ InitialExpression = {+$h} ];

Sean.

 

Thanks, Sean!

JSON-RPC library

Would be great to look into this if you want to expose it some day.

 

Very interesting question. Rubens Silva's answer is accurate - %OnAddToSaveSet provides more flexibility when you need to modify the object or the graph of objects to be serialized. 

The callbacks we provide as part of Caché Objects are limited to Objects and are often quite useful. However, Caché Persistent classes also project to SQL as tables. Users can perform CRUD operations using either Objects or SQL. Callbacks are not processed by SQL so any work you do in a callback must either be reproduced in some form in SQL or the user must recognize that accessing persistent data through SQL might produce different results from performing an equivalent access using Caché Objects. 

 

But we have better choices these days. Caché Objects provides two mechanisms for triggering behavior. SQL recognizes both of these mechanisms. The first is a compute trigger. You can define a property as SQLCOMPUTED. The value of the property can be defined to be computed whenever the value of a property defined in the SQLCOMPUTEONCHANGE list of properties is changed. We support two meta-properties to trigger a compute on insert and update - %%INSERT and %%UPDATE. If SQLCOMPUTEONCHANGE includes %%INSERT then the value of the property is computed when the object is about to be inserted into the database. This computation will override the current value of the property. Similarly, if %%UPDATE is specified in SQLCOMPUTEONCHANGE then the property's value is computed just prior to saving an updated version of the object. 

Compute triggers have been present in Caché for a very long time and %%INSERT/%%UPDATE were available in 2008.1.

 

The second mechanism is a TRIGGER. TRIGGERs were one the exclusive domain of SQL but we since added a new FOREACH keyword value of ROW/OBJECT. When FOREACH = ROW/OBJECT the trigger will be pulled at the defined time for each filing event using SQL or Objects. FOREACH = ROW indicates that the trigger is invoked only when SQL is used. There is no FOREACH value for invoking a trigger only when Objects is used.

We refer to this feature as "unified triggers". This feature first appears in Caché 2014.1.

-Dan

 

As a follow-up, I should have provided an example!


Class User.TriggerCompute Extends (%Persistent, %Populate)
{

Property foo As %String;

Property CreationDate As %TimeStamp [ SqlComputeCode = { set {*}=$zdt($h,3) }, SqlComputed, SqlComputeOnChange = %%INSERT ];
}

and I populated this class at random intervals. Here are five sample rows:

select top 5 * from triggercompute order by creationdate desc

 

IDCreationDatefoo
112017-05-26 08:40:15Y1407
102017-05-26 08:40:10J9793
92017-05-26 08:40:03Y5011
82017-05-26 08:39:57E51
72017-05-26 08:39:55L3468

5 row(s) affected

 

Please note that It's not recommended to use %OnAddToSaveSet(), especially performing heavy operations there.

I don't understand why it is not recommended. Can you elaborate?

%OnAddToSaveSet() can get called many times during modification or saving of our target object and all objects related to it. Therefore it's better to move code we need to execute once to triggers or initialexpression or sqlcomputed.

Comments

Why don't you just set the initialexpression to the current date? That's the easiest way in this case.

Thank you, Rubens, Fabian, Sean.

Agreed, that Initial expression is the best option here. And thanks for the explanations on the callbacks!