├── README.md └── cls └── SampleUtil └── DeleteHelper ├── AddHelper.cls └── OnDeleteSuper.cls /README.md: -------------------------------------------------------------------------------- 1 | # DeleteHelper 2 | A Super class for adding a code-generator %OnDelete method 3 | 4 | With regard to making sure that referenced instances are deleted when the referrer is deleted – 5 | This is especially important when purging messaged in Ensemble. 6 | 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. 7 | 8 | What you'd need to do is add this class as another Super class for your class. 9 | 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). 10 | 11 | See class reference documentation for more details. 12 | 13 | Find in the Developer Community (where you come to learn about InterSystems technology, sharing solutions and staying up-to-date on the latest developments), in this post further details and related discussion. 14 | 15 | -------------------------------------------------------------------------------- /cls/SampleUtil/DeleteHelper/AddHelper.cls: -------------------------------------------------------------------------------- 1 | /// A supplement class to the SampleUtil.DeleteHelper.OnDeleteSuper class, 2 | /// to ease the addition of it to existing classes (particularity when there are many) 3 | ///
4 | /// See the AddDeleteHelper method for further details 5 | Class SampleUtil.DeleteHelper.AddHelper 6 | { 7 | 8 | /// Add the SampleUtil.DeleteHelper.OnDeleteSuper class as a Super class for the desired classes 9 | ///

10 | /// Arguments: 11 | /// 18 | /// * Note regarding the Inheritance-direction-related argument:
19 | /// In some versions the implicit default was left, in some right, therefore in some upgrades an explicit 'Inheritance' keyword was added. In case it is left, then the %Persistent default %OnDelete will supercede the Delete Helper one
20 | ///

21 | /// Examples:
22 | /// Set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(1 /* Dry Run */, 10 /* Class Number Limit */,"Package.Sub.*.cls") 23 | /// This will perform a dry run of up to 10 classes in the 'Package.Sub' package, reporting the classes that would be changed in a real non-dry run (adding the Delete Helper to their Super class list) 24 | /// Set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(0 /* Non-Dry Run */, 1 /* Only One Class */,"Package.Sub.*.cls") 25 | /// This will perform a non-dry run of up to 1 class in the 'Package.Sub' package, adding the Delete Helper to it's Super class list 26 | /// Set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(0 /* Non-Dry Run */, -1 /* Unlimited Number of Classes */,"Package.Sub.*.cls") 27 | /// This will perform a non-dry run of an unlimited number of classes in the 'Package.Sub' package, adding the Delete Helper to their Super class list 28 | /// Set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(0 /* Non-Dry Run */, -1 /* Unlimited Number of Classes */,"Package.Sub.*.cls", 1 /* Change Inheritance to Right*/, 1 /* Remove Existing %OnDelete */) 29 | /// This will perform a non-dry run of an unlimited number of classes in the 'Package.Sub' package, adding the Delete Helper to their Super class list, Changing the Inheritance to Right, and removing an %OnDelete method if it already exists in the class 30 | ///

31 | /// Sample Output:
32 | /// 33 | /// 34 | /// ENSEMBLE>set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(1,1,"ITest.Proxy.s0.*.cls")
35 | /// handling class: ITest.Proxy.s0.Address
36 | ///
37 | /// ENSEMBLE>set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(1,-1,"ITest.Proxy.s0.*.cls")
38 | /// handling class: ITest.Proxy.s0.Address
39 | /// handling class: ITest.Proxy.s0.Employee
40 | /// handling class: ITest.Proxy.s0.Person
41 | /// handling class: ITest.Proxy.s0.PersonIdentification
42 | ///
43 | /// ENSEMBLE>set status = ##class(SampleUtil.DeleteHelper.AddHelper).AddDeleteHelper(0,-1,"ITest.Proxy.s0.*.cls")
44 | /// handling class: ITest.Proxy.s0.Address
45 | /// saved: 1
46 | ///
47 | /// Compilation started on 01/10/2017 13:24:54 with qualifiers 'ck'
48 | /// Compiling class ITest.Proxy.s0.Address
49 | /// Compiling table ITest_Proxy_s0.Address
50 | /// Compiling routine ITest.Proxy.s0.Address.1
51 | /// Compilation finished successfully in 0.172s.
52 | /// handling class: ITest.Proxy.s0.Employee
53 | /// saved: 1
54 | ///
55 | /// Compilation started on 01/10/2017 13:24:54 with qualifiers 'ck'
56 | /// Compiling 2 classes, using 2 worker jobs
57 | /// Compiling class ITest.Proxy.s0.Person
58 | /// Compiling class ITest.Proxy.s0.Employee
59 | /// Compiling table ITest_Proxy_s0.Person
60 | /// Compiling table ITest_Proxy_s0.Employee
61 | /// Compiling routine ITest.Proxy.s0.Person.1
62 | /// Compiling routine ITest.Proxy.s0.Employee.1
63 | /// Compilation finished successfully in 0.363s.
64 | /// handling class: ITest.Proxy.s0.Person
65 | /// saved: 1
66 | ///
67 | /// Compilation started on 01/10/2017 13:24:55 with qualifiers 'ck'
68 | /// Compiling class ITest.Proxy.s0.Person
69 | /// Compiling table ITest_Proxy_s0.Person
70 | /// Compiling routine ITest.Proxy.s0.Person.1
71 | /// Compilation finished successfully in 0.152s.
72 | /// handling class: ITest.Proxy.s0.PersonIdentification
73 | /// saved: 1
74 | ///
75 | /// Compilation started on 01/10/2017 13:24:55 with qualifiers 'ck'
76 | /// Compiling class ITest.Proxy.s0.PersonIdentification
77 | /// Compiling table ITest_Proxy_s0.PersonIdentification
78 | /// Compiling routine ITest.Proxy.s0.PersonIdentification.1
79 | /// Compilation finished successfully in 0.122s.
80 | ///
81 | ///
82 | ClassMethod AddDeleteHelper(performDryRun As %Boolean = 1, classesNumLimit As %Integer = 0, classSpec As %String = "", makeRightInheritance As %Boolean = 0, removeExistingOnDelete As %Boolean = 0) 83 | { 84 | 85 | 86 | set statement = ##class(%SQL.Statement).%New() 87 | 88 | Set status = statement.%PrepareClassQuery("%Library.RoutineMgr","StudioOpenDialog") 89 | 90 | #dim result As %SQL.StatementResult 91 | 92 | set result=statement.%Execute(/* spec */ classSpec,/*dir */,/* order by*/,/* system files */0, 93 | /* flat */1,/* NotStudio*/,/*Show Generated */ 0) 94 | 95 | 96 | // Counting in order to allow limiting the number of classes 97 | set count=0 98 | 99 | While result.%Next() { 100 | 101 | // Getting the class name, removing the ".cls" extension at the end 102 | Set className = $Piece(result.%Get("Name"),".",1,*-1) 103 | 104 | // The classes that interest us are only Persistent ones 105 | If $ClassMethod(className,"%IsA","%Persistent") { 106 | 107 | // Will ignore classes that are Business Hosts (i.e. Business Service, Business Process or Business Operation) 108 | // As they do not require this handling 109 | If $ClassMethod(className,"%IsA","Ens.Host")||$ClassMethod(className,"%IsA","Ens.Rule.Definition") { 110 | Continue 111 | } 112 | 113 | // Increment the counter 114 | Set count=count+1 115 | 116 | // Check if the classes number has been limited and if yes if we passed the limit, of so stop 117 | If (classesNumLimit'=-1)&&(count>classesNumLimit) { 118 | Quit 119 | } 120 | 121 | // Report we are working on a specific class. This is important for the "Dry Run" 122 | Write "handling class: ",className,! 123 | 124 | // Open the class definition 125 | Set classDef = ##class(%Dictionary.ClassDefinition).%OpenId(className) 126 | 127 | // Check we are not already extending the Delete Helper class 128 | If classDef.Super '[ "SampleUtil.DeleteHelper.OnDeleteSuper" { 129 | 130 | // Add the Delete Helper to the Super classes list 131 | Set classDef.Super = classDef.Super _ ",SampleUtil.DeleteHelper.OnDeleteSuper" 132 | 133 | // Need also to make sure we add an include (%occErrors) 134 | /// as the Delete Helper code accesses a macro with a status error ($$$LoadObjectNotFound) 135 | 136 | // Depending on whether there is already an Include we might or might not need to start with a comma 137 | If classDef.IncludeCode '= "" { 138 | Set BeginInc = "," 139 | } else { 140 | Set BeginInc = "" 141 | } 142 | // Add the %occErrors include 143 | Set classDef.IncludeCode = classDef.IncludeCode _ BeginInc _ "%occErrors" 144 | 145 | // See note in class reference. In some cases (if explicit inheritance was set to left, we'd need to change this right) 146 | // Can consider to change this from being user-parameter-driven to simply check if the inheritance is left 147 | // and if so change to right, or in any case just change to right without checking 148 | // For now opted to have this depend on user input (as to minimize forced undesired "surprise" changes) 149 | If makeRightInheritance { 150 | Set classDef.Inheritance = "right" 151 | } 152 | 153 | // Assuming this was desired by the caller, if the class definition already includes an %OnDelete method 154 | // then it will take precedence over the Delete Helper inherited one. So this allows to remove the existing method 155 | If removeExistingOnDelete { 156 | 157 | // Loop over methods in the class 158 | For i=1:1:classDef.Methods.Count() { 159 | Set methodName = classDef.Methods.GetAt(i) 160 | // If it is an %OnDelete then remove it (and exit the loop) 161 | If methodName = "%OnDelete" { 162 | Do classDef.Methods.RemoveAt(i) 163 | Quit 164 | } 165 | } 166 | } 167 | 168 | // If this is not a Dry Run then go ahead and save (and compile) the class 169 | if 'performDryRun { 170 | 171 | // Save the class definition 172 | Set status = classDef.%Save() 173 | 174 | write "saved: ",status,! 175 | 176 | // Compile the class 177 | Do $system.OBJ.Compile(className,"ck") 178 | } 179 | 180 | } 181 | } 182 | } 183 | Quit $$$OK 184 | } 185 | 186 | } 187 | 188 | -------------------------------------------------------------------------------- /cls/SampleUtil/DeleteHelper/OnDeleteSuper.cls: -------------------------------------------------------------------------------- 1 | /// A class to help assist in "deep" deleting of an instance, including references to other persistent classes. 2 | ///

3 | /// To use simply add as a Super Class in your persistent class
4 | /// The class defines a Generator %OnDelete method that will generate code for your class, 5 | /// deleting, if needed, references (inclduing collections) to other persistent classes
6 | ///
7 | /// See the SampleUtil.DeleteHelper.AddHelper class that can help in adding this class as a Super class to several classes 8 | ///
9 | ///
10 | /// Here is an example of a class and what it's generated %OnDelete will look like -

11 | /// A snippet from the Class Definition:

12 | /// 13 | /// Property Name As %String(MAXLEN = "", XMLNAME = "Name") [ Required ];
14 | ///
15 | /// Property SSN As %String(MAXLEN = "", XMLNAME = "SSN") [ Required ];
16 | ///
17 | /// Property DOB As %Date(XMLNAME = "DOB");
18 | ///
19 | /// Property Home As ITest.Proxy.s0.Address(XMLNAME = "Home");
20 | ///
21 | /// Property Office As ITest.Proxy.s0.Address(XMLNAME = "Office");
22 | ///
23 | /// Property Spouse As ITest.Proxy.s0.Person(XMLNAME = "Spouse");
24 | ///

25 | /// And this is the generated %OnDelete (from the generated INT routine):
26 | /// 27 | ///
%OnDelete(oid) public { 28 | ///
Set status = 1 29 | ///
Set obj = ..%Open(oid,,.status) 30 | ///
If ('status) { 31 | ///
 Set errorCode = $System.Status.GetErrorCodes(status) 32 | ///
  If errorCode [ 5809 { 33 | ///
   Quit 1 34 | ///
  } Else { 35 | ///
   Quit status 36 | ///
  } 37 | ///
} 38 | ///
If $IsObject(obj.Home) { 39 | ///
 Set delStatus = obj.Home.%DeleteId(obj.Home.%Id()) 40 | ///
  If ('delStatus) { 41 | ///
   Set errorCode = $System.Status.GetErrorCodes(delStatus) 42 | ///
   If errorCode [ 5810 { 43 | ///
  } Else { 44 | ///
   Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) 45 | ///
  } 46 | ///
 } 47 | ///
} 48 | ///
If $IsObject(obj.Office) { 49 | ///
 Set delStatus = obj.Office.%DeleteId(obj.Office.%Id()) 50 | ///
 If ('delStatus) { 51 | ///
  Set errorCode = $System.Status.GetErrorCodes(delStatus) 52 | ///
  If errorCode [ 5810 { 53 | ///
  } Else { 54 | ///
   Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) 55 | ///
  } 56 | ///
 } 57 | ///
} 58 | ///
If $IsObject(obj.Spouse) { 59 | ///
 Set delStatus = obj.Spouse.%DeleteId(obj.Spouse.%Id()) 60 | ///
 If ('delStatus) { 61 | ///
  Set errorCode = $System.Status.GetErrorCodes(delStatus) 62 | ///
  If errorCode [ 5810 { 63 | ///
  } Else { 64 | ///
   Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) 65 | ///
  } 66 | ///
 } 67 | ///
} 68 | ///
Quit status } 69 | ///
70 | ///

71 | /// Here is a sample with a list collection -
72 | ///
73 | /// The class definition snippet:
74 | /// Property GetListByNameResult As list Of ITest.Proxy.s0.PersonIdentification;
75 | ///
76 | /// And the generated code snippet:
77 | /// 78 | ///
If $IsObject(obj.GetListByNameResult) { 79 | ///
 Set key="" 80 | ///
 Set item = obj.GetListByNameResult.GetNext(.key) 81 | ///
 While key'="" { 82 | ///
  If $IsObject(item) { 83 | ///
   Set delStatus = item.%DeleteId(item.%Id()) 84 | ///
   If ('delStatus) { 85 | ///
    Set errorCode = $System.Status.GetErrorCodes(delStatus) 86 | ///
    If errorCode [ 5810 { 87 | ///
    } Else { 88 | ///
     Set status=$select(+status:delStatus,1:$$AppendStatus^%occSystem(status,delStatus)) 89 | ///
    } 90 | ///
   } 91 | ///
  } 92 | ///
 Set item = obj.GetListByNameResult.GetNext(.key) 93 | ///
} 94 | ///
95 | ///
96 | ///
97 | Class SampleUtil.DeleteHelper.OnDeleteSuper [ Abstract ] 98 | { 99 | 100 | /// This callback method is invoked by the %Delete method to 101 | /// provide notification that the object specified by oid is being deleted. 102 | /// 103 | ///

If this method returns an error then the object will not be deleted. 104 | ///
105 | /// This method was implemented as a generator method and creates the code according to the properties defined in the class 106 | ClassMethod %OnDelete(oid As %ObjectIdentity) As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ] 107 | { 108 | 109 | // a list ($ListBuild format) of "simple" (non-collection) property names we'll want to delete the references of (because they're Persistent) 110 | Set delPropNames = "" 111 | 112 | // a list ($ListBuild format) of collection (List) property names we'll want to delete the references of (because they're Persistent) 113 | Set delCollectionPropNames = "" 114 | 115 | // NOTE! - this sample might not cover all cases of references to be deleted 116 | // (perhaps for example a reference to a Serial class that has a reference to a Persistent one. 117 | /// Also the code uses the "Class Definition" as its source for the properties 118 | /// and not the "Compiled Class" so inherited references will not be covered) 119 | 120 | // Lists and non-lists or arrays need separate handling (since for lists we'll need to iterate its members) 121 | 122 | // A flag whether we have any Persistent reference properties at all, default to 0/false 123 | Set hasDelProps = 0 124 | 125 | #dim prop As %Dictionary.PropertyDefinition 126 | 127 | // Loop over all properties in the class definition 128 | for i=1:1:%class.Properties.Count() { 129 | 130 | Set prop = %class.Properties.GetAt(i) 131 | // grab the property type 132 | set propType=prop.Type 133 | 134 | // grab the property name 135 | set propName = prop.Name 136 | 137 | // grab the "collection-ness" of the property 138 | set propCollection = prop.Collection 139 | 140 | // check if relationship 141 | If (prop.Relationship)&&(prop.Cardinality="many") { 142 | set propCollection="list" 143 | } 144 | // check if relationship and referencing One or Parent 145 | If (prop.Relationship)&&((prop.Cardinality="one")||(prop.Cardinality="parent")) { 146 | Continue 147 | } 148 | 149 | 150 | // if this is a property who's type starts with '%' (e.g. %String) simply iterate to the next property 151 | If $Extract(propType)="%" Continue 152 | 153 | 154 | If $ClassMethod(propType,"%IsA","%Persistent") { 155 | // "Turn on" the flag that we'll be trying to delete some references in this method 156 | Set hasDelProps=1 157 | 158 | // If this is a collection put the property name in the appropriate property list 159 | If (propCollection="list")||(propCollection="array") { 160 | Set delCollectionPropNames = delCollectionPropNames _ $ListBuild(propName) 161 | 162 | 163 | // If it's not a list (but it's still Persistent) put it in the second 164 | } Else { 165 | Set delPropNames = delPropNames _ $ListBuild(propName) 166 | } 167 | } 168 | 169 | } 170 | Do %code.WriteLine(" Set status = $$$OK") 171 | // If the flag is on then we have references to delete 172 | If hasDelProps { 173 | 174 | // Open the object so we can access it's properties 175 | 176 | Do %code.WriteLine(" Set obj = ..%Open(oid,,.status)") 177 | Do %code.WriteLine(" If $$$ISERR(status) {") 178 | Do %code.WriteLine(" Set errorCode = $System.Status.GetErrorCodes(status)") 179 | 180 | // Maybe we simple already deleted this object (perhaps referenced from more than one object) 181 | // If so ignore this error 182 | Do %code.WriteLine(" If errorCode [ $$$LoadObjectNotFound { ") 183 | Do %code.WriteLine(" Quit $$$OK") 184 | Do %code.WriteLine(" } Else {") 185 | Do %code.WriteLine(" Quit status") 186 | Do %code.WriteLine(" }") 187 | Do %code.WriteLine(" }") 188 | 189 | // Loop over the list of "simple" (non-list) properties 190 | for i=1:1:$ListLength(delPropNames) { 191 | // grab the property name from the list 192 | Set propName = $ListGet(delPropNames,i) 193 | // check it actually currently references a valid object 194 | Do %code.WriteLine(" If $IsObject(obj."_propName_") {") 195 | // Delete the object 196 | Do %code.WriteLine(" Set delStatus = obj."_propName_".%DeleteId(obj."_propName_".%Id())") 197 | Do %code.WriteLine(" If $$$ISERR(delStatus) {") 198 | Do %code.WriteLine(" Set errorCode = $System.Status.GetErrorCodes(delStatus)") 199 | // As when opening the oject, maybe we simple already deleted this object (perhaps referenced from more than one object) 200 | // If so ignore this error 201 | Do %code.WriteLine(" If errorCode [ $$$DeleteObjectNotFound { ") 202 | // In this case simply do nothing and continue to next prop 203 | 204 | Do %code.WriteLine(" } Else {") 205 | // If this is a different error return it. 206 | Do %code.WriteLine(" Set status=$$$ADDSC(status,delStatus)") 207 | Do %code.WriteLine(" }") 208 | Do %code.WriteLine(" }") 209 | Do %code.WriteLine(" }") 210 | } 211 | 212 | // Loop over the list of List collection properties 213 | for i=1:1:$ListLength(delCollectionPropNames) { 214 | Set propName = $ListGet(delCollectionPropNames,i) 215 | // if this list prop is indeed an object 216 | Do %code.WriteLine(" If $IsObject(obj."_propName_") {") 217 | // Loop over all members of the list 218 | Do %code.WriteLine(" Set key=""""") 219 | // grab the next reference (and key) 220 | Do %code.WriteLine(" Set item = obj."_propName_".GetNext(.key)") 221 | Do %code.WriteLine(" While key'="""" {") 222 | 223 | 224 | Do %code.WriteLine(" If $IsObject(item) {") 225 | 226 | // delete it 227 | Do %code.WriteLine(" Set delStatus = item.%DeleteId(item.%Id())") 228 | Do %code.WriteLine(" If $$$ISERR(delStatus) {") 229 | Do %code.WriteLine(" Set errorCode = $System.Status.GetErrorCodes(delStatus)") 230 | // As when opening the oject, maybe we simple already deleted this object (perhaps referenced from more than one object) 231 | // If so ignore this error 232 | Do %code.WriteLine(" If errorCode [ $$$DeleteObjectNotFound { ") 233 | // In this case simply do nothing and continue to next prop 234 | 235 | Do %code.WriteLine(" } Else {") 236 | // If this is a different error return it. 237 | Do %code.WriteLine(" Set status=$$$ADDSC(status,delStatus)") 238 | Do %code.WriteLine(" }") 239 | 240 | Do %code.WriteLine(" }") 241 | Do %code.WriteLine(" }") 242 | Do %code.WriteLine(" Set item = obj."_propName_".GetNext(.key)") 243 | Do %code.WriteLine(" }") 244 | Do %code.WriteLine(" }") 245 | } 246 | 247 | } 248 | 249 | Do %code.WriteLine(" Quit status") 250 | 251 | 252 | 253 | Quit $$$OK 254 | } 255 | 256 | } 257 | 258 | --------------------------------------------------------------------------------