一、 前言
ADO.NET Entity Framework(以下简称EF)是微软推出的一套O/RM框架,如果用过Linq To SQL的人会比较容易理解,因为Linq To SQL是微软在.net FrameWork 3.0时推出的一套轻量级的O/RM框架,但是只支持SQL Server一种数据库。至.net FrameWork 3.5 sp1时,才推出Entity FrameWork,可以通过实现不同的Provider来支持不同的数据库(当然微软还是只内置SQL Server的Provider,其它数据库的Provider么,需要第三方开发)。EF加上linq,这是.net开发上的一个巨大进步,.net程序员以对象方式操作数据,以类sql语法在程序里查询数据,大大减少了繁琐的构造SQL语句的工作,可以更加专注于编写业务逻辑代码。但是在多层架构的分布式应用系统中,实体对象通过远程序列化到客户端时,这些实体会与其数据上下文(也就是实体容器)分离,在客户端无法对实体直接进行查询以及CUD(Create,Update,Delete)操作,下面以SQL Server为数据库,Remoting+Entity Framework3.5作为数据服务层,WinForm作为客户端, 讲述一下如何使用EF框架搭建多层分布式应用系统。
二、 预备知识
- Linq To SQL或者 EF方面的基础知识。此处不作赘述。
- 分布式应用系统方面的基础知识。所谓分布式应用系统,对于.net而言,就是使用了诸如COM+,.net Remoting,XML WEB Service,WCF,MSMQ等远程调用技术的多层架构应用系统,所以需要对上述技术有一定了解。此处不作赘述。
- 面向对象编程的一些基础知识。此处不作赘述。
三、 技术分析
- 通过远程客户端传输过来的实体,都是处于分离状态(EntityState属性值为Detached),所以在多层应用程序中的服务端实现实体的更新或删除时,关键是如何把实体附加回实体容器中。MSDN上关于对分离实体的查询和CUD操作描述如下:
1) 附加对象(实体框架)
在实体框架的某个对象上下文内执行查询时,返回的对象会自动附加到该对象上下文。还可以将从源而不是从查询获得的对象附加到对象上下文。您可以附加以前分离的对象、由 NoTracking 查询返回的对象或从对象上下文的外部获取的对象。还可以附加存储在 ASP.NET 应用程序的视图状态中的对象或从远程方法调用或 Web 服务返回的对象。
使用下列方法之一将对象附加到对象上下文:
- 调用 ObjectContext 上的 AddObject 将对象附加到对象上下文。当对象为数据源中尚不存在的新对象时采用此方法。
- 调用 ObjectContext 上的 Attach 将对象附加到对象上下文。当对象已存在于数据源中但当前尚未附加到上下文时采用此方法。有关更多信息,请参见如何:附加相关对象(实体框架)。
- 调用 ObjectContext 的 AttachTo,以将对象附加到对象上下文中的特定实体集。如果对象具有 null(在 Visual Basic 中为 Nothing)EntityKey 值,也可以执行此操作。
- 调用 ObjectContext 上的 ApplyPropertyChanges。当对象已存在于数据源中,并且分离的对象具有您希望保存的属性更新时采用此方法。如果简单地附加该对象,则属性更改将丢失。有关更多信息,请参见如何:应用对已分离对象的更改(实体框架)。
2) 应用对已分离对象的更改(实体框架)示例代码
private static void ApplyItemUpdates(SalesOrderDetail updatedItem)
{
// Define an ObjectStateEntry and EntityKey for the current object.
EntityKey key;
object originalItem;
using (AdventureWorksEntities advWorksContext =
new AdventureWorksEntities())
{
try
{
// Create the detached object's entity key.
key = advWorksContext.CreateEntityKey("SalesOrderDetail", updatedItem);
// Get the original item based on the entity key from the context
// or from the database.
if (advWorksContext.TryGetObjectByKey(key, out originalItem))
{
// Call the ApplyPropertyChanges method to apply changes
// from the updated item to the original version.
advWorksContext.ApplyPropertyChanges(
key.EntitySetName, updatedItem);
}
advWorksContext.SaveChanges();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.ToString());
}
}
}
- 实现动态条件查询。在本地环境中,对于Linq,我们可以通过动态构造Lambda表达式树来实现动态条件查询,但是在远程环境中,Lamdba表达式不支持远程序列化传输,只能通过ObjectContext的CreateQuery方法实现,但幸好微软后来又提供了一个LINQ动态查询扩展库Dynamic.cs,使用起来更方便,于是采用它实现。
- EF中核心抽象类是ObjectContext,实体容器都从它派生,实体容器上的CUD方法 其实都是通过调用ObjectContext的CUD操作方法实现的。
1) AddObject(string,object):表示添加实体object到实体容器,只要实体的EntityKey值为空,无论是否Detached状态均可以通过此方法实现添加操作。
2) ApplyPropertyChanges(string,object)表示把分离状态的实体object上的所作的修改更新回容器中已存在的对应的实体,执行条件有两个:①实体处于分离状态,②实体容器中存在主键值与其相同的且为Unchanged状态的实体,所以,当我们需要更新一个Detached状态的实体时,可以先把一个具有原始值的相同键值的实体附加回容器中,或者直接执行一下查询,从数据库中取出该实体。
3) DeleteObject(object)表示从实体容器中删除一个实体,执行条件是该实体存在于实体容器中,所以删除一个Detach状态的实体之前,需要把它通过Attach方法附加回实体容器中。
- 实体对象也是基于抽象类EntityObject派生的,由此我们完全可以用ContextObject和EntityObject实现服务端对实体的查询和CUD方法,其实现子类在运行时由客户端注入,从而使服务端和数据库实现松耦合。
- 下图是MSDN上关于在数据访问层中使用 LINQ to SQL 的 n 层应用程序的基本体系结构图,其实EF的结构也是一样的,不过是把DataContext换成ObjectContext。
四、 动手开发
- 利用EF建立数据库概念模型
新建一个解决方案EFServiceSystem,添加一个新项目,命名为EFModel,添加项目,在项目下添加一个ADO.NET Entity Data Model项,命名为EFModel.edmx,选择从数据库生成(假设我们已经建好了一个SQL Server数据库),一路点击下一步,直至完成。编译项目成功后就算完成。为什么要把数据库模型单独编译成一个dll呢,我将在后面给予解释。
- 建立数据服务层
在解决方案下再添加一个类库项目,命名为EFService。
1) 利用外观模式,我们把客户端常用的查询和CUD操作方法简化为3个方法Query<T>,Save(T t),Delete(T t),根据针对接口编程的设计原则,定义一个CUD方法接口供客户端调用。
using System;
using System.Collections.Generic;
using System.Text;
namespace EFService
{
public interface IEntityHelper
{
List<T> Query<T>(string filter,params object[] args);
T Save<T>(T t);
int Delete<T>(T t);
}
}
2) 实现类EntityHelper的代码。主要思路是通过构造函数注入数据上下文实例名称,在配置文件取出其程序集限定名,通过反射创建实例,调用实例的相应方法实现接口。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Reflection;
using System.Linq;
using System.Linq.Dynamic;
using System.Runtime.Remoting;
namespace EFService
{
/// <summary>
/// 为远程客户端提供实体查询和CUD操作的服务类
/// </summary>
public class EntityHelper : MarshalByRefObject, IEntityHelper
{
/// <summary>
/// 在config文件中配置的数据上下文名称
/// </summary>
string ContextName;
ServiceFactory factory = new ServiceFactory();
public EntityHelper(string contextName)
{
ContextName = contextName;
}
/// <summary>
/// 创建数据上下文实例
/// </summary>
/// <returns></returns>
public ObjectContext CreateObjectContext()
{
string typeName = System.Configuration.ConfigurationManager.AppSettings[ContextName].ToString();
return (ObjectContext)Activator.CreateInstance(Type.GetType(typeName));
}
/// <summary>
/// 查询操作
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filter">查询条件组合</param>
/// <param name="args">查询条件中的参数值</param>
/// <returns>返回实体集合</returns>
public List<T> Query<T>(string filter, params object[] args)
{
using (ObjectContext context = CreateObjectContext())
{
return (context.GetType().InvokeMember(
typeof(T).Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty,null, context, null)
as IQueryable<T>).Where(filter, args).ToList();
}
}
/// <summary>
/// 保存实体
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t">要添加或修改的实体</param>
/// <returns>返回添加或者更新后的实体</returns>
public T Save<T>(T t)
{
EntityObject eo = t as EntityObject;
using (ObjectContext context = CreateObjectContext())
{
if (eo.EntityKey == null)
{
context.AddObject(t.GetType().Name, t);
}
else
{
object target = ontext.GetObjectByKey(eo.EntityKey); context.ApplyPropertyChanges(eo.EntityKey.EntitySetName, eo);
}
context.SaveChanges();
return t;
}
}
/// <summary>
/// 删除实体
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns>成功删除的实体的数量</returns>
public int Delete<T>(T t)
{
EntityObject eo = t as EntityObject;
using (ObjectContext context = CreateObjectContext())
{
if (eo != null && eo.EntityKey != null)
context.Attach(eo);
context.DeleteObject(eo);
return context.SaveChanges();
}
}
}
}
3) 最后,我们创建一个服务工厂类,暴露给客户端,负责以接口方式向客户端提供远程服务对象,数据服务层创建完毕。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.Configuration;
namespace EFService
{
/// <summary>
/// 远程服务类工厂,负责创建服务对象
/// </summary>
public class ServiceFactory : MarshalByRefObject
{
/// <summary>
/// 创建远程服务对象
/// </summary>
/// <param name="connStringName">数据上下文名称</param>
/// <returns>远程服务接口</returns>
public IEntityHelper CreateEntityHelper(string contextName)
{
return new EntityHelper(contextName);
}
}
}
- 创建运行服务的宿主程序。实际开发中,通常选择创建一个windows服务程序来运行Remoting,但是服务需要安装才能启动,运行和调试起来都比较繁琐,所以这里创建一个简单的控制台程序来运行它。在解决方案下添加一个控制台程序项目,在program.cs编写如下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
namespace EFServiceHost
{
class Program
{
static void Main(string[] args)
{
ChannelServices.RegisterChannel(new TcpChannel(9932),false);
RemotingConfiguration.ApplicationName = "EFService"; RemotingConfiguration.RegisterActivatedServiceType(typeof(EFService.ServiceFactory));
Console.WriteLine("Start Server,Press any key to exit.");
Console.ReadLine();
}
}
}
配置文件App.Config主要包括数据库连接信息以及自己定义一个数据上下文名称(这里和数据库连接名称相同,事实上不必相同),数据库连接信息可以从EFModel项目中配置文件中直接拷贝过来。内容如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="SchoolEntities" connectionString="metadata=res://*/EFModel.csdl|res://*/EFModel.ssdl|res://*/EFModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=192.168.0.110/SQLEXPRESS;Initial Catalog=School;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" >
</add>
</connectionStrings>
<appSettings >
<add key="SchoolEntities" value="EFModel.SchoolEntities, EFModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</appSettings>
</configuration>
编译成功后,拷贝EFModel和和EFService两个项目生成的dll文件至可执行文件EFServiceHost.exe同一目录下,点击运行EFServiceHost.exe。
- 最后,我们建立一个winform客户端作为测试。在program.cs注册远程服务:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
//注册远程服务
RemotingConfiguration.RegisterActivatedClientType(
typeof(ServiceFactory), "tcp://localhost:9932/EFService");
Application.Run(new Form1());
}
添加一个窗体Form1,放入一个按钮,在按钮点击处理事件里加入下面的代码,示范客户端如何调用远程服务类实现查询和CUD操作,简单起见,我不演示数据库的数据变化了,反正,看代码,你懂的。
private void button1_Click(object sender, EventArgs e)
{
//通过远程服务工厂创建出远程服务对象,注意此处构造函数的参
注意此处参 // 数值为远程服务器上配置文件上的数据上下文名称
ServiceFactory factory = new ServiceFactory();
IEntityHelper entityHelper = factory.CreateEntityHelper("SchoolEntities");
//查询实体
List<Course> courses = entityHelper.Query<Course>("CourseID>@0",0);
Course course = courses.SingleOrDefault(p => p.CourseID == 1045);
//修改实体
course.Title = "update succeed";
entityHelper.Save(course);
//删除实体
entityHelper.Delete(course);
//新增实体
entityHelper.Save(new Course() { Title = "new course" });
}
五、 部署应用
- 至此,整个系统搭建完毕。在本例中,我把所有项目都统一建立在一个解决方案下,其实是为了演示方便,实际开发时候,完全可以各自独立创建。下面我们来分析一下各个项目的职能和相互之间的引用关系。
1) EFModel:由Visual Studio 的数据模型工具生成的数据库实例模型,提供数据的查询以及CUD操作。不需引用其它项目。
2) EFService:使用数据库实例模型以及实体的抽象基类编写完成,代码里不涉及具体数据库模型实例,运行时通过客户端注入参数和读取配置文件动态生成数据库模型实例,并调用实例的查询和CUD方法实现客户端的请求。不需引用其它项目。
3) EFServiceHost:负责运行Remoting服务,如果通过配置文件方式发布服务的话,编译时也不需引用其它项目,我这里引用了EFService项目,是因为使用了代码方式暴露EFSservice的服务类。运行时需要将EFService和EFModel的dll文件拷贝至运行目录下。
4) EFClient:需要引用EFModel和EFService。(注:因为本例中式使用了Remoting作为远程服务,如果是WebService或者WCF则只需添加服务引用,然后在本地生成客户端代理类)。事实上EFService中的实现类EntityHelper也可以独立出去,不必让客户端引用,对于客户端而言,仅仅是使用ServiceFactory和接口IentityHelper就足够了。这样只要接口不变, EntityHelper更新的时候,客户端无须更新引用,而且服务端代码可以完全被隔离开客户端,对一些服务端和客户端之间的保密性比较敏感的项目尤为有利。
- 通过分析我们发现,在开发下一个新项目的时候,即使整个数据库都变了,从SQL SERVER变成Oracle,数据库服务名变了,表也变了,我们仍然无需修改服务端代码,只需针对新的数据库,生成新的EFModel,然后拷贝DLL文件至EFServiceHost的运行目录下(这也就是我为什么要把EFModel独立成一个项目的原因),再修改一下EFServiceHost的配置文件中的数据库连接和实体容器名称即可完成新系统的部署。对于客户端来说,也就是更新一下EFModel.dll,还是调用服务端提供的那几个API,便可完成查询和CUD操作,不用关心底层的数据库是SQL Server还是Oracle,更不用自己实现对新库新表的查询和CUD操作(本来也不用)。当然,对于正在运行的系统,我们也可以针对新建数据库生成新的实体模型DLL,拷贝至EFServiceHost运行目录下,实现热插拔方式扩展数据库,而对原来的系统毫无影响,即使新加的库是不同类型的库。
六、 系统架构图示
七、 总结
从以上分析可以看出,该系统运用到项目开发中,对服务端来说,实现了最大程度组件重用(零代码修改),对客户端开发来说,高度简化了对数据的操作命令,并封装了实现细节,大大降低了开发的技术难度,提高了开发速度。当然,我这里写的代码仅仅是最简单的演示代码,在实际项目开发中,服务端要处理的细节和扩展的功能要比这复杂得多。比如性能优化,实现复杂查询和批量CUD操作,并发处理,事务控制,日志跟踪,数据缓存等等。另外,如果各层采用不同的技术实现,服务层实现的代码也有差异。比如EF可以选择最新版的更完善更强大的EF4.0,远程服务可以选择Remoting,WebService,WCF等,不同的远程服务,宿主程序也有所不同,Remoting和WCF可以选择winform,控制台程序,IIS,而Web Service只能选择IIS。不同的服务,不同的宿主程序,会有不同的通信通道 (Http,Tcp),不同的数据传输格式 (二进制,XML,JSON)。如果你嫌上面的实现方式涉及的技术太多,开发起来太麻烦,那么,微软现成的具有REST风格的远程数据服务WCF Data Services会是你的最佳选择。WCF Data Services由于只用Http方式通信,用XML或JSON格式传输数据,性能上会比用Tcp通信二进制格式传输的性能差一些,但是相信在将来基于SOA的分布式系统大行其道时,WCF Data Services会是.net实现SOA框架的主流技术,这里基于篇幅所限,不作详细介绍了,MSDN上有这方面的很详尽的技术参考文档,有兴趣的读者可以自行参阅。