在OR Mapping中,映射称得上是其灵魂。映射得当,概念模型中的实体可以很容易的持久化到数据库,开发周期短,易于维护;映射不得当,则可能出持久化性能低下、逻辑表达不清晰甚至概念模型到物理模型的不匹配等问题。本文将通过一个示例程序,来了解AEF(ADO.NET Entity Framework)中,如何进行物理模型到概念模型的映射,如何处理一对多、多对多关系,如何处理实体继承中的映射问题……
一、准备工作
- 将Visual Studio 2008及.NET Framework 3.5升级到SP1。点击转到升级地址。
- 安装SQL SERVER 2005,VS 2008中自带的EXPRESS版的SQL SERVER应该也可以用。
- 下载并附加数据库:点击下载DemoDbV2。
- 创建一个VB Console Application,并且取一个合适的名字(例如:AEFMapping之类的)。注意,目标Framework要设置成3.5版。
附加数据库后,大家可以顺便查看一下数据库当前的状况。偶这边的数据库当前状况如下:
图1
由于要形成多样的关系,本次采用的数据库示例似乎比一个表的那一种要复杂那么一点点^_^。一共分为5张表:
Notebook表用于存放笔记本信息;大家可以看到它有"IsForSale"列,用于表明,这个笔记本是不是用于销售的(在示例中:0代表不卖,1代表卖)。因此,届时,会将笔记本分为两类:笔记本和销售类笔记本。由于销售类笔记本也"是"笔记本,因此,它们之间就形成一个继承关系。这在呆会儿的映射中将会有所体现。
InitOS是指笔记本出厂时带有的操作系统,由于一台笔记本可能装有多个OS(比如偶自用的本本上装了Vista和Ubuntu),一个OS又会被多个笔记本所安装(偶的笔记本上有Vista,相信不少朋友的笔记本也装了Vista,Vista就被安装于多个笔记本上),因此,笔记本与OS之间的关系就是多对多关系,因此,我们引入一个叫NbOS的表,来记录它们两者之间的对应关系;
Warehouse代表仓库,一个笔记本只能存放在一个仓库中(要么放A仓,要么放B仓,总之不可能同时又放在A仓又放在B仓吧),但一个仓库可以存放多台笔记本(显然J),因此,笔记本与仓库之间就形成了一个一对多的关系;
WarehouseState是指当前仓库的状态,表明当前仓库是在用的啊,还是废弃了啊……每一个仓库只有一种状态(不会出现A仓库既可以用又不可以用的状况),而同一个状态下的仓库却可能有好多(A仓库,废弃;B仓库,废弃;C仓库,废弃……废弃!)。因此,仓库状态与仓库之间,也形成一对多的关系。
二、自动生成的映射
第一步,当然是添加实体数据模型。在项目上右击"Add->New Items->ADO.NET Entity Data Model",并且给它取一个名字,叫什么好呢~NbWh.edmx吧。
然后,设置连接啊、选择表啊……(此处省略步骤万千,如遇问题,参考《ADO.NET Entity Framework 试水——掠影》一文完成。)让我们来看一下最后自动生成的实体数据模型。
图2
大功告成……了一半。让我们来看一揣摩一下生成的这个东东有什么特点:五个表踏踏实实的生成了五个实体;清一色的一对多联系;我们说过要把笔记本分为销售和非销售的两类,这一点没有体现……看来,自动生成的这个模型还是有其局限性的。
三、一对多映射中,数据的映射方法
生成的概念模型中,最让人满意的要数表中列到实体中属性的映射以及一对多的联系了。列到属性的映射无非就是一个名称及一个数据类型的映射,这里就不多说了。让我们来看看一对多的联系。
以Warehouse和Notebook形成的一对多关系为例。点击一下Warehouse和Notebook之间的"小桥梁",并且打开映射详细信息面板,如图3。
图3
由于键属性在实体中就可以代表实体(例如,笔记本的唯一序列号可以把不同的笔记本区分出来),因此,两个实体间的联系,可以看成是两个键属性间的联系。对于这个"桥梁",就可以抽象两个实体键属性之间的联系。因此,把这两个键属性,映射到多端表中的主、外键,一对多的联系的映射就完成了。
大家可以试试手动来建立一个一对多的关系,例如把上图中的WarehouseState实体类删除并重新创建,然后,添加一个联系,选择好相应的主、从关系并且把主键属性映射到多端表的主键、外键。
我们来作一个一对多的查询:
目标:查询出所有的Warehouse的名称,并且,把Warehouse中的所有笔记本信息输出到屏幕上。
'Create object context Using objContext As New NbWhEntities 'Create Query Dim warehouseSet = From aWarehouse In objContext.Warehouse _ Select aWarehouse 'Output all warehouse For Each aWarehoues In warehouseSet Console.WriteLine(aWarehoues.Name) 'Load data from lazy bind aWarehoues.Notebook.Load() 'Output all notebooks in current warehouse For Each aNotebook In aWarehoues.Notebook Console.WriteLine(vbTab + "Brand:{0} Type:{1} Weight:{2} Color:{3}", _ aNotebook.Brand, _ aNotebook.Type, _ aNotebook.Weight.ToString(), _ aNotebook.Color) Next Next End Using |
查询模式,我们在前面已经见到过很多了,这里不作具体解释了。大家需要注意的是代码中橘黄色背景的那一行。我们这里,通过一个Navigation属性,即Warehouse实体集的Notebook属性,来访问到了当前这个Warehouse实体下对应的全部的笔记本实体集。这里与LinQ to SQL有一点点细微的差别,AEF中使用 Lazy Load(懒惰的载入?)方式,因此,如果程序员不主动的去载入(Load),Navigation属性中的数据不会自动出现。大家可以试一下把橘黄色的那行代码注释掉,结果看到的,就只是三个空的仓库。
本例运行结果如下:
图4
四、处理多对多映射
在自动生成的实体图上,没有体现多对多联系。而是跟关系型数据库一样,用两个一对多来模拟。如果大家从概念模型出发,是不会出现这样的情况的。因此,对于多对多关系的映射,我们需要作一下处理。
首先,中间表"NbOS"是不存在的。现实生活中没有这么一个东西代表笔记本与操作系统中的联系。因此,我们做的第一件事,就是把这个中间表删除。
由于中间表被删除,Notebook与InitOS之间的羁绊亦被斩断了。我们需要重新将其建立起来,让他们比以前联系得更紧密。在实体模型设计器上右击选择"添加->关系",出现"Add Association"对话框,见图5,两个端(End)分别设置为"Notebook"和" InitOS"。Multiplicity均设置为Many,Navigation属性可以根据个人喜好设置。
图5
点击OK,完成联系的添加,大家看到Notebook和InitOS之间,就建立了一条"* to *"的桥梁。但是,这个桥梁目前只是一个空架子,多对多联系,需要映射到中间表上。点击Notebook和InitOS之间的联系,打开映射详细信息面板,添加一个表格:NbOS,并且把两个实体的键属性映射到中间表的组合关键字上——千万别对反喽。经过这两步,一个多对多关系的映射就完成了。
OK,让我们来试一下这个新的桥梁是否好使。
目标:
- 查询出所有笔记本名称、型号以及其出厂时的操作系统。
- 查询出所有操作系统的名称,以及安装了此操作系统的笔记本的名称、型号。
'Create object context Using objContext As New NbWhEntities 'Query out all notebooks Dim query1 = From aNotebook In objContext.Notebook _ Select aNotebook 'Output all notebook information For Each aNotebook In query1 Console.WriteLine("{0}({1})", aNotebook.Brand, aNotebook.Type) 'Load data through navigation property aNotebook.InitOSes.Load() 'Output the os list in curret notebook For Each anOs In aNotebook.InitOSes Console.WriteLine(vbTab + "{0} {1}", anOs.Name, anOs.Version) Next Next Console.WriteLine()
'Query out all os Dim allOSes = From anOs In objContext.InitOS _ Select anOs 'Output all init os For Each anOS In allOSes Console.WriteLine("{0} {1}", anOS.Name, anOS.Version) 'Load data through navigation property anOS.Notebooks.Load() 'Output the notebook list using the current OS For Each aNotebook In anOS.Notebooks Console.WriteLine(vbTab + "{0}({1})", aNotebook.Brand, aNotebook.Type) Next Next End Using |
我们首先查询出每一台笔记本下存在的多个操作系统;然后查询出了每个操作系统下的多个笔记本。这样子,一个多对多的映射就完成了。
输出结果下图:
图6
五、继承
大家注意到,在Notebook实体类中,描述了笔记本的属性:品牌、型号、重量、颜色、售价、代理商、是否出售用笔记本。在这里,后面这三个属性,可以说是专为出售笔记本而设计的。因此,我们可以把笔记本分为两类:出售用的和非出售用的。对于非出售用的笔记本,售价、代理商对其没有意义。因此,我们希望让出售用笔记本继承自普通笔记本,并且拥有这三个出售相关的属性。
那么,让我们一看看这种继承关系,在AEF中如何体现。
第一步,删除Notebook实体类中的售价、代理商、是否出售用三个属性。
第二步,右击实体模型设计器,添加一个新的实体。填写上实体的名称,例如SalesNotebook,选择基类(Base type)为Notebook。如下图:
图7
完成以后,就可以看到一在Notebook下面拖了一个大大的"尾巴"——它的子类SalesNotebook。只不过,这个子类没有任何属性。属性映射也不难,点击实体以后,在映射详细信息面板中,先添加一个表Notebook,然后,选择映射三个属性即可。完成以后,我们在实体设计器里将下将看到类似下图的效果:
图8
此时,大家试一下Ctrl+Shift+B编译一下项目,却发现有问题,出现了两个错误。呵呵,这是因为,在当前的映射中,同一个关系表Notebook对应到了两个类:实体类Notebook和SalesNotebook。于是AEF就困惑了:到底什么样的行映射到Notebook的实体,什么样的行映射到SalesNotebook的实体呢?在数据库里,我们怎么区分一个行是用于销售的笔记本还是非销售的笔记本?是通过IsForSales属性。因此,IsForSales不应该是SalesNotebook或者Notebook实体类的属性,因为两个类本身就可以将其区分。
大家注意到映射详细信息中,出现了一个绿绿的图标,叫做Add a Condition。它就是用来告诉AEF,哪些行对应到父类,哪些行对应到子类。我们把SalesNotebook中的IsForSale属性删除,然后,分别添加父、子类的条件:IsForSales=0和IsForSales=1。重新编译,成功啦。这时大家看到的实体设计器中的情形大致如下:
图9
这样,我们就可以来看看这两个类怎么来使用了。
目标:
- 查询出所有笔记本(销售或非销售用途的,只要是个笔记本就输出来);
- 查询出所有非用于销售的笔记本。
'Create object context Using objContext As New NbWhEntities 'Create query to select all notebooks (both for sales or not for sales) Dim query = From aNotebook In objContext.Notebook.OfType(Of Notebook)() _ Select aNotebook 'Output the results: For Each aNotebook In query Console.WriteLine("Brand:{0} Type:{1} Weight:{2}", _ aNotebook.Brand, aNotebook.Type, aNotebook.Weight.ToString()) Next
Console.WriteLine("Notebooks that for sales") 'Create query to select all notebooks that for sales Dim query2 = From aNotebook In objContext.Notebook.OfType(Of SalesNotebook)() _ Select aNotebook
For Each aSalesNotebook In query2 Console.WriteLine("Brand:{0} Type:{1} Weight:{2}", _ aSalesNotebook.Brand, aSalesNotebook.Type, aSalesNotebook.Weight.ToString()) Next End Using |
这里,我们使用了LinQ for Entity的From In子句中的OfType属性,指定了查询结果的类型。因此,让我们使用OfType(Of SalesNotebook)的时候,我们就找到了全部用于销售的笔记本的实体。运行结果如下:
图10
六、小结
本文,我们主要讨论了从关系模型到概念模型之间的映射。可以看到,从数据库生成概念模型时,AEF对一对多关系的映射支持得比较好;对于多对多关系,需要删除中间表映射到的实体并重新创建它们之间的多对多联系;我们还谈到了概念中的继承以及其与数据库中表的映射关系。
七、示例代码下载
点击下载。
Little knowledge is dangerous.