第九章 Caché 定义持久类
定义持久类
要定义定义持久对象的类,请确保类的主类(第一个)父类是%persistent或其他某个持久类。例如:
Class User.MyClass Extends %Persistent
{
}
将包映射到架构:
对于持久类,包在SQL中表示为SQL模式。例如,如果一个类被称为Team.Player(“Team”包中的Player类),则对应的表是“Team.Player”(在“Team”架构中的Player表)。
默认包是“User”,它在SQL中表示为“SQLUser”模式。因此,一个名为User.Person的类对应于一个名为SQLUser.Person的表。
如果包名包含句点,则相应的表名在每个句点的位置使用下划线。例如,类MyTest.Test.MyClass(“MyTest.Test”包中的MyClass类)成为表MyTest_Test.MyClass(“MyTest_Test”架构中的MyClass表)。
如果引用的SQL表名没有模式名,则使用默认的模式名(SQLUser)。例如,命令:
Select ID, Name from Person
同:
Select ID, Name from SQLUser.Person
为持久类指定表名
对于持久类,默认情况下,短类名变为表名。要指定不同的表名,请使用SqlTableName类关键字。例如:
Class User.MyClass Extends %Persistent [ SqlTableName = My_Class ]
{
Storage Default
{
<Data name="MyClassDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
</Data>
<DataLocation>^User.MyClassD</DataLocation>
<DefaultData>MyClassDefaultData</DefaultData>
<IdLocation>^User.MyClassD</IdLocation>
<IndexLocation>^User.MyClassI</IndexLocation>
<StreamLocation>^User.MyClassS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}
}
尽管Caché对类名没有限制,但是SQL表不能有SQL保留字的名称。因此,如果创建一个名称为保留字的持久类,Caché class编译器将生成一条错误消息。在这种情况下,必须重命名类或为映射指定与类名不同的表名。
存储定义和存储类
%Persistent类提供用于在数据库中存储和检索对象的高级接口。
每个持久对象和每个序列对象都使用一个存储类来生成用于在数据库中存储、加载和删除对象的实际方法。这些内部方法称为存储接口。存储接口包括%LoadData()、%SaveData()和%DeleteData()等方法。应用程序从不直接调用这些方法;而是在适当的时间由持久性接口的方法(如%OpenId()和%Save())调用。
持久类使用的存储类由存储定义指定。存储定义包含一组关键字和值,这些关键字和值定义存储类以及存储接口使用的其他参数。
持久类可以包含多个存储定义,但一次只能有一个处于活动状态。
使用类的StorageStrategy关键字指定活动存储定义。默认情况下,持久类有一个称为“default”的存储定义。
更新存储定义
类的存储定义是在首次编译该类时创建的。类映射(如对于SQL或多值)在编译后发生。如果类编译正确,然后映射失败,Caché不会删除存储定义。此外,如果类的更改方式可能会影响存储定义,则应用程序开发人员有责任确定存储定义是否已更新,并在必要时修改存储定义以反映更改。
%CacheStorage存储类 %CacheStorage Storage
%CacheStorage是持久对象使用的默认存储类。它自动创建并维护持久类的默认存储结构。
新的持久类自动使用%CacheStorage存储类.%CacheStorage类允许通过存储定义中的各种关键字来控制用于类的存储结构的某些方面。
%CacheSQLStorage存储类 %CacheSQLStorage Storage
%CacheSQLStorage类是一个特殊的存储类,它使用生成的SQL SELECT、INSERT、UPDATE和DELETE语句来提供对象持久性。
%CacheSQLStorage通常用于:
- 将对象映射到旧应用程序使用的现有全局结构。
- 使用SQL网关在外部关系数据库中存储对象。
%CacheSQLStorage的限制大于%CacheStorage。具体来说,它不自动支持模式演化或多类扩展。
注意:也就不是支持Extent模式
架构进化
%CacheStorage存储类支持自动架构演化。
编译使用默认%CacheStorage存储类的持久(或序列)类时,类编译器将分析该类定义的属性,并自动添加或删除它们。
如果希望看到模式的演进,请尝试以下操作:
- 启动CachéStudio并创建一个包含一个或多个属性的新持久类。
- 编译该类,然后在整个类定义中查看该类自动生成的存储定义(作为XML文本)。或者,可以使用类检查器查看存储的更图形化表示。单击Inspector中的存储,单击存储定义列表中的“默认”,单击关键字列表中的数据节点,然后单击出现的浏览按钮(…)。这将调用图形存储编辑器。
在为类生成的存储中,将看到伪属性%%CLASSNAME。这是一个占位符,用于表示将来可能从类派生的任何子类的类名,并用于告诉存储在数据库中的对象类型。对于数据块的根类,此值始终为空。
- 向类中添加一个或多个新属性并再次编译它。请注意,这些新属性是自动添加到存储定义中的,并且是以与以前的存储兼容的方式添加的。
重置存储定义
在开发过程中,可以对持久类进行许多修改:添加、修改和删除属性。因此,当类编译器试图维护兼容的结构时,可能会得到一个相当复杂的存储定义。如果希望类编译器生成干净的存储结构,请删除存储定义并重新编译该类。
可以执行以下操作:
- 在Caché Studio打开类
- 右键单击类检查器中的默认存储定义。
- 在弹出菜单中调用Delete命令。
-
编译类。这将导致类编译器为类生成新的存储定义。
如何控制生成ID
第一次保存对象时,Caché会为该对象生成一个ID。身份证是永久的。
默认情况下,Caché使用一个整数作为ID,从上次保存的对象开始递增1。
可以定义一个给定的持久类,以便它以以下任一方式生成ID:
- 如果每个实例的属性都是唯一的,则ID可以基于类的特定属性。例如,可以使用药品代码作为ID。若要以这种方式定义类,请在类中添加如下索引:
Index IndexName On PropertyName [ IdKey ];
相当于
Index IndexName On PropertyName [ IdKey, Unique ];
其中IndexName是索引的名称,PropertyName是属性的名称。
如果这样定义类,当Caché首次保存对象时,它将使用该属性的值作为ID。此外,Caché需要属性的值并强制该属性的唯一性。如果为指定的属性创建另一个具有相同值的对象,然后尝试保存新对象,则Caché会发出以下错误:
ERROR #5805: ID key not unique for extent
此外,Caché防止在将来更改该属性。也就是说,如果打开已保存的对象,更改属性值,并尝试保存已更改的对象,则Caché会发出以下错误:
ERROR #5814: Oid previously assigned
这个消息引用的是OID而不是ID,因为底层逻辑阻止了OID的更改;OID是基于ID的。
- ID可以基于多个属性。要以这种方式定义类,请向类添加如下索引:
Index IndexName On (PropertyName1,PropertyName2,...) [ IdKey, Unique ];
相当于
Index IndexName On (PropertyName1,PropertyName2,...) [ IdKey ];
其中IndexName是索引的名称,PropertyName1、PropertyName2等是属性名称。
如果这样定义类,当Caché第一次保存对象时,它会生成一个ID,如下所示:
PropertyName1||PropertyName2||...
此外,Caché要求属性的值,并强制给定属性组合的唯一性。阻止更改任何这些属性。
重要提示:如果文字属性(即属性)包含一对连续的竖线(||),则不要添加使用该属性的IdKey索引。这种限制是由CachéSQL机制的工作方式强加的。在IdKey属性中使用| |会导致不可预测的行为。
Caché也会产生一个OID。在所有情况下,OID都有以下形式:
$LISTBUILD(ID,Classname)
其中ID是生成的ID,Classname是类的名称。
Class User.MyClass Extends %Persistent [ SqlTableName = My_Class ]
{
Index IndexName On (PropertyName, PropertyName1) [ IdKey ];
Property PropertyName As %String(MAXLEN = 50);
Property PropertyName1 As %String(MAXLEN = 50);
Storage Default
{
<Data name="MyClassDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
</Data>
<DataLocation>^User.MyClassD</DataLocation>
<DefaultData>MyClassDefaultData</DefaultData>
<IdLocation>^User.MyClassD</IdLocation>
<IndexLocation>^User.MyClassI</IndexLocation>
<StreamLocation>^User.MyClassS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}
}
/// d ##class(PHA.OP.MOB.Test).AddMyClass()
ClassMethod AddMyClass(WebSocketID)
{
s one=##class(User.MyClass).%New()
s one.PropertyName="2"
s one.PropertyName1="3"
s sc=one.%Save()
s one=##class(User.MyClass).%New()
s one.PropertyName="yaoxin"
s one.PropertyName1="test"
s sc=one.%Save()
w sc
}
控制子类的SQL映射
当几个持久类位于父类/子类层次结构中时,Caché可以用两种方式存储它们的数据。默认情况是目前最常见的。
子类的默认SQL映射
类编译器映射持久类的“flattened”表示,这样映射的表包含该类的所有适当字段,包括继承的字段。因此,对于子类,SQL映射是一个表,由以下内容组成:
- 所有列继承父类
- 仅基于子类中的属性的附加列
- 表示子类的已保存实例的行
此外,在默认场景中,超类的范围包含一条记录,用于超类及其所有子类的每个已保存对象。每个子类的范围是超类范围的子集。
例如,考虑持久类Sample.Person和Sample.Employee。Sample.Employee类继承自Sample.Person并添加一些附加属性。在示例中,两个类都保存了数据
- Sample.Person的SQL映射是一个包含Sample.Person类的所有适当属性的表。Sample.Person表包含Sample.Person类的每个已保存实例和Sample.Employee类的每个已保存实例的一条记录。
- Sample.Employee表包含与Sample.Person相同的列,还包含特定于Sample.Employee类的列。Sample.Employee表包含Sample.Employee类的每个已保存实例的一条记录。
要了解这一点,请使用以下SQL查询。第一个列表列出Sample.Person的所有实例并显示其属性:
SELECT * FROM Sample.Person
第二个查询列出Sample.Employee的所有实例及其属性:
SELECT * FROM Sample.Employee
注意:Sample.Person表包含id在1到5之间的记录。ID 2的记录是employees,Sample.Employee表显示相同的employees(具有相同的id和附加的列)。Sample.Person表被分为两个明显的“组”,这仅仅是因为SAMPLES数据库是以人工方式构建的。填充Sample.Person表,然后填充Sample.Employee表。
通常,子类的表比其父类有更多的列和更少的行。子类中有更多的列,因为它在扩展父类时通常会添加其他属性;通常行数较少,因为子类的实例通常比父类少。
子类的可选SQL映射
默认的映射是最方便的,但是有时,可能会发现有必要使用替代的SQL映射。在这个场景中,每个类都有自己的继承。要引用这种形式的映射,请在父类的定义中包含以下内容:
[ NoExtent ]
例如:
Class User.MyClass Extends %Persistent [ NoExtent, SqlTableName = My_Class ]
{
}
这个类的每个子类都接收自己的范围(extent继承)。
重新定义存储了数据的持久类
在开发过程中,重新定义类是很常见的。如果已经为类创建了示例数据,请注意以下几点:
- 编译器对存储类数据的全局变量没有影响。
事实上,当删除一个类定义时,它的数据全局变量是不变的。如果不再需要这些全局参数,请手动删除它们。
如果添加或删除类的属性,但不修改该类的存储定义,则访问该类数据的所有代码将继续像以前一样工作。
如果确实修改了类的存储定义,则访问数据的代码可能会继续工作,也可能不会继续工作,这取决于更改的性质。
如果修改属性定义的方式导致属性验证更加严格,则在处理不再通过验证的对象(或记录)时将收到错误。例如,如果减少某个属性的MAXLEN参数,则当处理的对象的值现在太长时,将收到验证错误。