文章目錄
- 第七章 Caché 持久性對象介紹
- 持久化類
- 介紹預設的SQL映射
- 儲存對象辨別符:ID和OID
-
- 對象ID映射到SQL
- SQL中的對象ID
- 特定于持久類的類成員
-
- 存儲定義
- 索引
- 外鍵
- 觸發器
- 其他類成員
- 繼承管理
- 繼承查詢
- 附錄
第七章 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将在類定義的末尾顯示存儲定義。以下是部分例子:
<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>
...
在大多數情況下,編譯器也會生成和更新存儲定義。
索引
與其他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
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)
附錄
/// This sample persistent class represents a person.
/// <p>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 <property>SSN</property>.
Index SSNKey On SSN [ Type = index, Unique ];
/// Define an index for <property>Name</property>.
Index NameIDX On Name [ Data = Name ];
/// Define an index for embedded object property <b>ZipCode</b>.
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.<br>
/// This is a calculated field whose value is derived from <property>DOB</property>.
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 <var>date</var>.
ClassMethod CurrentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
{
$Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
}
/// Prints the property <property>Name</property> to the console.
Method PrintPerson()
{
Write !, "Name: ", ..Name
Quit
}
/// A simple, sample method: add two numbers (<var>x</var> and <var>y</var>)
/// 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 <method>PrintPerson</method> on all <class>Person</class> 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.<br>
/// 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.<br>
/// This method performs an SQL update operation on the database
/// using embedded SQL. The update modifies the embedded properties
/// <var>Home.City</var> and <var>Home.State</var> for all rows whose
/// <var>Home.Zip</var> is equal to <var>zip</var>.
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 <property>Name</property>.<br>
/// This query can be used within another Caché method (using the
/// <class>%ResultSet</class> class), from Java, or from ActiveX.<br>
/// This query is also accessible from ODBC and/or JDBC as the SQL stored procedure
/// <b>SP_Sample_By_Name</b>.
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
{
<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">
<AverageFieldSize>8.5</AverageFieldSize>
<Selectivity>50.0000%</Selectivity>
</Property>
<Property name="%%ID">
<AverageFieldSize>2.46</AverageFieldSize>
<Selectivity>1</Selectivity>
</Property>
<Property name="Age">
<AverageFieldSize>1.88</AverageFieldSize>
<Selectivity>1.1765%</Selectivity>
</Property>
<Property name="DOB">
<AverageFieldSize>5</AverageFieldSize>
<Selectivity>0.5000%</Selectivity>
</Property>
<Property name="FavoriteColors">
<AverageFieldSize>6.71</AverageFieldSize>
<OutlierSelectivity>.34:</OutlierSelectivity>
<Selectivity>1.4043%</Selectivity>
</Property>
<Property name="Home">
<AverageFieldSize>36.23,City:7.27,State:2,Street:16.58,Zip:5</AverageFieldSize>
<Selectivity>0.5000%,City:3.8462%,State:2.0408%,Street:0.5000%,Zip:0.5000%</Selectivity>
</Property>
<Property name="Name">
<AverageFieldSize>15.83</AverageFieldSize>
<Selectivity>0.5000%</Selectivity>
</Property>
<Property name="Office">
<AverageFieldSize>36.43,City:7.15,State:2,Street:16.91,Zip:5</AverageFieldSize>
<Selectivity>0.5000%,City:3.8462%,State:2.0408%,Street:0.5000%,Zip:0.5000%</Selectivity>
</Property>
<Property name="SSN">
<AverageFieldSize>11</AverageFieldSize>
<Selectivity>1</Selectivity>
</Property>
<Property name="Spouse">
<AverageFieldSize>.95</AverageFieldSize>
<OutlierSelectivity>.5:</OutlierSelectivity>
<Selectivity>0.7937%</Selectivity>
</Property>
<SQLMap name="$Person">
<BlockCount>-4</BlockCount>
</SQLMap>
<SQLMap name="IDKEY">
<BlockCount>-20</BlockCount>
</SQLMap>
<SQLMap name="NameIDX">
<BlockCount>-8</BlockCount>
</SQLMap>
<SQLMap name="SSNKey">
<BlockCount>-8</BlockCount>
</SQLMap>
<SQLMap name="ZipCode">
<BlockCount>-8</BlockCount>
</SQLMap>
<StreamLocation>^Sample.PersonS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}
}
/// This sample persistent class represents an employee.<br>
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 <class>Person</class>.<br>
/// Prints the properties <property>Name</property> and <property>Title</property>
/// 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
{
<Data name="EmployeeDefaultData">
<Subscript>"Employee"</Subscript>
<Value name="1">
<Value>Company</Value>
</Value>
<Value name="2">
<Value>Notes</Value>
</Value>
<Value name="3">
<Value>Salary</Value>
</Value>
<Value name="4">
<Value>Title</Value>
</Value>
<Value name="5">
<Value>Picture</Value>
</Value>
</Data>
<DefaultData>EmployeeDefaultData</DefaultData>
<ExtentSize>100</ExtentSize>
<Property name="%%CLASSNAME">
<AverageFieldSize>17</AverageFieldSize>
<Selectivity>100.0000%</Selectivity>
</Property>
<Property name="%%ID">
<AverageFieldSize>3</AverageFieldSize>
<Selectivity>1</Selectivity>
</Property>
<Property name="Age">
<AverageFieldSize>1.85</AverageFieldSize>
<Selectivity>1.5873%</Selectivity>
</Property>
<Property name="Company">
<AverageFieldSize>1.45</AverageFieldSize>
<Selectivity>5.0000%</Selectivity>
</Property>
<Property name="DOB">
<AverageFieldSize>5</AverageFieldSize>
<Selectivity>1.0000%</Selectivity>
</Property>
<Property name="FavoriteColors">
<AverageFieldSize>5.81</AverageFieldSize>
<OutlierSelectivity>.39:</OutlierSelectivity>
<Selectivity>2.2593%</Selectivity>
</Property>
<Property name="Home">
<AverageFieldSize>36.56,City:7.66,State:2,Street:16.5,Zip:5</AverageFieldSize>
<Selectivity>1.0000%,City:3.8462%,State:2.4390%,Street:1.0000%,Zip:1.0000%</Selectivity>
</Property>
<Property name="Name">
<AverageFieldSize>15.92</AverageFieldSize>
<Selectivity>1.0000%</Selectivity>
</Property>
<Property name="Notes">
<Selectivity>100.0000%</Selectivity>
</Property>
<Property name="Office">
<AverageFieldSize>36.83,City:7.22,State:2,Street:17.19,Zip:5</AverageFieldSize>
<Selectivity>1.0000%,City:4.0000%,State:2.1739%,Street:1.0000%,Zip:1.0000%</Selectivity>
</Property>
<Property name="Picture">
<Selectivity>100.0000%</Selectivity>
</Property>
<Property name="SSN">
<AverageFieldSize>11</AverageFieldSize>
<Selectivity>1</Selectivity>
</Property>
<Property name="Salary">
<AverageFieldSize>4.91</AverageFieldSize>
<Selectivity>1.0000%</Selectivity>
</Property>
<Property name="Spouse">
<AverageFieldSize>1.89</AverageFieldSize>
<Selectivity>1.5873%</Selectivity>
</Property>
<Property name="Title">
<AverageFieldSize>21.36</AverageFieldSize>
<Selectivity>1.5385%</Selectivity>
</Property>
<SQLMap name="$Employee">
<BlockCount>-4</BlockCount>
</SQLMap>
<Type>%Library.CacheStorage</Type>
}
}