第七章 Caché 持久性对象介绍

第七章 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)
  1. extent 通常是类名
  2. extenttype 是继承类型
  3. 对于类,这是cls,它也是这个参数的默认值
  • 使用以下调用之一:
  1. $SYSTEM.OBJ.Delete(classname,flags) classname是要删除的类,flags 包含e
  2. $SYSTEM.OBJ.DeletePackage(packagename,flags) packagename 是要删除的包 ,flags 包含e
  3. $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 } }

你可能感兴趣的:(第七章 Caché 持久性对象介绍)