Using Caché Objects
Working with Persistent Objects
[Home] [Back] [Next]
InterSystems: The power behind what matters   
Class Reference   
Search:    

The %Persistent class is the API for objects that can be saved (written to disk). This chapter describes how to use this API. Information in this chapter applies to all subclasses of %Persistent. It discusses the following topics:

Also see the chapters Introduction to Persistent Objects, Defining Persistent Classes,” and Other Options for Persistent Classes.”
When viewing this book online, use the preface of this book to quickly find other topics.
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 %Status 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 in the previous chapter.
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 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.
    These callbacks are passed an Insert argument which indicates whether an object is being inserted (saved for the first time) or updated.
    If either of these callback methods fails (returns an error code) then the call to %Save() fails and the current transaction is rolled back.
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 Object 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, Caché 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.
    Caché 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 Caché generates IDs by default. The next chapter describes how you can 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 %Persistent. 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 %Persistent and the SAMPLES database provides data for this class.
You can also use the %Exists() method, which requires an OID rather than an ID.
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 %Persistent 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 the chapter Embedded SQL in Using Caché SQL. For more information on SQLCODE, see SQLCODE Values and Error Messages in the Caché 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:
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
 
Note that in Caché Basic, the OpenId command is equivalent to the %OpenId() method:
person = OpenId Sample.Person(1)
PrintLn "Name: " & person.Name
 
You can also use the %Open() method, which requires an OID rather than an ID.
Multiple Calls to %OpenId()
If %OpenId() is called multiple times within a Caché process for the same ID and the same, only one object instance is created in memory: all subsequent calls to %OpenId() will return a reference to the object already loaded into memory.
This is demonstrated in the following example:
' open and modify Person 1 in memory
personA = OpenId Sample.Person(1)
personA.Name = "Black,Jimmy Carl"

' open Person 1 "again"
personB = OpenId Sample.Person(1)

PrintLn "NameA: " & personA.Name
PrintLn "NameB: " & personB.Name
 
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.
For more information on the possible object concurrency levels, see Object Concurrency Options,” later in this chapter.
If the %OpenId() method is unable to acquire a lock on an object, it will fail.
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)
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 Atomic read.
A swizzled object is removed from memory as soon as no objects or variables refer to it.
Reloading an Object from Disk
To reload an in-memory object with the values stored within the database, call its %Reload() method.
 // 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 reload the object from disk
 Do person.%Reload()
 Write "Reloaded value: ",person.Name,!
 
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 an SQL statement (SQL is always executed against the database; not against objects in memory).
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()
 &sql(SELECT Name INTO :name
         FROM Sample.Person WHERE %ID = :id)

 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. This method is as follows:
classmethod %DeleteId(id As %String, concurrency As %Integer = -1) as %Status
Where:
For example:
 Set sc = ##class(MyApp.MyClass).%DeleteId(id)
%DeleteId() returns a %Status value indicating whether the object was deleted or not.
%DeleteId() calls the %OnDelete() callback method (if present) before deleting the object. %OnDelete() returns a %Status 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.
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.
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 %DeleteId() method on each instance.
The %KillExtent() Method
The %KillExtent() method directly deletes the globals that store an extent of objects (not including data associated with streams). It does not invoke the %DeleteId() method and performs no referential integrity actions. This method is simply intended to serve as a help to developers during the development process. (It is similar to the TRUNCATE TABLE command found in older relational database products.)
Caution:
%KillExtent() is intended for use only 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.
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. You can call the %Oid() method of the object. For example:
     write oref.%Oid()
  2. you can 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"
Object Concurrency Options
It is important to specify concurrency appropriately when you open or delete objects. You can specify concurrency at several different levels:
  1. You can specify the concurrency argument for the method that you are using.
    Many of the methods of the %Persistent class allow you to specify this argument, an integer. This argument determines how locks are used for concurrency control. A later subsection lists the allowed values.
    If you do not specify the concurrency argument, Caché uses the value of the DEFAULTCONCURRENCY class parameter for the class you are working with; see the next item.
  2. You can specify the DEFAULTCONCURRENCY class parameter for the associated class. All persistent classes inherit this parameter from %Persistent, which defines the parameter as an expression that obtains the default concurrency for the process; see the next item.
    You could override this parameter in your class and specify a hardcoded value or an expression that determines the concurrency via your own rules. In either case, the value of the parameter must be one of the allowed concurrency values discussed later in this section.
  3. You can set the default concurrency for a process. To do so, use the $system.OBJ.SetConcurrencyMode() method (which is the SetConcurrencyMode() method of the %SYSTEM.OBJ class).
    As in the other cases, you must use one of the allowed concurrency values.
    The $system.OBJ.SetConcurrencyMode() method has no effect on any classes that specify an explicit value for the DEFAULTCONCURRENCY class parameter.
Why Specify Concurrency?
The following scenario demonstrates why it is important to control concurrency appropriately when you read or write objects. Consider the following scenario:
  1. Process A opens an object.
    SAMPLES>set o=##class(Sample.Person).%OpenId(5)
     
    SAMPLES>w o
    1@Sample.Person
  2. Process B deletes that object from disk:
    SAMPLES>set sc=##class(Sample.Person).%DeleteId(5)
     
    SAMPLES>w sc
    1
  3. Process A saves the object using %Save() and receives a success status.
    SAMPLES>set sc=o.%Save()
     
    SAMPLES>w sc
    1
Examining the data on disk shows that the object was not written to disk.
This is an example of concurrent operations without adequate concurrency control. For example, if process A could possibly save the object back to the disk, it should open the object with concurrency 3 or 4 (these values are discussed later in this chapter). In this case, Process B would then be denied access (failed with a concurrency violation) or would have to wait until Process A releases the object.
Concurrency Values
This section describes the possible concurrency values. First, note the following details:
The possible concurrency values are as follows; each value has a name, also shown in the list.
Concurrency Value 0 (No locking)
No locks are used.
Concurrency Value 1 (Atomic read)
Locks are acquired and released as needed to guarantee that an object read will be executed as an atomic operation.
Caché does not acquire any lock when creating a new object.
While opening an object, Caché acquires a shared lock for the object, if that is necessary to guarantee an atomic read. Caché releases the lock after completing the read operation.
The following table lists the locks that are present in each scenario:
  When object is created While object is being opened After object has been opened After save operation is complete
New object no lock N/A N/A no lock
Existing object N/A shared lock, if that is necessary to guarantee an atomic read no lock no lock
Concurrency Value 2 (Shared locks)
The same as 1 (atomic read) except that opening an object always acquires a shared lock (even if the lock is not needed to guarantee an atomic read). The following table lists the locks that are present in each scenario:
  When object is created While object is being opened After object has been opened After save operation is complete
New object no lock N/A N/A no lock
Existing object N/A shared lock no lock no lock
Concurrency Value 3 (Shared/retained locks)
Caché does not acquire any lock when creating a new object.
While opening an existing object, Caché acquires a shared lock for the object.
After saving a new object, Caché has a shared lock for the object.
The following table lists the scenarios:
  When object is created While object is being opened After object has been opened After save operation is complete
New object no lock N/A N/A shared lock
Existing object N/A shared lock shared lock shared lock
Concurrency Value 4 (Exclusive/retained locks)
When an existing object is opened or when a new object is first saved, Caché acquires an exclusive lock.
The following table lists the scenarios:
  When object is created While object is being opened After object has been opened After save operation is complete
New object no lock N/A N/A exclusive lock
Existing object N/A exclusive lock exclusive lock exclusive lock
Concurrency and Swizzled Objects
An object referenced by a property is swizzled on access using default concurrency. (If the object being swizzled is already in memory, then swizzling does not actually open the object — it simply references the existing in-memory object; in that case, the current state of the object is maintained and the concurrency is unchanged.)
For example, if one object (Obj1) has a property (Prop1) that references another object (Obj2), then, on access to Prop1, Obj2 is swizzled; Obj2 has the default concurrency, unless it is already open.
There are two ways to override this default behavior:
Version Checking (Alternative to Concurrency Argument)
Rather than specifying the concurrency argument when you open or delete an object, you can implement version checking. To do so, you specify a class parameter called VERSIONPROPERTY. All persistent classes have this parameter. When defining a persistent class, the procedure for enabling version checking is:
  1. Create a property of type %Integer that holds the updateable version number for each instance of the class.
  2. For that property, set the value of the InitialExpression keyword to 0.
  3. For the class, set the value of the VERSIONPROPERTY class parameter equal to the name of that property. The value of VERSIONPROPERTY cannot be changed to a different property by a subclass.
This incorporates version checking into updates to instances of the class.
When version checking is implemented, the property specified by VERSIONPROPERTY is automatically incremented each time an instance of the class is updated (either by objects or SQL). Prior to incrementing the property, Caché compares its in-memory value to its stored value. If they are different, then a concurrency conflict is indicated and an error is returned; if they are the same, then the property is incremented and saved.
Note:
You can use this set of features to implement optimistic concurrency.
To implement a concurrency check in an SQL update statement for a class where VERSIONPROPERTY refers to a property called InstanceVersion, the code would be something like:
 SELECT InstanceVersion,Name,SpecialRelevantField,%ID
     FROM my_table
     WHERE %ID = :myid

     // Application performs operations on the selected row

 UPDATE my_table
     SET SpecialRelevantField = :newMoreSpecialValue
     WHERE %ID = :myid AND InstanceVersion = :myversion
where myversion is the value of the version property selected with the original data.