随笔- 26  评论- 139  文章- 1 

我对Entity Data Model的一些理解

一些废话

不知不觉,已经有好久好久的没有时间更新自己园子里的blog,觉着一方面对不起大家,另一方面也对不起自己……
这回准备在团队里和大家讨论关于数据实体的东西,于是做了一些整理,因此以下内容不免有些算是我的偏见,也有些是我认识不全的地方,本着共同学习的目的,跟大家讨论。
废话说够了……

引入

使用.net java这样的“高级的”语言进行企业级别的开发,自然会接触到“数据实体”这个东西。这节内容首先面向初学者大体介绍一下数据实体是什么,仅供参考。如果你觉得很熟悉,建议直接跳过^_^本节
EDM = Entity Data Model, 实体数据模型
不要被这种英文缩写弄迷糊了,看一段非常简单的代码,所谓的EDM并不是什么高深的东西。

Code

上面这段代码取自Petshop 4.0,ItemInfo这个类,正如他的名字,描述了每个购买项的信息,包括购买商品项的名称,数量,价格等等,我们就成为实体类(Entity Class)。在这里这些属性和数据库中数据表的字段是一一对应的。于是,我们在程序中对数据进行操作时,就不需要面对一个个的数据表而是一个个我们熟悉的实体类了。大体上,所谓EDM就是这么个意思 

 我贴一个从MSDN上copy下来的关于Entity的解释(其实很废话):
Entity: entities are instances of Entity Types, which are richly structured records with a key. Entities are grouped in Entity-Sets.
这里提到实体需要一个Key,这里就相当于对应数据库类型中的主键,在上面的ItemInfo中,这个主键就是Id这个属性。
好,本节就介绍这么多,对实体类有个感性认识就差不多了。

领域驱动设计(Domain Drivern Design = DDD)

我认为,提到EDM,就一定要提DDD的,因为EDM这种模型就是为了实现DDD这种设计方式而出现的。
什么是领域驱动呢?
这里我摆出Martin Folwer提出的三种模式:

事务脚本 Transaction Script

领域模型 Domain Model

表模块 Table Module

虽说马丁大叔有些言论值得商榷,但关于这三种模式的归纳我认为在近几年内还是非常到位的。关于这三种模型的详细内容,可以参考马丁大叔的那本《企业架构模式设计》。我只为了方便说明稍微带上一些。
这三种模式的分类标准,应该是按照系统中业务逻辑的组织形式来划分的。

简单地说,如果你把所有的业务逻辑归纳起来,成为一个个“函数”,应该很容易理解,就好象当初学习C语言的时候老是让我们写函数一样。把这些函数以存储过程或其他形式存放在数据库里,然后在程序中直接调用,这便是是“事务脚本”。注明的是,这和所谓的“三层架构”无关,就好象Petshop里那样虽然没有使用存储过程,但是把那些函数以一个个函数调用的形式写在了DAL中,仍然属于“事务脚本”,用一些人的话说叫“不OO”。
“事务脚本”这种方式简单易懂,问题主要在于系统在变的庞大后对于一堆堆的函数的维护成了问题。于是便有了大名鼎鼎的“领域模型”。简单地说,领域模型希望完全脱离数据库的限制,让程序员进入一个彻底的面向对象的世界,以面向对象的方式,运用各种复杂的设计模式,获得一个复杂的面向对象的体系。我们用这种方法来描述业务逻辑。而这样设计的问题在于数据库大多不是面向对象的,因此我们需要通过某种方式保存(“行话”叫“持久化”)这个面向对象的体系中的数据(这里埋下伏笔,这个“保存数据”的内容就是和EDM大显身手地方)。
而“表模块”显然“很net”。我认为这种模式大体上是一种领域模型的妥协,可能马丁大叔对这种模型的归纳也就来自ado.net中的的Dataset。“表模块”中程序员使用面向对象的方式来进行业务逻辑的设计的,但是在设计中,必须用到一种“表对象”(其实就是DataTable了),这实质上是数据库中数据表的映射,程序员只需要在表对象上进行数据的修改,然后通过一些方式这些数据就能自动的更新到数据库中去。也许是ado.net提供的强大功能将“表模块”支持的太完美了,以至于在.net平台下的开发很多人都不知不觉使用了“表模块”这种模型。
而DDD,简单地说就是在领域模型中,先设计领域模型再有数据库,于是叫领域驱动,相反地,对于某些业务逻辑简单的系统,可以先有数据库再有领域逻辑,也就是数据驱动了。

可能有些走题了,这里插一句,我不想讨论到底哪种模型最好,也不讨论领域模型到底有哪些好处(此类文章太多),每种模型都有他适用的地方,总之一句话,适合的就是最好的。

使用数据实体

不管使用哪一种方式设计我们的系统,我们几乎都会做一个工作,就是将数据库中的数据表以某种方式映射到程序中,也就是使用数据实体。这样我们就不必在程序中每次需要涉及对数据库的操作都直接去查找数据表了,如果你早期做过Delphi或者asp的开发,就会明白数据实体是多么有效。

翻出一些以前的代码:

procedure TfrmPrint_2_1.Button4Click(Sender: TObject);
var
  startNum,EndNum:integer;
begin
  ADOQuery_Info.SQL.Clear;
  ADOQuery_Info.Close;
  ADOQuery_Info.SQL.Add(
'SELECT bmxx.cqh AS 抽签号, bmxx.XM AS 姓名, ');
  ADOQuery_Info.SQL.Add(
'config_xb.title AS 性别, config_bkxm.title AS 报考项目');
  ADOQuery_Info.SQL.Add(
'FROM ((bmxx LEFT OUTER JOIN');
  ADOQuery_Info.SQL.Add(
'    config_xb ON config_xb.special = bmxx.XB) LEFT OUTER JOIN');
  ADOQuery_Info.SQL.Add(
'    config_bkxm ON config_bkxm.special = bmxx.BKXM)'); 

    ADOQuery_Info.SQL.Add(
'WHERE (config_bkxm.special between :startNum and :endNum)');
    
case cmb_BKDL.ItemIndex of
      
0:begin startNum:=1;EndNum:=5 end;
      
1:begin startNum:=6;EndNum:=7 end;
      
2:begin startNum:=10;EndNum:=15 end;
      
3:begin startNum:=16;EndNum:=31 end;
      
4:begin startNum:=8;EndNum:=9 end;
      
5:begin startNum:=32;EndNum:=33 end;
    
end

    ADOQuery_Info.SQL.Add(
' and bmxx.stat<>3'); 

    ADOQuery_Info.Parameters.ParamByName(
'startNum').Value:=startNum;
    ADOQuery_Info.Parameters.ParamByName(
'endNum').Value:=endNum; 

   
//ADOQuery_Info.SQL.Add('ORDER BY config_bkxm.title,ID');
   ADOQuery_Info.SQL.Add(
'ORDER BY CQH'); 

   ADOQuery_Info.Open;
   qlblGroup.Caption:
=cmb_BKDL.Text;
    QuickRep1.Preview;
end

Delphi,摘自一个报名管理系统

if Request("membercode")<>"" then
sql
="select usermail from [user] where membercode="&Request("membercode")&""
else
sql
="select usermail from [user]"
end if 

rs.Open sql,Conn
do while not rs.eof 

mailaddress
=""&rs("usermail")&""
mailtopic
=request("title")
body
=""&request("content")&""&vbCrlf&""&vbCrlf&"该邮件通过 BBSxp 群发系统发送 程序制作:Yuzi工作室(http://www.yuzi.net)"
%
><!-- #include file="inc/mail.asp" --><

rs.movenext
loop
rs.close 

ASP,选自BBSXP,这是我很喜欢的一个BBS系统

没有鄙视asp或者delphi的意思,良好的架构师用asp或delphi一样能够构建出强大的很漂亮系统,使用.net也可以写出像上面那样的代码。
这些程序的一个共同点是,程序和数据库紧密的耦合在一起,业务逻辑和前台代码完全揉在一块儿,而使用了数据实体之后的代码就清爽多了,具体的我就不贴了,大家可以好好参考一下Petshop,也就是大家常说的“标准三层架构”。

领域模型中的数据实体和ORM

上面已经说过,理想中的领域模型,是一个完全面向对象的世界,而现实中,还是关系数据库用的最多,因此前面提到的那个问题“通过某种方式保存这个面向对象的体系中的数据”成了一个很棘手的问题。
例如在“事务脚本”模式中,数据实体可以和数据表一一匹配,然后业务逻辑实际上就是数据访问,所以系统很简单轻巧(还是参考Petshop)
如果是“表模块”,我们一般不会自己写数据实体,而直接利用“表对象”来作为数据实体进行数据访问,因此业务逻辑中大多也就是一些对Dataset或者Datatable的操作
但是如果是“领域模型”,就变得很困难了。因为面向在对象的世界里,并没有纯粹存放数据的对象,数据对象和与逻辑相关的对象都是在一起的,大多数情况下为了能够持久化数据,我们不得不想一些办法进行妥协,让面向对象的设计不要太理想化,大致有这么几种做法:

    1. 在面向对象设计结束后,专门设计一些数据对象,然后为这些数据对象设计一个持久化器;
    2. 在完成领域设计前,先考虑数据库的设计,也就是不纯粹的进行领域驱动设计而是适当妥协考虑数据存放的问题;
    3. 算了,不考虑领域模型了。

其实大多数情况下,大家都选择了第三种,而专用比较简单的“事务脚本”,这没什么不好,能用简单的方式解决问题为什么不用呢?但是有些情况下,我们确实需要进行比较复杂的系统设计,这时经常就会使用ORM(Object Relation Mapping)工具
现在的ORM工具一般都会提供一些方式持久化(就理解成“保存”了)领域逻辑中的数据对象,或是通过数据库中的数据表产生数据对象,并且完成一些基本的增删改查的数据操作方法。
看一个例子,例如在领域设计中可能有一个"Student"对象,他是一个继承于"Person"的对象,他拥有一些属性(例如:Name, Class),也拥有一些方法(例如:RegisterContest()等),由于有一些方法和逻辑,并且存在继承关系,因此对于这样一个Student对象,我们无法直接存储到关系数据库中。(程序被尽量简单化了)

//a sample domain object
public class Student : Person
{
    
//some Properties
    public Class ownClass;
    
public string StuId; 

    
//some methods in domain
    public void RegisterContest(Contest contest)
    
{
    }

}
 

按照上面的方式一,我们创建一个单独的“数据对象”例如StudentEntity,这个对象中只包含一些公共属性如“Name”等,甚至我们可以使StudentEntity可以直接和数据库中的数据表对应,然后生成一些数据操作类,于是便解决了这个“保存”的问题。

 

public class StudentEntity
{
    
//some properties from Person
    public string Name;
    
public int age; 

    
//some Properties from Student
    public School school;
    
public string StuId;
}
 

数据操作类大概类似下面的形式:



public class StudentDAO
{
    
public StudentEntity Get(Guid id)
    
{
    }
 

    
public void Insert(StudentEntity stu)
    
{
    }
 

    
public void Delete(StudentEntity stu)
    
{
    }
 

    
public void Update(StudentEntity stu)
    
{
    }

}
 

使用ORM工具,上面的这些代码都是可以根据数据库的字段全部自动生成的,在设计完领域模型之后,我们要做的,仅仅就是设计一个适合的数据库,然后使用ORM工具生成一系列代码,也就是在编写业务逻辑的时候就可以方便的调用数据存储操作的代码。
也许你会说这样在领域逻辑的设计里面就和数据扯上了关系,不纯OO了,是这样的,不过这样的设计已经比较完美了,其实在大多数时候(也许是我没见过真正复杂的大系统),领域逻辑里的代码都是数据访问代码的直接的映射。
这只是一种实现方法,各种ORM的实现方法都不同。现在可用的ORM工具很多,这里并不讨论这些ORM工具的具体使用,因此就此打住。
另外,如果采用“数据驱动”的方式进行设计,ORM工具也提供了很好的解决方案,还是上面的例子,如果我们事先设计好了Student这张表,使用ORM工具,我们很容易就能得到上面的StudentEntity类(利用代码生成)。
总的说来,ORM提供了一扇传送门,在没有“面向对象数据库”的情况下,让我们在传统的关系数据库世界和面向对象的世界中来回切换。
如果你认真的看到这里,还认为ORM工具仅仅是一种数据库字段读取工具,可能你还没理解我的意思,建议你再仔细一下上面的文字或相关书籍。
其实ORM还能做得更好,在讨论更加深入的话题之前,我插入一个与主题关系不大的细节,如果不关心可直接跳过下一节。

ORM中EDM设计的一些问题

正如上面所说,本节与主题关系不大,仅供参考。

这一节列举了一些我归纳的一般ORM工具所需要解决的问题,这些问题不是很难,但集合在一起都解决好确实也不易。这些内容旨在让大家对ORM工具所解决的问题有一个粗略的了解。

  • Update和Insert问题(主键问题)

在Update, Insert, Delete时,我们一般都需要得到主键,而哪个是主键的指定在实体中是不那么好实现的,毕竟对象不是关系数据库。另外Update和Insert这两个操作其实很类似,大多数情况下都会用到"UpdateOrInsert"这个操作,而他们对应的Sql完全不同,并且Update操作中确定需要更新的字段也不是简单的事情,这些都需要一些机制来完成。

  • 数据库事务和应用程序事务问题(系统事务,业务事务)

映射一些字段的同时,数据库事务也是需要映射的。而这个映射不是那么方便了。举个例子,当一个用户注册完成时,数据库中可能需要对多张表进行更新,假若每个数据表的操作类负责一个操作的话,如何让这些操作属于一个数据库事务呢?

  • 实体生命周期问题(连接问题)

如果实体中包含连接,并且数据实体需要在各层之间传递,那在传递的过程中,是否需要维护数据库的链接呢?如果实体是在表现层被生成的,那么他如何获得和数据库的连接呢?(其实上面的DAO访问方式就是一种这个问题的解决方法,直接不在实体中包含连接就行了)

  • 综合查询问题(麻烦的SQL)

各种复杂的综合查询一般是项目里面最耗时间的一项“体力活”,你必须动态的生成各种复杂的SQL,这不是简单的事情。各种ORM工具都有自己的解决方式,比如NHibernate的hql语言。

  • 关系处理和层次处理的矛盾

实体类之间的关系一般有两种,包含——对应数据表的关系,继承——对应什么?所以需要用各种Tricks来存储有继承关系的数据实体(上面的例子其实就是一种处理方法),当然,用XML来存储继承关系的实体也是一种很不错的想法,不过,XML毕竟不是数据库,大数据量他是吃不消的。

另外,在数据库中为了处理关系,我们经常都会用xxxID这样的字段(可以看看本文最开头的Petshop中的实体类)这些字段我们是并不希望看到的,ORM能够屏蔽他们吗?

  • 不同的层次实体的转换问题(各种"O"的互相转换)

这可能算不上是ORM工具的问题,但是使用了ORM工具的系统倒是会常常遇到这种问题,借用一些概念:

BO--Business Object(商业对象)
VO--Value Object(值对象)
PO--Persistence Object(持久对象)

在数据的那一层,我们定义了数据实体,在领域逻辑那一层,也会有相应的数据实体,在界面显示的地方,我们还可能有不同的数据实体,他们之间有联系,又不完全一样,于是我们经常只能辛辛苦苦的写各种实体转换代码。最终导致系统代码的丑陋和难以维护。

总结:从更高的层次看EDM

扯了不少关于ORM的东西,也许大家头有些晕了,现在我从更高一些的层次总结一下我的观点。
作为程序员,我们需要为世界编程,而我们要面对的是一堆一堆的二进制数,因此我们需要拥有一个转换两者的模型,文件和DBMS是不错的工具,文件能让我们拥有一个管理数据的单位,而DBMS能让我们把二进制数据抽象成一张一张的数据表。
三层架构的概念让我们开始思考使用数据实体的重要性,而在领域驱动的设计方法中,数据实体担任了更重要的角色。
我们希望能以面向对象的方式描述世界,但是我们又无法摆脱传统的关系数据库。因此我们需要ORM工具,在这其中,我们需要一些东西来保存数据,便于对于数据的增删改查等操作,这个东西就是数据实体。
用某些人的说法,就是EDM让我们更加OO了。

后记

还真没想到一口气写了这么多。后面还准备了一些内容,主要有

解释一些我接触过的系统中的数据访问结构设计;
对一些我用过的ORM框架的分析,以及我对DLINQ和ADO.net Entity Framework的看法和分析;
希望炒炒冷饭,给大家解释下前段时间园子里火热的贫血模型充血模型的讨论,包括以及Book.Save()问题的讨论分析。

时间关系,这些就暂时省去了。睡了 呵呵。

posted on 2008-04-02 00:52 Yuxin Yang 阅读(...) 评论(...) 编辑 收藏