在 .NET 中实现模型约束





5.00/5 (7投票s)
2002年3月17日
23分钟阅读

144972
.NET 扩展,提供用于强制执行类似数据库数据完整性约束的基础设施
摘要
文章的第一部分描述了用于强制执行类似数据库数据完整性约束(如实体完整性、域完整性、参照完整性、用户定义完整性)的 .NET 元数据扩展。(请参阅第二部分以获取工作原型和更多详细信息。)
引入了一组新的元数据表——所谓的元模型表。这些表通过查询相关的元模型和约束定义来填充,这些约束定义可以用建模/约束语言(如统一建模语言 (UML)/对象约束语言 (OCL) 和对象角色建模 (ORM)/概念查询语言 (ConQuer))表示。
元数据 API(非托管和反射 API)应相应扩展,以允许编译器/设计工具发出额外的元数据/约束定义。运行时/类加载器也应更新,以便处理基于新元数据信息的附加逻辑。
请注意,所提出的方法不要求使用 UML/OCL 或 ORM/ConQuer(或任何其他语言)来描述约束。它们可以以任何格式表示(例如,它们可以是 XML 编码的)。唯一的要求是它们必须正确映射到元数据表和 MSIL(Microsoft 中间语言)。
背景信息请参阅
- .NET 元数据表布局。http://msdn.microsoft.com/net/ecma/, TG3, CLI Partition II 部分, PartitionIIMetadataOct01.doc 文件。
- OMG 统一建模语言规范。版本 1.3,1997 年 6 月。
- 对象约束语言规范。版本 1.1,1997 年 9 月。
- Halpin, T.A. and Bloesch, A.C. 1999,《UML 和 ORM 中的数据建模:比较》Journal of Database Management, vol. 10, no. 4, Idea group Publishing Company, Hershey, USA, pp. 4-13。
- Halpin, T.A. 2001,《用面向事实增强 UML》。在研讨会论文集:UML:批判性评估和建议的未来,HICCS-34 会议(毛伊岛,2001 年 1 月)。
- Halpin, T.A. 1999,《从 ORM 角度看实体关系建模 第 1-3 部分》。
- Halpin, T.A. 1999,《从 ORM 角度看 UML 数据模型:第 1-10 部分》。Journal of Conceptual Modeling, InConcept, Minneapolis USA。
- 企业 Java Beans 规范。版本 2.0,2001 年 8 月。
- Bertrand Meyer:《面向对象软件构建》。Prentice Hall PTR, Upper Saddle River (1997) 第二版,1260 页。
- Web 上的 Eiffel:将 Eiffel 系统集成到 Microsoft .NET 框架中 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/pdc_eiffel.asp
- Geoff Eldridge:Java 和“契约式设计”及其缺乏... http://www.elj.com/eiffel/feature/dbc/java/ge/
1. 引言
CLR 最重要的特性之一是关于类型及其成员的重要信息的语言独立表示形式,即元数据。这些信息为运行时提供了类型及其成员的规范以及应用程序的全局信息。
不幸的是,更复杂的元数据信息形式,如类实例之间的关系、预定义/用户定义约束等,无法直接映射到现有的元数据表布局。
在某些简单的情况下,例如,我们可以使用自定义 .NET 属性(派生自 ContextAttribute)和 .NET 拦截器(用于对对象的调用进行预处理和后处理)——详情请参阅 http://msdn.microsoft.com/msdnmag/issues/02/03/AOP/AOP.asp。但这仍然是一个部分解决方案。
在本文中,我们将考虑如何扩展 .NET Framework,以表示与元模型和约束相关的附加元数据。
现有元数据格式在 [1] 中描述。UML 1.3 和 OCL 1.1 被用作建模/约束语言的示例 [2, 3]。UML 与其他方法(如 ORM 和实体关系 (ER) 建模)之间的关系在 [4, 5, 6, 7] 中得到了完美的讨论。
让我们从模型约束开始。
UML 模型(类图、活动图等)可以具有基本的预定义约束,例如关联的多重性约束(如 *、1..n、0..1)、子集、XOR、聚合/组合等。UML 还允许用户添加约束,这些约束可以用 OCL [3] 表示。一个典型的例子是为给定方法声明前置条件和后置条件
-- OCL code
ClassName::MethodName(param1 : Type1, ... ): ReturnType
pre : param1 > SomeValue ...
post: result = ...
另一个简单的例子声明了一个不变式“客户年龄不能超过 75 岁”
-- OCL code
Customer
self.age <= 75
通常,这些条件在编程语言中实现为断言。此外,例如,在 Eiffel 中,前置条件、后置条件和类不变式可以通过编程语言本身来表达。这与契约式设计的不变式概念相对应 [9]。
对于其他面向 .NET 的语言,在不重新构建现有程序集的情况下向类添加前置条件、后置条件和类不变式的过程可以按如下方式实现(请注意,Contract Wizard 工具使用了类似的方法,详情请参阅 [10])。假设我们有一个程序集 A,其中包含一个命名空间 A,该命名空间包含一个类 C,该类具有一个方法 void foo()
// MSIL code
// "non-contracted" class
...
.namespace A
...
[A]A.C::foo()
{
// method body
...
}
然后,适当的工具将生成一个新的代理程序集 A1 并添加类似以下内容:
// MSIL code
// "contracted" class
.assembly extern A
...
.namespace A1
...
[A1]A1.C::foo()
{
// generated preconditions
...
// call original component
call [A]A.C::foo()
//generated postconditions
...
}
这个解决方案有一些明显的局限性,因为我们必须使用一个实现契约的代理程序集,然后调用未契约的原始组件。因此,对象的身份没有得到保留。在更复杂的情况下,例如参照完整性约束或组合规范(联接或子类型,例如前置条件弱化和后置条件强化),如何实现约束也不清楚。
另一种解决方案,在 iContract-like 工具中实现(参见 [11]),是将前置条件和后置条件作为特殊代码注释添加。这种方法也有明显的缺点。
相比之下,我们的方法类似于关系数据库管理系统中实现约束的方式。
让我们以 MS SQL Server 为例,看看典型的数据库设计和实现过程。
ER 模型和约束描述可以转换为 Transact-SQL 脚本,实际上是数据定义语言 (DDL) 脚本,然后,作为执行结果,存储在数据库目录系统表中:sysobjects、syscolumns、sysreferences、sysindexes、syscomments 等。考虑一对表 A 和 B(作为上述 ER 模型的一部分),它们通过最简单的关系链接:

图 1. 实体关系图 - A 是引用表,B 是被引用表。
假设它们还有几个额外的约束。生成的 DDL 脚本可能看起来像这样(为了简单起见,我们使用主键和外键约束来实现参照完整性,不考虑触发器):
-- Transact-SQL code
create table B
(
B_id int identity,
f1 int,
CONSTRAINT PK_B PRIMARY KEY (B_id),
CONSTRAINT f1_check CHECK (f1 > 1)
)
go
create table A
(
A_id int identity,
B_id int NOT NULL,
f2 int,
CONSTRAINT PK_A PRIMARY KEY (A_id),
CONSTRAINT FK_A_B FOREIGN KEY (B_id) REFERENCES B(B_id)
ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT f2_check CHECK (f2 > 0)
)
go
运行此脚本后,SQL Server 将自动使用所需的记录填充系统表。
假设我们还有一组存储过程,它们执行一些标准操作,例如对表 A 和 B 进行添加/更新/删除操作。
由于我们声明了参照完整性约束(使用 NO ACTION 选项),数据库服务器将在我们修改两个表中相关记录时确保数据完整性——如果 B 中的记录在 A 中有引用记录,我们不能删除 B 中的记录;由于服务器在没有编程的情况下检查完整性,这通常称为声明性参照完整性。另一个约束(B_id int NOT NULL)意味着表 A 中的记录不能没有表 B 中对应的记录(强制角色)。
SQL Server 还会阻止用户在列 f1 和 f2 中输入错误的值(分别小于 2 和 1)。这些约束和验证规则由服务器“动态实现”,服务器使用存储在系统表中的相关元信息。我们不会讨论服务器如何为我们完成这项工作的细节(这已经足够复杂)。通常,CHECK 约束用于强制域完整性,PRIMARY KEY 和 UNIQUE 约束强制实体完整性,而 FOREIGN KEY 约束提供参照完整性。
通过查看这种方法,我们可以注意到几个重要的细节
- 约束可以被视为相应 ER 模型的一部分;
- 约束定义存储在元数据表中,并与存储过程分离(实际上,SQL Server 将每个视图、规则、默认值、触发器、CHECK 约束、DEFAULT 约束和存储过程的 Transact-SQL 创建脚本存储在 syscomments 表中);例如,列 f1 上的 CHECK 列约束将作为 SQL 语句存储在 syscomments.text 字段中:([f1] > 1);
- 约束实现可以独立于存储过程实现进行修改,并且通过提供适当的设计,约束的修改不会影响存储过程(或相关 Transact-SQL 脚本)的实现。
此外,我们的 ER 模型和相应的约束可以映射到任何其他支持类似元数据格式的 RDBMS(这对于大多数数据库系统来说基本上是正确的)。
因此,约束是关联元模型的特征,它们是语言独立的。CLR 已经以元数据表的形式提供了关于类型及其成员的重要信息的语言独立表示(在某些方面,它们看起来类似于系统表)。因此,我们可以预期约束定义及其实现可以有效地与类实现分离。我们还可以假设运行时能够以类似数据库的方式支持相关类(相当于表)的实例(相当于表中的行)之间的关系。因此,我们需要向系统“解释”什么是约束/业务规则,然后系统将自动强制执行它们。
为了向现有基础设施添加约束描述及其实现,我们引入了新的元数据表
- MethodConstraint - 描述方法约束,用于前置条件和后置条件;
- FieldConstraint - 描述字段约束,用于实现不变式;
- TypeConstraint - 描述类约束,用于实现参照完整性。
这些表称为元模型表。它们的布局在第 2.5 - 2.6 节中描述。
公共语言运行时还应提供元数据 API 的扩展,以允许编译器/设计工具在设计/编译时将约束定义及其实现与现有元数据和 MSIL 一起发出到 PE 文件中(“PE”代表可移植可执行文件,用于可执行文件 (EXE) 和动态链接库 (DLL) 文件)。详情请参阅图 2。
元数据 API 扩展可以双向使用——从元模型表中检索和向元模型表发出元信息。类加载器将能够在加载/JIT 编译期间检索约束定义(以及其他元数据信息)。此 API 也可以由应用程序在运行时使用。我们应该能够在不重新构建整个程序集的情况下更新元模型表。
在某些情况下,CLR 还应该能够根据约束定义动态生成约束的实现。例如,为了实现参照完整性约束,与类继承相关的约束(例如前置条件弱化和后置条件强化)或防止聚合链接链形成循环。
应该有一个工具能够将模型约束(用 UML/ORM 等表示)直接转换为 .NET 程序集或相应的存储库引擎格式(如 Microsoft 元数据服务模型),以便以后可以由约束发射器使用。此工具可以是相应建模系统(如 Rational Rose 或 Visio)的一部分。
在 Microsoft 元数据服务(前身为 Microsoft Repository)的情况下,信息模型可以使用 Microsoft 模型开发工具包 (MDK) 模型编译器及其相应的扩展进行编译并存储在存储库数据库中。模型编译器将生成各种预选的应用程序文件,这些文件是使用元数据服务部署模型所必需的。由于信息模型存储在存储库数据库中,因此可以通过存储库 API 检索和使用它们。此 API 还支持 XML 编码,以提供存储元数据的 XML 格式导入和导出。为了处理约束定义及其实现,存储库 API 必须相应扩展。
在编译时,源代码编译器(面向 CLR)会查阅相关的信息模型,将与类约束相关的元数据和 MSIL 指令作为 PE 文件的一部分放置在元模型表中,以及常规元数据信息(如类型/类/字段/方法/...定义)和生成的 MSIL。为了查阅元信息,编译器可以与适当的工具集成。请注意,所提出的方法不要求使用 UML/OCL 或 ORM/ConQuer(或任何其他语言)来描述约束。业务规则可以以任何格式表达。唯一的要求是它们必须正确映射到元数据表和 MSIL 集。

所提出的方法的“副作用”是,约束相关功能将适用于所有面向 .NET 的语言。非常重要的细节是,我们的方法完全基于现有的 CLR 基础设施。约束被映射到 CLR 元数据表(带有一些额外的扩展),并由运行时管理。
请注意,Enterprise Java Beans 规范 2.0 (EJB 2.0) 也允许多个实体 Bean 之间存在容器管理的关联。EJB 容器使用每个实体 Bean 的抽象持久化方案(由 Bean 提供者提供)来维护实体 Bean 关联的参照完整性,该方案定义了其容器管理的字段和关联,并确定了访问它们的方法。Bean 开发人员还定义了部署描述符,该描述符指定了实体 Bean 之间的关联。与我们的方法不同,容器管理的参照完整性是相关 EJB 容器的一个特性。
2. 实施
2.1. 不变式、前置条件和后置条件。
考虑以下类 C
// C# code
using System;
...
namespace SomeApplication
{
public class C
{
public int m_f;
public C()
{
...
}
public void foo()
{
...
} // foo()
public static int Main(string[] args)
{
...
return 0;
}
} // class C
...
}
假设方法 foo() 的编码为元数据标记 0x06000002,即存储在方法表的第 2 行,m_f 字段为 0x04000001(字段表的第 1 行)。假设我们使用形式为 0x89XXXXXX 的标记来表示存储在 MethodConstraint 表中的方法约束(参见 2.5. - 2.6. 获取表布局)。让方法 C::foo() 有一个前置条件和一个后置条件约束,这些约束以前由约束编译器发出并存储为 IL 指令集,分别位于 MethodConstraint 表的第 1 行和第 2 行。也就是说,相关的标记分别是 0x89000001 和 0x89000002,并且每行在相关虚拟地址 (RVA) 列中都有一个正确的值,该值指向映像文件中的实际 IL 实现。
显然,方法表也应该有一个 MethodConstraintList 列(MethodConstraint 表的索引)。这种关系类似于 Method - Param 链接,通过 ParamList 列实现(详情请参阅 PartitionIIMetadataOct01.doc)

在 JIT 编译期间,运行时遇到 C::foo 方法的元数据令牌 (0x06000002),并使用此令牌查阅 Method 表的第二行。之后,它意识到此行有一个指向 MethodConstraint 表的索引。运行时检查 MethodConstraint 中的相关记录,并使用它们获取前置条件或后置条件相关 MSIL 实现的 RVA。
以下是它可能如何工作的一个例子
- 编译为本机代码之前(类 C,方法 foo);
C::foo(...) // ( before JIT compilation)
{
method body //MSIL
}
JIT 编译成本机代码后,CLR 使用在元模型表中找到的 RVAs 将前置条件和后置条件添加到方法的实现中
C::foo(...) //(after JIT compilation to native code)
{
if ( !(preconditions && invariants) ) //{native code}
// throw an exception;
method body //{native code}
if ( !(postconditions && invariants) ) //{native code}
// throw an exception;
}
更通用的实现方式如下。假设运行时有一个内部类 ConstraintsChecker,它有一个方法 CheckMethod
HRESULT ConstraintsChecker::CheckMethod ( ..., mdMethodDef md,
CorConstraintType ConstraintType, ... )
{
// use Method and MethodConstraint tables
// to find constraints for a given mdMethodDef token
...
if ( ConstraintType & ctPreCondition )
{
// check preconditions //{native code}
// return result and/or throw an Exception
// implementation could work like this
if ( !(preconditions) )
// set error code, and/or throw an exception;
else
// OK
}
if ( ConstraintType & ctPostCondition )
{
// check postconditions //{native code}
// return result and/or throw an exception
}
if ( ConstraintType & ctInvariant )
{
// check invariants //{native code}
// return result and/or throw an exception
}
...
} // ConstraintsChecker::CheckMethod
其中 mdMethodDef 在 CorHdr.h 中定义,约束类型描述如下
typedef enum _CorConstraintType
{
ctNone = 0x0000,
ctPreCondition = 0x0001,
ctPostCondition = 0x0002,
ctInvariant = 0x0004,
...
} CorConstraintType
因此,结果代码可能看起来像这样
//class C, Method foo (after JIT compilation to native code)
C::foo(...)
{
// call CLR's ConstraintsChecker class for method C::foo
// to check preconditions
HRESULT hr = ConstraintsChecker.CheckMethod ( ..., 0x06000002,
ctPreCondition | ctInvariant );
method body //{native code}
// call CLR's ConstraintsChecker class for method C::foo
// to check postconditions
HRESULT hr = ConstraintsChecker.CheckMethod ( ..., 0x06000002,
ctPostCondition | ctInvariant );
} // C::foo
我们假设一个方法可以有任意数量的约束,并且条件可以包含多个用 AND 和 OR 组合的逻辑表达式。结果条件必须求值为布尔表达式。
类型的任何不变式必须在任何时候对该类型的所有实例都为真。为简单起见,我们假设不变式表达了同一类中属性之间所需关系的一致性规则。因此,对于给定类,类不变式可以作为一组字段约束来实现。
在我们的考虑中,类中的字段(数据成员)可以有任意数量的约束,并且条件可以包含多个用 AND 和 OR 组合的逻辑表达式。结果条件必须求值为布尔表达式,并且不能引用另一个类。这类似于用于强制域完整性的 CHECK 约束。
对象上的任何外部方法调用后,不变式条件都应评估为 TRUE。为了存储这些约束,我们使用 FieldConstraint 表,其布局类似于 MethodConstraint 表,并且至少应有两列。有关表布局,请参阅 2.5 节。

因此,在前面的例子中,如果 m_f 字段(Field 表的第 1 行)有一个约束(或一组约束),该字段将索引到 FieldConstraint 表中相应的行。
由于约束定义与类实现分离,我们可以独立更改它们。例如,如果我们修改了类 C 的给定方法 foo() 的前置条件,我们不必更新方法的 IL 代码。我们只需更新关联的元模型表(以及可能的约束相关 IL 代码),这将导致运行时环境在 JIT 编译期间添加必要的约束。
2.2. 不变式、前置条件、后置条件和子类化。
在继承的情况下,我们可以使用众所周知的规则(参见 [9])
- 不变式累积规则:一个类的所有父类的不变式都适用于该类本身(所有不变式的逻辑“与”)
- 断言重声明规则:新的前置条件必须弱于或等于原始前置条件(所有前置条件的逻辑“或”),新的后置条件必须强于或等于原始后置条件(所有后置条件的逻辑“与”)。
对于给定类的某个方法,CLR 会根据 TypeDef.Extends 字段追踪扩展链,以查找该方法的所有父约束。之后,运行时会遵循不变式累积和断言重声明规则来实现约束的正确验证。
例如,考虑一个类 D 扩展类 B 并覆盖其虚函数 foo()。那么元数据表布局可能如下所示:

运行时使用 TypeDef、Method 和 MethodConstraint 表检查 B::foo 和 D::foo 的约束。假设 0x06000001 和 0x06000004 分别是 B::foo 和 D::foo 的方法令牌,它可以这样工作:
class B //implicitly extends System.Object
{
...
virtual void B::foo(...)
{
// call ConstraintsChecker class for method B::foo
...
method body //{native code}
// call ConstraintsChecker class for method B::foo
...
} // B::foo
...
} // class B
class D: public B
{
...
virtual void D::foo(...)
{
// call ConstraintsChecker class for method B::foo
HRESULT hrPreB = ConstraintsChecker.CheckMethod ( ...,
0x06000001, ctPreCondition );
// call ConstraintsChecker class for method D::foo
HRESULT hrPreD = ConstraintsChecker.CheckMethod ( ...,
0x06000004, ctPreCondition );
// assert ( SUCCEEDED(hrPreB) || SUCCEEDED(hrPreD) );
method body //{native code}
// call ConstraintsChecker class for method B::foo
HRESULT hrPostB = ConstraintsChecker.CheckMethod ( ...,
0x06000001, ctPostCondition );
// call ConstraintsChecker class for method D::foo
HRESULT hrPostD = ConstraintsChecker.CheckMethod ( ...,
0x06000004, ctPostCondition );
// assert ( SUCCEEDED(hrPostB) && SUCCEEDED(hrPostD) );
} // D::foo
...
} // class D
2.3. 参照完整性约束和级联参照完整性约束。
在数据库中,参照完整性表示表之间的关系已正确维护,即一个表中的数据应该只指向另一个表中存在的行;它不应该指向不存在的行。在 MS SQL Server 中,我们可以使用相应的主键和外键约束以及指定 ON DELETE { CASCADE | NO ACTION } 或 ON UPDATE { CASCADE | NO ACTION } 选项来声明参照完整性约束。CASCADE 允许键值的删除或更新通过被定义为具有外键关系并可追溯到进行修改的表的表级联。
UML 中有三种类型的关联(参见 [7],第 8 部分)
普通关联(无聚合,无菱形),共享或简单聚合(带空菱形),复合或强聚合(带实心菱形)。
对于二进制关联,有四种可能的唯一性约束模式
- 1:1(一对一);
- 1:n(一对多);
- n:1(多对一);
- m:n(多对多)。
四种可能的强制角色模式
- 只有左侧角色是强制性的;
- 只有右侧角色是强制性的;
- 两个角色都是强制性的;
- 两个角色都是可选的。
两种方向类型
- 单向的;
- 双向的。
我们假设所有关联都是双向的(这不是一个非常重要的限制)。因此,对于二进制关联有 16 种可能的多重性组合(仅考虑双向链接)
// C++ code
typedef enum _eRelationTypeEnum
{
// None
rtNone = 0x0000,
// One-to-One
/*One-to-One bidirectional, left role is mandatory relationship*/
rtOneToOneBidirectionalLeftMandatory = 0x0001,
/*One-to-One bidirectional, right role is mandatory relationship*/
rtOneToOneBidirectionalRightMandatory = 0x0002,
/*One-to-One bidirectional, both roles are mandatory relationship*/
rtOneToOneBidirectionalBothMandatory = 0x0004,
/*One-to-One bidirectional, both roles are optional relationship*/
rtOneToOneBidirectionalBothOptional = 0x0008,
// One-to-Many
/*One-to-Many bidirectional, left role is mandatory relationship*/
rtOneToManyBidirectionalLeftMandatory = 0x0010,
/*One-to-Many bidirectional, right role is mandatory relationship*/
rtOneToManyBidirectionalRightMandatory = 0x0020,
/*One-to-Many bidirectional, both roles are mandatory relationship*/
rtOneToManyBidirectionalBothMandatory = 0x0040,
/*One-to-Many bidirectional, both roles are optional relationship*/
rtOneToManyBidirectionalBothOptional = 0x0080,
// Many-to-One
/*Many-to-One bidirectional, left role is mandatory relationship*/
rtManyToOneBidirectionalLeftMandatory = 0x0100,
/*Many-to-One bidirectional, right role is mandatory relationship*/
rtManyToOneBidirectionalRightMandatory = 0x0200,
/*Many-to-One bidirectional, both roles are mandatory relationship*/
rtManyToOneBidirectionalBothMandatory = 0x0400,
/*Many-to-One bidirectional, both roles are optional relationship*/
rtManyToOneBidirectionalBothOptional = 0x0800,
...
} eRelationTypeEnum;
在我们的例子中,我们将使用类似主键/外键的属性和 Set/Get 方法来模拟这些关联。
约束发射器
- 填充元模型表
- 为给定的一对类(例如 A 和 B)指定代表 PK 和 FK 约束的字段
- 指定 Set 和 Get 方法来访问这些字段,可选,与 Enterprise Java Beans 规范 2.0 中的容器管理持久性 (CMP) 不同。
- 设置级联参照完整性选项:ON DELETE { CASCADE | NO ACTION } 或 ON UPDATE { CASCADE | NO ACTION }
运行时
- 使用与 PK 和 FK 属性相关的数据成员来处理约束。
- (在没有 Setter 和 Getter 方法的情况下)动态实现访问约束相关字段的方法。
- 提供级联参照完整性约束(例如 ON DELETE)的实现。
让我们从一对多双向关联开始,它也是类 A 和 B 之间的复合聚合,Ra 是可选的,也就是说,每个 B 有零个或多个 A 的实例,每个 A 恰好属于一个 B

为了说明一些重要的细节,考虑使用 STL 库的 C++ 实现
// C++ code
class A
{
private:
B* m_pB;
...
public:
...
void Set_B( const B* pB ) { m_pB = pB; }
B* Get_B() const { return m_pB; }
...
};
class B
{
private:
vector<A> m_vectorA;
public:
...
B()
{
for ( int i = 0; i < n; ++i )
{
A a;
a.Set_B( this );
// set some other fields
// ...
m_vectorA.push_back( a );
}
}
void Set_A( const vector<A>& vectorA )
{
// assign items from the source collection to the destination one
// A's assign operator can be overloaded if needed
// m_vectorA = vectorA;
// should provide additional implementation to make sure
// for each value in m_vectorA
// assert( value.Get_B() == this )
}
const vector<A>& Get_A() const
{
return m_vectorA;
}
// helper
static void ShowVectorA( const vector<A>& vectorA )
{
vector<A>::const_iterator iter = vectorA.begin();
while ( iter != vectorA.end() )
{
cout << "ShowVectorA " << (*iter).Get_B() << endl;
++iter;
}
}
};
这是一个使用示例
// C++ code
int main(...)
{
...
B* pB1 = new B();
B* pB2 = new B();
// show collections
cout << "-- Show B1:vector<A> before ... " << endl;
B::ShowVectorA( pB1->Get_A() );
cout << "-- Show B2:vector<A> before ... " << endl;
B::ShowVectorA( pB2->Get_A() );
// make a copy of original collection
vector<A> vectorA1 = pB1->Get_A();
vector<A> vectorA2 = pB2->Get_A();
// exchange collections
cout << "-- Set B1 ... " << endl;
pB1->Set_A( vectorA2 );
cout << "-- Set B2 ... " << endl;
pB2->Set_A( vectorA1 );
// show collections again
cout << "-- Show B1:vector<A> after ... " << endl;
B::ShowVectorA( pB1->Get_A() );
cout << "-- Show B2:vector<A> after ... " << endl;
B::ShowVectorA( pB2->Get_A() );
...
}
我们已在 B::Set_A 方法中添加注释
// should provide additional implementation to make sure
// for each value in m_vectorA
// assert( value.Get_B() == this )
否则,交换向量后,它们将指向错误的 B 值。
基本上,有几种方法可以实现 B::Set_A 方法。其中一种可能的实现可能如下所示:
void Set_A( const vector<A>& vectorA )
{
// m_vectorA = vectorA implementation;
// 1. Clear destination collection
m_vectorA.clear();
// 2. Add items from src collection to dst one
vector<A>::const_iterator iterSrc = vectorA.begin();
while ( iterSrc != vectorA.end() )
{
// use A's copy ctr
//
m_vectorA.push_back( (*iterSrc) );
++iterSrc;
}
// 3. Some other steps ...
// 4. should provide constraint validation to make sure
// for each value in vectorA
// assert ( value.Get_B() == this );
}
在这种情况下,目标集合的内容被源集合的内容替换。我们还可以注意到,在一般情况下,A 的拷贝构造函数应该实现深拷贝语义。
源实例(pB2)的集合被复制后会发生什么?
让我们首先看看在 SQL Server 中类似的操作是如何进行的。考虑表 B 中 ID 分别为 b1 和 b2 的两条记录(参见图 1),它们由表 A 中的 {a1, a2} 和 {a3, a4} 记录引用。假设我们需要将记录 {a3, a4}“附加”到“实例”b1。基本上,有四种可能的情况
情况 1:替换 {a1, a2},即删除 {a1, a2} 并将 {a3, a4} 设置为指向 b1;b2 没有任何引用记录。
情况 2:合并 {a1, a2} 和 {a3, a4},即 {a1, a2, a3, a4} 指向 b1;b2 没有任何引用记录。
情况 3:用 {a3, a4} 的副本替换 {a1, a2},即 {a3 的副本, a4 的副本} 指向 b1;{a3, a4} 仍然指向 b2。
情况 4:合并 {a1, a2} 和 {a3, a4} 的副本,即 {a1, a2, a3 的副本, a4 的副本} 指向 b1;{a3, a4} 仍然指向 b2。
我们可以说这些情况定义了不同的设置策略(因为它们与 Set 方法相关)。我们可以看到,通常,参照约束的类型并不能完全确定设置策略。因此,我们需要明确指定策略类型。
例如,在后两种情况(情况 3 和情况 4)中,复制 a3 和 a4 可能无法工作,因为存在实体完整性约束(PRIMARY KEY 和 UNIQUE 约束)。
在非托管代码的情况下,我们可以通过提供额外的模板参数来改变 STL 的 vector 类的实现,这些参数指定父类(容器)和关系类型(以及,可能还有一些其他信息,例如使用哪种设置策略)
template<class T, class A = allocator<T>, class Container,
eRelationTypeEnum RelationType, ... >
class vector
{
public:
typedef A allocator_type;
...
}
所以,类 B 现在可能看起来像这样
// C++ code
class B
{
private:
vector<A, B, rtOneToManyBidirectionalLeftMandatory, ...> m_vectorA;
...
}
考虑情况 1,并假设两个角色都是强制性的,即字段 A.B_id 不为空,并且 B 中的任何记录都应被至少一条记录引用(在实际情况中,这可以通过触发器实现)。要将 b2 的记录分配给 b1,我们可以这样做:
-- Transact-SQL code
-- 1. detach {a1, a2}
-- since both roles are mandatory:
-- select records {a1, a2} to be deleted into a temporary table
SELECT * INTO #deleted_b1 FROM A WHERE B_id = b1
-- cannot use:
-- delete A where B_id = b1
-- because b1 in B cannot exist w/o records in A!!!
-- otherwise
-- if A.B_id allows NULL or both roles are optional:
-- update A set B_id = NULL where B_id = b1
-- 2. attach {a3, a4} to b1.
-- Note that a1 and a2 still linked to b1!!!
UPDATE A SET B_id = b1 WHERE B_id = b2
DELETE B WHERE B_id = b2 -- since b2 cannot exist w/o a3 and a4!!!
-- delete {a1, a2} since they were stored in #deleted_b1
DELETE A WHERE A_id IN ( SELECT A_id FROM #deleted_b1 )
DROP TABLE #deleted_b1
因此,在我们之前的示例中,可以这样写:
pB1->Set_A( pB2->Get_A() );
在此调用之前,以下断言为真
assert ( {a1, a2} == pB1->Get_A() );
assert ( {a3, a4} == pB2->Get_A() );
此调用之后,以下断言为真
assert ( {a3, a4} == pB1->Get_A() );
assert ( null == pB2 ); // pB2 should be deleted!
同样,在 CLR 中,pB2 实例将符合垃圾回收的条件。如果存在对 pB2 的其他引用,则 pB1->Set_A( pB2->Get_A() ) 调用应该失败。可能的情况可以描述如下:

我们可以这样表示
typedef enum _CorSetPolicyTypeEnum
{
spNone = 0x0000,
spReplaceDelete = 0x0001,
spMergeDelete = 0x0002,
spReplaceCopy = 0x0004,
spMergeCopy = 0x0008,
...
} CorSetPolicyTypeEnum
在 CLR 中,我们应该填充适当的元模型表,并且每个类都应该有实现 PK/FK 关系(被引用/引用字段)的字段。
为了简单起见,我们假设在一对多关系中,被引用类(类 B)使用集合类型来保存引用实例(类 A),该集合类型实现了 ICloneable、ICollection、IEnumerable 和 IList 接口,或者 ICloneable、ICollection、IEnumerable 和 IDictionary。也就是说,它通过 ArrayList 和 Hashtable 类的类来实现
// C# code
using System;
using System.Collections;
...
namespace SomeApplication
{
...
public class A
{
// holds pointer to parent instance of B
private B b_B;
// some other fields
private int m_n;
...
public B _B // Get_B and Set_B
{
get { return b_B; }
set { b_B = value; }
}
...
} // A
public class B
{
// holds pointer to referencing instances of A
ArrayList vectorA;
// some other fields
...
public B()
{
vectorA = new ArrayList();
for ( int i = 0; i < N; ++i )
{
A a = new A();
// set parent.
a._B = this;
// set other fields.
...
vectorA.Add( a );
}
}
public ArrayList _A // Get_A and Set_A
{
get { return vectorA; }
set {
// vectorA = value;
// implementation depends on the set policy
// should provide constraint validation
// to make sure
// for each item in vectorA
// assert ( item._B() == this );
...
}
}
...
} // B
根据 Set 操作策略,我们可能要求引用类也实现 ICloneable::Clone 方法作为深拷贝。请注意,如果被引用类也引用另一个类,它应该使用深拷贝语义实现 ICloneable 接口。
运行时环境控制
- 保存子(=引用)A 实例的集合;
- A 的数据成员字段,存储指向 B 对应实例的指针。
这可以通过挂钩到 IDictionary::Item 属性、IDictionary::Add 和 IDictionary::Remove 等方法的实现来完成。
每次我们修改 A 和 B 实例时,环境都应通过强制执行相应的约束来确保类实例之间的关系得到适当维护。
ICloneable 接口的深拷贝实现允许 CLR 使用类似于 SQL Server 中 deleted 和 inserted 表的机制来验证约束。如果我们要从表 A 中删除记录,我们可以创建一个触发器来验证这些记录中没有一个引用表 B
-- Transact-SQL code
CREATE TRIGGER tr_A_D
ON A FOR DELETE
AS
If EXISTS ( select * from deleted d, B b where d.B_id = b.B_id )
begin
RAISERROR ('records from A point to B, cannot delete...', 16, 1)
ROLLBACK TRANSACTION
end
GO
同样,让方法 foo 修改 A 的输入集合
// C# code
public bool foo( ArrayList vectorA )
{
foreach (A a in vectorA )
// {
// delete some items
// }
}
在 JIT 编译期间,运行时添加了一个“触发器”来验证约束
// ( before JIT compilation)
public bool foo( ArrayList vectorA )
{
method body //MSIL
}
JIT 编译为本机代码后,CLR 使用 TypeConstraint 和 TypeConstraint_Method 表中找到的信息添加所需的实现(有关表布局,请参阅 2.6.3. 和 2.6.4.)
// (after JIT compilation to native code)
public bool foo( ArrayList vectorA )
{
{native code}
// check preconditions and invariants as usual
// if ( !(preconditions && invariants) )
// throw an exception;
{native code}
// make a deep copy of the destination collection
// class A should have proper implementation of ICloneable
// ArrayList vectorA_copy = vectorA.Clone();
{native code}
method body
{native code}
// using vectorA_copy find "deleted rows" in vectorA
// check postconditions and invariants using this information
// if ( !(postconditions && invariants) )
// throw an exception;
}
在这种情况下,如果出现问题,运行时能够“回滚”更改。
在更复杂的场景中,已删除的实例应将其删除(或任何其他更改)级联到所有相关实例,这些实例之前与“已删除行”存在关系,并且已为这些关系指定了 ON DELETE CASCADE 选项。否则,如果设置了 ON DELETE NO ACTION,CLR 应阻止该方法删除相应的 A 实例
// (after JIT compilation to native code)
public bool foo( ArrayList vectorA )
{
...
method body //{native code}
//{native code}
// using vectorA_copy find "deleted rows" in vectorA
// check postconditions and invariants using this information
// if there are instances of any other class C
// which reference to deleted instances of A
// and cascade options ON DELETE NO ACTION was specified
// CLR should throw an exception and/or "rollback" changes;
...
}
2.4. 更新元数据层次结构。
扩展后的元数据树如下所示
Assembly Module Type TypeConstraint MethodInfo ParameterInfo MethodConstraint FieldInfo FieldConstraint EventInfo PropertyInfo ...
我们声明新的元数据令牌
// CorHdr.h
...
typedef mdToken mdMethodConstraintDef
typedef mdToken mdFieldConstraintDef
typedef mdToken mdTypeConstraintDef
typedef enum CorTokenType
{
...
mdtTypeConstraintDef = 0x87000000, //
mdtFielConstraintdDef = 0x88000000, //
mdtMethodConstraintDef = 0x89000000, //
} CorTokenType
我们还需要扩展 System.Reflection.Emit 命名空间中提供的类型和功能,以允许编译器/工具发出约束(元数据和相关的 MSIL 指令)。例如,必须修改 TypeBuilder、MethodBuilder 和 FieldBuilder 类以支持类型、方法和字段约束。
2.5. 元数据表变更。
2.5.1. Method 表变更。
MethodConstraintList 列(MethodConstraint 表的索引)标记当前方法拥有的一系列连续方法约束的第一个。此系列一直持续到
- MethodConstraint 表的最后一行
- 通过检查 Method 表中下一行的 MethodConstraintList 列找到的下一系列方法约束,取两者中较小的一个。
2.5.2. Field 表变更。
FieldConstraintList 列(FieldConstraint 表的索引)标记当前字段拥有的一系列连续字段约束的第一个。此系列一直持续到
- FieldConstraint 表的最后一行
- 通过检查 Field 表中下一行的 FieldConstraintList 列找到的下一系列字段约束,取两者中较小的一个。
2.5.3. TypeDef 表变更。
TypeConstraintList 列(TypeConstraint 表的索引)标记此类型(类)拥有的连续类型约束的第一个。此系列持续到以下两者中的较小者:
- TypeConstraint 表的最后一行
- 通过检查 TypeDef 表中下一行的 TypeConstraintList 列找到的下一系列类型约束。
变更总结如下图所示

2.6 元模型表布局。
2.6.1. MethodConstraint 描述每个方法的约束。
MethodConstraint 表中的每一行都归 Method 表中的一行所有,且仅归一行所有。Method 表中的 MethodConstraintList 列(MethodConstraint 表的索引)标记当前方法拥有的连续方法约束的第一个。
2.6.2. FieldConstraint 描述类的每个字段的约束。
2.6.3. TypeConstraint 描述每个类类型的约束
TypeConstraint 表中的每一行都归 TypeDef 表中的一行所有,且仅归一行所有。TypeDef 表中的 TypeConstraintList 列(TypeConstraint 表的索引)标记当前类型拥有的连续类型约束的第一个。此表类似于 SQL Server 中的 sysforeignkeys/sysreferences 表
请注意,为了描述 Setter/Getter 方法,我们可以使用类似 MethodSemantics 的机制(由 Event/Properties 表使用)。在这种情况下,我们还需要扩展 MethodSemanticsAttributes 类型
2.6.4. TypeConstraint_Method 描述了引用类和被引用类的哪些方法必须经过验证,以支持这些类之间的参照完整性约束。
当实现事件和属性时,它的作用类似于 MethodSemantics 表。
新表如下图所示
