DeleteHelper - A Class to Help with Deleting Referenced Persistent Classes
Following up on the topic of making sure that referenced instances are deleted when the referrer is deleted –
This topic was discussed in this "Ensemble Orphaned Messages" post, and was also touched in my post with a utility that can help verify all related data was purged.
What is the DeleteHelper
In the "Ensemble Orphaned Messages" one, Suriya suggests to use an %OnDelete() method in your related classes (also as documented), and provides a sample for one.
In this post I want to share a class that could help with this.
You can find it here on GitHub.
This is based on some code that evolved through being used by several sites, and then adapted for this public share. You are of course welcome to fix any issues you find, or add any enhancements.
The class includes a Code Generator %OnDelete method, which generates code at compile-time that should take care of deleting persistent objects referenced by the instance being deleted.
[Note in this case the %OnDelete approach was taken, but in general there are various ways of "telling" a class to delete "other objects" when an instance is deleted – there are some "cascading" mechanisms, for Relationships and for Foreign Keys, as well as using an SQL Trigger (which nowadays also will be called upon object events as well)].
What you'd need to do is add this class as another Super class for your class.
There is also an accompanying class that should help with adding this as a Super class for several classes at once (useful for example for classes that were generated by the SOAP Wizard, which might be quite a few).
Note the use-case in which this class is brought here is Ensemble Message (and Business Process Context) purging, but this is relevant also for "pure" Caché scenarios.
Example
There is more information in the class reference of the 2 classes involved, including usage examples and sample cases. From there:
Here is an example of a class and what it's generated %OnDelete will look like -
A snippet from the Class Definition:
Property Name As %String(MAXLEN = "", XMLNAME = "Name") [ Required ];
Property SSN As %String(MAXLEN = "", XMLNAME = "SSN") [ Required ];
Property DOB As %Date(XMLNAME = "DOB");
Property Home As ITest.Proxy.s0.Address(XMLNAME = "Home");
Property Office As ITest.Proxy.s0.Address(XMLNAME = "Office");
Property Spouse As ITest.Proxy.s0.Person(XMLNAME = "Spouse");
(Assuming the Address and Person classes are Persistent)
And this is the generated %OnDelete (from the generated INT routine):
%OnDelete(oid) public { Set status = 1 Set obj = ..%Open(oid,,.status) If ('status) { Set errorCode = $System.Status.GetErrorCodes(status) If errorCode [ 5809 { Quit 1 } Else { Quit status } } If $IsObject(obj.Home) { Set delStatus = obj.Home.%DeleteId(obj.Home.%Id()) If ('delStatus) { Set errorCode = $System.Status.GetErrorCodes(delStatus) If errorCode [ 5810 { } Else { Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) } } } If $IsObject(obj.Office) { Set delStatus = obj.Office.%DeleteId(obj.Office.%Id()) If ('delStatus) { Set errorCode = $System.Status.GetErrorCodes(delStatus) If errorCode [ 5810 { } Else { Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) } } } If $IsObject(obj.Spouse) { Set delStatus = obj.Spouse.%DeleteId(obj.Spouse.%Id()) If ('delStatus) { Set errorCode = $System.Status.GetErrorCodes(delStatus) If errorCode [ 5810 { } Else { Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) } } } Quit status }
(The code uses some macros, primarily for Status handling, that are displayed above, as this is INT code, in their translated manner)
Here is another sample, this time with a list collection -
The class definition snippet:
Property GetListByNameResult As list Of ITest.Proxy.s0.PersonIdentification;
And the generated code snippet:
If $IsObject(obj.GetListByNameResult) {
Set key=""
Set item = obj.GetListByNameResult.GetNext(.key)
While key'="" {
If $IsObject(item) {
Set delStatus = item.%DeleteId(item.%Id())
If ('delStatus) {
Set errorCode = $System.Status.GetErrorCodes(delStatus)
If errorCode [ 5810 {
} Else {
Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus))
}
}
}
Set item = obj.GetListByNameResult.GetNext(.key)
}
Upcoming New 2017.1 Planned Related Feature
Version 2017.1 (available now as a Field Test) includes a new feature (internal reference – MXT2018) that allows you to choose, as part of the XML Schema Wizard, or the SOAP Wizard, that your generated classes will include %OnDelete methods to do exactly what was discussed above:
And this is a sample method that was created in the Wizard-generated class (not via the DeleteHelper):
/// The %OnDelete method is generated in order to cascade deletes of an XML tree.
/// If this class is modified, then the %OnDelete method will need to change to accommodate changed persistent object references.
ClassMethod %OnDelete(deleteOid As %ObjectIdentity) As %Status [ Private, ProcedureBlock = 1, ServerOnly = 1 ]
{
Set oref=..%Open(deleteOid,,.sc) If $$$ISERR(sc) Quit sc
Set oid=oref.HomeGetObject()
If oid'="" Set sc=$$$ADDSC(sc,##class(WSTest.Proxy.s0.Address).%Delete(oid))
Set oid=oref.OfficeGetObject()
If oid'="" Set sc=$$$ADDSC(sc,##class(WSTest.Proxy.s0.Address).%Delete(oid))
Set oid=oref.SpouseGetObject()
If oid'="" Set sc=$$$ADDSC(sc,##class(WSTest.Proxy.s0.Person).%Delete(oid))
Quit sc
}
[The method generated by the DeleteHelper is similar, yet slightly different. See sample in Class Reference and above]
So when 2017.1 is released you can definitely use this approach.
A few notes though:
- See the comment in the generated method: "If this class is modified then the %OnDelete method will need to change to accommodate changed persistent object references", with the DeleteHelper as it is an inherited generator-method and changes should be reflected automatically in the re-generated method. Of course if you run the Wizard (XML Schema or SOAP) then the classes will also be regenerated with "new" appropriate methods.
- This will add this to the Wizard XML/WSDL-generated data-related classes, but not for example for the Message classes (even if the Request/Response classes were created by the same Wizard, at least as it is now). The DeleteHelper could help with that.
- And obviously this new feature will not cover manually developed classes, but the DeleteHelper could.
A quick disclaimer regarding the availability of this upcoming feature - as is always true with regards to Field Tests and planned features - changes to the contents of the 2017.1 version, or to it's release itself, could occur (possibly without notice). So eventually you might not find this feature in 2017.1, or it might behave different than mentioned above.