Using Caché Objects
Introduction to Persistent Objects
[Back] [Next]
   
Server:docs2
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

This chapter presents the concepts that are useful to understand when working with persistent classes. It discusses the following topics:

Also see the chapters Working with 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.
Persistent Classes
A persistent class is any class that inherits from %Persistent. A persistent object is an instance of such a class.
The %Persistent class is a subclass of %RegisteredObject and thus is an object class. In addition to providing the methods described in the previous chapter, the %Persistent class defines the persistence interface, a set of methods. Among other things, these methods enable you to save objects to the database, load objects from the database, delete objects, and test for existence.
Introduction to the Default SQL Projection
For any persistent class, the compiler generates an SQL table definition, so that the stored data can be accessed via SQL in addition to via the object interface described in this book.
The table contains one record for each saved object, and the table can be queried via Caché SQL. The following shows the results of a query of the Sample.Person table:
The following table summarizes the default projection:
The Object-SQL Projection
From (Object Concept) ... To (Relational Concept) ...
Package Schema
Class Table
OID Identity field
Data type property Field
Reference property Reference field
Embedded object Set of fields
List property List field
Array property Child table
Stream property BLOB
Index Index
Class method Stored procedure
Later chapters provide more information and describe any changes you can make:
Identifiers for Saved Objects: ID and OID
When you save an object for the first time, Caché creates two permanent identifiers for it, either of which you can later use to access or remove the saved objects. The more commonly used identifier is the object ID. An ID is a simple literal value that is unique within the table. By default, Caché generates an integer to use as an ID.
An OID is more general: it also includes the class name and is unique in the database. In general practice, an application never needs to use the OID value; the ID value is usually sufficient.
The %Persistent class provides methods that use either the ID or the OID. You specify an ID when you use methods such as %OpenId(), %ExistsId(), and %DeleteId(). You specify the OID as the argument for methods such as %Open(), %Exists(), and %Delete(). That is, the methods that use ID as an argument include Id in their names. The methods that use OID as the argument do not include Id in their names; these methods are used much less often.
When a persistent object is stored in the database, the values of any of its reference attributes (that is, references to other persistent objects) are stored as OID values. For object attributes that do not have OIDs, the literal value of the object is stored along with the rest of the state of the object.
Projection of Object IDs to SQL
The ID of an object is available in the corresponding SQL table. If possible, Caché uses the field name ID. Caché also provides a way to access the ID if you are not sure what field name to use. The system is as follows:
The OID is not available in the SQL table.
Object IDs in SQL
Caché enforces uniqueness for the ID field (whatever its actual name might be). Caché also prevents this field from being changed. This means that you cannot perform SQL UPDATE or INSERT operations on this field. For instance, the following shows the SQL needed to add a new record to a table:
INSERT INTO PERSON (FNAME, LNAME)VALUES (:fname, :lname)
Notice that this SQL does not refer to the ID field. Caché generates a value for the ID field and inserts that when it creates the requested record.
Class Members Specific to Persistent Classes
Caché classes can include several kinds of class members that are meaningful only in persistent classes. These are storage definitions, indices, foreign keys, and triggers.
Storage Definitions
In most cases (as discussed later), each persistent class has a storage definition. The purpose of the storage definition is to describe the global structure that Caché uses when it saves data for the class or reads saved data for the class. Studio displays the storage definition at the end of the class definition, when you view the class in edit mode. The following shows a partial example:
<Storage name="Default">
<Data name="PersonDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
<Value name="3">
<Value>SSN</Value>
</Value>
<Value name="4">
<Value>DOB</Value>
</Value>
<Value name="5">
<Value>Home</Value>
</Value>
<Value name="6">
<Value>Office</Value>
</Value>
<Value name="7">
<Value>Spouse</Value>
</Value>
<Value name="8">
<Value>FavoriteColors</Value>
</Value>
</Data>
<DataLocation>^Sample.PersonD</DataLocation>
<DefaultData>PersonDefaultData</DefaultData>
<ExtentSize>200</ExtentSize>
<IdLocation>^Sample.PersonD</IdLocation>
<IndexLocation>^Sample.PersonI</IndexLocation>
<Property name="%%CLASSNAME">
<Selectivity>50.0000%</Selectivity>
</Property>
...
Also in most cases, the compiler generates and updates the storage definition. For more information on storage, see the chapter Defining Persistent Classes.”
Indices
As with other SQL tables, a Caché SQL table can have indices; to define these, you add index definitions to the corresponding class definition.
An index can add a constraint that ensures uniqueness of a given field or combination of fields. For information on such indices, see the chapter Defining Persistent Classes.”
Another purpose of an index is to define a specific sorted subset of commonly requested data associated with a class, so that queries can run more quickly. For example, as a general rule, if a query that includes a WHERE clause using a given field, the query runs more rapidly if that field is indexed. In contrast, if there is no index on that field, the engine must perform a full table scan, checking every row to see if it matches the given criteria — an expensive operation if the table is large. See the chapter Other Options for Persistent Classes.”
Foreign Keys
A Caché SQL table can also have foreign keys. To define these, you add foreign key definitions to the corresponding class definition.
Foreign keys establish referential integrity constraints between tables that Caché uses when new data is added or when data is changed. If you use relationships, described later in this book, Caché automatically treats these as foreign keys. But you can add foreign keys if you do not want to use relationships or if you have other reasons to add them.
For more information on foreign keys, see the chapter Other Options for Persistent Classes.”
Triggers
A Caché SQL table can also have triggers. To define these, you add trigger definitions to the corresponding class definition.
Triggers define code to be executed automatically when specific events occur, specifically when a record is inserted, modified, or deleted.
For more information on triggers, see the chapter Other Options for Persistent Classes.”
Other Class Members
A class method or a class query can be defined so that it can be invoked as a stored procedure, which enables you to invoke it from SQL.
For class members not discussed in this chapter, there is no projection to SQL. That is, Caché does not provide a direct way to use them from SQL or to make them usable from SQL.
Extents
The term extent refers to all the records, on disk, for a given persistent class. As shown in the next chapter, the %Persistent class provides several methods that operate on the extent of class.
Caché uses an unconventional and powerful interpretation of the object-table mapping. By default, the extent of a given persistent class includes the extents of any subclasses. Therefore:
Indices automatically span the entire extent of the class in which they are defined. The indices defined in Person contain both Person instances and Employee instances. Indices defined in the Employee extent contain only Employee instances.
The subclass can define additional properties not defined in its superclass. These are available in the extent of the subclass, but not in the extent of the superclass. For example, the Employee extent might include the Department field, which is not included in the Person extent.
The preceding points mean that it is comparatively easy in Caché to write a query that retrieves all records of the same type. For example, if you want to count people of all types, you can run a query against the Person table. If you want to count only employees, run the same query against the Employee table. In contrast, with other object databases, to count people of all types, it would be necessary to write a more complex query that combined the tables, and it would be necessary to update this query whenever another subclass was added.
Similarly, the methods that use the ID all behave polymorphically. That is, they can operate on different types of objects depending on the ID value it is passed.
For example, the extent for Sample.Person objects includes instances of Sample.Person as well as instances of Sample.Employee. When you call %OpenId() for the Sample.Person class, the resulting OREF could be an instance of either Sample.Person or Sample.Employee, depending on what is stored within the database:
 // Open person "10"
 Set obj = ##class(Sample.Person).%OpenId(10)

 Write $ClassName(obj),!    // Sample.Person
 

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

 Write $ClassName(obj),!    // Sample.Employee
 
Note that the %OpenId() method for the Sample.Employee class will not return an object if we try to open ID 10, because the ID 10 is not the Sample.Employee extent:
 // Open employee "10"
 Set obj = ##class(Sample.Employee).%OpenId(10)

 Write $IsObject(obj),!  // 0

 // Open employee "110"
 Set obj = ##class(Sample.Employee).%OpenId(110)

 Write $IsObject(obj),!  // 1
 
Extent Management
For classes that use the default storage class (%Library.CacheStorage), Caché maintains extent definitions and globals that those extents have registered for use with its Extent Manager. The interface to the Extent Manager is through the %ExtentMgr.Util class. This registration process occurs during class compilation. If there are any errors or name conflicts, these cause the compile to fail. For compilation to succeed, resolve the conflicts; this usually involves either changing the name of the index or adding explicit storage locations for the data.
The MANAGEDEXTENT class parameter has a default value of 1; this value causes global name registration and a conflicting use check. A value of 0 specifies that there is neither registration nor conflict checking.
Note:
If an application has multiple classes intentionally sharing a global reference, specify that MANAGEDEXTENT equals 0 for all the relevant classes, if they use default storage. Otherwise, recompilation will generate the error such as
ERROR #5564: Storage reference: '^This.App.Global used in 'User.ClassA.cls' 
is already registered for use by 'User.ClassB.cls'
To delete extent metadata, there are multiple approaches:
Extent Queries
Every persistent class automatically includes a class query called "Extent" that provides a set of all the IDs in the extent.
For general information on using class queries, see the chapter Defining and Using Class Queries.” The following example uses a class query to display all the IDs for the Sample.Person class:
 set query = ##class(%SQL.Statement).%New()
 set status= query.%PrepareClassQuery("Sample.Person","Extent")
 if 'status {
   do $system.OBJ.DisplayError(status)
 }
 set rset=query.%Execute()

 While (rset.%Next()) {
     Write rset.%Get("ID"),!
 }
 
The Sample.Person extent include all instances of Sample.Person as well as its subclasses. For an explanation of this, see the chapter Defining Persistent Classes.”
The "Extent" query is equivalent to the following SQL query:
SELECT %ID FROM Sample.Person
 
Note that you cannot rely on the order in which ID values are returned using either of these methods: Caché may determine that it is more efficient to use an index that is ordered using some other property value to satisfy this request. You can add an ORDER BY %ID clause to the SQL query if you need to.