DLinq深入实体类
5.1 使用特性
在您的应用程序中定义一个实体类和普通对象类没什么区别,但是实体类需要附加特殊的说明来阐述实体类和数据库表格之间的关联。这些说明就是实体类的自定义特性。只有在您使用DLinq时,这些类的特性才会有意义。这和.NET framework中的XML序列化特性机制是一样的。DLinq利用这些“数据”特性获取足够的信息来将实体转化为对数据库的SQL查询以及将实体变化转化为SQL insert,update和delete命令。
5.1.1 Database特性
如果没有指定Connection,Database特性用于指定默认的数据库名称。Database特性应用到强类型的DataContext。这个特性是可选的。
属性 |
类型 |
描述 |
Name |
String |
指定数据库的名称。这个特性仅在没有指定Connection时有效。如果在DataContext声明中没有指定Database特性,并且没有指定Connection,那么数据库的名称同DataContext类名。 |
[Database(Name="Database#5")]
publicclassDatabase5 : DataContext {
...
}
5.1.2 Table特性
Table特性用于指定实体类相关联的数据库表格。拥有Table特性的实体类被DLinq做了特殊处理。
属性 |
类型 |
描述 |
Name |
String |
指定数据库表格的名称。如果没有指定,DLinq假设数据库表格的名称同实体类的名称。 |
[Table(Name="Customers")]
publicclassCustomer {
...
}
5.1.3 Column特性
Column特性用于指定实体类成员对应的数据库表格的列。它可用于任何public、private或者internal成员或者属性。只有被标识了Column特性的成员才会被DLinq保存到据到数据库中。
属性 |
类型 |
描述 |
Name |
String |
表格或视图列的名称。如果没有被指定,列的名称同类的成员的名称。 |
Storage |
String |
用于数据存储的成员名称。这个属性告诉DLinq怎样忽略数据成员的public属性访问器,而是同原始的数据成员进行数据交换。如果没有被指定,DLinq使用public访问器的get和set方法。 |
DBType |
String |
指定数据库类型和修饰。使用T-SQL语法。如果没有被指定,则根据成员的类型来推断数据库的列类型。这个属性只在您需要调用CreateDatabase()来新建一个数据库实例的时候才有意义。 |
Id |
Bool |
如果为true,则表明这个成员映射到表格的主键。如果实体类中有多个成员被指定了Id特性,那么这些成员所映射的列共同组成了表格的主键。至少需要指定一个成员具有这个特性,并将之映射到相应表格/视图中的主键或者唯一约束键。DLinq不支持无唯一约束键的表格/视图 |
AutoGen |
Boolean |
表示这个成员列的值是否是数据库自动产生的。指定AutoGen=true的主键的DBType也被指定为IDENTITY。SubmitChanges()被调用实现插入行后AutoGen的成员值会被立即同步更新。 |
IsVersion |
Boolean |
表示这个成员列是否有一个数据库时间戳或版本号。相应行的每次更新,版本号会递增,时间戳列会被更新。某行被更新后,IsVersion=true的成员会被立即更新。SubmitChanges()被调用后,成员值会被立即更新。 |
UpdateCheck |
UpdateCheck |
指定DLinq怎样利用“乐观式并发控制”来进行冲突检测。如果没有任何成员被指定为IsVersion=true,则比较原始成员值和当前的数据库状态类进行冲突检测。您可以为每个成员指定UpdateCheck枚举值,从而控制它们怎样用于DLinq的冲突检测。 Always – 总是使用此列进行冲突检测 |
IsDiscriminator |
Boolean |
指定这个类成员是否为继承层次的标识值 |
一个典型的实体类将在public属性上使用Column特性,而将实际的值保存到某个private成员中。
private string _city;
[Column(Storage="_city", DBType="NVarChar(15)")]
public string City {
get { ... }
set { ... }
}
DBType声明数据库列元数据类型, 仅用于CreateDatabase()创建数据库。另外,类型字符串的长度不可以超过15个字符。
标识为数据库主键的成员的往往是通过数据库自动产生的。
private string _orderId;
[Column(Storage="_orderId", Id=true, AutoGen=true,
DBType="int NOT NULL IDENTITY")]
public string OrderId {
get { ... }
set { ... }
}
如果您确实需要指定主键的DBType,务必包含IDENTITY修饰。否则DLinq不会递增指定DBType自定义成员的值。尽管如此,当调用CreateDatabase()方法创建数据库时,如果DBType没有被指定,那么DLinq默认指定了IDENTITY。
同样地,如果IsVersion属性值为true,那么DBType必须指定版本号或者时间戳列的正确修饰。如果DBType没有被指定,那么DLinq将推断出正确的修饰。
您能够控制与自动生成列、版本戳列或者映射到任何列的成员的访问。您可以通过指定成员的访问限定符来隐藏甚至限制对其的访问。
private string _customerId;
[Column(Storage="_customerId", DBType="NCHAR(5) ")]
public string CustomerID {
get { ... }
}
即便我们通过不定义的Order的CustomerID属性的set方法来定义一个只读属性。但DLinq仍旧可以通过制定为存储字段的私有成员来设置属性的值。
您也可以设置某个成员为private,让它完全不可见,但设置了这个成员的Column特性。这可以让这个实体类封装必要的业务逻辑而不暴露给外部。尽管private成员值可以被更新,但在语言集成查询中您还是不能访问它。
默认情况下,所有的成员都参与乐观式并发冲突检测。您可以指定某个成员的UpdateCheck值来控制其并发冲突检测行为。
[Column(Storage="_city", UpdateCheck=UpdateCheck.WhenChanged)]
public string City {
get { ... }
set { ... }
}
下面的表格显示了数据库类型和CLR类型之间的映射。下面的映射表是推荐的,并不是强制的。您可以参考此表来获取到某个CLR类型所映射的数据库列类型。
数据库类型 |
.NET CLR 类型 |
描述 |
bit, tinyint, smallint, int, bigint |
Byte, Int16, Uint16, Int32, Uint32, Int64, Uint64 |
值转化的时候可能有损失。值可能不会被圆整 |
Bit |
Boolean |
|
decimal, numeric, smallmoney, money |
Decimal |
值范围差异可能导致转化损失,可能不会被圆整 |
real, float |
Single, double |
精度有差异 |
char, varchar, text, nchar, nvarchar, ntext |
String |
区域可能不同 |
datetime, smalldatetime |
DateTime |
不同的精度可能导致转化损失和圆整问题 |
Uniqueidentifier |
Guid |
不同的排序规则。排序结果不可预期 |
Timestamp |
Byte[], Binary |
字节数组被视为标量值。用户负责分配足够的内存来构造。字节数组可视为是不可变的,并且值变化不被跟踪。 |
binary, varbinary |
Byte[], Binary |
Binary类型在System.Data.DLinq 中,它的本质是一个不可变的字节数组。
5.1.4 Association特性
Association特性用于指定数据库关联属性,如外键-主键关联。
属性 |
类型 |
描述 |
Name |
String |
关联的名称,往往同数据外键约束的名称。用于在调用CreateDatabase()创建数据库实例时产生相关的约束。也用于区分引用同一个目标实体类的单个实体类的多个关联。在这种情况下,关联的实体两边的关联属性(如果两边都有定义)都必须使用相同的名称。 |
Storage |
String |
用于存储关联的成员名称。如果被指定了,它告诉DLinq怎样忽略数据成员的public属性访问器,而使用原始数据成员值进行存储。如果没有被指定,DLinq使用public属性访问器的set和get来访问数据成员值。我们推荐,所有的关联成员都表示为属性,而存储使用原始的数据成员值。 |
ThisKey |
String |
逗号分开的一个或者多个实体类成员的名称列表,用于表示关联中This端的键值。如果没有被指定,这个键值为实体类中指定为主键的成员。 |
OtherKey |
String |
逗号分开的一个或者多个目标实体类成员的名称列表,用于表示关联中被Other端的键值。如果没有被指定,这个键值为相关联实体类中指定为主键的成员。 |
Unique |
Boolean |
True表示外键上的唯一性约束,隐含表示1:1关联关系。这个属性很少使用,因为数据库中很少有1:1的关联。大部分实体模型都使用1:n关联即便它被应用程序员指定为1:1关联。 |
IsParent |
Boolean |
True表示关联的Other 端是This端的父亲。对于外键-主键关联,拥有外键的为儿子,拥有主键的为父亲。 |
关联属性或表示对另外一个实体的单一引用,或表示对另外一个实体集合的引用。单一引用必须表示为EntityRef<T>值类型来存储实际的引用。DLinq使用EntityRef类型来实现“延迟加载”。
classOrder
{
...
privateEntityRef<Customer> _Customer;
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
publicCustomer Customer {
get { returnthis._Customer.Entity; }
set { this._Customer.Entity = value; }
}
}
上面的例子中,public属性的类型为是Customer,而不是EntityRef<Customer>。您没必要返回EntityRef,在一个查询中这个类型的引用不会转化为SQL。
同样地,关联属性若为集合类型则需要表示为EntitySet<T>来存储关联。
classCustomer
{
...
privateEntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
publicEntitySet<Order> Orders {
get { returnthis._Orders; }
set { this._Orders.Assign(value); }
}
}
但是因为EntitySet<T>是一个集合类型,所以返回EntitySet是合法的。您也可以使用一个真实集合类型来替代,如ICollection<T>。
classCustomer
{
...
privateEntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
publicICollection<Order> Orders {
get { returnthis._Orders; }
set { this._Orders.Assign(value); }
}
}
确保使用EntitySet 的Assign()方法来进行属性赋值。因为这个集合类型的实例已经被绑定到“变化跟踪服务”中了,这样做就可以保证这个实例是唯一的。
5.1.5 StoredProcedure特性
StoredProcedure特性用于声明在DataContext或Schema中定义的方法可以转化为数据库的存储过程。
属性 |
类型 |
描述 |
Name |
String |
数据库存储过程的名称。如果没有被指定,存储过程的名称同DataContext或Schema定义的方法的名称。 |
5.1.6 Function特性
Function特性用于声明在DataContext或Schema中定义的方法可以转化为数据库的返回标量或者数据库表格的自定义方法。
属性 |
类型 |
描述 |
Name |
String |
数据库存储自定义方法的名称。如果没有被指定,方法的名称同DataContext或Schema定义的方法名称。 |
5.1.7 Parameter特性
Parameter特性用于声明成员方法和数据库存储过程或者自定义方法的参数映射。
属性 |
类型 |
描述 |
Name |
String |
数据库中的参数名称。如果没有被指定,名称同成员方法的参数名称 |
DBType |
String |
数据库参数类型。 |
5.1.8 InheritanceMapping特性
InheritanceMapping特性指定一个特殊的标识符来描述继承关系中的子类。继承层次中,InheritanceMapping必须在根基类中声明。
属性 |
类型 |
描述 |
Code |
Object |
标识值。 |
Type |
Type |
继承子类类型。这个值可以为非抽象子类或者根基类。 |
IsDefault |
Boolean |
当DLinq没有找到标识码的时候是否默认使用此类来构建实体。有仅只有一个InheritanceMapping必须指定IsDefault为true。 |
5.2 图一致性
图是用于表示实体之间相互引用的数据结构的通用术语。一个层次图(或者一棵树)是图的一种退化的形式。领域相关模型往往描述了一个网状引用,这是实体图最好的描述形式。实体图的健康对于应用程序的稳定性是至关重要的。所以,很重要的一点是,您需要保证图中实体之间的引用和业务规则以及/或者数据库定义的约束保持一致。
DLinq不能够为您自动管理关联引用的一致性。当关联是双向的时候,需要同步关联两边的变化。请注意,一般的对象通常不需要这样做,所以您需要以一种不同的方式来设计您的实体。
DLinq确实提供了一些简单的机制来简化您的工作以及一种规则来确保您能够正确的管理实体之间的引用。自动生成的实体类自动实现了这些正确的规则。
public Customer() {
this._Orders =
newEntitySet<Order>(
delegate(Order entity) { entity.Customer = value; },
delegate(Order entity) { entity.Customer = null; }
);
}
EntitySet<T>类型的构造函数包含了两个delegate参数用于回调,一个是当实体加入到集合中时,另外一个是当您从集合中删除实体时。从这个例子中您可以看到,您指定的这些delegate更新了另外一边的关联属性。当将一个Order实体添加到Customer的Orders集合中时,Order实体的Customer属性值也被自动更新。
在关联的另外一端实现了关联并不是想象中那么简单。EntityRef<T>是一个值类型,不包含实际引用对象的更多的信息。它没有这样的两个delegate。所以,为了管理引用图的一致性,一种替代的方案就是,将这些管理引用一致性的代码放到属性访问器中。
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
publicCustomer Customer {
get {
returnthis._Customer.Entity;
}
set {
Customer v = this._Customer.Entity;
if (v != value) {
if (v != null) {
this._Customer.Entity = null;
v.Orders.Remove(this);
}
this._Customer.Entity = value;
if (value != null) {
value.Orders.Add(this);
}
}
}
}
看看set方法。当我们改变Customer属性值时,首先将Order实例从当前的Customer的Orders集合中删除,然后将这个Order加入到新的Customer的Orders集合中。请注意,在调用Remove()方法之前,我们需要将将实际实体应用设置为null。这可以避免Remove()方法的递归调用。请记住,EntitySet会使用回调delete来设置Customer属性值为null。Add()方法调用之前也是同样的道理。实际的实体引用被更新为一个新值。这样再次避免了潜在的递归调用,当然我们有限使用set访问器来实现。
1:1关联的定义和1:n关联的实现方式非常相似。我们不需要调用Add() 和Remove()方法,实体的关联设置为null,就是切断实体之间的关联。
再次强调,保持实体之间的引用一致性是至关重要的。请考虑使用代码自动生成工具为您做好这样的工作。
5.3 变化通知
您的实体可能参与了变化跟踪过程。但这并不是必要的,如果您的实体的确参与了这个过程,在很大程度上来说我们需要减少实体变化的跟踪的开销。很有可能的是,您的应用程序通过查询将获取到一个全新的而不是被更新了的实体。为了保证变化跟踪服务的有效性,实体类需要添加某些处理逻辑,否则变化跟踪服务在跟踪的能力上是非常有限的。
应用程序在运行的过程中实际上不可能被真正地截断,因此也根本不会有真正的变化跟踪。相反,我们将保留第一次获取到的实体的拷贝。然后,当您调用SubmitChanges()是,DLinq将比较变化的实体和实体的拷贝。如果他们值不相同,就表示实体发生了变化。这就意味着,每个实体在内存中其实有两分拷贝,即便实体从来都没有发生任何变化。
一个更好的解决方案就是,在实体真正发生变化的时候,由实体通知变化跟踪服务。那么实体类可以实现某个接口来公布这些事件。然后变化跟踪服务就能够绑定到任何一个实体,当实体发生变化的时候获取变化通知。
[Table(Name="Customers")]
publicpartialclassCustomer : INotifyPropertyChanging
{
publiceventProperyChangedEventHandler PropertyChanging;
privatevoid OnPropertyChanging(string propertyName) {
if (this.PropertyChanging != null) {
this.PropertyChanging(this,
new PropertyChangingEventArgs(propertyName)
);
}
}
privatestring _CustomerID;
[Column(Storage="_CustomerID", Id=true)]
publicstring CustomerID {
get {
returnthis._CustomerID;
}
set {
if ((this._CustomerID != value)) {
this.OnPropertyChanging(“CustomerID”);
this._CustomerID = value;
}
}
}
}
为了帮助实现变化跟踪服务,您的实体类必须实现INotifyPropertyChanging接口。您只需要实现PropertyChanging事件即可。当实体被创建时,变化跟踪服务会注册实体的这个事件。实体所要做的事情就是,当实体的属性发生变化之前立即触发这个事件。
同时也别忘了在关联属性里加上同样的事件触发逻辑。对于EntitySet集合,您可以在delegate中触发事件。
public Customer() {
this._Orders =
newEntitySet<Order>(
delegate(Order entity) {
this.OnPropertyChanging(“Orders”);
entity.Customer = value;
},
delegate(Order entity) {
this.onPropertyChanging(“Orders”);
entity.Customer = null;
}
);
}