%Persistent类是可以保存(写入磁盘)的对象的API。本章描述了如何使用这个API。本章中的信息适用于%Persistent的所有子类。
若要将对象保存到数据库,请调用其%save()方法。例如:
/// d ##class(PHA.OP.MOB.Test).TestSaveObject()
ClassMethod TestSaveObject()
{
Set obj = ##class(Sample.Person).%New()
Set obj.Name = "姚鑫"
Set obj.SSN="111-11-1112"
Set sc = obj.%Save()
q sc
}
DHC-APP> w ##class(PHA.OP.MOB.Test).TestSaveObject()
1
方法 %Save()的返回的是:返回一个%Status值,该值指示保存操作是否成功。例如,如果对象具有无效的属性值或违反唯一性约束,则可能发生故障;
调用对象上的%Save()会自动保存所有被修改对象:也就是说,如果需要,所有嵌入的对象、集合、流、引用对象和涉及对象的关系都会自动保存。整个保存操作作为一个事务执行:如果任何对象保存失败,整个事务将失败并回滚(对磁盘不做任何更改;所有内存中的对象值都是调用%Save()之前的值。
当对象第一次被保存时,%Save()方法的默认行为是自动为其分配一个对象ID值,该值用于以后在数据库中查找对象。在默认情况下,使用$Increment函数生成ID;另外,类可以根据具有idkey索引的属性值使用用户提供的对象ID(在本例中,属性值不能包含字符串“||”)。一旦赋值,就不能更改特定对象实例的对象ID值(即使它是用户提供的ID)。
可以使用% ID()方法找到保存对象的对象ID值:
ClassMethod TestSaveObjectOpen()
{
Set person = ##class(Sample.Person).%OpenId(6)
Write "Object ID: ",person.%Id(),!
q ""
}
DHC-APP>w ##class(PHA.OP.MOB.Test).TestSaveObjectOpen()
Object ID: 6
更详细地说,%Save()方法执行以下操作:
当将每个对象添加到SaveSet时,如果存在,将调用它的%OnAddToSaveSet()回调方法。
这些回调传递一个Insert参数,该参数指示对象是被插入(第一次保存)还是被更新。
如果其中一个回调方法失败(返回一个错误代码),那么对%Save()的调用将失败,并且当前事务将回滚。
如果当前对象未修改,则%Save()不将其写入磁盘;它返回成功,因为对象不需要保存,因此,不可能存在保存失败。事实上,%Save()的返回值表明,保存操作要么完成了请求的所有操作,要么无法完成请求的所有操作(而且不明确是否向磁盘写入任何内容)。
重要:在多进程环境中,一定要使用适当的并发控制;
%Save()方法自动将其SaveSet中的所有对象保存为单个事务。如果这些对象中的任何一个保存失败,那么整个事务将回滚。在这个回滚,Caché 做以下工作:
如前所述,%Save()方法自动将其SaveSet中的所有对象保存为单个事务。如果这些对象中的任何一个保存失败,那么整个事务将回滚。但是,如果希望将两个或多个不相关的对象保存为单个事务,则必须将对%save()的调用封装在显式事务中:也就是说,必须使用TSTART命令启动事务,并使用TCOMMIT命令结束事务。
例如:
/// d ##class(PHA.OP.MOB.Test).TestSaveTransaction()
ClassMethod TestSaveTransaction()
{
// start a transaction
TSTART
Set obj = ##class(Sample.Person).%New()
Set obj.Name = "姚鑫"
Set obj.SSN="111-11-1113"
Set sc = obj.%Save()
w sc,!
// save second object (if first was save)
If ($$$ISOK(sc)) {
Set obj2 = ##class(Sample.Person).%New()
Set obj2.Name = "姚鑫"
Set obj2.SSN="111-11-1114"
Set sc = obj2.%Save()
}
w sc,!
// if both saves are ok, commit the transaction
If ($$$ISOK(sc)) {
TCOMMIT
}
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestSaveTransaction()
1
1
再次调用
DHC-APP>d ##class(PHA.OP.MOB.Test).TestSaveTransaction()
0 °?Sample.Person:SSNKey:^Sample.PersonI("SSNKey"," 111-11-1113")??%SaveData+18^Sample.Person.1 DHC-APP??"e^%SaveData+18^Sample.Person.1^7(e^%SerializeObject+9^Sample.Person.1^2e^%Save+8^Sample.Person.1^5.e^zTestSaveTransaction+6^PHA.OP.MOB.Test.1^1d^^^0
0 °?Sample.Person:SSNKey:^Sample.PersonI("SSNKey"," 111-11-1113")??%SaveData+18^Sample.Person.1 DHC-APP??"e^%SaveData+18^Sample.Person.1^7(e^%SerializeObject+9^Sample.Person.1^2e^%Save+8^Sample.Person.1^5.e^zTestSaveTransaction+6^PHA.OP.MOB.Test.1^1d^^^0
关于这个例子有两点需要注意:
TL1:DHC-APP>w $tlevel
1
如果事务中的%Save()方法失败,则回滚整个事务(调用TROLLBACK命令)。这意味着应用程序必须测试显式事务中对%Save()的每个调用,如果一个调用失败,则跳过对其他对象调用%Save(),并跳过调用最后的TCOMMIT命令。
有两种基本的方法来测试一个特定的对象实例是否存储在数据库中:
在这些示例中,ID是一个整数,默认情况下,Caché 就是这样生成ID的.下一章将介绍如何定义一个类,使ID基于对象的惟一属性。
%ExistsId()类方法检查指定的ID;如果指定的对象存在于数据库中,则返回true值(1),否则返回false(0)。所有从%Persistent继承的类都可以使用它。例如:
/// d ##class(PHA.OP.MOB.Test).TestObjectScriptExists()
ClassMethod TestObjectScriptExists()
{
Write ##class(Sample.Person).%ExistsId(1),! // should be 1
Write ##class(Sample.Person).%ExistsId(-1),! // should be 0
}
TL1:DHC-APP>d ##class(PHA.OP.MOB.Test).TestObjectScriptExists()
1
0
这里,第一行应该返回1,因为是Sample.Person从%Persistent继承,SAMPLES数据库为该类提供数据。
还可以使用%Exists()方法,该方法需要一个OID而不是ID。
要测试使用SQL保存的对象是否存在,可以使用SELECT语句来选择其%ID字段与给定ID匹配的行。(已保存对象的标识属性被投影为%ID字段。)
例如,使用嵌入式SQL:
/// d ##class(PHA.OP.MOB.Test).TestSQLExists()
ClassMethod TestSQLExists()
{
&sql(SELECT %ID FROM Sample.Person WHERE %ID = '1')
Write SQLCODE,!
&sql(SELECT %ID FROM Sample.Person WHERE %ID = '-1')
Write SQLCODE,!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestSQLExists()
0
100
在这里,第一种情况结果SQLCODE为0(表示成功),因为Sample.Person从%Persistent继承,sample数据库为该类提供数据。第二种情况结果SQLCODE为100,这意味着语句成功执行,但是没有返回任何数据。符合预期想象,因为系统不会自动生成小于零的ID值。
要打开一个对象(将对象实例从磁盘加载到内存中),使用%OpenId()方法,方法如下:
classmethod %OpenId(id As %String, concurrency As %Integer = -1, ByRef sc As %Status = $$$OK) as %ObjectHandle
如果方法可以打开给定对象,则返回OREF。如果无法找到或以其他方式打开对象,则返回空值("")。
例如:
/// d ##class(PHA.OP.MOB.Test).TestOpeningSavedObjects()
ClassMethod TestOpeningSavedObjects()
{
Set person = ##class(Sample.Person).%OpenId(10)
Write "Person: ",person,!
Set person = ##class(Sample.Person).%OpenId(-10)
Write "Person: ",person,!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestOpeningSavedObjects()
Person: [email protected]
Person:
注意,在Cache Basic中,OpenId命令相当于%OpenId()方法:
/// d ##class(PHA.OP.MOB.Test).TestOpeningSavedObjectsBasic()
ClassMethod TestOpeningSavedObjectsBasic() [ Language = basic ]
{
person = OpenId Sample.Person(1)
PrintLn "Name: " & person.Name
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestOpeningSavedObjectsBasic()
Name: yaoxin
还可以使用%Open()方法,该方法需要一个OID而不是ID。
如果在Caché 进程中多次调用相同ID的%OpenId(),那么在内存中只创建一个对象实例:所有对%OpenId()的后续调用都将返回对已加载到内存中的对象的引用。
下面的例子说明了这一点:
/// d ##class(PHA.OP.MOB.Test).TestMultiple()
ClassMethod TestMultiple() [ Language = basic ]
{
personA = OpenId Sample.Person(1)
personA.Name = "Black,Jimmy Carl"
personB = OpenId Sample.Person(1)
PrintLn "NameA: " & personA.Name
PrintLn "NameB: " & personB.Name
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestMultiple()
NameA: Black,Jimmy Carl
NameB: Black,Jimmy Carl
方法的输入是一个可选的并发参数。此参数指定用于打开对象实例的并发级别(锁的类型)。
如果%OpenId()方法无法获取对象上的锁,则会失败。
若要提高或降低对象的当前并发设置,请使用%OpenId()重新打开该对象并指定不同的并发级别。例如,
Set person = ##class(Sample.Person).%OpenId(6,0)
打开0的并发和以下有效地升级到4的并发:
Set person = ##class(Sample.Person).%OpenId(6,4)
如果打开(加载到内存中)一个持久对象的实例,并使用它引用的对象,那么这个引用的对象将被自动打开。这个过程被称为swizzling;它有时也被称为“懒加载”。
例如,下面的代码打开Sample.Employee一个实例,并自动懒加载相关Sample.Company对象引用它使用点语:
/// d ##class(PHA.OP.MOB.Test).TestSwizzling()
ClassMethod TestSwizzling()
{
// Open employee "101"
Set emp = ##class(Sample.Employee).%OpenId(101)
// Automatically open Sample.Company by referring to it:
Write "Company: ",emp.Company.Name,!
}
当一个对象被swizzled时,它将使用默认的原子读并发值打开。
当没有对象或变量引用swizzled对象时,它就从内存中删除。
注意:所谓懒加载,就是对象引用。
要用存储在数据库中的值重新加载内存中的对象,请调用它的%reload()方法。
/// d ##class(PHA.OP.MOB.Test).TestReload()
ClassMethod TestReload()
{
// Open person "1"
Set person = ##class(Sample.Person).%OpenId(1)
Write "Original value: ",person.Name,!
// 修改对象
Set person.Name = "Black,Jimmy Carl"
Write "Modified value: ",person.Name,!
// 重新加载对象
Do person.%Reload()
Write "Reloaded value: ",person.Name,!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestReload()
Original value: yaoxin
Modified value: Black,Jimmy Carl
Reloaded value: yaoxin
假设打开了一个持久对象的实例,修改了它的属性,然后希望在保存对象之前查看数据库中存储的原始值。最简单的方法是使用SQL语句(SQL总是针对数据库执行;而不是针对内存中的对象)。
例如:
/// d ##class(PHA.OP.MOB.Test).TestReading()
ClassMethod TestReading()
{
// Open person "1"
Set person = ##class(Sample.Person).%OpenId(1)
Write "Original value: ",person.Name,!
// 修改对象
Set person.Name = "Black,Jimmy Carl"
Write "Modified value: ",person.Name,!
// 重新加载对象
Set id = person.%Id()
&sql(SELECT Name INTO :name
FROM Sample.Person WHERE %ID = :id)
Write "Disk value: ",name,!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestReading()
Original value: yaoxin
Modified value: Black,Jimmy Carl
Disk value: yaoxin
持久性接口包括从数据库中删除对象的方法。
方法的作用是:删除存储在数据库中的对象。具体方法如下:
classmethod %DeleteId(id As %String, concurrency As %Integer = -1) as %Status
例如:
Set sc = ##class(MyApp.MyClass).%DeleteId(id)
%DeleteId()返回一个%Status值,该值指示对象是否被删除。
%DeleteId()在删除对象之前调用%OnDelete()回调方法(如果存在)。%OnDelete()返回一个%Status值;如果%OnDelete()返回一个错误值,那么对象将不会被删除,当前事务将回滚,%DeleteId()返回一个错误值。
注意,%DeleteId()方法对内存中的任何对象实例都没有影响。
您还可以使用%Delete()方法,该方法需要OID而不是ID。
%DeleteExtent()方法:删除继承内的所有对象(以及对象的子类)。具体来说,它遍历整个继承并在每个实例上调用%DeleteId()方法。
%KillExtent()方法直接删除存储对象范围的全局变量(不包括与流关联的数据)。它不调用%DeleteId()方法,也不执行引用完整性操作。此方法只是为了在开发过程中为开发人员提供帮助。(它类似于在旧的关系数据库产品中找到的TRUNCATE表命令。)
注意:%KillExtent()仅用于开发环境,不应在实际应用程序中使用。%KillExtent()绕过约束和用户实现的回调,可能会导致数据完整性问题。
如果保存了对象,那么它有一个ID和一个OID,这是磁盘上使用的永久标识符。如果对象有一个OREF,则可以使用它来获得这些标识符。
要查找与OREF关联的ID,请调用对象的%ID()方法。例如:
write oref.%Id()
要查找与OREF关联的OID,有两个选项:
write oref.%Oid()
write oref."%%OID"
/// d ##class(PHA.OP.MOB.Test).TestIdentifiers()
ClassMethod TestIdentifiers()
{
// Open person "1"
Set person = ##class(Sample.Person).%OpenId(1)
Write "Original value: ",person.Name,!
Write person.%Id(),!
Write person.%Oid(),!
Write person."%%OID",!
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestIdentifiers()
Original value: yaoxin
1
1Sample.Person
1Sample.Person
zTestIdentifiers+7^PHA.OP.MOB.Test.1
DHC-APP 2d1>zw person.%Oid()
$lb("1","Sample.Person")
DHC-APP 2d1>zw person."%%OID"
$lb("1","Sample.Person")
DHC-APP 2d1>
在打开或删除对象时,适当地指定并发性非常重要。可以在几个不同的级别指定并发性:
%Persistent类的许多方法都允许您指定这个参数,一个整数。此参数确定如何将锁用于并发控制。如果未指定并发参数,则Caché 将使用正在处理的类的DEFAULTCONCURRENCY类参数的值;
可以在类中重写此参数,并指定硬编码的值或表达式,通过自己的规则来确定并发性
与其他情况一样,必须使用一个允许的并发值。$system.OBJ.SetConcurrencyMode() 方法对为DEFAULTCONCURRENCY类参数指定显式值的任何类无效。
下面的场景演示了在读取或写入对象时适当控制并发性的重要性。考虑以下场景:
SAMPLES>set o=##class(Sample.Person).%OpenId(1)
SAMPLES>w o
[email protected]
SAMPLES>set sc=##class(Sample.Person).%DeleteId(5)
SAMPLES>w sc
1
SAMPLES>set sc=o.%Save()
SAMPLES>w sc
1
检查磁盘上的数据表明,对象没有写入磁盘。
这是一个没有足够并发控制的并发操作的例子。例如,如果进程A可能将该对象保存回磁盘,那么它应该以并发3或4打开该对象(这些值将在本章后面讨论)。在这种情况下,进程B将被拒绝访问(并发冲突导致失败),或者必须等待,直到进程a释放该对象。
可能的并发值如下:
对于%LoadData(),Caché 获取对象上的共享锁(如果需要确保原子读)。在完成读操作后释放锁。
没有为新对象获取锁。
%SaveData()在保存过程中获得一个独占锁。
对于%LoadData(),Caché 在所有情况下都获取对象上的共享锁。在完成读操作后释放锁。
没有为新对象获取锁。
%SaveData()在保存过程中获得一个独占锁。
最初不会为新对象获取锁,但在保存期间会获取锁;
%SaveData()获取任何对象的独占锁,包括新对象。这个锁将一直保持到对象被析构(从内存中删除)。
注意:
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
我们在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)
属性引用的对象在访问时使用默认并发性进行swizzle。(如果正在swizzle的对象已经在内存中,那么swizzle实际上不会打开该对象—它只是引用内存中现有的对象;在这种情况下,对象的当前状态将保持不变,并发性也将保持不变。)
例如,如果一个对象(Obj1)有一个引用另一个对象(Obj2)的属性(Prop1),那么在访问Prop1时,Obj2将被swizzle;Obj2具有默认并发性,除非它已经打开。
有两种方法可以覆盖此默认行为:
Do person.Spouse.%Open(person.Spouse.%Oid(),4,.status)
其中%Open()的第一个参数指定OID,第二个参数指定新的并发性,第三个参数(通过引用传递)接收方法的状态。
在刷对象之前设置进程的默认并发性。例如:
Set olddefault = $system.OBJ.SetConcurrencyMode(4)
此方法以新的并发模式作为参数,并返回以前的并发模式。
当不再需要不同的并发模式时,请按以下方式重置默认并发模式:
Do $system.OBJ.SetConcurrencyMode(olddefault)
在打开或删除对象时,可以实现版本检查,而不是指定并发参数。为此,需要指定一个名为VERSIONPROPERTY的类参数。所有持久类都有这个参数。定义持久类时,启用版本检查的过程是:
这将版本检查合并到类实例的更新中。
实现版本检查时,每次更新类的实例(通过对象或SQL)时,由version property指定的属性都会自动递增。在递增属性之前,Caché将其内存值与存储值进行比较。如果它们不同,则指示并发冲突并返回错误;如果它们相同,则属性将递增并保存。
注意:可以使用这组特性来实现乐观并发。
若要对VERSIONPROPERTY引用名为InstanceVersion的属性的类在SQL update语句中实现并发检查,代码如下:
SELECT InstanceVersion,Name,SpecialRelevantField,%ID
FROM my_table
WHERE %ID = :myid
// Application performs operations on the selected row
UPDATE my_table
SET SpecialRelevantField = :newMoreSpecialValue
WHERE %ID = :myid AND InstanceVersion = :myversion
其中myversion是用原始数据选择的version属性的值。
重要:如果从第二个对象引用具有version property参数的对象,并且第二个对象的保存失败,则默认情况下第一个对象的version属性将递增。你的代码应该检查这个场景,并适当地处理它。特别是,如果无法保存第二个对象,则代码应将第一个对象的version属性还原为其先前的值。否则,当保存第一个对象时,将收到以下错误:
ERROR #5800: Concurrency failure on update: object versions not the same for 'ConcurrencyFail.ClassA:1'