Skip to main content

Working with Persistent Objects

The %PersistentOpens in a new tab class is the API for objects that can be saved (written to disk). This topic describes how to use this API. Information in this topic applies to all subclasses of %PersistentOpens in a new tab.

Most of the samples shown in this topic are from the Samples-Data sample (https://github.com/intersystems/Samples-DataOpens in a new tab). InterSystems recommends that you create a dedicated namespace called SAMPLES (for example) and load samples into that namespace. For the general process, see Downloading Samples for Use with InterSystems IRIS® data platform.

Saving Objects

To save an object to the database, invoke its %Save() method. For example:

 Set obj = ##class(MyApp.MyClass).%New()
 Set obj.MyValue = 10

 Set sc = obj.%Save()

The %Save() method returns a %StatusOpens in a new tab value that indicates whether the save operation succeeded or failed. Failure could occur, for example, if an object has invalid property values or violates a uniqueness constraint; see Validating Objects.

Calling %Save() on an object automatically saves all modified objects that can be reached from the object being saved: that is, all embedded objects, collections, streams, referenced objects, and relationships involving the object are automatically saved if needed. The entire save operation is carried out as one transaction: if any object fails to save, the entire transaction fails and rolls back (no changes are made to disk; all in-memory object values are what they were before calling %Save()).

When an object is saved for the first time, the default behavior is for the %Save() method to automatically assign it an object ID value that is used to later find the object within the database. In the default case, the ID is generated using the $Increment function; alternately, the class can use a user-provided object ID based on property values that have an idkey index (and, in this case, the property values cannot include the string ||) . Once assigned, you cannot alter the object ID value for a specific object instance (even if it is a user-provided ID).

You can find the object ID value for an object that has been saved using the %Id() method:

 // Open person "22"
 Set person = ##class(Sample.Person).%OpenId(22)
 Write "Object ID: ",person.%Id(),!

In more detail, the %Save() method does the following:

  1. First it constructs a temporary structure known as a SaveSet. The SaveSet is simply a graph containing references to every object that is reachable from the object being saved. (Generally, when an object class A has a property whose value is another object class B, an instance of A can reach an instance of B.) The purpose of the SaveSet is to make sure that save operations involving complex sets of related objects are handled as efficiently as possible. The SaveSet also resolves any save order dependencies among objects.

    As each object is added to the SaveSet, its %OnAddToSaveSet() callback method is called, if present.

  2. It then visits each object in the SaveSet in order and checks if they are modified (that is, if any of their property values have been modified since the object was opened or last saved). If an object has been modified, it will then be saved.

  3. Before being saved, each modified object is validated (its property values are tested; its %OnValidateObject() method, if present, is called; and then its uniqueness constraints are tested); if the object is valid, the save proceeds. If any object is invalid, then the call to %Save() fails and the current transaction is rolled back.

  4. Before and after saving each object, the %OnBeforeSave() and %OnAfterSave() callback methods are called, if present. Similarly, any defined BEFORE INSERT, BEFORE UPDATE, AFTER INSERT, and AFTER UPDATE triggers are called as well.

    These callbacks and triggers are passed an Insert argument, which indicates whether an object is being inserted (saved for the first time) or updated.

    If one of these callback methods or triggers fails (returns an error code) then the call to %Save() fails and the current transaction is rolled back.

  5. Lastly, the %OnSaveFinally() callback method is called, if present.

    This callback method is called after the transaction has completed and the status of the save has been finalized.

If the current object is not modified, then %Save() does not write it to disk; it returns success because the object did not need to be saved and, therefore, there is no way that there could have been a failure to save it. In fact, the return value of %Save() indicates that the save operation either did all that it was asked or it was unable to do as it was asked (and not specifically whether or not anything was written to disk).

Important:

In a multi-process environment, be sure to use proper concurrency controls; see Specifying Concurrency Options.

Rollback

The %Save() method automatically saves all the objects in its SaveSet as a single transaction. If any of these objects fail to save, then the entire transaction is rolled back. In this rollback, InterSystems IRIS does the following:

  1. It reverts assigned IDs.

  2. It recovers removed IDs.

  3. It recovers modified bits.

  4. It invokes the %OnRollBack() callback method, if implemented, for any object that had been successfully serialized.

    InterSystems IRIS does not invoke this method for an object that has not been successfully serialized, that is, an object that is not valid.

Saving Objects and Transactions

As noted previously, the %Save() method automatically saves all the objects in its SaveSet as a single transaction. If any of these objects fail to save, then the entire transaction is rolled back.

If you wish to save two or more unrelated objects as a single transaction, however, you must enclose the calls to %Save() within an explicit transaction: that is, you must start the transaction using the TSTART command and end it with the TCOMMIT command.

For example:

 // start a transaction
 TSTART

 // save first object
 Set sc = obj1.%Save()

 // save second object (if first was save)
 If ($$$ISOK(sc)) {
     Set sc = obj2.%Save()
 }

 // if both saves are ok, commit the transaction
 If ($$$ISOK(sc)) {
     TCOMMIT
 }

There are two things to note about this example:

  1. The %Save() method knows if it is being called within an enclosing transaction (because the system variable, $TLEVEL, will be greater than 0).

  2. If any of the %Save() methods within the transaction fails, the entire transaction is rolled back (the TROLLBACK command is invoked). This means that an application must test every call to %Save() within a explicit transaction and if one fails, skip calling %Save() on the other objects and skip invoking the final TCOMMIT command.

Testing the Existence of Saved Objects

There are two basic ways to test if a specific object instance is stored within the database:

In these examples, the ID is an integer, which is how InterSystems IRIS generates IDs by default. You can instead define a class so that the ID is instead based on a unique property of the object.

Testing for Object Existence with ObjectScript

The %ExistsId() class method checks a specified ID; it returns a true value (1) if the specified object is present in the database and false (0) otherwise. It is available to all classes that inherit from %PersistentOpens in a new tab. For example:

 Write ##class(Sample.Person).%ExistsId(1),!   // should be 1 
 Write ##class(Sample.Person).%ExistsId(-1),!  // should be 0

Here, the first line should return 1 because Sample.Person inherits from %PersistentOpens in a new tab and the SAMPLES database provides data for this class.

You can also use the %Exists() method, which requires an OID rather than an ID. An OID is a permanent identifier unique to the database that includes both the ID and the class name of the object. For more details, see Identifiers for Saved Objects: ID and OID.

Testing for Object Existence with SQL

To test for the existence of a saved object with SQL, use a SELECT statement that selects a row whose %ID field matches the given ID. (The identity property of a saved object is projected as the %ID field.)

For example, using embedded SQL:

 &sql(SELECT %ID FROM Sample.Person WHERE %ID = '1')
 Write SQLCODE,!  // should be 0: success

 &sql(SELECT %ID FROM Sample.Person WHERE %ID = '-1')
 Write SQLCODE,!  // should be 100: not found

Here, the first case should result in an SQLCODE of 0 (meaning success) because Sample.Person inherits from %PersistentOpens in a new tab and the SAMPLES database provides data for this class.

The second case should result in an SQLCODE of 100, which means that the statement successfully executed but returned no data. This is expected because the system never automatically generates an ID value less than zero.

For more information on embedded SQL, see Embedded SQL. For more information on SQLCODE, see SQLCODE Values and Error Messages in the InterSystems IRIS Error Reference.

Opening Saved Objects

To open an object (load an object instance from disk into memory), use the %OpenId() method, which is as follows:

classmethod %OpenId(id As %String, 
                    concurrency As %Integer = -1, 
                    ByRef sc As %Status = $$$OK) as %ObjectHandle

Where:

  • id is the ID of the object to open.

    In these examples, the ID is an integer. You can instead define a class so that the ID is instead based on a unique property of the object.

  • concurrency is the concurrency level (locking) used to open the object.

  • sc, which is passed by reference, is a %StatusOpens in a new tab value that indicates whether the call succeeded or failed.

The method returns an OREF if it can open the given object. It returns a null value ("") if it cannot find or otherwise open the object.

For example:

 // Open person "10"
 Set person = ##class(Sample.Person).%OpenId(10)

 Write "Person: ",person,!    // should be an object reference

 // Open person "-10"
 Set person = ##class(Sample.Person).%OpenId(-10)

 Write "Person: ",person,!    // should be a null string

You can also use the %Open() method, which requires an OID rather than an ID. An OID is a permanent identifier unique to the database that includes both the ID and the class name of the object. For more details, see Identifiers for Saved Objects: ID and OID.

To perform additional processing that executes when an object opens, you can define the %OnOpen() and %OnOpenFinally() callback methods. For more details, see Defining Callback Methods.

Multiple Calls to %OpenId()

If %OpenId() is called multiple times within an InterSystems IRIS process for the same ID and the same object, only one object instance is created in memory. All subsequent calls to %OpenId() return a reference to the object already loaded into memory.

For example, consider this class featuring multiple %OpenId() calls to the same object:

Class User.TestOpenId Extends %RegisteredObject
{

ClassMethod Main()
{
   set A = ##class(Sample.Person).%OpenId(1)
   write "A: ", A.Name

   set A.Name = "David"
   write !, "A after set: ", A.Name

   set B = ##class(Sample.Person).%OpenId(1)
   write !, "B: ", B.Name

   do ..Sub()

   job ..JobSub()
   hang 1
   write !, "D in JobSub: ", ^JobSubD
   kill ^JobSubD

   kill A, B
   set E = ##class(Sample.Person).%OpenId(1)
   write !, "E:", E.Name
}

ClassMethod Sub()
{
   set C = ##class(Sample.Person).%OpenId(1)
   write !, "C in Sub: ", C.Name
}

ClassMethod JobSub()
{
   set D = ##class(Sample.Person).%OpenId(1)
   set ^JobSubD = D.Name
}

}

Calling the Main() method produces these results:

  1. The initial %OpenId() call loads the instance object into memory. The A variable references this object and you can modify the loaded object through this variable.

       set A = ##class(Sample.Person).%OpenId(1)
       write "A: ", A.Name
    

    A: Uhles,Norbert F.

       set A.Name = "David"
       write !, "A after set: ", A.Name
    

    A after set: David

  2. The second %OpenId() call does not reload the object from the database and does not overwrite the changes made using variable A. Instead, variable B references the same object as variable A.

       set B = ##class(Sample.Person).%OpenId(1)
       write !, "B: ", B.Name
    

    B: David

  3. The third %OpenId() call, in the Sub() method, also references the previously loaded object. This method is part of the same InterSystems IRIS process as the Main() method. Variable C references the same object as variables A and B, even though those variables are not available within the scope of this method. At the end of the Sub() method, the process returns back to the Main() method, the variable C is destroyed, and variables A and B are back in scope.

       do ..Sub()
    
    ClassMethod Sub()
    {
       set C = ##class(Sample.Person).%OpenId(1)
       write !, "C in Sub: ", C.Name
    }

    C in Sub: David

  4. The fourth %OpenId() call, in the JobSub() method, is run in a separate InterSystems IRIS process by using the JOB command. This new process loads a new instance object into memory, and variable D references this new object.

       job ..JobSub()
       hang 1
       write !, "D in JobSub: ", ^JobSubD
       kill ^JobSubD
    
    ClassMethod JobSub()
    {
       set D = ##class(Sample.Person).%OpenId(1)
       set ^JobSubD = D.Name
    }

    D in JobSub: Uhles,Norbert F.

  5. The fifth %OpenId() call occurs after deleting all variables (A and B) that reference the loaded object in the original process, thereby removing that object from memory. As with the previous %OpenId() call, this call also loads a new instance object into memory, and variable E references this object.

       kill A, B
       set E = ##class(Sample.Person).%OpenId(1)
       write !, "E:", E.Name

    E: Uhles,Norbert F.

To force a reload of an object being loaded multiple times, you can use the %Reload() method. Reloading the object reverts any changes made to the object that were not saved. Any previously assigned variables reference the reloaded version. For example:

 set A = ##class(Sample.Person).%OpenId(1)
 write "A: ", A.Name // A: Uhles,Norbert F.
 
 set A.Name = "David"
 write !, "A after set: ", A.Name // David
 
 set B = ##class(Sample.Person).%OpenId(1)
 write !, "B before reload: ", B.Name // David

 do B.%Reload()
 write !, "B after reload: ", B.Name // Uhles,Norbert F.
 write !, "A after reload: ", A.Name // Uhles,Norbert F.

Concurrency

The %OpenId() method takes an optional concurrency argument as input. This argument specifies the concurrency level (type of locks) that should be used to open the object instance.

If the %OpenId() method is unable to acquire a lock on an object, it fails.

To raise or lower the current concurrency setting for an object, reopen it with %OpenId() and specify a different concurrency level. For example,

 set person = ##class(Sample.Person).%OpenId(6,0)

opens person with a concurrency of 0 and the following effectively upgrades the concurrency to 4:

 set person = ##class(Sample.Person).%OpenId(6,4)

Specifying a concurrency level of 3 (shared, retained lock) or 4 (shared, exclusive lock) forces a reload of the object when opening it. For more information on the possible object concurrency levels, see Specifying Concurrency Options.

Swizzling

If you open (load into memory) an instance of a persistent object, and use an object that it references, then this referenced object is automatically opened. This process is referred to as swizzling; it is also sometimes known as lazy loading.

For example, the following code opens an instance of Sample.Employee object and automatically swizzles (opens) its related Sample.Company object by referring to it using dot syntax:

 // Open employee "101"
 Set emp = ##class(Sample.Employee).%OpenId(101)

 // Automatically open Sample.Company by referring to it:
 Write "Company: ",emp.Company.Name,!

When an object is swizzled, it is opened using the default concurrency value of the class it is a member of, not the concurrency value of the object that swizzles it. See Specifying Concurrency Options.

A swizzled object is removed from memory as soon as no objects or variables refer to it.

Sometimes you might want to manually swizzle an object or check to see if an object is swizzled. For both of these tasks, you can use the property method <prop>GetSwizzled(). In the case of the Company property in the above example, calling CompanyGetSwizzled() swizzles the Company object and returns an OREF for the object. If you pass in a 1 as an argument, as in CompanyGetSwizzled(1), the method returns an OREF for the object, but only if it is already swizzled. Otherwise, it returns a null OREF.

As an example, in Terminal:

USER>set emp = ##class(Sample.Employee).%OpenId(102)
 
USER>set co = emp.CompanyGetSwizzled(1)
 
USER>if (co = "") {write "Company is not swizzled"} else {write co.Name}
Company is not swizzled
USER>set co = emp.CarGetSwizzled()
 
USER>if (co = "") {write "Company is not swizzled"} else {write co.Name}
Acme Company, Inc.

Reading Stored Values

Suppose you have opened an instance of a persistent object, modified its properties, and then wish to view the original value stored in the database before saving the object. The easiest way to do this is to use one of the generated property methods, specifically propertynameGetStored(). For example:

 // Open person "1"
 Set person = ##class(Sample.Person).%OpenId(1)
 Write "Original value: ",person.Name,!

 // modify the object
 Set person.Name = "Black,Jimmy Carl"
 Write "Modified value: ",person.Name,!

 // Now see what value is on disk
 Set id = person.%Id()
 Set name = ##class(Sample.Person).NameGetStored(id)
 Write "Disk value: ",name,!

Another easy option is to use an embedded SQL statement (SQL is always executed against the database; not against objects in memory). Instead of calling NameGetStored(), use the following:

 // Now see what value is on disk
 &sql(SELECT Name INTO :name
         FROM Sample.Person WHERE %ID = :id)

 If (SQLCODE = 0) {Write "Disk value: ",name,!}

Deleting Saved Objects

The persistence interface includes methods for deleting objects from the database.

The %DeleteId() Method

The %DeleteId() method deletes an object that is stored within a database, including any stream data associated with the object. The signature of this method is as follows:

classmethod %DeleteId(id As %String, concurrency As %Integer = -1) as %Status

Where:

  • id is the ID of the object to open.

    In these examples, the ID is an integer. You can instead define a class so that the ID is instead based on a unique property of the object.

  • concurrency is the concurrency level (locking) used when deleting the object.

For example:

Set sc = ##class(MyApp.MyClass).%DeleteId(id)

%DeleteId() returns a %StatusOpens in a new tab value indicating whether the object was deleted or not.

%DeleteId() calls the %OnDelete() callback method (if present) before deleting the object. %OnDelete() returns a %StatusOpens in a new tab value; if %OnDelete() returns an error value, then the object will not be deleted, the current transaction is rolled back, and %DeleteId() returns an error value.

%DeleteId() calls the %OnAfterDelete() callback method (if present) after deleting the object. This call occurs immediately after %DeleteData() is called, provided that %DeleteData() does not return an error. If %OnAfterDelete() returns an error, then the current transaction is rolled back, and %DeleteId() returns an error value.

Lastly, %DeleteId() calls the %OnDeleteFinally() callback method, if present. This callback method is called after the transaction has completed and the status of the deletion has been finalized.

Note that the %DeleteId() method has no effect on any object instances in memory.

You can also use the %Delete() method, which requires an OID rather than an ID. An OID is a permanent identifier unique to the database that includes both the ID and the class name of the object. For more details, see Identifiers for Saved Objects: ID and OID.

The %DeleteExtent() Method

The %DeleteExtent() method deletes every object (and subclass of object) within its extent. Specifically it iterates through the entire extent and invokes the %Delete() method on each instance of the object. By default, it calls the %KillExtent() method when all instances are successfully deleted.

The signature of this method is as follows:

classmethod %DeleteExtent(concurrency As %Integer = -1, ByRef deletecount, ByRef instancecount, pInitializeExtent As %Integer = 1, Output errorLog As %Status) as %Status

Where:

  • concurrency is the concurrency level (locking) used when deleting the object.

  • deletecount is the number of instances deleted.

  • instancecount is the original number of instances.

  • pInitializeExtent determines whether to call %KillExtent() when the extent is empty.

    • A value of 0 indicates that %KillExtent() will not be called. Some empty globals may remain, as well as the ID counter, if it exists.

    • A value of 1 indicates that %KillExtent() will be called, but the default stream global will not be deleted. This is the default.

    • A value of 2 indicates that %KillExtent() will be called, and the default stream global will be deleted.

    Note:

    If the globals used by the extent are not exclusively used by the extent, some globals may remain even if %KillExtent() is called.

  • errorLog is used to return any applicable errors.

For example:

Set sc = ##class(MyApp.MyClass).%DeleteExtent(-1, .deletecount, .instancecount, 2, .errorLog)

The %KillExtent() Method

The %KillExtent() method directly deletes the globals that store an extent of a class, its subextents (subclasses), and its child extents (extents of classes that have a child relationship of the class). The default stream storage global is optionally deleted. %KillExtent() does not invoke the %Delete() method and performs no referential integrity actions.

The signature of this method is as follows:

classmethod %KillExtent(pDirect As %Integer = 1, killstreams As %Boolean = 0) as %Status

Where:

  • pDirect determines whether the filing timestamps are also deleted from the ^OBJ.DSTIME global. This is used in InterSystems IRIS Business Intelligence. See Keeping the Cubes Current. The default is 1.

  • killstreams determines whether the default stream global is deleted. The default is 0.

For example:

Set sc = ##class(MyApp.MyClass).%KillExtent(1, 0)
Caution:

%KillExtent() should only be called directly in a development environment and should not be used in a live application. %KillExtent() bypasses constraints and user-implemented callbacks, potentially causing data integrity problems. If you need your application to delete every object in an extent, use %DeleteExtent() instead.

Accessing Object Identifiers

If an object has been saved, it has an ID and an OID, the permanent identifiers that are used on disk. If you have an OREF for the object, you can use that to obtain these identifiers.

To find the ID associated with an OREF, call the %Id() method of the object. For example:

 write oref.%Id()

To find the OID associated with an OREF, you have two options:

  1. Call the %Oid() method of the object. For example:

     write oref.%Oid()
  2. Access the %%OID property of the object. Because this property name contains % characters, you must enclose the name in double quotes. For example:

     write oref."%%OID"

See Also

FeedbackOpens in a new tab