第一章:欢迎来到Code First

微软的ADO.NET实体框架(Entity Framework),泛称EF,把外部(out-of-the-box,译成“外部”不一定对,我是这样理解的:the-box可能意指.NET框架,合起来的意思就可能是“外部”了 — 译者ro1cn注)的对象关系映射(ORM)引到了.NET和Visual Studio。实体框架的核心是实体数据模型,这是应用程序域向数据库架构进行映射的一个概念模型。这个概念模型描述了应用程序中的核心类。实体框架在查询数据库,或根据数据创建对象,然后把修改持久化回数据库时,都会使用这个概念模型。(译者注:此段全文引用r01cn园友的译文,确实有深度,佩服!)

在编写代码前构建EF模型

在使用.Net 3.5和Visual Studio 2008构建的第一代EF中,给予了开发者通过将现存数据库转化为XML文件的形式来创建概念模型的能力。这种XML文件使用EDMX扩展名,可以使用设计器来查看和定制模型以便更好地适应程序域。Visual Studio 2010 and .NET 4带来了EF的新版本:称之为Entity Framework 4 (EF4)。EF4还引入了一种称为Model First的功能,通过此功能可以先创建概念模型然后根据此模型再创建数据库。

Model First 允许开发者在没有传统的数据库时就可以开始一个新的项目,这是EF框架带来的好处。开发者可以通过设计概念模型而首先关注应用程序域,从而让数据库的创建工作水到渠成。

无论是通过database-first还是通过model-first设计EDMX,下一步都是为了创建您的域以便让自动代码生成器基于实体以及找到的各种关系来创建类。通过这种方式,开发人员拥有强类型的类来指向域对象,无论它们是客户,棒球卡,还是童话人物,都可以围绕着这些类来愉快地开发软件应用。

另一个关键的变化在EF4。在.NET 3 5,实体框架只能够管理处于内存中的对象。这些对象的类都继承自实体框架的EntityObject对象。EntityObject对象向实体框架通知变更,并由实体框架跟踪这些变化并最终持久化到数据库中。NET 4中不仅仍有这些功能,还推出了 POCO(Plain Old CLR Object,简单传统CLR对象),支持实体框架跟踪简单类的变化而不需要EntityObject的参与,这使开发者可以自由使用自己的类,从而独立于实体框架。EF在运行时可以通过监视对象在内存中的行为保持对这些类的响应和跟踪。

Code First的发起

基于前面介绍的有关EF4的片断,我们可以看到,微软已经提供了多途径的建模方式,其中有一种新的方式是在EF发布之初,开发人员就所期待的。这种新型的建模方法就是所谓的Code First. Code first 首先让您定义域模型,而不是使用基于XML的edmx文件。即使采用Model First或Database First来生成代码,开发者也必需使用设计器或类生成器来辅助工作。而使用Code First您可以直接通过使用POCO类来定义域模型,而无需依赖于EF框架。Code First可以通过构建的类模型推断出大量信息。你也可以提供额外的配置,进一步描述有关模型的信息或者是覆写Code Firstr推断的信息。这种配置也是用代码来定义的,不需要XML文件或设计器。

使用设计器工作的EF4也支持POCO技术。EF开发团队提供了一种POCO模板来生成POCO类(可以使用NuGet下载这种T4模板--译者注)。这些生成的类可以随着设计器的更改而自动更新。也可以使用你自己创建的POCO类。但如果你决定这样做,就需要负责保持您的类与EDMX文件的同步。这意味着任何变更都必须在两个位置进行。Code First的一个最大好处就是让你的类变成了模型。这意味着改变模型就只需要在一处(你自已的POCO类)中作出更改即可。

Code First, Database First, 和 Model First都只是一种建立可用于EF框架中以进行数据访问的实体数据模型的方式。一旦模型构建,EF框架在运行时的表现是相同的,与如何构建模型无关。选择从设计器开始还从代码开始完全取决于您的选择。图1-1列出了不同的可选项。

微软将Database First, Model First, Code First分别形成了可选的工作流程。这是因为每种选项都是一系列步骤,无论这些步骤是手工实施还是自动进行。例如,使用Database First工作流,你需要将数据库转化为工程,然后让代码生成器创建类。而Code First工作流则开始于类的代码构建,然后用代码生成所用的数据库。

在.NET发行版本中间获取Code First

Code First 在.Net4发布时尚未准备好。微软不想将其在.Net5发布时才带给开发者,就在2011年4月推出了"特别"版,称为EF4.1.版本号随后在更新时顺延。2011年10月,EF4.2推出 ,替代了EF4.1,作为包含Code first的最新版本。核心API,System.Data.Entity.dll,仍然是.Net Framework的一部分,没有与EF4.1,4.2紧密连接。EF4.2还包含了另一个重要的功能,称之为DbContext API.DbContext是这个API的核心,除此之外还包含其他支持类。DbContext是一个轻量级的EF ObjectContext.这是一个包装过的ObjectContext,只暴露了那些微软认为大多数开发人员最常用的EF功能。DbContext也支持通过代码模式访问由ObjectContext所提供的复杂功能。DbContext提供了大量的通用任务,可以使开发者用较少的代码完成这些任务,这在使用Code First时尤其明显。由于微软推荐使用DbContext来进行Code First操作,您会看到本书将贯穿始终介绍此方法。但是,另一本书,叫做Programming Entity Framework: DbContext,将深入探讨DbContext,DbSet,验证API以及DbContext带来的其他特征。

图1-2帮助你查看如何通过构建EF4 API核心添加Code First和DbContext功能。

灵活的发布日程

微软将继续通过VS提供的NuGet工具发布基于EF4.2框架的新功能。EF的核心库将会随着.Net的新版本的发布而内置在其中。但是诸如CodeFirst 和DbContext等核心功能会在EF的NuGet包中不断得到更新。

先输入代码。。。

Code First 是个好听的名字:先写代码,然后继续。让我们先看看一些基本的默认功能,不要管各种你可能会遇到的场景(后续的内容我们会专注于此)。

我们并不期望你重建本章的代码案例。这些案例只是作一概览,并不会深入。在第2章,你将会逐步深入。你将会跟随我们在VS中的操作进行,然后尝试你想实现的功能。

当然,首先需要一些代码,以便足以描述业务域。本案例是关于一个宠物医院收治患畜的业务模型。

Example 1-1 Domain classes

using System;
using System.Collections.Generic;
namespace ChapterOneProject {
    class Patient {
        public Patient() {
            Visits = new List < Visit > ();
        }
        public int Id {
            get;
            set;
        }
        public string Name {
            get;
            set;
        }
        public DateTime BirthDate {
            get;
            set;
        }
        public AnimalType AnimalType {
            get;
            set;
        }
        public DateTime FirstVisit {
            get;
            set;
        }
        public List < Visit > Visits {
            get;
            set;
        }
    }
    class Visit {
        public int Id {
            get;
            set;
        }
        public DateTime Date {
            get;
            set;
        }
        public String ReasonForVisit {
            get;
            set;
        }
        public String Outcome {
            get;
            set;
        }
        public Decimal Weight {
            get;
            set;
        }
        public int PatientId {
            get;
            set;
        }
    }
    class AnimalType {
        public int Id {
            get;
            set;
        }
        public string TypeName {
            get;
            set;
        }
    }
}

Code First 的核心是约定,这些默认的规则使我们可以用我们自己的类来创建模型。EF框架要求一个类必须有一个键属性。规则约定如果一个属性名为Id或者是类名+Id的形式(如PatientId),这一属性就被自动配置为键。如果无法找到满足这一规则的属性,将会在运行时抛出一个异常告诉你没有找到Key.其他约定包括确定字符串的默认长度,或者默认表结构,以及当类相互继承时如何在数据库内建表等等。

如果Code First完全依赖于这些规则去创建自己的类,我们需要做的工作是很有限的。但是Code First并不强制你按此要求设计类,这些规则的设计是为了Code First能够自动处理一些通用的场景。如果你的类遵从这些规则,Code First不会需要更多信息。EF框架会以你的类直接工作。如果不遵从这些规则,你就必须通过Code First的一些配置选项来提供一些附加信息以确保你的类能够被Code First 所理解。

I代码1-1的三个类,Id属性均符合键属性的规则要求。使用这三个类按Code First工作不需要增加任何配置。

使用DbContext管理对象

前述的域与EF框架无关。Code First很美好,你直接就使用你自己的类。如果你从其他项目中现存的域类开始将获得特别的好处。

使用Code First,您应该定义一个继承自DbContext的类。此类的其中一个角色,应被指作为context,以便让Code First知道有关类需要用于创建模型。这就是EF框架能够感知类以及如何保持跟踪这些类的原因。这需要引入另一个新类:DbSet。正如DbContext是一个简单包装的ObjectContext, DbSet是一个包装过的EF4的ObjectSet对象,同样简化了正常使用ObjectSet的编码任务量。

例1-2展示了这种context类的基本样式。注意到其中有一个用于Patients 和Visits的DbSet属性。DbSets允许你使用类型查询。如果我们不希望直接查询AnimalTypes,就不需要一个AnmialType的DbSet.Code First 足够聪明知道Patient使用AnimalType类,会包含在模型内。

Example 1-2. VetContext class which derives from DbContext

using System.Data.Entity;
namespace ChapterOneProject {
    class VetContext: DbContext {
        public DbSet < Patient > Patients {
            get;
            set;
        }
        public DbSet < Visit > Visits {
            get;
            set;
        }
    }
}

使用数据层和域类

现在你可能会有一些惊讶:这些可以满足数据层的需要了吗?假如你100%按照Code First的约定就是这样。

这里没有数据库连接字符串,甚至连数据库也没有。但你已经做好使用数据层的准备。例1-3显示了一个创建新病患对象的方法。这个方法同时创建了该病患的信息以及第一次治疗记录;

然后我们将此实例装入context,添加到DbSet<Patient>(Patinets)中(前已定义为context),最后调用DbContext的SaveChanges方法保存数据。

Example 1-3. Adding a patient to the database with the VetContext

private static void CreateNewPatient() {
    var dog = new AnimalType {
        TypeName = "Dog"
    };
    var patient = new Patient {
        Name = "Sampson",
        BirthDate = new DateTime(2008, 1, 28),
        AnimalType = dog,
        Visits = new List < Visit > {
            new Visit {
                Date = new DateTime(2011, 9, 1)
            }
        }
    };
    using(var context = new VetContext()) {
        context.Patients.Add(patient);
        context.SaveChanges();
    }
}

记住目前还没有任何连接字符串,也没有任何数据库。然后运行这些代码后,我们会在本地的SQL Server Express找到名字与context类完全匹配的新的数据库:ChapterOneProject.VetContext

有关此数据的结构图示见图1-3.

将数据库构架与例1中定的类相对比,表与类,字段与属性几乎完全匹配,唯一的区别是外键,尽管在Patient类中并没有外键属性,但Patients.AnimalType_Id外键依然创建了。Code First得到此外键是根据类中表现出的关系确定的。(注意到Patient对AnimalType有一个引用),在数据库中外键用来维持表间的关系。在这种处理关系的方面Code First提供了很多便利。在类中有很多方式来表达类间的关系。Code First能够方便地识别这些关系。注意,PatientId字段,在类中本来是明确指向Visit类的属性,是非空的,而来自于导航属性的AnimalType_Id字段是可空的。约定再一欠确定了外键的可空性,如果想要修改这些字段,让Code First按照您想法来表达,就需要使用附加的配置。

从类到数据库

如果您在EF框架下工作过,一定会熟悉采用EDMX文件表达的可视化模型。你也可以意识到EDMX文件实际上就是一个XML文件,但是设计器使其工作更加容易。使用XML来描述模型需要专门的构架元素,没有设计器处理XML源文件是相当耗费脑筋的。

很显然显示在设计器上的内容并非是XML文件内容的全部。这一文件中还有一些数据库构架的描述,以使类能够映射到类似图1-3中相应的表和列中。模型XML,数据库构架XML和映射XML都被引用为元数据。

在运行时,EF框架读取前述三部分的XML文件并在内存中创建元数据的映像。内存中的影像不再是XML,而是强类型的对象,如EntityType, EdmProperty和ssociationType.EF框架与这种在内存中的类对象进行交互,然后在需要时再与数据库进行交互。

Code First并没有XML文件,其内存中的数据对象是根据您的域类集合直接生成的。这里是约定与配置在起作用。Code First有一个称为DbModelBuilder的类。这个类读取域中的类集合,然后构建内存模型。由于其也可以构建代表数据库构架的元数据,因此可以用来创建数据库。如果增加配置信息帮助模型构建器确定哪一个模型和数据库构架应该看起来一致,模型构建器就会检查这些类,并将有关信息集成进模型中,使数据库构架与设想的一致。

图1-4显示了EF框架如何从代码或XML文件(由设计器维护和操作)中创建内存模型。一旦内存模型得以构建,EF框架不再需要知道模型是如何创建的。也可使用内存的模型来确定数据库架构是怎样的,可以构建查询访问数据,将查询结果返回对象,将对象的变化更新回数据库。

使用配置

在某些场合下需要协助Code First理解你的意图时,你有两个选择来实施配置:Data Annotations(数据注解)和Code First的Fluent API.根据个人喜好可以任选其一。有一些高级配置只有在使用Fluent API时才能实现。

Code First允许您配置各种属性,关系,继承,级别和数据库映射。现在我们给出一个示例,先睹为快。本书将把精力和时间放在解释约定和对你有用的配置属性上去。

使用Data Annotations配置

多开发者喜欢使用Data Annotations配置方式,因为其非常简单。Data Annotations将一些特性直接应用于想要影响的类或属性上。这些特性可以在System.ComponentModel.DataAnnotations命名空间里找到。

例如,如果你想确保一个属性应总是有一个值,你就可以使用Required特性。例1-4在AnimalType类的TypeName属性上应用了这一特性。

Example 1-4. Using an annotation to mark a property as required

class AnimalType {
    public int Id {
        get;
        set;
    } [Required] public string TypeName {
        get;
        set;
    }
}

这会有两个效果。第一个是数据库中的TypeName字段将会设置为非空。第二个将会实现由EF框架提供的验证,得益于EF4.1框架引入的验证API。默认情况下,当保存更改时,EF框架将检查确保被标记为Required的属性是非空的。如果为空,将会抛出异常。

Required特性影响数据库列的各个方面和属性验证。许多特性以用于数据库映射。例如,Table特性告诉Code First该类映射到一个具有某个名子的表中。应用程序中的AnimailType数据完全可以存储在称为Species的表中。Table特性允许指定这种映射。

Example 1-5. Specifying a table name to map to

[Table("Species")] class AnimalType {
    public int Id {
        get;
        set;
    } [Required] public string TypeName {
        get;
        set;
    }
}

使用Fluent API配置

使用Data Annotations非常简单,在域中的类中指定元数据可能不适应你的开发风格。有一个替代的方法来添加配置,就是使用Code First's Fluent API.使用Fluent API,你的域类始终保持"清洁"。你的配置信息通过重写DbContext暴露的方法OnModelCreating来实现。例1-6展示了与前述相同的配置,这个示例使用的是Fluent APIn。在每个配置中,代码指出了model bulider如何配置AnimalType.

Example 1-6. Configuring the model using the Fluent API

class VetContext: DbContext {
    public DbSet < Patient > Patients {
        get;
        set;
    }
    public DbSet < Visit > Visits {
        get;
        set;
    }
    protected override void OnModelCreating(DbModelBuilder modelBuilder) {
        modelBuilder.Entity < AnimalType > ().ToTable("Species");
        modelBuilder.Entity < AnimalType > ().Property(p = >p.TypeName).IsRequired();
    }
}

第一个使用Fluent API的配置等价于Data Annotation 的Table特性,现在是ToTable方法,传递了一个表名指定了AnimalType类应该生成为该表;第二个配置使用了一个Lambda表达式指定AnimalType的一个属性应用IsRequired方法。

这只是一个建立配置的方法。你将会在后续的章节中学到更多有关使用Data Annotations 和Fluent API的知识,用于配置属性、关系,继承,层级和数据库映射等。

创建或指向数据库

前面,你已经看到默认代码创建了一个SQL Server Express数据库。Code First完全自动调用了内置的连接字符串生成了数据库。

你将在第6章全面了解Code First是如何与你的数据库进行交互的。

本书中的示例将逐步介绍如何配置数据库映射。这些概念同样适用于生成数据库和映射到一个已经存在的数据库。当生成数据库时,会影响生成的构架。当映射到存在的数据库时,数据库定义的构架必须与EF框架在运行时生成的保持一致。

在我们探索Code First的约定和配置时,我们也会允许Code First创建数据库。这就使得运行应用程序的每个步骤都可以看到数据库构架的变化。如果映射到一个已经存在的数据库,唯一的区别是为数据库指定Code First。最容易的办法在"使用配置文件控制数据库位置"(第6章),也可以看看"逆向Code First"(第8章)。

Code First 不支持的情况

Code First 是新加入到EF框架中的技术,有几个特性目前尚不支持。EF开发团队指出他们正在计划在未来版本中加入这些特性。

数据迁移(Database migrations)

在写这本书的时候,Code First尚不支持数据迁移,换句话说,修改数据反映在模型的变化上。但是这一特性在不久的将来就可能发布。也可以在该团队的blog上获取有关数据迁移的早期预览版。

映射到视图

Code First目前只支持映射到表。这一不幸意味着不能将Code First直接映射到存储过程,视图或其他数据库对象。如果你正使用Code First来生成数据库,没有办法直接将这些特性加到数据库,必须手工添加在Code First已经创建的数据里。如果映射到一个现存的数据库,有一些其他的技术可以用来获取数据库中非表格数据。

这些技术描述为"映射到非表格数据库对象",见第7章。

架构定义查询

EF实体框架包含一个定义查询功能,允许您指定数据库查询直接访问XML元数据。还有一个查询视图功能,可让您使用概念模型定义一个用来加载实体的查询。这允许您指定的查询提供独立的数据库。Code First不支持这些功能。

每类型多重实体(Multiple Entity Sets per Type (MEST))

Code First尚不支持MEST。MEST允许使用同一个类映射到不同的表中。这是EF构架的一个隐含特性,很少用到。EF团队表示,为了确保Code First API的简化,他们不打算加入对MEST的支持。

条件列映射

继承层次结构时,Code First要求一个属性始终具有相同的名称映射到列,而不能实现条件列映射。例如,有一个Person基类具有一个NationalIdentifier属性。American和Australian类都是派生自Person基类分别映射到数据库中单独的Americans和Australians表中。当使用设计器时,你可映射NationalIdentifier属性到Americans表中的SSN列,映射到Australians表中的PassportNumber列。Code First 并不支持这种情况。映射到各个表中的NationalIdentifier必须具有同一名称。

选择Code First

现在你已经知道什么是Code First,你可能还在犹豫是否这是一种正确的应用程序开发模型工作流。好消息是这完全取决于你和你团队的开发风格。

如果你喜欢书写自己的POCO类然后使用代码定义如何映射到数据库,Code First是你要选择的。正如前面提到的,Code First可以为你生成数据库或者映射到已存在的数据库;

如果你更喜欢用设计器定义你的类,并且用可视化的方式映射到数据库,你可能不会使用Code First.如果你需要映射到一个现存的数据库,你可能会想用Database First来从database逆向生成为模型。这可以使用VS的实体数据模型向导生成基于数据库的EDMX文件。你可以使用设计器查看和编辑生成的模型。如果尚没有数据库而仍然要使用设计器,你应该使用设计器以Model First方式定义你的模型。根据创建的模型再创建数据库。这些方法工作良好,在设计器中你会很好地应用你的类。

最后,如果你想针对现存的类使用EF框架,你可能会选择Code First,即便你第一选择可能是基于模型的设计器。如果选择了设计器,你需要在设计器对模型和类作出调整。这种方式低效易出错,而使用Code First可能会获得好处。在Code First中,你的类就是模型,因此模型变更只需要在一处,不需要所谓的同步操作。

EF团队正在对设计器进行改进以加入附加功能:数据库逆向生成支持Code First和Fluent配置的 类。这一工具将为已经拥有数据库但是更想使用Code First的开发者提供。你可以在第8章获取更多信息。

EF工作流的选择总结如图1-5.

从本书中可以学到什么?

本书关注采用Code First构建和配置模型。这是一种采用EF框架编程的扩展,在本书背后你会找到很多参考,不会在这复制近900页的有关EF框架的详细信息,而是关注于如何实现查询,更新,如何在各种应用类型中使用,如何自动测试,如何处理异常,安全,数据库连接和事务。采用Code First 创建模型只是EF框架中诸多特性中的一个。事实上,当进入第二章,你会发现宠物医院的域模型将会变为可编为应用程序的业务模型,称之Break Away Geek Adventures(奇客冒险之旅)。另一本书名为《编程实体框架:DbContext》的书,将集中于DbContext,DbSet,验证API,并使用NuGet包装的Enfity Framework的一部分的功能,这些不是本书的重点。