Skip to main content

Defining Persistent Classes

A persistent class is a class that defines persistent objects. This chapter describes how to create such classes. It discusses the following topics:

Also see the chapters “Introduction to Persistent Objects,” “Working with Persistent Objects,” and “Other Options for Persistent Classes.”

When viewing this book online, use the preface of this book to quickly find other topics.

Defining a Persistent Class

To define a class that defines persistent objects, ensure that the primary (first) superclass of your class is either %PersistentOpens in a new tab or some other persistent class.

For example:

Class MyApp.MyClass Extends %Persistent
{
}

Projection of Packages to Schemas

For persistent classes, the package is represented in SQL as an SQL schema. For instance, if a class is called Team.Player (the Player class in the “Team” package), the corresponding table is “Team.Player” (the Player table in the “Team” schema).

The default package is “User”, which is represented in SQL as the “SQLUser” schema. Hence, a class called User.PersonOpens in a new tab corresponds to a table called SQLUser.Person.

If a package name contains periods, the corresponding table name uses an underscore in the place of each. For example, the class MyTest.Test.MyClass (the MyClass class in the “MyTest.Test” package) becomes the table MyTest_Test.MyClass (the MyClass table in the “MyTest_Test” schema).

If an SQL table name is referenced without the schema name, the default schema name (SQLUser) is used. For instance, the command:

Select ID, Name from Person

is the same as:

Select ID, Name from SQLUser.Person

Specifying the Table Name for a Persistent Class

For a persistent class, by default, the short class name becomes the table name.

To specify a different table name, use the SqlTableName class keyword. For example:

Class App.Products Extends %Persistent [ SqlTableName = NewTableName ]

Although Caché places no restrictions on class names, SQL tables cannot have names that are SQL reserved words. Thus if you create a persistent class with a name that is a reserved word, the class compiler will generate an error message. In this case, you must either rename the class or specify a table name for the projection that differs from the class name, using the technique described here.

Storage Definitions and Storage Classes

The %PersistentOpens in a new tab class provides the high-level interface for storing and retrieving objects in the database. The actual work of storing and loading objects is performed by what is called a storage class.

Every persistent object and every serial object uses a storage class to generate the actual methods used to store, load, and delete objects in a database. These internal methods are referred to as the storage interface. The storage interface includes methods such as %LoadData(), %SaveData(), and %DeleteData(). Applications never call these methods directly; instead they are called at the appropriate time by the methods of the persistence interface (such as %OpenId() and %Save()).

The storage class used by a persistent class is specified by a storage definition. A storage definition contains a set of keywords and values that define a storage class as well as additional parameters used by the storage interface.

A persistent class may contain more than one storage definition but only one can be active at a time. The active storage definition is specified using the StorageStrategy keyword of the class. By default, a persistent class has a single storage definition called “Default”.

For information on the names of the globals that store the data for a class, see “Globals.”

Updates to a Storage Definition

The storage definition for a class is created when the class is first compiled. Class projection, such as for SQL or MultiValue, occurs after compilation. If a class compiles properly and then projection fails, Caché does not remove the storage definition. Also, if a class is changed in such a way that might affect the storage definition, it is the responsibility of the application developer to determine if the storage definition has been updated and, if necessary, to modify the storage definition to reflect the change. See “Resetting the Storage Definition.”

The %CacheStorage Storage Class

%CacheStorageOpens in a new tab is the default storage class used by persistent objects. It automatically creates and maintains a default storage structure for a persistent class.

New persistent classes automatically use the %CacheStorageOpens in a new tab storage class. The %CacheStorageOpens in a new tab class lets you control certain aspects of the storage structure used for a class by means of the various keywords in the storage definition.

Refer to the Class Definition Reference for details on the various storage keywords.

Also see “Extent Management” in the previous chapter for information on the MANAGEDEXTENT class parameter.

The %CacheSQLStorage Storage Class

The %CacheSQLStorageOpens in a new tab class is a special storage class that uses generated SQL SELECT, INSERT, UPDATE, and DELETE statements to provide object persistence.

%CacheSQLStorageOpens in a new tab is typically used for:

  • Mapping objects to preexisting global structures used by older applications.

  • Storing objects within an external relational database using the SQL Gateway.

%CacheSQLStorageOpens in a new tab is more limited than %CacheStorageOpens in a new tab. Specifically, it does not automatically support schema evolution or multi-class extents.

Schema Evolution

The %CacheStorageOpens in a new tab storage class supports automatic schema evolution.

When you compile a persistent (or serial) class that uses the default %CacheStorageOpens in a new tab storage class, the class compiler analyzes the properties defined by the class and automatically adds or removes them.

If you would like to see schema evolution in action, try the following:

  1. Start Studio and create a new persistent class with one or more properties in it.

  2. Compile the class and then view the automatically generated storage definition (as XML text) for the class within the class definition as a whole. Alternatively, you can see a more graphical representation of storage using the Class Inspector. Click on Storage in the Inspector, click on “Default” in the list of storage definitions, click on Data Nodes in the keyword list, and click on the browse button (...) that appears. This invokes a graphical storage editor.

    Within the generated storage for your class, you will see the pseudo-property %%CLASSNAME. This is a placeholder for the class name of any future subclasses you may derive from your class and is used to tell the type of objects stored in the database. For the root class of an extent, this value is always empty.

  3. Add one or more new properties to your class and compile it again. Notice that these new properties have been added to your storage definition automatically and in a way that is compatible with the previously existing storage.

Resetting the Storage Definition

During the development process, you may make many modifications to your persistent classes: adding, modifying, and deleting properties. As a result, you may end up with a fairly convoluted storage definition as the class compiler attempts to maintain a compatible structure. If you want the class compiler to generate a clean storage structure, delete the storage definition and recompile the class.

You can do this as follows:

  1. Open the class in Studio.

  2. Right-click on the default Storage definition in the Class Inspector.

  3. Invoke the Delete command in the popup menu.

  4. Compile the class. This will cause the class compiler to generate a new storage definition for the class.

Controlling How IDs Are Generated

When you save an object for the first time, the system generates an ID for the object. IDs are permanent.

By default, Caché uses an integer for the ID, incremented by 1 from the last saved object.

You can define a given persistent class so that it generates IDs in either of the following ways:

  • The ID can be based on a specific property of the class, if that property is unique per instance. For example, you could use a drug code as the ID. To define a class this way, add an index like the following to the class:

    Index IndexName On PropertyName [ IdKey ];
    

    Or (equivalently):

    Index IndexName On PropertyName [ IdKey, Unique ];
    

    Where IndexName is the name of the index, and PropertyName is the name of the property.

    If you define a class this way, when Caché saves an object for the first time, it uses the value of that property as the ID. Furthermore, Caché requires a value for the property and enforces uniqueness of that property. If you create another object with the same value for the designated property and then attempt to save the new object, Caché issues this error:

    ERROR #5805: ID key not unique for extent 
    

    Also, Cache prevents you from changing that property in the future. That is, if you open a saved object, change the property value, and try attempt to save the changed object, Caché issues this error:

    ERROR #5814: Oid previously assigned
    

    This message refers to the OID rather than the ID, because the underlying logic prevents the OID from being changed; the OID is based on the ID.

  • The ID can be based on multiple properties. To define a class this way, add an index like the following to the class:

    Index IndexName On (PropertyName1,PropertyName2,...) [ IdKey, Unique ];
    

    Or (equivalently):

    Index IndexName On (PropertyName1,PropertyName2,...) [ IdKey ];
    

    Where IndexName is the name of the index, and PropertyName1, PropertyName2, and so on are the property names.

    If you define a class this way, when Caché saves an object for the first time, it generates an ID as follows:

    PropertyName1||PropertyName2||...
    

    Furthermore, Caché requires values for the properties and enforces uniqueness of the given combination of properties. It also prevents you from changing any of those properties.

Important:

If a literal property (that is, an attribute) contains a sequential pair of vertical bars (||), do not add an IdKey index that uses that property. This restriction is imposed by the way in which the Caché SQL mechanism works. The use of || in IdKey properties can result in unpredictable behavior.

The system generates an OID as well. In all cases, the OID has the following form:

$LISTBUILD(ID,Classname)

Where ID is the generated ID, and Classname is the name of the class.

Controlling the SQL Projection of Subclasses

When several persistent classes are in superclass/subclass hierarchy, there are two ways in which Caché can store their data. The default scenario is by far the most common.

Default SQL Projection of Subclasses

The class compiler projects a “flattened” representation of a persistent class, such that the projected table contains all the appropriate fields for the class, including those that are inherited. Hence, for a subclass, the SQL projection is a table composed of:

  • All the columns in the extent of the superclass

  • Additional columns based on properties only in the subclass

  • Rows that represent the saved instances of the subclass

Furthermore, in the default scenario, the extent of the superclass contains one record for each saved object of the superclass and all its subclasses. The extent of each subclass is a subset of the extent of the superclass.

For example, consider the persistent classes Sample.PersonOpens in a new tab and Sample.EmployeeOpens in a new tab in SAMPLES. The Sample.EmployeeOpens in a new tab class inherits from Sample.PersonOpens in a new tab and adds some additional properties. In the SAMPLES, both classes have saved data.

To see this, use the following SQL queries. The first lists all instances of Sample.PersonOpens in a new tab and shows their properties:

SELECT * FROM Sample.Person

The second query lists all instances of Sample.EmployeeOpens in a new tab and their properties:

SELECT * FROM Sample.Employee

Notice that the Sample.PersonOpens in a new tab table contains records with IDs in the range 1 to 200. The records with IDs in the range 101 to 200 are employees, and the Sample.EmployeeOpens in a new tab table shows the same employees (with the same IDs and with additional columns). The Sample.PersonOpens in a new tab table is arranged in two apparent “groups” only because of the artificial way that the SAMPLES database is built. The Sample.PersonOpens in a new tab table is populated and then the Sample.EmployeeOpens in a new tab table is populated.

Typically, the table of a subclass has more columns and fewer rows than its parent. There are more columns in the subclass because it usually adds additional properties when it extends the parent class; there are often fewer rows because there are often fewer instances of the subclass than the parent.

Alternative SQL Projection of Subclasses

The default projection is the most convenient, but on occasion, you might find it necessary to use the alternative SQL projection. In this scenario, each class has its own extent. To cause this form of projection, include the following in the definition of the superclass:

[ NoExtent ]

For example:

Class MyApp.MyNoExtentClass [ NoExtent ] 
{
//class implementation
}

Each subclass of this class then receives its own extent.

If you create classes in this way and use them as properties of other classes, see “Variation: CLASSNAME Parameter” in the chapter “Defining and Using Object-Valued Properties.”

Redefining a Persistent Class That Has Stored Data

During the development process, it is common to redefine your classes. If you have already created sample data for the class, note the following points:

  • The compiler has no effect on the globals that store the data for the class.

    In fact, when you delete a class definition, its data globals are untouched. If you no longer need these globals, delete them manually.

  • If you add or remove properties of a class but do not modify the storage definition of the class, then all code that accesses data for that class continues to work as before. See “Schema Evolution,” earlier in this chapter.

  • If you do modify the storage definition of the class, then code that accesses the data may or may not continue to work as before, depending on the nature of the change.

  • If you modify a property definition in a way that causes the property validation to be more restrictive, then you will receive errors when you work with objects (or records) that no longer pass validation. For example, if you decrease the MAXLEN parameter for a property, then you will receive validation errors when you work with an object that has a value for that property that is now too long.

FeedbackOpens in a new tab