接口式实体定义之——自定义实体属性+实体多根继承
从SF.Net下载NBear及用户手册
从博客园下载用户手册
最新版本的NBear中除了本文中提到的两个功能之外,还包括如下内容:
1)支持EntityFactory.CreateObject和CreateObjectList现在支持基于DataSet或IDataReader中的字段名称而不仅仅是原来的基于字段顺序的数据填充了;
2)Gateway.Save和Insert方法现在支持自动返回新插入的纪录的自增长ID字段了(当然,前提是,这个实体对应的表确实使用自增长主键字段)。
自定义实体属性
什么是CustomProperty呢?
CustomProperty是一种可以为Entity添加的,不映射到数据表字段的,只读的,用于解析Clob或Blog属性的,自定义类型的属性。
简单的说,如果你的实体包含Clob或Blob大字段,而又想方便的直接读取大字段真正代表的内容,就可以给Entity定义CustomProperty,来封装对大字段内容的访问。
让我们用一个实例来说明:
假设有这样一个Entity:
public interface EntityWithCustomProperty : IEntity
{
[PrimaryKey]
int ID { get; }
string Name { get; set; }
string XmlServerConfig { get; set; }
string XmlContactConfig { get; set; }
[CustomProperty("XmlServerConfig", "XmlContactConfig")]
SampleCustomPropertyType SampleProperty { get; }
}
我们这个Entity除了一些标准的映射到数据表字段的属性(ID,Name,XmlServerConfig和XmlContactConfig)之外,还包含了一个SampleProperty属性。
首先,这个属性是只读的(只有get,注意,必须设为只读,否则,运行时使用该Entity会报错),它的返回类型是一个自定义类型SampleCustomProperty,它的定义如下:
[Serializable]
public class SampleServerConfig
{
public string ServerAddress;
public int ServerPort;
}
[Serializable]
public class SampleContactConfig
{
public string WorkPhone;
public string HomePhone;
}
public class SampleCustomPropertyType : CustomPropertyType
{
public SampleServerConfig ServerConfig
{
get
{
return SerializeHelper.Deserialize<SampleServerConfig>(typeof(SampleServerConfig), (string)values[0]);
}
}
public SampleContactConfig ContactConfig
{
get
{
return SerializeHelper.Deserialize<SampleContactConfig>(typeof(SampleContactConfig), (string)values[1]);
}
}
public SampleCustomPropertyType(object[] inputValues) : base(inputValues)
{
}
}
请注意加粗的代码,这个SampleCustomProperty类是一个定义于任意位置的自定义类,对该类的唯一约束是必须从基类NBear.Common.CustomPropertyType继承。因为
基类包含一个protected的构造函数,所以,实现类必须包含同样列表的构造函数,并调用基类构造函数。
这个类的内容很简单,它包含两个属性,分别也是两个自定义的config类。values[0]和values[1]分别是这两个config类的XML序列化文本。在这两个属性的get实现中,
只是简单地进行反序列化。
values是哪儿来的呢?它是定义于基类CustomPropertyType的一个protected字段。包含了构造函数传入的inputValues参数。
inputValues的值又是哪来的呢?换句话说,既然Entity只是一个接口,谁负责实例化Entity的SampleProperty属性,并传入SampleCustomProperty类的构造函函数需要的inputValues参数呢?
我们注意到,EntityWithCustomProperty.SampleProperty属性包含一个CustomPropertyAttribute修饰,它的参数XmlServerConfig和XmlContactConfig表示,名为这两个名称的属性的值,将会被Entity的实现类用来构造成一个obejct[]数组,传递给SampleCustomPropertyType的构造函数。
注意,NBear.Common.CustomPropertyAttribute的构造函数接受一个params string[]类型的参数,可以是任意数量的Property的名称。
那么,这样一个SampleProperty属性,到底有什么用呢?
让我们来看看测试代码:
[TestMethod]
public void TestCustomPropertyMethod()
{
//provided the entity obj's value is read from database
EntityWithCustomProperty obj = EntityFactory<EntityWithCustomProperty>.CreateObject();
SampleServerConfig sc = new SampleServerConfig();
sc.ServerAddress = "127.0.0.1";
sc.ServerPort = 8888;
obj.XmlServerConfig = SerializeHelper.Serialize(sc);
SampleContactConfig cc = new SampleContactConfig();
cc.WorkPhone = "110";
cc.HomePhone = "119";
obj.XmlContactConfig = SerializeHelper.Serialize(cc);
//now we can use Custom Property like following
Assert.AreEqual(obj.SampleProperty.ServerConfig.ServerAddress, "127.0.0.1");
Assert.AreEqual(obj.SampleProperty.ServerConfig.ServerPort, 8888);
Assert.AreEqual(obj.SampleProperty.ContactConfig.WorkPhone, "110");
Assert.AreEqual(obj.SampleProperty.ContactConfig.HomePhone, "119");
}
在这段测试代码中,我们先构造了一个obj对象,它是一个EntityWithCustomProperty实体的实例。我们假设,这个实例的数据是从数据库中读取的,ID,Name,
XmlServerConfig和XmlContactConfig属性分别对应于数据表中的字段值。XmlServerConfig和XmlContactConfig的值,分别是两组XML序列化文本。
很显然,如果我们直接读取XmlServerConfig和XmlContactConfig属性,因为他们是XML,并不易于使用(XML也许还好,如果它是一组二进制压缩数据呢?)。但是如果我们调用SampleProperty属性,我们就能很方便的读取XML中的真实内容(XML被自动反序列化为SampleProperty属性实例中的值了)。
--
实体多根继承
NBear中的Entity因为是以接口形式定义的,所以,它可以很方便的支持实体继承关系,甚至支持多根继承。也就是从2个以上的基类(接口)继承。
NBear支持两种类型的非常自然的继承关系映射:单表继承体系方式和一实体一表方式。
让我们先看第一个例子:
[Table("AllInOneTable")]
public interface Parent : IEntity
{
[PrimaryKey]
int ID { get; }
string Name { get; set; }
}
[Table("AllInOneTable")]
public interface AnotherParent : IEntity
{
[PrimaryKey]
int ID { get; }
int Age { get; set; }
}
[Table("AllInOneTable")]
public interface Child : Parent, AnotherParent
{
[PrimaryKey]
new int ID { get; set; }
DateTime Birthday { get; set; }
}
我们可以看到,在上例中,我们定义了两个基实体Parent和AnotherParent,Child实体同时从两个基类继承。注意,代码中加粗的行,如果多个不同的基接口包含相同名称的属性,代码会编译失败,此时,需要像这样使用new关键字来避免编译失败。
再注意,在这个例子中,我们和数据库表是如何对应的呢?
这里,我们采用的是,单表继承体系方式。也就是说,用一张单独的表,存储整个继承体系的类。注意每个实体都映射到AllInOneTable这个表。
那么,是不是只能使用单表继承体系方式来映射继承关系的实体呢?当然不是(见是比较推荐,因为,该方案相对比较方便灵活)。
我们也可以采用一实体一表方式。代码如下:
[Table("ParentTable")]
public interface Parent : IEntity
{
[PrimaryKey]
int ID { get; }
string Name { get; set; }
}
[Table("AnotherParentTable")]
public interface AnotherParent : IEntity
{
[PrimaryKey]
int ID { get; }
int Age { get; set; }
}
[Table("ChildTable")]
public interface Child : Parent, AnotherParent
{
[PrimaryKey]
new int ID { get; set; }
DateTime Birthday { get; set; }
}
这种方案下,每个实体对应一张表,每张表包含冗余的父类的数据。