第七章 Caché 持久性对象介绍
持久化类
持久类是继承自%persistent的任何类。持久对象就是这样一个类的实例。
%Persistent类是%RegisteredObject的子类,因此是一个对象类。除了提供前一章中描述的方法之外,%Persistent类还定义了持久性接口和一组方法。除其他外,这些方法能够将对象保存到数据库、从数据库加载对象、删除对象和测试是否存在。
介绍默认的SQL映射
对于任何持久性类,编译器都会生成一个SQL表定义,以便除了通过本书中描述的对象接口之外,还可以通过SQL访问存储的数据。
该表包含每个保存对象的每一条记录,可以通过 Caché SQL查询该表。下面显示了示例的查询结果。Sample.Person 表:
下表总结了默认的映射:
Object-SQL映射
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 |
保存对象标识符:ID和OID
当第一次保存一个对象时,Caché会为它创建两个永久标识符,可以使用其中一个来访问或删除保存的对象。更常用的标识符是对象ID。ID是表中惟一的值。默认情况下,Caché生成一个整数作为ID。
OID更加通用:它还包含类名,并且在数据库中是惟一的。在一般实践中,应用程序永远不需要使用OID值;ID值通常就足够了。
%Persistent类提供使用ID或OID的方法。在使用%OpenId()、%ExistsId()和%DeleteId()等方法时指定ID。将OID指定为方法的参数,如%Open()、%Exists()和%Delete()。也就是说,使用ID作为参数的方法在它们的名称中包含ID。使用OID作为参数的方法的名称中不包含Id;这些方法的使用频率要低得多。
当持久对象存储在数据库中时,其任何引用属性(即对其他持久对象的引用)的值都存储为OID值。对于没有oid的对象属性,对象的文字值与对象的其他状态一起存储。
对象ID映射到SQL
对象的ID在对应的SQL表中可用。如果,Caché 使用字段名ID。如果不确定要使用哪个字段名,Caché 还提供了访问ID的方法。系统如下:
- 对象ID不是对象的属性,与属性不同。
- 如果类不包含名为ID的属性,那么表也包含字段ID,而该字段包含对象ID。
- 如果类包含一个属性,这个属性用名称ID(在任何情况下都是变体)映射到SQL,那么表也包含字段ID1,这个字段包含对象ID的值。
类似地,如果类包含映射为ID和ID1的属性,那么表也包含ID2字段,该字段包含对象ID的值。
- 在所有情况下,表还提供了伪字段%ID,其中保存了对象ID的值。
OID在SQL表中不可用。
SQL中的对象ID
Caché 强制ID字段的唯一性(无论它的实际名称是什么)。Caché也防止更改此字段。这意味着不能在该字段上执行SQL更新或插入操作。
例如,下面显示了向表添加新记录所需的SQL:
INSERT INTO PERSON (FNAME, LNAME)VALUES (:fname, :lname)
注意,此SQL不引用ID字段。Caché 为ID字段生成一个值,并在创建请求的记录时插入该值。
特定于持久类的类成员
Caché 类可以包含几种只有在持久类中才有意义的类成员。存储过程、索引、外键和触发器。 storage definitions, indices, foreign keys, and triggers.
存储定义
在大多数情况下(如后面讨论的),每个持久类都有一个存储定义。存储定义的目的是描述Caché在为类保存数据或为类读取保存的数据时使用的全局结构。在以编辑模式查看类时,Studio将在类定义的末尾显示存储定义。以下是部分例子:
%%CLASSNAME
Name
SSN
DOB
Home
Office
Spouse
FavoriteColors
^Sample.PersonD
PersonDefaultData
200
^Sample.PersonD
^Sample.PersonI
50.0000%
...
在大多数情况下,编译器也会生成和更新存储定义。
索引
与其他SQL表一样,Caché SQL表可以有索引;要定义这些,需要将索引定义添加到相应的类定义中。
索引可以添加约束,以确保给定字段或字段组合的唯一性。
索引的另一个用途是定义与类关联的常用请求数据的特定排序子集,以便查询可以更快地运行。例如,作为一般规则,如果一个查询包含使用给定字段的WHERE子句,那么如果该字段被索引,则查询运行得更快。相反,如果该字段上没有索引,则引擎必须执行一个完整的表扫描,检查每一行,以查看它是否符合给定的条件——如果表很大,这是一个耗时的操作。
外键
Caché SQL表也可以有外键。要定义这些,需要将外键定义添加到相应的类定义中。
外键在表之间建立引用完整性约束,Caché在添加新数据或更改数据时使用这些约束。如果使用的是关系,那么Caché将自动将这些关系视为外键。但是,如果不想使用关系或者有其他原因需要添加他们,则可以添加外键。
触发器
Caché SQL表也可以有触发器。要定义这些,需要将触发器定义添加到相应的类定义中。
触发器定义在特定事件发生时自动执行的代码,特别是在插入、修改或删除记录时。
其他类成员
可以定义类方法或类查询,以便将其作为存储过程调用,能够从SQL调用它。
对于本章没有讨论的类成员,SQL没有对应映射。也就是说,Caché 不提供直接的方式使用它们 从SQL或使它们从SQL可用的直接方法。
术语继承是指磁盘上给定持久类的所有记录。如下一章所示,%Persistent类提供了几个对类继承进行操作的方法。
- 如果持久类Person拥有子类Employee,则Person继承包括Person的所有实例和Employee的所有实例。
- 对于类Employee的任何给定实例,该实例都包含在Person继承和Employee继承中。
索引自动跨越其定义的类的整个范围。Person中定义的索引同时包含Person实例和Employee实例。在Employee继承中定义的索引只包含Employee实例。
子类可以定义父类中未定义的其他属性。这些在子类范围内可用,但在父类范围内不可用。例如,Employee继承可能包括Department字段,而人员继承不包括该字段。
前面几点意味着在Caché中编写一个查询来检索相同类型的所有记录相对容易。例如,如果希望统计所有类型的人员,可以对Person表运行查询。如果只想计算雇员数量,请对Employee 表运行相同的查询。与其他对象数据库相反,为了统计所有类型的人员,需要编写一个更复杂的组合表的查询,并且需要在添加另一个子类时更新这个查询。
类似地,使用ID的方法都具有多态性。也就是说,它们可以根据传递的ID值对不同类型的对象进行操作。
例如,Sample.Person对象包括Sample.Person实例和Sample.Employee 实例。当调用 Sample.Person类的%OpenId()时,得到的OREF是Sample.Person或Sample.Employee实例,取决于是存储在数据库的是什么:
/// d ##class(PHA.OP.MOB.Test).TestObjectID()
ClassMethod TestObjectID()
{
Set obj = ##class(Sample.Person).%OpenId(1)
Write $ClassName(obj),!
Set obj = ##class(Sample.Person).%OpenId(2)
Write $ClassName(obj),!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestObjectID()
Sample.Person
Sample.Employee
注意示例的%OpenId()方法。如果尝试打开ID 1, Sample.Employee 类将不会返回对象,因为ID 1不是Sample.Employee的继承:
ClassMethod TestIsObject()
{
Set obj = ##class(Sample.Employee).%OpenId(1)
Write $IsObject(obj),!
Set obj = ##class(Sample.Employee).%OpenId(2)
Write $IsObject(obj),!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestIsObject()
0
1
继承管理
对于使用默认存储类(%Library.CacheStorage)的类,Caché维护继承定义和那些继承注册到其继承管理器中使用的全局变量。到继承管理器的接口是通%ExtentMgr.Util实现的。这个注册过程发生在类编译期间。如果存在任何错误或名称冲突,则会导致编译失败。若要编译成功,解决冲突;这通常涉及更改索引的名称或添加数据的显式存储位置。
MANAGEDEXTENT类参数的默认值为1;此值将导致全局名称注册和冲突使用检查。值0指定既不进行注册也不进行冲突检查。
注意:如果一个应用程序有多个类有意共享一个全局引用,那么对于所有相关的类指定MANAGEDEXTENT类参数的默认值为1为0(如果它们使用默认存储)。否则,重新编译将生成以下错误
ERROR #5564: Storage reference: '^This.App.Global used in 'User.ClassA.cls'
is already registered for use by 'User.ClassB.cls'
要删除继承元数据,有多种方法:
- 使用 ##class(%ExtentMgr.Util).DeleteExtentDefinition(extent,extenttype)
- extent 通常是类名
- extenttype 是继承类型
- 对于类,这是cls,它也是这个参数的默认值
- 使用以下调用之一:
- $SYSTEM.OBJ.Delete(classname,flags) classname是要删除的类,flags 包含e
- $SYSTEM.OBJ.DeletePackage(packagename,flags) packagename 是要删除的包 ,flags 包含e
- $SYSTEM.OBJ.DeleteAll(flags) flags 包含e
继承查询
每个持久化类都会自动包含一个类查询。称为“范围”,它提供范围中的所有id的集合。称为“继承”,它提供继承中所有id的集合。
有关使用类查询的一般信息,下面的示例使用一个类查询来显示示例的所有id。Sample.Person :
/// d ##class(PHA.OP.MOB.Test).TestExtentQueries()
ClassMethod TestExtentQueries()
{
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"),!
}
}
DHC-APP> d ##class(PHA.OP.MOB.Test).TestExtentQueries()
1
2
Sample.Person 拓展包含 Sample.Person 的实例和它的子类
“extent”查询相当于以下SQL查询:
SELECT %ID FROM Sample.Person
注意,不能依赖的顺序,其中的ID值返回使用这些方法之一:Caché 可以确定使用其他属性值排序的索引来满足此请求的效率更高。如果需要,可以将ORDER BY %ID子句添加到SQL查询中。
INSERT INTO Sample.Person (Age,SSN,Name) VALUES (1,"3N1","yaoxin")
INSERT INTO Sample.Employee (Age,SSN,Name,Title,Salary) VALUES (30,"111-11-1111","xiaoli","test",2000)
附录
Sample.Person
/// This sample persistent class represents a person.
/// Maintenance note: This class is used by some of the bindings samples.
Class Sample.Person Extends (%Persistent, %Populate, %XML.Adaptor)
{
Parameter EXTENTQUERYSPEC = "Name,SSN,Home.City,Home.State";
// define indices for this class
/// Define a unique index for SSN .
Index SSNKey On SSN [ Type = index, Unique ];
/// Define an index for Name .
Index NameIDX On Name [ Data = Name ];
/// Define an index for embedded object property ZipCode.
Index ZipCode On Home.Zip [ Type = bitmap ];
// define properties for this class
/// Person's name.
Property Name As %String(POPSPEC = "Name()") [ Required ];
/// Person's Social Security number. This is validated using pattern match.
Property SSN As %String(PATTERN = "3N1""-""2N1""-""4N") [ Required ];
/// Person's Date of Birth.
Property DOB As %Date(POPSPEC = "Date()");
/// Person's home address. This uses an embedded object.
Property Home As Address;
/// Person's office address. This uses an embedded object.
Property Office As Address;
/// Person's spouse. This is a reference to another persistent object.
Property Spouse As Person;
/// A collection of strings representing the person's favorite colors.
Property FavoriteColors As list Of %String(JAVATYPE = "java.util.List", POPSPEC = "ValueList("",Red,Orange,Yellow,Green,Blue,Purple,Black,White""):2");
/// Person's age.
/// This is a calculated field whose value is derived from DOB .
Property Age As %Integer [ Calculated, SqlComputeCode = { Set {Age}=##class(Sample.Person).CurrentAge({DOB})
}, SqlComputed, SqlComputeOnChange = DOB ];
/// This class method calculates a current age given a date of birth date.
ClassMethod CurrentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
{
$Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
}
/// Prints the property Name to the console.
Method PrintPerson()
{
Write !, "Name: ", ..Name
Quit
}
/// A simple, sample method: add two numbers (x and y)
/// and return the result.
Method Addition(x As %Integer = 1, y As %Integer = 1) As %Integer
{
Quit x + y // comment
}
/// A simple, sample expression method: returns the value 99.
Method NinetyNine() As %Integer [ CodeMode = expression ]
{
99
}
/// Invoke the PrintPerson on all Person objects
/// within the database.
ClassMethod PrintPersons()
{
// use the extent result set to find all person
Set extent = ##class(%ResultSet).%New("Sample.Person:Extent")
Do extent.Execute()
While (extent.Next()) {
Set person = ..%OpenId(extent.GetData(1))
Do person.PrintPerson()
}
Quit
}
/// Prints out data on all persons within the database using SQL to
/// iterate over all the person data.
ClassMethod PrintPersonsSQL()
{
// use dynamic SQL result set to find person data
Set query = ##class(%ResultSet).%New("%DynamicQuery:SQL")
Do query.Prepare("SELECT ID, Name, SSN FROM Sample.Person ORDER BY Name")
Do query.Execute()
While (query.Next()) {
Write !,"Name: ", query.Get("Name"), ?30, query.Get("SSN")
}
Quit
}
/// This is a sample of how to define an SQL stored procedure using a
/// class method. This method can be called as a stored procedure via
/// ODBC or JDBC.
/// In this case this method returns the concatenation of a string value.
ClassMethod StoredProcTest(name As %String, ByRef response As %String) As %Integer [ SqlName = Stored_Procedure_Test, SqlProc ]
{
// Set response to the concatenation of name.
Set response = name _ "||" _ name
QUIT 29
}
/// This is a sample of how to define an SQL stored procedure using a
/// class method. This method can be called as a stored procedure via
/// ODBC or JDBC.
/// This method performs an SQL update operation on the database
/// using embedded SQL. The update modifies the embedded properties
/// Home.City and Home.State for all rows whose
/// Home.Zip is equal to zip.
ClassMethod UpdateProcTest(zip As %String, city As %String, state As %String) As %Integer [ SqlProc ]
{
New %ROWCOUNT,%ROWID
&sql(UPDATE Sample.Person
SET Home_City = :city, Home_State = :state
WHERE Home_Zip = :zip)
// Return context information to client via %SQLProcContext object
If ($g(%sqlcontext)'=$$$NULLOREF) {
Set %sqlcontext.SQLCode = SQLCODE
Set %sqlcontext.RowCount = %ROWCOUNT
}
QUIT 1
}
/// A sample class query that defines a result set that returns Person data
/// ordered by Name .
/// This query can be used within another Caché method (using the
/// %ResultSet class), from Java, or from ActiveX.
/// This query is also accessible from ODBC and/or JDBC as the SQL stored procedure
/// SP_Sample_By_Name.
Query ByName(name As %String = "") As %SQLQuery(CONTAINID = 1, SELECTMODE = "RUNTIME") [ SqlName = SP_Sample_By_Name, SqlProc ]
{
SELECT ID, Name, DOB, SSN
FROM Sample.Person
WHERE (Name %STARTSWITH :name)
ORDER BY Name
}
Storage Default
{
%%CLASSNAME
Name
SSN
DOB
Home
Office
Spouse
FavoriteColors
^Sample.PersonD
PersonDefaultData
200
^Sample.PersonD
^Sample.PersonI
8.5
50.0000%
2.46
1
1.88
1.1765%
5
0.5000%
6.71
.34:
1.4043%
36.23,City:7.27,State:2,Street:16.58,Zip:5
0.5000%,City:3.8462%,State:2.0408%,Street:0.5000%,Zip:0.5000%
15.83
0.5000%
36.43,City:7.15,State:2,Street:16.91,Zip:5
0.5000%,City:3.8462%,State:2.0408%,Street:0.5000%,Zip:0.5000%
11
1
.95
.5:
0.7937%
-4
-20
-8
-8
-8
^Sample.PersonS
%Library.CacheStorage
}
}
Sample.Employee
/// This sample persistent class represents an employee.
Class Sample.Employee Extends Person
{
/// The employee's job title.
Property Title As %String(MAXLEN = 50, POPSPEC = "Title()");
/// The employee's current salary.
Property Salary As %Integer(MAXVAL = 100000, MINVAL = 0);
/// A character stream containing notes about this employee.
Property Notes As %Stream.GlobalCharacter;
/// A picture of the employee
Property Picture As %Stream.GlobalBinary;
/// The company this employee works for.
Relationship Company As Company [ Cardinality = one, Inverse = Employees ];
/// This method overrides the method in Person .
/// Prints the properties Name and Title
/// to the console.
Method PrintPerson()
{
Write !,"Name: ", ..Name, ?30, "Title: ", ..Title
Quit
}
/// writes a .png file containing the picture, if any, of this employee
/// the purpose of this method is to prove that Picture really contains an image
Method WritePicture()
{
if (..Picture="") {quit}
set name=$TR(..Name,".") ; strip off trailing period
set name=$TR(name,", ","__") ; replace commas and spaces
set filename=name_".png"
set file=##class(%Stream.FileBinary).%New()
set file.Filename=filename
do file.CopyFrom(..Picture)
do file.%Save()
write !, "Generated file: "_filename
}
Storage Default
{
"Employee"
Company
Notes
Salary
Title
Picture
EmployeeDefaultData
100
17
100.0000%
3
1
1.85
1.5873%
1.45
5.0000%
5
1.0000%
5.81
.39:
2.2593%
36.56,City:7.66,State:2,Street:16.5,Zip:5
1.0000%,City:3.8462%,State:2.4390%,Street:1.0000%,Zip:1.0000%
15.92
1.0000%
100.0000%
36.83,City:7.22,State:2,Street:17.19,Zip:5
1.0000%,City:4.0000%,State:2.1739%,Street:1.0000%,Zip:1.0000%
100.0000%
11
1
4.91
1.0000%
1.89
1.5873%
21.36
1.5385%
-4
%Library.CacheStorage
}
}