Defining and Using Classes
Working with Persistent Objects
|
|
When viewing this book online, use the
preface of this book to quickly find other topics.
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()
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:
-
-
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.
-
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.
-
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).
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:
-
-
-
It recovers modified bits.
-
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.
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.
// 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:
-
The
%Save() method knows if it is being called within an enclosing transaction (because the system variable,
$TLEVEL, will be greater than 0).
-
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.
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. The
next chapter describes how you can define a class so that the ID is instead based on a unique property of the object.
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
You can also use the
%Exists() method, which requires an OID rather than an ID.
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
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.
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
-
id is the ID of the object to open.
In these examples, the ID is an integer. The
next chapter describes how you can define a class so that the ID is instead based on a unique property of the object.
-
-
sc, which is passed by reference, is a
%Status 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.
// 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.
If
%OpenId() is called multiple times within an InterSystems IRIS 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.
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 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)
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.
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,!
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).
// 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,!
The persistence interface includes methods for deleting objects from the database.
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
-
id is the of the object to open.
In these examples, the ID is an integer. The
next chapter describes how you can define a class so that the ID is instead based on a unique property of the object.
-
Set sc = ##class(MyApp.MyClass).%DeleteId(id)
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 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 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.
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:
To find the OID associated with an OREF, you have two options:
-
You can call the
%Oid() method of the object. For example:
-
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:
It is important to specify concurrency appropriately when you open or delete objects. You can specify concurrency at several different levels:
-
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, InterSystems IRIS uses the value of the
DEFAULTCONCURRENCY class parameter for the class you are working with; see the next item.
-
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.
-
The following scenario demonstrates why it is important to control concurrency appropriately when you read or write objects. Consider the following scenario:
-
Process A opens an object.
SAMPLES>set o=##class(Sample.Person).%OpenId(5)
SAMPLES>w o
1@Sample.Person
-
Process B deletes that object from disk:
SAMPLES>set sc=##class(Sample.Person).%DeleteId(5)
SAMPLES>w sc
1
-
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.
This section describes the possible concurrency values. First, note the following details:
-
Atomic writes are guaranteed when concurrency is greater than 0.
-
InterSystems IRIS acquires and releases locks during operations such as saving and deleting objects; the details depend upon the concurrency value, what constraints are present, lock escalation status, and the storage structure.
-
In all cases, when an object is removed from memory, any locks for it are removed.
The possible concurrency values are as follows; each value has a name, also shown in the list.
Concurrency Value 0 (No locking)
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.
InterSystems IRIS does not acquire any lock when creating a new object.
The following table lists the locks that are present in each scenario:
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:
Concurrency Value 3 (Shared/retained locks)
InterSystems IRIS does not acquire any lock when creating a new object.
While opening an existing object, InterSystems IRIS acquires a shared lock for the object.
After saving a new object, InterSystems IRIS has a shared lock for the object.
The following table lists the scenarios:
Concurrency Value 4 (Exclusive/retained locks)
When an existing object is opened or when a new object is first saved, InterSystems IRIS acquires an exclusive lock.
The following table lists the scenarios:
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:
-
Upgrade the concurrency on the swizzled object with a call to the
%Open() method that specifies the new concurrency. For example:
Do person.Spouse.%Open(person.Spouse.%Oid(),4,.status)
where the first argument to
%Open() specifies the OID, the second specifies the new concurrency, and the third (passed by reference) receives the status of the method.
-
Set the default concurrency for the process before swizzling the object. For example:
Set olddefault = $system.OBJ.SetConcurrencyMode(4)
This method takes the new concurrency mode as its argument and returns the previous concurrency mode.
When you no longer need a different concurrency mode, reset the default concurrency mode as follows:
Do $system.OBJ.SetConcurrencyMode(olddefault)
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:
-
Create a property of type
%Integer that holds the updateable version number for each instance of the class.
-
For that property, set the value of the InitialExpression keyword to 0.
-
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, InterSystems IRIS 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.
Content Date/Time: 2019-02-23 01:10:53