Defining and Using Classes
Introduction to Persistent Objects
|
|
This chapter presents the concepts that are useful to understand when working with persistent classes. It discusses the following topics:
When viewing this book online, use the
preface of this book to quickly find other topics.
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 InterSystems 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:
-
That chapter also has information on how you can control the projection of subclasses.
-
-
-
-
-
When you save an
object for the first time, the system 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, the system 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.
The ID of an object is available in the corresponding SQL table. If possible, InterSystems IRIS uses the field name
ID. InterSystems IRIS also provides a way to access the ID if you are not sure what field name to use. The system is as follows:
-
An object ID is not a property of the object and is treated differently from the properties.
-
If the class does not contain a property named
ID (in any case variation), then the table also contains the field
ID, and that field contains the object ID. For an example, see the previous section.
-
If the class contains a property that is projected to SQL with the name
ID (in any case variation), then the table also contains the field
ID1, and this field holds the value of the object ID.
Similarly, if the class contains properties that are projected as
ID and
ID1, then the table also contains the field
ID2, and this field holds the value of the object ID.
-
In all cases, the table also provides the pseudo-field
%ID, which holds the value of the object ID.
The OID is not available in the SQL table.
InterSystems IRIS enforces uniqueness for the ID field (whatever its actual name might be). InterSystems IRIS 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. InterSystems IRIS generates a value for the ID field and inserts that when it creates the requested record.
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 InterSystems IRIS uses when it saves data for the class or reads saved data for the class. Atelier 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 the globals used for persistent classes, see
“Globals.”
As with other SQL tables, an InterSystems 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.”
An InterSystems 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 InterSystems IRIS uses when new data is added or when data is changed. If you use
relationships, described later in this book, the system 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.
An InterSystems 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 class members not discussed in this chapter, there is no projection to SQL. That is, InterSystems IRIS does not provide a direct way to use them from SQL or to make them usable from SQL.
InterSystems IRIS 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:
-
-
For any given instance of class
Employee, that instance is included in the
Person extent and in the
Employee extent.
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 InterSystems IRIS 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.
// 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
// 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
For classes that use the default storage class (
%Storage.Persistent), InterSystems IRIS 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:
-
-
Use one of the following calls:
Every persistent class automatically includes a
class query called
"Extent" that provides a set of all the IDs in the extent.
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
"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: InterSystems IRIS 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.
Persistent classes allow you to save objects to the database and retrieve them as objects or via SQL. Regardless of how they are accessed, these objects are stored in globals, which can be thought of as persistent multidimensional arrays. For more information on working with globals, see
Using Globals.
When you define a class that uses the default storage class (
%Storage.Persistent), global names for your class are generated when you compile the class. You can see these names in the storage definition at the bottom of the code in Atelier.
When you define a class in Atelier, global names for your class are generated based on the class name.
Class GlobalsTest.President Extends %Persistent
{
/// President's name (last,first)
Property Name As %String(PATTERN="1U.L1"",""1U.L");
/// Year of birth
Property BirthYear As %Integer;
/// Short biography
Property Bio As %Stream.GlobalCharacter;
/// Index for Name
Index NameIndex On Name;
/// Index for BirthYear
Index DOBIndex On BirthYear;
}
After compiling the class, we can see the following storage definition generated at the bottom of the class:
Storage Default
{
<Data name="PresidentDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
<Value name="3">
<Value>BirthYear</Value>
</Value>
<Value name="4">
<Value>Bio</Value>
</Value>
</Data>
<DataLocation>^GlobalsTest.PresidentD</DataLocation>
<DefaultData>PresidentDefaultData</DefaultData>
<IdLocation>^GlobalsTest.PresidentD</IdLocation>
<IndexLocation>^GlobalsTest.PresidentI</IndexLocation>
<StreamLocation>^GlobalsTest.PresidentS</StreamLocation>
<Type>%Storage.Persistent</Type>
}
Notice, in particular, the following storage keywords:
-
The
DataLocation is the global where class data will be stored. The name of the global is the complete class name (including the package name) with a D appended to the name, in this case,
^GlobalsTest.PresidentD.
-
The
IdLocation (often the same as the
DataLocation) is the global where the ID counter will be stored, at its root.
-
The
IndexLocation is the global where the indices for the class will be stored. The name of the global is the complete class name with an I appended to the name, or,
^GlobalsTest.PresidentI.
-
The
StreamLocation is the global where any stream properties will be stored. The name of the global is the complete class name with an S appended to the name, or,
^GlobalsTest.PresidentS.
After creating and storing a few objects of our class, we can view the contents of these globals in the Terminal:
USER>zwrite ^GlobalsTest.PresidentD
^GlobalsTest.PresidentD=3
^GlobalsTest.PresidentD(1)=$lb("",1732,"1","Washington,George")
^GlobalsTest.PresidentD(2)=$lb("",1735,"2","Adams,John")
^GlobalsTest.PresidentD(3)=$lb("",1743,"3","Jefferson,Thomas")
USER>zwrite ^GlobalsTest.PresidentI
^GlobalsTest.PresidentI("DOBIndex",1732,1)=""
^GlobalsTest.PresidentI("DOBIndex",1735,2)=""
^GlobalsTest.PresidentI("DOBIndex",1743,3)=""
^GlobalsTest.PresidentI("NameIndex"," ADAMS,JOHN",2)=""
^GlobalsTest.PresidentI("NameIndex"," JEFFERSON,THOMAS",3)=""
^GlobalsTest.PresidentI("NameIndex"," WASHINGTON,GEORGE",1)=""
USER>zwrite ^GlobalsTest.PresidentS
^GlobalsTest.PresidentS=3
^GlobalsTest.PresidentS(1)=1
^GlobalsTest.PresidentS(1,0)=239
^GlobalsTest.PresidentS(1,1)="George Washington was born to a moderately prosperous family of planters in colonial Virginia. He was commander-in-chief of the Continental Army during the Revolutionary War and was elected the first president of the United States in 1789."
^GlobalsTest.PresidentS(2)=1
^GlobalsTest.PresidentS(2,0)=195
^GlobalsTest.PresidentS(2,1)="John Adams was born in Braintree, Massachusetts, and entered Harvard College at age 16. He served as vice president under George Washington for two terms and became the nation's second president."
^GlobalsTest.PresidentS(3)=1
^GlobalsTest.PresidentS(3,0)=202
^GlobalsTest.PresidentS(3,1)="Thomas Jefferson was born in the colony of Virginia and attended the College of William & Mary. Jefferson was the principal author of the Declaration of Independence and became the third U.S. president."
Important:
Only the first 31 characters in a global name are significant, so if a complete class name is very long, you might see global names like
^package1.pC347.VeryLongCla4F4AD. The system generates names such as these to ensure that all of the global names for your class are unique. If you plan to work directly with the globals of a class, make sure to examine the storage definition so that you know the actual name of the global. Alternatively, you can control the global names by using the
DEFAULTGLOBAL parameter in your class definition. See
“User-Defined Global Names.”
The system will generate shorter global names if you set the
USEEXTENTSET parameter to the value 1. (The default value for this parameter is 0, meaning use the standard global names.) These shorter global names are created from a hash of the package name and a hash of the class name, followed by a suffix. While the standard names are more readable, the shorter names can contribute to better performance.
When you set
USEEXTENTSET to 1, each index is also assigned to a separate global, instead of using a single index global with different first subscripts. Again, this is done for increased performance.
To use hashed global names for the
GlobalsTest.President class we defined earlier, we would add the following to the class definition:
/// Use hashed global names
Parameter USEEXTENTSET = 1;
After deleting the storage definition and recompiling the class, we can see the new storage definition with hashed global names:
Storage Default
{
...
<DataLocation>^Ebnm.EKUy.1</DataLocation>
<DefaultData>PresidentDefaultData</DefaultData>
<ExtentLocation>^Ebnm.EKUy</ExtentLocation>
<IdLocation>^Ebnm.EKUy.1</IdLocation>
<Index name="DOBIndex">
<Location>^Ebnm.EKUy.2</Location>
</Index>
<Index name="IDKEY">
<Location>^Ebnm.EKUy.1</Location>
</Index>
<Index name="NameIndex">
<Location>^Ebnm.EKUy.3</Location>
</Index>
<IndexLocation>^Ebnm.EKUy.I</IndexLocation>
<StreamLocation>^Ebnm.EKUy.S</StreamLocation>
<Type>%Storage.Persistent</Type>
}
Notice, in particular, the following storage keywords:
-
The
ExtentLocation is the hashed value that will be used to calculate global names for this class, in this case,
^Ebnm.EKUy.
-
The
DataLocation (equivalent to the
IDKEY index), where class data will be stored, is now the hashed value with a .1 appended to the name, in this case,
^Ebnm.EKUy.1.
-
Each index now has its own
Location and thus its own separate global. The name of the IdKey index global is equivalent to the hashed value with a .1 appended to the name, in this example,
^Ebnm.EKUy.1. The globals for the remaining indices have .2 to .N appended to the name. Here, the DOBIndex is stored in global
^Ebnm.EKUy.2 and the NameIndex is stored in
^Ebnm.EKUy.3.
-
The
IndexLocation is the hashed value with .I appended to the name, or
^Ebnm.EKUy.I, however, this global is often not used.
-
After creating and storing a few objects, the contents of these globals might look as follows, again using the Terminal:
USER>zwrite ^Ebnm.EKUy.1
^Ebnm.EKUy.1=3
^Ebnm.EKUy.1(1)=$lb("","Washington,George",1732,"1")
^Ebnm.EKUy.1(2)=$lb("","Adams,John",1735,"2")
^Ebnm.EKUy.1(3)=$lb("","Jefferson,Thomas",1743,"3")
USER>zwrite ^Ebnm.EKUy.2
^Ebnm.EKUy.2(1732,1)=""
^Ebnm.EKUy.2(1735,2)=""
^Ebnm.EKUy.2(1743,3)=""
USER>zwrite ^Ebnm.EKUy.3
^Ebnm.EKUy.3(" ADAMS,JOHN",2)=""
^Ebnm.EKUy.3(" JEFFERSON,THOMAS",3)=""
^Ebnm.EKUy.3(" WASHINGTON,GEORGE",1)=""
USER>zwrite ^Ebnm.EKUy.S
^Ebnm.EKUy.S=3
^Ebnm.EKUy.S(1)=1
^Ebnm.EKUy.S(1,0)=239
^Ebnm.EKUy.S(1,1)="George Washington was born to a moderately prosperous family of planters in colonial Virginia. He was commander-in-chief of the Continental Army during the Revolutionary War and was elected the first president of the United States in 1789."
...
CREATE TABLE GlobalsTest.State (NAME CHAR (30) NOT NULL, ADMITYEAR INT)
Using the Terminal, the contents of the globals might look like:
USER>zwrite ^Ebnm.BndZ.1
^Ebnm.BndZ.1=3
^Ebnm.BndZ.1(1)=$lb("Delaware",1787)
^Ebnm.BndZ.1(2)=$lb("Pennsylvania",1787)
^Ebnm.BndZ.1(3)=$lb("New Jersey",1787)
USER>zwrite ^Ebnm.BndZ.2
^Ebnm.BndZ.2(1)=$zwc(412,1,0)/*$bit(2..4)*/
If we wanted to use standard global names with a class created via SQL, we could set the
USEEXTENTSET parameter to the value 0:
CREATE TABLE GlobalsTest.State (%CLASSPARAMETER USEEXTENTSET 0, NAME CHAR (30) NOT NULL, ADMITYEAR INT)
For finer control of the global names for a class, use the
DEFAULTGLOBAL parameter. This parameter works in conjunction with the
USEEXTENTSET parameter to determine the global naming scheme.
/// Use hashed global names
Parameter USEEXTENTSET = 1;
/// Set the root of the global names
Parameter DEFAULTGLOBAL = "^GT.Pres";
After deleting the storage definition and recompiling the class, we can see the following global names:
Storage Default
{
...
<DataLocation>^GT.Pres.1</DataLocation>
<DefaultData>PresidentDefaultData</DefaultData>
<ExtentLocation>^GT.Pres</ExtentLocation>
<IdLocation>^GT.Pres.1</IdLocation>
<Index name="DOBIndex">
<Location>^GT.Pres.2</Location>
</Index>
<Index name="IDKEY">
<Location>^GT.Pres.1</Location>
</Index>
<Index name="NameIndex">
<Location>^GT.Pres.3</Location>
</Index>
<IndexLocation>^GT.Pres.I</IndexLocation>
<StreamLocation>^GT.Pres.S</StreamLocation>
<Type>%Storage.Persistent</Type>
}
Likewise, we can use the
DEFAULTGLOBAL parameter when defining a class using SQL:
CREATE TABLE GlobalsTest.State (%CLASSPARAMETER USEEXTENTSET 0, %CLASSPARAMETER DEFAULTGLOBAL = '^GT.State', NAME CHAR (30) NOT NULL, ADMITYEAR INT)
If you edit a class definition in a way that redefines the previously existing global names, for example, by changing the values of the
USEEXTENTSET or
DEFAULTGLOBAL parameters, you must delete the existing storage definition to allow the compiler to generate a new storage definition. Note that any data in the existing globals is preserved. Any data to be retained must be migrated to the new global structure.
Content Date/Time: 2019-02-22 00:52:40