2013年12月24日星期二

使用Entity Framework和WCF Ria Services开发SilverLight

使用Entity Framework和WCF Ria Services开发SilverLight之1:简单模型


本文目的是通过Silverlight ria service完成一次数据的读取过程,并且在此基础上建立测试项目。
Ria service借助于WCF和ADO.NET Entity Framework构建分布式开发框架。使用它可以快速构建自己的开发模式。

1:基础结构
首先,创建SL APP,如下:
image
然后,选择创建web:
image
可以,也可以不勾选enable wcf ria service,如果勾选了,在SL APP中会多几个DLL的引用,其它没有任何差别。如图:
image
2:创建一个示例数据库
这是一个小而轻型的数据库,在MDSN的课程中有使用到它。如下:
3:RIA之ADO.NET ENTITY DATA MODEL
为web创建ADO.NET ENTITY DATA MODEL,如图:
image
注意,习惯命名规则:Model数据库名.edmx。
在下一步中,我们选择刚才创建的数据库:
image
选择新建连接,按照指示完成数据库连接配置。
进入下一步:
image
在这一步中,注意一定要选择第一个红框中的内容,否则默认不能生成联表查询。在下一步中,我们选择“Generate from database”,如下:
image
点击Finish后,VS为我们生成了一个以edmx为后缀的文件。OK,到此暂停,我们先来看看这个edmx文件是干什么用的,为我们完成了什么工作。
3.1:什么是EDM
ENTITY DATA MODEL,简写为EDM,中文为实体数据模型。它由三个概念组成。概念模型由概念架构定义语言文件 (.csdl)来定义,映射由映射规范语言文件 (.msl),存储模型(又称逻辑模型)由存储架构定义语言文件 (.ssdl)来定义。这三者合在一起就是EDM模型。EDM模型在项目中的表现形式就是扩展名为.edmx的文件。
Entity Framework实现了一套类似于ADO.NET2.0中的连接类来操作EDM完成持久化。EntityFramework中所有发往EDM的操作都是经过EntityClient,包括使用LINQ to Entity进行的操作。目前可用操作如下:
image
4:针对web EMD的测试
理解了EMD在开发中所处的作用,我们就可以针对EDM来写个测试项目。为了简便期间,我们直接在EMD中的SchoolEntities类型的构造方法中直接创建测试(严格意义来说,这不是个单元测试,这仅是测试)。
image
针对这个操作,会在测试项目中生成一个SchoolEntitiesTest的类型,同时,在这个类型中会生成一个SchoolEntitiesConstructorTest的方法,如下:
image
由于我们仅仅测试EDM,而跟WEB本身没有关系,所有我们注释掉了红框中的内容。同时我们写入真正的测试代码,如下:
image
这里的一个小细节是,连接字符串我们通过编码的方式传入到测试方法。EDM的连接字符串和ADO.NET的连接字符串有很大不同,在这里,可公开一下我们的生成EDM连接字符串的方法:
调试我们的测试代码,最终我们会获取到数据库中的4条记录。
image
通过了以上的阐述,我们了解了EDM的作用,以及如何测试EDM。接下来,我们需要知道如何在SL APP中调用WEB中的EDM。
5:Domain Service Class
在SL APP中调用EDM是通过WEB的Domain Service Class来实现的。简单的说来,它是RIA SERVICE框架中的一个重要内容。
在WEB选择添加新项,选择Domain Service Class,命名,下一步会出现如下界面:
image
我们可以做出如上勾选。确定后,编译整个解决方法,会发现目前的解决方案结构会变成如下形式:
image
注意,第一个红框部分,是VS自动为我们在SL APP生成的。它是第二个红框在客户端的对应版本。我们需要将它包含到项目中去。它包含了所有服务端版本类中定义的方法、实体等,而可在客户端直接调用。
6:Ria Service数据流转
好了,经过以上的描述我们知道了:
EDM:直接操作数据库;
Domain Service Class:调用EDM,并把数据接口通过WCF的形式开放给客户端;
*.Web.g.cs:调用Domain Service Class,完成对数据的读取,并最终呈现给UI;
7:最终展示
在SL APP中的UI调用呈现数据。前台:
image
后台,直接使用*.Web.g.cs中的DomainServiceMySample 加载数据:
public MainPage()
{
    InitializeComponent();
    DomainServiceMySample context = new DomainServiceMySample();
    context.Load(context.GetEmpQuery());
    lb1.ItemsSource = context.emps;
}
最后界面显示:
image
8:问题
该示例演示了使用Entity Framework和WCF Ria Services进行Silverlight开发。我们当然可以继续使用此模式完善功能,知道一个功能复杂的应用程序开发完毕。但是,当前,此示例起码存在如下几个问题。
1:实体模型被紧耦合在EDM中,同时它不能项目(模块)使用。随着每一次更新EDM,实体模型会被覆盖;
2:EDM和BLL紧耦合在一起;
3:没有提炼出数据接口,导致我们没有办法在此示例中进行单元测试。示例中虽然存在测试项目,但那是数据库相关的,达不到单元测试的要求;
下篇我们将继续重构该示例,以逐步解决这些问题。

使用Entity Framework和WCF Ria Services开发SilverLight之2:POCO


在上一篇中《使用Entity Framework和WCF Ria Services开发SilverLight之1:简单模型》我们提出这类简单模型的几个问题:
1:实体模型被紧耦合在EDM中,同时它不能项目(模块)使用。随着每一次更新EDM,实体模型会被覆盖;
2:EDM和BLL紧耦合在一起;
3:没有提炼出数据接口,导致我们没有办法在此示例中进行单元测试。示例中虽然存在测试项目,但那是数据库相关的,达不到单元测试的要求;
1:创建POCO
本篇我们主要来解决第一个问题,那就是在EF中使用Plain Old CLR Object(简称POCO)。POCO是我们最熟悉不过实体模型。本示例所用的数据中Department的POCO形式为:
如果不使用POCO,我们可以在EDM的后台代码中找到这个类的定义,它看上去增加了很多Attribute,并且复杂多了。
为了便于实体模型被更多的项目使用,我们为提升所有的实体模型到一个新创建的项目中,名为SchoolModel。
2:使用EF创建数据访问层
在本篇中,我们不打算再把数据访问耦合在WEB中,而是为其新建了一个项目,名为SchoolData。现在,我们已经创建了本篇所要展示的4个项目,如下:
image
接着,我们按照第一篇中步骤,在SchoolData中创建EDM。由于本文已经采用POCO,所以我们必须关闭EF自动为我们的EDM生成实体模型。要关闭代码生成,在Model.edmx上点属性,然后,清楚自定义工具中的内容。
image
关闭代码生成后,我们会发现EDM的后台文件消失了。
接着,手动创建SchoolContext,完成数据库操作。如下:
(你应该修改conn,以满足你的开发设置)。到目前为止,我们知道,项目SchoolModel可以被任何其它项目使用,SchoolData也同样,它即可以被WEB引用作为RIA SERVICE公开给SL APP,也可以被类似Winform的项目使用,这很COOL,离我们的目标越来越近了。
3:创建Ria Service
本步骤,我们仍旧将DomainService创建在WEB中,名为DomainServiceSchool。
image
由于我们已经把数据访问层,也就是EF解耦出去了,所以我们无法和在第一篇中一样,在这里选择DataContext了。没有关系,确保选择“Enable client access”,点OK。
这个时候我们去查看DomainServiceSchool,由于它已经不直接从EF中获取数据(确切滴说,它也不应该知道数据访问层通过什么来获取数据),所以它的基类,已经从LinqToEntitiesDomainService<SchoolContext>变为DomainService。
4:DomainServiceSchool从数据访问层SchoolData获取数据
DomainService不会再直接从SchoolContext获取数据,也就是说,DAL需要负责公开自己的数据接口给DomainServiceSchool。比如,为了获取Department数据,我们需要在DAL中创建一个类型DepartmentRepository,由该类型负责向DomainServiceSchool公开数据接口。DepartmentRepository的实现如下:
接着,我们在DomainServiceSchool调用它,如下:
5:小结
SL APP部分和第一篇一样,本篇不在赘述。
本篇源码下载:SilverlightApplication20110612.zip(要让程序可以运行,需要安装第一篇中的数据库)
通过以上的改造,我们的系统架构呈现这样的形式:
image
1:SchoolData(DAL),SchoolModel(Object Model),作为公共模块,可以提供给其它调用方;
2:Winform或者Web作为模块的消费者;
3:SilverLight通过Ria Services消费Web中的DomainService。


使用Entity Framework和WCF Ria Services开发SilverLight之3:Map
在上一篇中《使用Entity Framework和WCF Ria Services开发SilverLight之2:POCO》,我们将映射交给edmx文件处理。如下:


image
该文件是由EF框架自动生成的,通过查看原始文件,我们可以看到EF框架为我们建立了实体类间的各种映射。现在,借助于EF4.1框架,我们可以进行更方便的进行手动映射。

1:清理SchoolData
首先,删除ModelSchool.edmx。
其次,由于不再使用edmx映射,我们将SchoolContext中数据库连接串,换成传统的连接串。即:
image
上图中的红框部分,换成:
image
相信你和我一样,非常熟悉这样的连接字符串。
二:手动建立Map
在SchoolModel中,我们建立一个Mapping子目录,将所有的映射类全部建立在这个位置。如DepartmentMap:
需要注意的一点,理论上,我们可以借助于Map,生成完全意义上的Poco,因为我们使用EF4.1框架,通过重写DbContext类型的OnModelCreating方法,如下:
我们完全可以动态为POCO实体类生成EF所需要的各种映射关系。但是,很遗憾,在这里我们使用的Ria Service。在使用Ria Service的使用,必须要为Poco指定一个Key(这是一个BUG?),否则,我们会得到这样的一个错误:
而如果我们在使用别的应用,我指的是仅仅使用EF,则不会有这样的编译期错误出现。
所以,这是一个小小的瑕疵,请为POCO使用KEY特性。当然,别的映射关系我们可以在Map类中处理。
最后的运行效果为:
image
三:使用Entity Framework Power Tools CTP1自动生成POCO和映射
也许是考虑到手动生成POCO和映射太繁琐了,现在微软为我们提供了这样一个工具为我们自动生成POCO和映射。你需要到这里去下载它:http://visualstudiogallery.msdn.microsoft.com/72a60b14-1581-4b9b-89f2-846072eff19d
安装完毕后,请首先生成一个C#类库项目,然后在这个类库项目上点右键,我们会发现多了一个菜单项:
image
然后,在下一界面,输入相应参数:
image
注意:一定要注册图中的红框部分。请选择第一项。如果我们使用sql server身份认证,则极有可能出现如下错误:
有博友曾经对此问题进行过讨论,但是仍旧无解,具体请见博文及文后回复《Entity Framework Code First使用者的福音 --- EF Power Tool使用记之二(问题探究)
最后,该工具为我们生成的代码大致如下:
image
如果对手动映射感兴趣,还可以参考下列文章:


使用Entity Framework和WCF Ria Services开发SilverLight之4:Map之主外键映射

上一篇粗粗讲了一下如何使用EF4.1,针对POCO进行MAP,此篇在此基础上进行一下深入,具体讲一下如何进行映射。
1:主外键中的1对多映射
还是针对School数据库。查看如下两个表:
image
可以知道,两者的关系是:
image
也就是说,Department对于Course来说,是1对多的关系。数据库中实际的数据如下:
image
1.1:对应的实体类
Department实体类(关系中的1):
image
Course实体类(关系中的多):
image
1.2:对应的映射类
DepartmentMap映射类,对于Department来说,无需针对Course的特殊说明:
image
CourseMap映射类,对于Course来说,需要对Department指出,自己是多,Department是1:
image
2:主外键中的1对1映射
image
对应的数据:
image
可见,Course实体对于OnlineCourse来说,是一对一中的left join关系。
2.1:对应的实体类
Course实体类:
image
OnlineCourse实体类:
image
从以上两个实体类来看,这种一对一的主外键关系,并没有任何的特殊之处。
2.2:对应的映射类
CourseMap映射类我们在1.2中已经展示过了,并无针对OnlineCourse的特殊之处。
OnlineCourseMap映射类:
image


使用Entity Framework和WCF Ria Services开发SilverLight之5:客户端主外键联表查询
在上一篇中我们对EF中的POCO进行了映射,如果这个时候我们使用主外键来进行联表查询,会很遗憾的发现凡是关联表的信息,在客户端获取的全部是null。
本篇我们介绍如何使用Entity Framework和WCF Ria Services在SL客户端获取主外键联表查询的数据。
1:添加引用
第一步,为Model项目添加引用System.ServiceModel.DomainServices.Server,如下:
image
我们接下来需要使用的特性Include是在此DLL下。
2:为实体类添加特性
image
为实体类添加的映射有两部分内容需要说明:
首先,必须加Include特性,否则,即使我们的DAL所查询出来的数据包含了外键表Course的数据,通过Ria Services传送到客户端的也会是null;
其次,Assocaition指定了两个实体类之间的关系。第一个参数,可以随便取,第二个参数对应的当前类Department的Key,也就是数据库中的逐渐,第三个参数对应的Course实体类中的外键属性。
Department和Course的数据库表对应关系为:
image
3:开放Ria Service
DAL部分代码:
image
你可能会觉得奇怪,但是代码中的红框部分,并不是必须的,只要我们指定了实体中的Include和Association属性,获取的Department对象中,就已经包含了Courses信息。
Ria Service部分:
image
4:最后的运行结果
image
数据库请参看本系列第一篇。


使用Entity Framework和WCF Ria Services开发SilverLight之6:查找指定字段
对数据库表指定字段的查找,又是实际工作中的一项必要工作。SL客户端仅获取实际需要的指定的字段,好处很多,比如:有助于减少网络流量。
有两类这样的使用场景。
1:联表查询不需要外键表
在上一篇中,我们使用了联表查询,在联表查询中,DAL部分的代码是这样的:
image
我们已经指出,无论是你是否使用Include方法,只要实体类指定了Include特性,客户端都会获取到被关联的Course记录。诚然,有时候我们需要满足联表查询,但是其他时候我们不需要。
2:单表中仅仅需要部分字段
比如:一个表有5个字段,我仅需要返回两个字段。
3:实现
要满足这样的需求,仅仅需要修改DAL部分。
第一种需求,取出指定字段:
image
这段代码所生成的SQL语句如下:
SELECT
[Extent1].[DepartmentID] AS [DepartmentID],
[Extent1].[Name] AS [Name]
FROM [dbo].[Department] AS [Extent1]
可以发现,EF只为我们查询两个字段。并且,EF也没有为我们自动关联Course表的数据。
注意,在这段代码中,我们先select new了一个匿名类型,这样就绕过了DbContext中的DbSet<T>对于Department类型校验。如果我们想当然的将这段代码写成:
image
那么,我们会发现无法获取数据,跟踪错误,会发现:
   {"The entity or complex type 'SchoolData.Department' cannot be constructed in a LINQ to Entities query."}    System.SystemException {System.NotSupportedException}
最终运行的效果如下:
image
回过头来说,如果既要指定字段,又要联表查询,应该怎么做呢?如下:
image
最终运行效果如下:
image
由于本部分的代码在上一篇中修改而成,故不再提供下载。
PS:整了一圈回来,才发现DUDU已经把这个问题研究的很深刻了,在:



使用Entity Framework和WCF Ria Services开发SilverLight之7:多个Domain Service间共享Poco实体
在前两篇中,我们讲了在SL端通过Entity Framework和WCF Ria Services实现联表查询,其中最关键的是为实体类中的相关属性设定为Include特性,如下:

image
遗憾的是,这样的实体Include只能在单个域服务间共享实体。如果你尝试撰写多个域服务,并用到关联实体,会报如下错误:
The entity type 'MiniNWModel.Entities.Product' is exposed by multiple DomainService types.
Entity types cannot be shared across DomainServices.    SilverlightApplicationSample
有人指出,域服务上下文应该是long life的,所以整个应用程序应该只有一个域服务。不过,这难道不是有点扯吗?应用程序所支撑的数据库动辄上百张表,不同的业务范畴建立多个域服务基本是必须的。
一:数据库支撑
本篇所采用的数据库来自于《Entity Framework 4.1 and Poco 使用存储过程联表查询》。
二:数据实体
本篇共涉及两个实体,要让两个实体在多个域服务间共享彼此,必须依赖特性ExternalReference。以下是主表实体:
image
以下是从表实体:
image
一定要注意正确匹配Association,否则关联数据的时候会不正确。
三:域服务
两个域服务类没有任何特殊之处。唯一需要注意的是,在调试的时候我们会发现EF联表查询中得到的数据,如果不做特殊处理,从表数据全部会丢失。如下图源码:
image
在方法GetCategoryWithProductsWithID中,我们会得到从表的数据,这是EF为我们得到的,但是千万不要以为数据会被Ria Service带到SL客户端。
四:SL获取主从表数据
如果SL的某个功能需要同时得到主从表数据,我们必须同时提供包含这两个实体的域服务,在下图代码中,我们首先创建好这两个域服务(图中1)。然后,首先我们必须获取主表数据(图中2),其次是从表数据(图中3),最后,SL客户端要指定两者的关联(图中4):
image
获取主从表的全部代码如下:
public class PrinSubVm : NotificationObject
    {
        public DomainServiceCategory DomainServiceCategory { get; set; }
        public DomainServiceProduct DomainServiceProduct { get; set; }
        private IList<Category> categoryWithProducts;
        public IList<Category> CategoryWithProducts
        {
            get { return categoryWithProducts; }
            set { categoryWithProducts = value; this.RaisePropertyChanged<IList<Category>>(() => this.CategoryWithProducts); }
        }
        public PrinSubVm()
        {
            DomainServiceCategory = new Web.DomainServiceCategory();
            DomainServiceProduct = new Web.DomainServiceProduct();
            //获取CategoryID(cid)为1的目录
            DomainServiceCategory.Load<Category>(DomainServiceCategory.GetCategoryWithProductsWithIDQuery(1), new Action<System.ServiceModel.DomainServices.Client.LoadOperation<Category>>(this.GetCategoryWithProductsWithIDCallBack), null);
            //获取CategoryID(cid)为1的目录下的商品
            DomainServiceProduct.Load<Product>(DomainServiceProduct.GetProductsByCategoryIDQuery(1), new Action<System.ServiceModel.DomainServices.Client.LoadOperation<Product>>(this.GetProductsByCategoryIDCallBack), null);
            //为多个域之间共享实体
            DomainServiceCategory.AddReference(typeof(Product), DomainServiceProduct);
        }
        void GetCategoryWithProductsWithIDCallBack(LoadOperation<Category> arg)
        {
            CategoryWithProducts = arg.Entities as IList<Category>;
        }
        void GetProductsByCategoryIDCallBack(LoadOperation<Product> arg)
        {
            //ProductAndCategorys = arg.Entities as IList<Product>;
        }
    }
有一点我们必须注意,如果要获取从表的数据,仅获取需要的从表记录就可以了,不要加载全部记录,想想那些动辄几百万记录的业务表。SL客户端会自动根据实体的KEY值去关联。
反过来,我们也可以实现从表关联主表。在这里就不一一举例了。但是最后的UI可以作为演示。
image
image
image
本文源码下载:SLOperation20110705.rar