第十四章 Caché 定义和使用关系

本章介绍关系,这是一种特殊的属性,只能在持久性类中定义。

关系概述

关系是两个持久性对象之间的关联,每个持久性对象都是特定的类型。要在两个对象之间创建关系,每个对象都必须具有一个Relationship属性,该属性定义其一半的关系。Caché直接支持两种关系:一对多和父子关系。

Caché关系具有以下特征:

  • 关系是在两个(只有两个)类之间或在一个类与自身之间定义了一个关系。
  • 关系只能为持久性类定义。
  • 关系是双向的-必须定义关系的双方。
  • 关系自动提供参照完整性。在SQL中作为外键可见。
  • 关系会自动管理其内存中和磁盘上的行为。
  • 与对象集合相比,关系提供了出色的扩展性和并发性。

注意:也可以在持久类之间定义外键,而不是添加关系。使用外键,可以更好地控制添加,更新或删除一个类中的对象时发生的情况。

一对多关系

在类A和类B之间的一对多关系中,类A的一个实例与类B的零个或多个实例相关联。

例如,company 类别可以定义与 employee 类别的一对多关系。在这种情况下,与每个company对象关联的employee对象可能为零个或多个。

这些类彼此独立,如下所示:

  • 创建任何一个类的实例时,它可能与另一个类的实例关联或不关联。
  • 如果类B的实例与类A的给定实例相关联,则可以删除或更改此关联。B类的实例可以与A类的另一个实例关联。B类的实例不必与A类的实例有任何关联(反之亦然)。

一个类中可能存在一对多关系。该类的一个实例可以与该类的零个或多个其他实例相关联。

主子关系

在类A和类B之间的父子关系中,类A的一个实例与类B的零个或多个实例相关联。而且,子表依赖于父表,如下所示:

  • 保存类B的实例时,必须将其与类A的实例关联。如果尝试保存实例,但未定义该关联,则保存操作将失败。
  • 关联不能更改。即,不能将类B的实例与类A的其他实例相关联。
  • 如果删除了类A的实例,则也删除了所有与类B相关的实例。
  • 可以删除类B的实例。不需要类A具有关联的类B实例。

例如,发票类可以定义与订单项类的父子关系。在这种情况下,发票包含零个或多个订单项。这些订单项无法移至其他发票。它们本身也没有意义。

重要说明:此外,在子表(B类)中,标识也不是纯数字。结果,尽管允许其他形式的索引(并且很有用,如本章稍后部分所述),但是无法在此类中的关系属性中添加位图索引。

主子关系和储存

如果在编译类之前定义了父子关系,则两个类的数据都存储在相同的全局变量中。子级的数据从属于父级的数据,其结构类似于以下内容:

^Inv(1)
^Inv(1, "invoice", 1)
^Inv(1, "invoice", 2)
^Inv(1, "invoice", 3)
...

结果,Caché可以更快地读取和写入这些相关对象。

共同关系术语

本节举例说明在讨论关系时为方便起见常用的短语。

考虑公司与其员工之间的一对多关系;也就是说,一家公司有多名员工。在这种情况下,公司被称为 “一”,而员工被称为“多”。

同样,考虑公司与其产品之间的父子关系;也就是说,公司是parent,产品是children。在这种情况下,公司称为 “主表”,而雇员称为 “子表”。

定义关系

要在两个类的记录之间创建关系,请创建一对互补的关系属性,每个类中一个。要在同一类的记录之间创建关系,请在该类中创建一对互补的关系属性。

Studio提供了一个方便的向导(“新属性向导”),可以简化此任务。

以下各节描述了常规语法,然后讨论了如何定义一对多关系和父子关系。

一般语法

关系属性的语法如下:

Relationship Name As classname [ Cardinality = cardinality_type, Inverse = inverseProp ];
  • classname 是此关系引用的类。这必须是一个持久类。
  • cardinality_type(必填)定义该关系如何从这一方面“出现”,以及它是独立关系(一对多)还是从属关系(父子关系)。

inverseProp(必需)是互补关系属性的名称,该属性在另一个类中定义。

在互补关系属性中,cardinality_type关键字在此处必须是cardinality_type关键字的补充。一个和多个值是彼此的补充。同样,父和子的值是互补的。

注意:一对多或父对子,只能成对出现

由于关系是一种属性,因此可以在其中使用其他属性关键字,包括Final,Required,SqlFieldName和Private。某些属性关键字(例如MultiDimensional)不适用。

定义一对多关系

本节描述如何定义classA和classB之间的一对多关系,其中classA的一个实例与零个或多个classB的实例相关联。

注意:单个类的记录之间可能存在一对多关系。也就是说,在以下讨论中,classA和classB可以是同一类。

类A必须具有以下形式的关系属性:

Relationship manyProp As classB [ Cardinality = many, Inverse = oneProp ];

其中:oneProp是互补关系属性的名称,该属性在classB中定义。

B类必须具有以下形式的关系属性:

Relationship oneProp As classA [ Cardinality = one, Inverse = manyProp ];

其中manyProp是互补关系属性的名称,该属性在classA中定义。

重要提示:在"一"(类A),该关系使用查询来填充关系对象。通过在互补关系属性上添加索引(即,在多端,类B)添加索引,可以在几乎所有情况下提高此查询的性能。

定义父子关系

本节描述如何定义classA和classB之间的父子关系,其中classA的一个实例是classB的零个或多个实例的父级。这些不能是同一类。

A类必须具有以下形式的关系属性:

Relationship childProp As classB [ Cardinality = children, Inverse = parentProp ];

其中parentProp是互补关系属性的名称,该属性在classB中定义。
B类必须具有以下形式的关系属性:

Relationship parentProp As classA [ Cardinality = parent, Inverse = childProp ];

其中childProp是互补关系属性的名称,该属性在classBA类中定义。

重要提示:在"父"(类A)上,该关系使用查询来填充关系对象。通过在互补关系属性上添加索引(即,在子级B类上添加索引),可以在几乎所有情况下提高此查询的性能。

父子关系和编辑。

对于父子关系,Caché可以生成一个存储定义,该存储定义将单个对象中的父对象和子对象的数据存储在单个全局中,如先前所示。这样的存储定义提高了访问这些相关对象的速度。

如果在编译类后添加关系,则Caché不会生成此优化的存储定义。在这种情况下,可以删除任何可能拥有的测试数据,删除两个类的存储定义,然后重新编译。

示例

本节介绍一对多关系和父子关系的示例。

一对多关系示例

此示例表示公司与其员工之间的一对多关系。Company类别如下:

Class MyApp.Company Extends %Persistent
{

Property Name As %String;

Property Location As %String;

Relationship Employees As MyApp.Employee [ Cardinality = many, Inverse = Employer ];

}

Employee类别如下:

Class MyApp.Employee Extends (%Persistent, %Populate)
{

Property FirstName As %String;

Property LastName As %String;

Relationship Employer As MyApp.Company [ Cardinality = one, Inverse = Employees ];

Index EmployerIndex On Employer;

}
主子关系示例

此示例表示发票及其订单项之间的一对多关系。invoice类别如下:

Class MyApp.Invoice Extends %Persistent
{

Property Buyer As %String;

Property InvoiceDate As %TimeStamp;

Relationship LineItems As MyApp.LineItem [ Cardinality = children, Inverse = Invoice ];

}

line item项类如下:

Class MyApp.LineItem Extends %Persistent
{

Property ProductSKU As %String;

Property UnitPrice As %Numeric;

Relationship Invoice As MyApp.Invoice [ Cardinality = parent, Inverse = LineItems ];

Index InvoiceIndex On Invoice;

}
连接对象

关系是双向的。具体来说,如果更新一个对象中的关系属性的值,则将立即影响相关对象中相应关系属性的值。因此,可以在一个对象中指定Relationship属性的值,并且效果将同时出现在两个对象中。

由于两个类中的关系属性的性质不同,因此存在两种更新任何关系的方案:

  • 方案1:Relationship属性是一个简单的引用属性。将属性设置为等于适当的对象。
  • 方案2:Relationship属性是%RelationshipObject的实例,它具有类似数组的接口。使用该接口的方法将对象插入关系中。

以下小节提供了详细信息。第三小节介绍了方案1的一种变体,它在关系中包含大量对象时特别适用。

此处的信息描述了如何向关系中添加对象。修改对象的过程类似,但在父子关系的情况下(根据设计)有一个重要的例外:一旦与特定的父对象关联(然后保存),子对象就永远无法与其他父对象关联。

方案1:更新一对多

在“多”或“子”(ObjA)上,关系属性是指向ObjB的简单引用属性。要从这一侧连接对象:

  • 获取另一个类的实例的OREF(ObjB)。 (根据需要创建新对象或打开现有对象。)
  • 将ObjA的关系属性设置为等于ObjB。

例如,请考虑前面显示的示例父子类。以下步骤将从MyApp.LineItem端更新关系:

  // 获取invoice类的OREF
 set invoice=##class(MyApp.Invoice).%New()
 //...定发票日期等

 set item=##class(MyApp.LineItem).%New()
 //...设置此对象的某些属性,例如产品名称和销售价格...

 //连接对象
 set item.Invoice=invoice

当项目对象调用%Save()方法时,Caché将保存两个对象(项目和发票)。

方案2:更新父子关系

在子或父,关系属性是%RelationshipObject的实例。在这一方面,可以执行以下操作来连接对象:

  • 获取另一个对象的实例的OREF。 (根据需要创建新对象或打开现有对象。)
  • 调用此一侧的Relationship属性的Insert()方法,并将该OREF作为参数传递。

考虑前面显示的示例父子类。对于这些类,以下步骤将从MyApp.Invoice一侧更新关系:

 set invoice=##class(MyApp.Invoice).%OpenId(100034)
 //设置一些属性,例如客户名称和发票日期


 set item=##class(MyApp.LineItem).%New()
 //...设置此对象的某些属性,例如产品名称和销售价格...

 //连接对象
 do invoice.LineItems.Insert(item)

当为发票对象调用%Save()方法时,Caché会保存两个对象(项目和发票)。

重要说明:Caché不会保留有关将对象添加到关系中的顺序的信息。也就是说,如果打开以前保存的对象并使用GetNext()或类似方法来迭代关系,则该关系中对象的顺序与创建对象时的顺序不同。

连接对象的最快方法

当需要向关系中添加相对大量的对象时,请使用方案1中提供的技术的一种变体。在这种变体中:

  1. 获取A类的OREF(ObjA)。
  2. 获取ClassB实例的ID。
  3. 使用ObjA的关系属性的属性设置器方法,将ID作为参数传递。

如果关系属性名为MyRel,则属性设置器方法名为MyRelSetObjectId()。

请考虑场景1中描述的示例类。对于这些类,以下步骤将在发票中插入大量发票项目(并且比该部分中给出的技术执行得更快):

 set invoice=##class(MyApp.Invoice).%New()
 do invoice.%Save()
 set id=invoice.%Id()
 kill invoice  //OREF 不在需要
 
 for index = 1:1:(1000)
  {
    set Item=##class(MyApp.LineItem).%New()

    do Item.InvoiceSetObjectId(id)
    do Item.%Save()
  } 
删除关系

在一对多关系的情况下,可以删除两个对象之间的关系。一种方法如下:

  1. 打开子对象(或“多的”对象)的实例。
  2. 将此对象的适用属性设置为null。

例如,SAMPLES名称空间中的Sample.Company和Sample.Employee之间存在一对多关系。下面显示了ID为101的员工为ID为5的公司工作。请注意,该公司有四名员工:

SAMPLES>set e=##class(Sample.Employee).%OpenId(101)
 
SAMPLES>w e.Company.%Id()
5
SAMPLES>set c=##class(Sample.Company).%OpenId(5)
 
SAMPLES>w c.Employees.Count()
4

接下来,对于该员工,我们将Company属性设置为null。请注意,该公司现在有三名员工:

SAMPLES>set e.Company=""
 
SAMPLES>w c.Employees.Count()
3

也可以通过修改另一个对象来删除该关系。在这种情况下,我们使用collection属性的RemoveAt()方法。例如,以下示例说明对于ID为17的公司,第一位员工为员工ID 102:

SAMPLES>set e=##class(Sample.Employee).%OpenId(102)
 
SAMPLES>w e.Company.%Id()
17
SAMPLES>set c=##class(Sample.Company).%OpenId(17)
 
SAMPLES>w c.Employees.Count()
4
SAMPLES>w c.Employees.GetAt(1).%Id()
102

为了删除该company与该employee之间的关系,我们使用RemoveAt()方法(将值1传递为参数)来删除第一个集合项。请注意,这样做之后,该公司拥有三名员工:

SAMPLES>do c.Employees.RemoveAt(1)
 
SAMPLES>w c.Employees.Count()
3

在父子关系的情况下,不可能删除两个对象之间的关系。但是,可以删除子对象。
完整示例

/// d ##class(PHA.OP.MOB.Test).TestRemoveOneAndMany()
ClassMethod TestRemoveOneAndMany()
{
	set e=##class(Sample.Employee).%OpenId(150)
	w e.Company.%Id(),!
	;33
	set c=##class(Sample.Company).%OpenId(33)
	w c.Employees.Count(),!
	;1
	w c.Employees.GetAt(1).%Id(),!
	;150
	do c.Employees.RemoveAt(1)
	w c.Employees.Count(),!
	
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestRemoveOneAndMany()
33
1
150
0
 
删除对象关系

对于一对多关系,以下规则控制尝试删除对象时发生的情况:

  • 该关系阻止从"一“删除一个对象,如果在“一”有任何引用该对象的对象。例如,如果尝试删除公司,而雇员表中有指向该公司的记录,则删除操作将失败。

因此,有必要首先从“多”删除记录。

  • 这种关系不会阻止从多个方面(employee表)删除对象。

对于父子关系,规则不同:

  • 这种关系会导致父端的删除影响子端。具体来说,如果删除父侧的对象,则子侧的关联对象将自动删除。

例如,如果发票和订单项之间存在父子关系,则如果删除发票,则会删除其订单项。

  • 这种关系不会阻止删除子级上的对象。
使用关系

关系是属性。具有一个或多个基数的关系的行为类似于原子(非集合)引用属性。具有许多基数或子基数的关系是%RelationshipObject类的实例,该类具有类似数组的接口。

例如,可以通过以下方式使用上面定义的Company和Employee对象:

 // create a new instance of Company
 Set company = ##class(MyApp.Company).%New()
 Set company.Name = "Chiaroscuro LLC"

 // create a new instance of Employee
 Set emp = ##class(MyApp.Employee).%New()
 Set emp.LastName = "Weiss"
 Set emp.FirstName = "Melanie"

 // Now associate Employee with Company
 Set emp.Employer = company

 // Save the Company (this will save emp as well)
 Do company.%Save()

 // Close the newly created objects 
 Set company = ""
 Set emp = ""

关系在内存中是完全双向的;任何一侧的任何操作都立即在另一侧可见。因此,上面的代码等同于以下代码,而是在公司上运行:

 Do company.Employees.Insert(emp)

 Write emp.Employer.Name 
 // this will print out "Chiaroscuro LLC"

可以从磁盘加载关系并像使用其他任何属性一样使用它们。从"一"引用相关对象时,相关对象会自动以与引用(对象值)属性相同的方式插入到内存中。当从"多"个方面引用相关对象时,相关对象不会立即加载引用。而是创建一个临时%RelationshipObject集合对象。在此集合上调用任何方法后,它将立即建立一个包含关系中对象ID值的列表。只有当引用此集合中的一个对象时,实际的相关对象才引入内存中。

这是显示与特定Company相关的所有Employee对象的示例:

/// d ##class(PHA.OP.MOB.Test).TestGetNext()
ClassMethod TestGetNext()
{
 // open an instance of Company
 Set company = ##class(Sample.Company).%OpenId(10)

 // iterate over the employees; print their names
 Set key = ""

 Do {
    Set employee = company.Employees.GetNext(.key)
    If (employee '= "") {
        Write employee.Name,!
    }
 } While (key '= "")
}
DHC-APP>d ##class(PHA.OP.MOB.Test).TestGetNext()
Tweed,Al O.
Malkovich,Elmo D.
 

在此示例中,关闭公司将从内存中删除Company对象及其所有相关的Employee对象。但是请注意,该关系中包含的每个Employee对象都将在循环完成时进入内存。为了减少此操作使用的内存量(也许有数千个Employee对象),然后在显示名称后,通过调用%UnSwizzleAt()方法,修改循环以“取消显示” Employee对象:

/// d ##class(PHA.OP.MOB.Test).TestSwizzle()
ClassMethod TestSwizzle()
{
 // open an instance of Company
 Set company = ##class(Sample.Company).%OpenId(10)

 // iterate over the employees; print their names
 Set key = ""

 Do {
    Set employee = company.Employees.GetNext(.key)
    If (employee '= "") {
        Write employee.Name,!
        // remove employee from memory
        Do company.Employees.%UnSwizzleAt(key)
    }
 } While (key '= "")
}

DHC-APP> d ##class(PHA.OP.MOB.Test).TestSwizzle()
Tweed,Al O.
Malkovich,Elmo D.

重要:关系不支持列表接口。这意味着无法获得相关对象的数量,并且无法通过将指针从一(1)递增一(1)到相关对象的数量来迭代关系。相反,必须使用数组集合样式迭代。

SQL关系映射

如前面所述,一个持久化类被映射为一个SQL表。本节描述如何将此类的关系映射到SQL。

注意:尽管可以修改所涉及类的其他属性的映射,但无法修改关系本身的SQL映射。

一对多关系的SQL映射

本节描述一对多关系的SQL映射。作为示例,请考虑前面显示的示例一对多类。在这种情况下,类的映射如下:

  • “一”(即在Company类中),没有字段表示关系。Company表具有其他属性的字段,但没有保存employees的字段。
  • 在很"多"(即在employee类中),关系是一个简单的引用属性,并且以与其他引用属性相同的方式映射到SQL。employee表具有一个名为Employer的字段,该字段指向Company表。

要一起查询这些表,可以查询employee表并使用箭头语法,如以下示例所示:

SELECT Employer->Name, LastName,FirstName FROM MyApp.Employee

或者,可以执行显式联接,如以下示例所示:

SELECT c.Name, e.LastName, e.FirstName FROM MyApp.Company c, MyApp.Employee e WHERE e.Employer = c.ID 

同样,这对关系属性对employee表隐式添加了一个外键。外键的UPDATE和DELETE都指定为NOACTION。

父子关系的SQL映射

同样,请考虑前面显示的示例父子类,该子类在发票及其订单项之间具有父子关系。在这种情况下,类的映射如下:

  • 在父级(即在invoice类中),没有代表关系的字段。发票表具有用于其他属性的字段,但是没有用于保存订单项的字段。

  • 在子方面(即在line item项类中),该关系是一个简单的引用属性,并且以与其他引用属性相同的方式映射到SQL。订单项表具有一个名为“发票”的字段,该字段指向发票表。

  • 同样在子方面,即使明确尝试仅基于子项创建IDKey,这些ID也始终包含父记录的ID。另外,如果子类中IDKey的定义显式包括父关系,则编译器将识别出此关系,并且不会再次添加它。这使可以更改父引用在生成的全局引用中作为下标出现的顺序。

结果,尽管允许其他形式的索引,但无法向该属性添加位图索引。

要一起查询这些表,可以查询发票表并使用箭头语法,如以下示例所示:

SELECT 
Invoice->Buyer, Invoice->InvoiceDate, ID, ProductSKU, UnitPrice
FROM MyApp.LineItem

或者,您可以执行显式联接,如以下示例所示:

SELECT 
i.Buyer, i.InvoiceDate, l.ProductSKU,l.UnitPrice 
FROM MyApp.Invoice i, MyApp.LineItem l 
WHERE i.ID = l.Invoice 

另外,对于子类,映射表被“采用”为另一表的子表。

建立多对多关系

Caché不直接支持多对多关系,但是本节介绍如何间接建立这种关系的模型。

要在A类和B类之间建立多对多关系,请执行以下操作:

  1. 创建一个将定义每个关系的中间类。
  2. 定义该类和A类之间的一对多关系。
  3. 在该类和B类之间定义一对多关系。

然后,在中间类中为类A的实例和类B的实例之间的每个关系创建一条记录。

例如,假设A类定义了doctors;此类定义属性Name和Specialty。B类定义了patients;此类定义属性Name和Address。

为了模拟doctors 和patients之间的多对多关系,我们可以定义一个中间类,如下所示:

/// Bridge class between MN.Doctor and MN.Patient
Class MN.DoctorPatient Extends %Persistent
{

Relationship Doctor As MN.Doctor [ Cardinality = one, Inverse = Bridge ];

Index DoctorIndex On Doctor;

Relationship Patient As MN.Patient [ Cardinality = one, Inverse = Bridge ];

Index PatientIndex On Patient;
}

然后,Doctor类如下所示:

Class MN.Doctor Extends %Persistent
{

Property Name;

Property Specialty;

Relationship Bridge As MN.DoctorPatient [ Cardinality = many, Inverse = Doctor ];

}

Patient类如下所示:

Class MN.Patient Extends %Persistent
{

Property Name;

Property Address;

Relationship Bridge As MN.DoctorPatient [ Cardinality = many, Inverse = Patient ];

}

查询医生和患者的最简单方法是查询中间表。下面显示了一个示例:

SELECT top 20 Doctor->Name as Doctor, Doctor->Specialty, Patient->Name as Patient 
FROM MN.DoctorPatient order by doctor
 
Doctor  Specialty       Patient
Davis,Joshua M. Dermatologist   Wilson,Josephine J.
Davis,Joshua M. Dermatologist   LaRocca,William O.
Davis,Joshua M. Dermatologist   Dunlap,Joe K.
Davis,Joshua M. Dermatologist   Rotterman,Edward T.
Davis,Joshua M. Dermatologist   Gibbs,Keith W.
Davis,Joshua M. Dermatologist   Black,Charlotte P.
Davis,Joshua M. Dermatologist   Dunlap,Joe K.
Davis,Joshua M. Dermatologist   Rotterman,Edward T.
Li,Umberto R.   Internist       Smith,Wolfgang J.
Li,Umberto R.   Internist       Ulman,Mo O.
Li,Umberto R.   Internist       Gibbs,Keith W.
Li,Umberto R.   Internist       Dunlap,Joe K.
Quixote,William Q.      Surgeon Black,Charlotte P.
Quixote,William Q.      Surgeon LaRocca,William O.
Quixote,William Q.      Surgeon Black,Charlotte P.
Quixote,William Q.      Surgeon Smith,Wolfgang J.
Quixote,William Q.      Surgeon LaRocca,William O.
Quixote,William Q.      Surgeon LaRocca,William O.
Quixote,William Q.      Surgeon Black,Charlotte P.
Salm,Jocelyn Q. Allergist       Tsatsulin,Mark S.

作为一种变体,可以使用父子关系代替一对多关系之一。如本章前面所述,这提供了数据的物理群集,但是这意味着不能在该关系上使用位图索引。

外键变化

可以使用引用属性和外键,而不是在中间类与类A和B之间定义关系,以便中间类如下所示:

Class MNFK.DoctorPatient Extends %Persistent
{

Property Doctor As MNFK.Doctor;

ForeignKey DoctorFK(Doctor) References MNFK.Doctor();

Property Patient As MNFK.Patient;

ForeignKey PatientFK(Doctor) References MNFK.Patient();

}

使用简单外键模型的一个优点是不会发生大量对象的意外混乱。缺点之一是没有自动懒加载功能。