Entity Framework 6.0 Code First(转)
源自:http://www.cnblogs.com/panchunting/tag/Code%20First/
6 存储过程
(一)Conventions
Entity Framework 简言之就是一个ORM(Object-Relational Mapper)框架.
Code First 使得你能够通过C#的类来描述一个模型,模型如何被发现/检测就是通过一些约定(Conventions)。Conventions 就是一系列规则的集合,被用于对基于类别定义的概念模型的自动装配。
这些约定都被定义于 System.Data.Entity.ModelConfiguration.Conventions 命名空间下。
当然你可以进一步地对你的模型作出配置,例如使用 Data Annotations 或者 Fluent API. 推荐的配置顺序如下:优先使用 Data Annotations,然后是 Fluent API ,最后才是 Conventions.
本文将会给出一个关于 Conventions 的初步介绍,具体详细的列表请参考 http://msdn.microsoft.com/en-us/library/system.data.entity.modelconfiguration.conventions(v=vs.103).aspx
一、类型发现约定 Type Discovery
当我们使用 Entity Framework 进行开发的时候,通常是以构建一些定义概念模型的类开始,除此之外,我们还需要让 DbContext 知道哪些类型需要包含在此模型中,为了达到此目的,我们又会定义一个继承自 DbContext 并且为这些类型暴露 DbSet 属性。这样 Code First 将包含这些类型以及这些类型的引用类型(即便引用类型定义于不同的程序集中)
如果类型位于继承体系中,也只需为基类(base class)定义 DbSet 属性,那么位于同一程序集中的派生类型也将被自动包含与其中。
下面的例子中,在类 SchoolEntities 中只定义了一个 DbSet 属性 Departments . Code First 能够发现并钻取所有的引用类型
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Web; namespace ContosoUniversity.DAL { public class SchoolEntities : DbContext { public DbSet<Department> Departments { get; set; } } public class Department { // Primary key public int DepartmentID { get; set; } public string Name { get; set; } // Navigation property public virtual ICollection<Course> Courses { get; set; } } public class Course { // Primary key public int CourseID { get; set; } public string Title { get; set; } public int Credits { get; set; } // Foreign key public int DepartmentID { get; set; } // Navigation properties public virtual Department Department { get; set; } } public partial class OnlineCourse : Course { public string URL { get; set; } } public partial class OnsiteCourse : Course { public string Location { get; set; } public string Days { get; set; } public System.DateTime Time { get; set; } } }
如果你想排除模型中的某个类型,你可以使用 NotMapped 属性(Attribute)或者 DbModelBuilder.Ignore fluent API.
[NotMapped] public class Department
NotMapped 位于 System.ComponentModel.DataAnnotations.Schema 命名空间下
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Ignore<Department>(); }
二、主键约定 Primary Key Convention
如果类中的属性(Property)名称为 ID (不区分大小写)或 ClassNameID(类名 + ID),Code First 则推断这个属性为主键。如果主键属性的类型为数字型或 GUID 则会被当成标识列(Identity Column)
public class Department { //Primary key public int ID { get; set; } //Primary key public int DepartmentID { get; set; } }
三、关系(外键/导航属性)约定 Relationship(Foreign Key/Navigation Properties) Convention
在 Entity Framework 中,导航属性(Navigation Properties)提供了一种对两个实体类型之间关系的驱动,每一个对象都能拥有它所参与的每一个关系的导航属性(每一个对象的每一个关系都能有一个导航属性),导航属性提供在两端来驱动或操作这个关系,也可以返回任何一方的引用对象(对象间的关系是 1:1 或者 1:0 )或 对象的集合(对象间的关系为 1:* 或 *:*), Code First 根据定义于类型上的导航属性能够推断这种关系
除了导航属性外,推荐的方式是再包含外键属性(Foreign Key)。Code First 能够推断如下的命名属性为外键(外键属性命名的默认约定):
- <导航属性名><主体主键属性名>;
- <主体类名><主键属性名>;
- <主体主键属性名>
优先顺序为从上到下(多个满足匹配/约定的时候)。外键属性命名约定也是大小写不敏感的。(PS: 个人觉得把 Principal 译为主体,Dependent Entity 译为从属实体较为稳妥,分别对应主从关系)
当外键属性被检测到,Code First 将会根据外键的可空性来推断关系的具体形式:如果属性是可空的,那么关系将被注册为可选的;否则则被认为是必须的。
如果从属实体上的外键是不可为空的,Code First 将会在关系上设置级联删除,反之,则不会级联删除,并且当主体被删除的时候,外键将会被置为 NULL
多说一句:以上所说的关系多重性(multiplicity)和级联删除可以被 Fluent API 重载(覆写)
以下示例展示用导航属性和外键来定义类 Department 和 Course 之间的关系
public class Department { // Primary key public int DepartmentID { get; set; } public string Name { get; set; } // Navigation property public virtual ICollection<Course> Courses { get; set; } } public class Course { // Primary key public int CourseID { get; set; } public string Title { get; set; } public int Credits { get; set; } // Foreign key public int DepartmentID { get; set; } // Navigation properties public virtual Department Department { get; set; } }
四、Complex Types Conventions
如果 Code First 无法从类定义中推断出主键,也没有通过 Data Annotations 或 Fluent API 注册的主键,则此类型自动注册为 Complex Types. Complex Types 此外还要求类型中不能含有对其它实体类型的引用,并且其它类型中也不能含有对本类型引用的属性集合。一个 Complex Type 示例如下所示
public partial class OnsiteCourse : Course { public OnsiteCourse() { Details = new Details(); } public Details Details { get; set; } } public class Details { public System.DateTime Time { get; set; } public string Location { get; set; } public string Days { get; set; } }
五、移除约定(Removing Conventions)
可以移除任何定义于命名空间 System.Data.Entity.ModelConfiguration.Conventions 中的约定,如下列示例移除约定 PluralizingTableNameConvention
public class SchoolContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } }
(二)Custom Conventions
------------------------------------------------------------------------------------------------------------
注意:以下所讨论的功能或 API 等只针对 Entity Framework 6 ,如果你使用早期版本,可能部分或全部功能不起作用!
------------------------------------------------------------------------------------------------------------
Entity Framework Code First 默认的 Conventions 约定解决了一些诸如哪一个属性是实体的主键、实体所 Map 的表名、以及列的精度等问题,但是某些时候,这些默认的约定对于我们的模型是不够理想的,此时我们就希望能够自定义一些约定。当然通过使用 Data Annotations 或者Fluent API 也能实现这样的目的,无非就是对许多实体作出配置,但是这样的工作是极其繁琐和繁重的。而定制约定能很好地解决我们的问题,接下来就将展示如何来实现这些定制约定。
Our Model
为了定制约定,本文引入了DbModelBuilder API ,这个 API 对于编程实现大部分的定制约定是足够的,但它还有更多的能力,例如 model-based 约定,更过信息,请参考 http://msdn.microsoft.com/en-us/data/dn469439
在开始之前,我们先定义一个简单的模型
Custom Conventions
下面这个约定使得任何以 key 命名的属性都将成为实体的主键
我们也可以使得约定变得更加精确:过滤类型属性(如只有 integer 型并且名称为 key 的才能成为主键)
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Properties<int>() .Where(p => p.Name == "Key") .Configure(p => p.IsKey()); }
关于 IsKey 方法,有趣的是它是可添加的,这意味着如果你在多个属性上施加这个方法,那么这些属性都将变成组合键的一部分,对于组合键,指定属性的顺序是必须的。指定的方法如下
modelBuilder.Properties<int>() .Where(x => x.Name == "Key") .Configure(x => x.IsKey().HasColumnOrder(1)); modelBuilder.Properties() .Where(x => x.Name == "Name") .Configure(x => x.IsKey().HasColumnOrder(2));
Convention Classes
另一种定义约定的方式是通过约定类来封装约定,为了使用约定类,你定义一个类型,继承约定基类(位于System.Data.Entity.ModelConfiguration.Conventions 命名空间下)
public class DateTime2Convention : Convention { public DateTime2Convention() { this.Properties<DateTime>() .Configure(c => c.HasColumnType("datetime2")); } }
为了通知 Entity Framework 使用这个约定,需把它添加到约定集合中,代码如下
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Properties<int>() .Where(p => p.Name == "Key") .Configure(p => p.IsKey()); modelBuilder.Conventions.Add(new DateTime2Convention()); }
如你所见,我们在约定集合中添加了一个上面定义的约定的实例。
从 Convention 继承为我们提供了一种非常方便的方式,使得组织、管理非常便捷并且易于跨项目使用。例如你可以为此建立一个类库,专门提供这些约定的合集。
Custom Attribute
定制属性:另一种使用约定的方式就是通过在模型上配置属性(Attribute)。示例如下:我们建立一个属性(Attribute)用于标识字符属性(Property)为非Unicode
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class NonUnicode : Attribute { }
现在让我们在模型上新建约定以使用此属性
modelBuilder.Properties() .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any()) .Configure(c => c.IsUnicode(false));
通过这个约定,我们可以把 NonUnicode 属性(Attribute)施加于任何字符属性(Property),这也意味着此列在数据库中将以 varchar 的而非 nvarchar 的形式存储。
需要注意的是,如果你把此约定施加于任何非字符属性都将引发异常,这是因为 IsUnicode 只能施加于 string (其它类型都不可以),为此我们需使得约定变得更加精确,即过滤掉任何非 string 的东西
上面的约定解决了定义定制属性的问题,我们需要注意的是还有另一个 API 非常易于使用,尤其是你想使用 Attribute Class 的 Properties
让我们对上面的类做一些更新
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class IsUnicode : Attribute { public bool Unicode { get; set; } public IsUnicode(bool isUnicode) { Unicode = isUnicode; } }
一旦我们有了这个,我们就可以在 Attribute 上设置一个 bool 通知约定 Property 是否是 Unicode. 配置如下
modelBuilder.Properties() .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any()) .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
上面的足够简单,但是还有一种更简洁的方式 - 就是使用 Conventions API 的 Having 方法,这个 Having 方法有一个 Func<PropertyInfo, T> 类型参数,这个参数能够像 Where 一样接收 PropertyInfo. 但是前者返回的是一个 object. 如果返回对象为 null, 那么 property 将不会被配置 -- 这意味着我们可以像 Where 一样过滤某些 properties -- 但是它们又是不同的,因为前者还可以捕获并返回 object 然后传递给 Configure方法
modelBuilder.Properties() .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault()) .Configure((config, att) => config.IsUnicode(att.Unicode));
当然定制属性并不是我们使用 Having 方法的唯一原因,在任何时候,当我们配置类型或属性,需要过滤某些东西的时候是非常有用的。
Configuring Types
到目前为止,所有的约定都是针对属性(properties)而言,其实还有其它的 conventions API 用于针对模型的类型配置。前者是在属性级别(Property Level),后者是在类型级别(Type Level)
Type Level Conventions 一个显而易见的用处是更改表的命名约定,既可以改变 Entity Framework 默认提供的从而匹配于现有的 schema, 也可以基于完全不同的命名约定创建一个全新的数据库,为此我们首先需要一个方法,接收 the TypeInfo for a type, 返回 the table name for that type
private string GetTableName(Type type) { var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); return result.ToLower(); }
上面的方法意味着,如果施加于 ProductCategory 类,则该类将会被映射于表名 product_category 而不是 ProductCategories
我们可以在一个约定中这样使用它
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
这个约定将配置模型中的每一个类型与方法 GetTableName 返回的表名相匹配,这与通过 Fluent API 为模型中每一个实体使用方法 ToTable是等效的。
需要注意的是方法 ToTable 需要一个字符串参数来作为确切的表名,如果没有复数化( pluralization )要求,我们通常会这么做。这也是为什么上面约定表名是 product_category 而不是 ProductCategories, 这可以在约定中通过调用 pluralization service 来解决
在接下来的示例中,我们将使用 Entity Framewrok 6 中新增加的功能 Dependency Resolution 来获得 pluralization service, 从而实现表名复数化
private string GetTableName(Type type) { var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>(); var result = pluralizationService.Pluralize(type.Name); result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); return result.ToLower(); }
注意:GetService 的泛型版本是命名空间 System.Data.Entity.Infrastructure.DependencyResolution 下的一个扩展方法
ToTable and Inheritance
ToTable 的另一个重要方面是如果你明确一个类型映射到给定的表,那么你可以改变 EF 使用的映射策略。如果你在继承层次中为每一个类型都调用此方法,像上面所做的那样 -- 把类型名当参数传递作为表名,那么你将改变默认的映射策略 Table-Per-Hierarchy (TPH) -- 使用 Table-Per-Type (TPT). 为了更好的说明举例如下
public class Employee { public int Id { get; set; } public string Name { get; set; } } public class Manager : Employee { public string SectionManaged { get; set; } }
默认情况下,Employee 和 Manager 都将映射成数据库中的同一张表(Employees),表中同时包含 employees 和 managers , 存储在每一行的实例类型将由一个标识列来决定,这就是 TPH 策略带来的结果 -- 对层级只有一张表。但是如果你对每一个类都使用 ToTable, 那么每一个类型都将各自映射成自己的表,这正如 TPT 策略所示的那样
modelBuilder.Types() .Configure(c=>c.ToTable(c.ClrType.Name));
上面代码映射成的表结构如下图
你可以通过如下几种方式来避免此问题并且维护默认的 TPH 映射
- 使用相同的表名为层级中的每一个类型调用 ToTable ;
- 只为层级中的基类调用ToTable (上例中为 Employee)
Execution Order
最后一个约定生效,这和 Fluent API 是一样的。这意味着如果在同一个属性上有两个约定,那最后一个起作用。
modelBuilder.Properties<string>() .Configure(c => c.HasMaxLength(500)); modelBuilder.Properties<string>() .Where(x => x.Name == "Name") .Configure(c => c.HasMaxLength(250));
由于最大长度250约定设置位于500后面,所以字符串的长度将会被限定在250。以这种方式可以实现约定的覆写(override)
在一些特殊的情况下,Fluent API 和 Data Annotations 也可被用来 override Conventions
Built-in Conventions
因为定制约定会受到默认 Code First Conventions 的影响,所以在一个约定运行之前或之后添加另一个约定是有意义的,为了实现这个,我们可以在约定集合中使用方法 AddBefore 和 AddAfter
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
内建约定列表请参考命名空间 System.Data.Entity.ModelConfiguration.Conventions Namespace
当然你也可以移除一个约定,示例如下
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); }
参考原文:http://msdn.microsoft.com/en-us/data/jj819164
(三)Data Annotations
Entity Framework Code First 利用一种被称为约定(Conventions)优于配置(Configuration)的编程模式允许你使用自己的 domain classes 来表示 EF 所依赖的模型去执行查询、更改追踪、以及更新功能,这意味着你的 domain classes 必须遵循 EF 所使用的约定。然而,如果你的 domain classes 不能遵循 EF 所使用的约定,此时你就需要有能力去增加一些配置使得你的 classes 能够满足 EF 所需要的信息。
Code First 提供了两种方式来配置你的类:
- DataAnnotations, 使用简单属性;
- Fluent API, 以编程命令行式的方式来描述你的配置
本文将关注 DataAnnotations(位于命名空间 System.ComponentModel.DataAnnotations)
为了便于示例,我们先创建两个类:Blog 和 Post
以上类都是遵循约定的,但是你也可以使用 annotations 为 EF 提供更多的信息
Key主键
EF 依赖于每一个实体都有一个主键从而能够追踪实体。 在约定下,主键是通过寻找名称为 ID 或 ClassName + ID 的属性确定。那么如果类中没有此类的属性呢?我们把类改造如下(把 Id 改为 PrimaryTrackingKey)
此时如果我们添加 Scaffolded 会出现如下错误
我们需要使用 key annotation 确定哪一个属性是主键
我们使用 Code First's Database Generation 功能看看数据库的情况
Required非空
用于指示字段非空
运行看一下结果
我们在看一下后台数据库情况
MaxLength and MinLength长度限制
顾名思义,就是限制字段的长度
看一下后台数据库
运行看结果
NotMapped忽略
类中有些属性,如通过计算获得或其它列的合并而来的,我们并不希望其记录在数据库中,此时就可以使用 NotMapped Annotation
对于类也是一样的
ComplexType复杂类型
一般这种情况是不常见的:类中包含另一个类即一个完整的实体是由一系列类的集合来描述。例如我们在我们的模型中增加一个叫BlogDetails 的类
注意 BlogDetails 不包含任何主键属性, 在领域驱动设计中被称为值对象,而在 EF 中则被称为复杂类型(ComplexType). Complex Types 是无法自我追踪的,但是我们可以通过 ComplexType Annotation 来改变这种尴尬
可以看到,在数据库中表 Blog 包含 BlogDetails 的所有两个属性,默认列名前缀为 complex type 即 BlogDetails
ConcurrencyCheck并发检查
ConcurrencyCheck Annotation 允许你可以在一个或多个属性上设置一个标记用于当用户更新或删除实体时做并发检查。
如果此时同时对此列进行更新,则会抛出异常 DbUpdateConcurrencyException
TimeStamp时间戳
通常用行版本号或时间戳来检查并发,除了使用 ConcurrencyCheck Annotation, 还可以使用更为精确的 TimsStamp, 前提是这个属性的类型是字节数组(byte array). Code First 在对待 TimsStamp 属性和 ConcurrencyCheck 属性是一样的,只不过能确保数据库中生成的字段是non-nullable
在一个给定的类中只能有一个 TimsStamp 属性
看一下后台数据库
Table and Column表/列名
指定匹配到数据库的表/列名
可以看到表/列名已更改
DatabaseGenerated自增长
DatabaseGeneratedOption 有三个选项:
- DatabaseGeneratedOption.Computed: 在用 Code First 生成数据库的时候你可以在 byte 或 timestamp 列上使用 DatabaseGenerated Annotation,否则就应该在数据库存在的情况下使用因为如果数据库不存在,此时 Code First 不知道为计算列(Computed Column)选择使用什么样的公式
- DatabaseGeneratedOption.Identity: 如果主键为 integer 型,则数据库默认为自增长(效果等同于设置DatabaseGeneratedOption.Identity), 如果主键是 GUID 类型,则要显式设置自增长
- DatabaseGeneratedOption.None: 如果不想自增长,可设置成 DatabaseGeneratedOption.None
ForeignKey 外键
看看数据库
InverseProperty属性反转
InverseProperty 一般使用在当类间有多重关系的时候。
例如在 Post 中你可能不仅需要追踪谁写的也还要追踪有谁编辑了博客
此时你会看到后台数据库有四个外键 Person_Id, Person_Id1, CreatedBy_Id and UpdatedBy_Id
为了解决这个问题,我们可以使用 InverseProperty annotation 来明确属性间的指向
再一次看看数据库,情况已经发生了变化
(四)Fluent API - 配置属性/类型
------------------------------------------------------------------------------------------------------------
注意:以下所讨论的功能或 API 等只针对 Entity Framework 6 ,如果你使用早期版本,可能部分或全部功能不起作用!
------------------------------------------------------------------------------------------------------------
Entity Framework Code First 默认的 Conventions 约定解决了一些诸如哪一个属性是实体的主键、实体所 Map 的表名、以及列的精度等问题,但是某些时候,这些默认的约定对于我们的模型是不够理想的,此时我们就希望能够自定义一些约定。当然通过使用 Data Annotations 或者Fluent API 也能实现这样的目的,无非就是对许多实体作出配置,但是这样的工作是极其繁琐和繁重的。而定制约定能很好地解决我们的问题,接下来就将展示如何来实现这些定制约定。
Our Model
为了定制约定,本文引入了DbModelBuilder API ,这个 API 对于编程实现大部分的定制约定是足够的,但它还有更多的能力,例如 model-based 约定,更过信息,请参考 http://msdn.microsoft.com/en-us/data/dn469439
在开始之前,我们先定义一个简单的模型
Custom Conventions
下面这个约定使得任何以 key 命名的属性都将成为实体的主键
我们也可以使得约定变得更加精确:过滤类型属性(如只有 integer 型并且名称为 key 的才能成为主键)
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Properties<int>() .Where(p => p.Name == "Key") .Configure(p => p.IsKey()); }
关于 IsKey 方法,有趣的是它是可添加的,这意味着如果你在多个属性上施加这个方法,那么这些属性都将变成组合键的一部分,对于组合键,指定属性的顺序是必须的。指定的方法如下
modelBuilder.Properties<int>() .Where(x => x.Name == "Key") .Configure(x => x.IsKey().HasColumnOrder(1)); modelBuilder.Properties() .Where(x => x.Name == "Name") .Configure(x => x.IsKey().HasColumnOrder(2));
Convention Classes
另一种定义约定的方式是通过约定类来封装约定,为了使用约定类,你定义一个类型,继承约定基类(位于System.Data.Entity.ModelConfiguration.Conventions 命名空间下)
public class DateTime2Convention : Convention { public DateTime2Convention() { this.Properties<DateTime>() .Configure(c => c.HasColumnType("datetime2")); } }
为了通知 Entity Framework 使用这个约定,需把它添加到约定集合中,代码如下
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Properties<int>() .Where(p => p.Name == "Key") .Configure(p => p.IsKey()); modelBuilder.Conventions.Add(new DateTime2Convention()); }
如你所见,我们在约定集合中添加了一个上面定义的约定的实例。
从 Convention 继承为我们提供了一种非常方便的方式,使得组织、管理非常便捷并且易于跨项目使用。例如你可以为此建立一个类库,专门提供这些约定的合集。
Custom Attribute
定制属性:另一种使用约定的方式就是通过在模型上配置属性(Attribute)。示例如下:我们建立一个属性(Attribute)用于标识字符属性(Property)为非Unicode
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class NonUnicode : Attribute { }
现在让我们在模型上新建约定以使用此属性
modelBuilder.Properties() .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any()) .Configure(c => c.IsUnicode(false));
通过这个约定,我们可以把 NonUnicode 属性(Attribute)施加于任何字符属性(Property),这也意味着此列在数据库中将以 varchar 的而非 nvarchar 的形式存储。
需要注意的是,如果你把此约定施加于任何非字符属性都将引发异常,这是因为 IsUnicode 只能施加于 string (其它类型都不可以),为此我们需使得约定变得更加精确,即过滤掉任何非 string 的东西
上面的约定解决了定义定制属性的问题,我们需要注意的是还有另一个 API 非常易于使用,尤其是你想使用 Attribute Class 的 Properties
让我们对上面的类做一些更新
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class IsUnicode : Attribute { public bool Unicode { get; set; } public IsUnicode(bool isUnicode) { Unicode = isUnicode; } }
一旦我们有了这个,我们就可以在 Attribute 上设置一个 bool 通知约定 Property 是否是 Unicode. 配置如下
modelBuilder.Properties() .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any()) .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
上面的足够简单,但是还有一种更简洁的方式 - 就是使用 Conventions API 的 Having 方法,这个 Having 方法有一个 Func<PropertyInfo, T> 类型参数,这个参数能够像 Where 一样接收 PropertyInfo. 但是前者返回的是一个 object. 如果返回对象为 null, 那么 property 将不会被配置 -- 这意味着我们可以像 Where 一样过滤某些 properties -- 但是它们又是不同的,因为前者还可以捕获并返回 object 然后传递给 Configure方法
modelBuilder.Properties() .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault()) .Configure((config, att) => config.IsUnicode(att.Unicode));
当然定制属性并不是我们使用 Having 方法的唯一原因,在任何时候,当我们配置类型或属性,需要过滤某些东西的时候是非常有用的。
Configuring Types
到目前为止,所有的约定都是针对属性(properties)而言,其实还有其它的 conventions API 用于针对模型的类型配置。前者是在属性级别(Property Level),后者是在类型级别(Type Level)
Type Level Conventions 一个显而易见的用处是更改表的命名约定,既可以改变 Entity Framework 默认提供的从而匹配于现有的 schema, 也可以基于完全不同的命名约定创建一个全新的数据库,为此我们首先需要一个方法,接收 the TypeInfo for a type, 返回 the table name for that type
private string GetTableName(Type type) { var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); return result.ToLower(); }
上面的方法意味着,如果施加于 ProductCategory 类,则该类将会被映射于表名 product_category 而不是 ProductCategories
我们可以在一个约定中这样使用它
modelBuilder.Types() .Configure(c => c.ToTable(GetTableName(c.ClrType)));
这个约定将配置模型中的每一个类型与方法 GetTableName 返回的表名相匹配,这与通过 Fluent API 为模型中每一个实体使用方法 ToTable是等效的。
需要注意的是方法 ToTable 需要一个字符串参数来作为确切的表名,如果没有复数化( pluralization )要求,我们通常会这么做。这也是为什么上面约定表名是 product_category 而不是 ProductCategories, 这可以在约定中通过调用 pluralization service 来解决
在接下来的示例中,我们将使用 Entity Framewrok 6 中新增加的功能 Dependency Resolution 来获得 pluralization service, 从而实现表名复数化
private string GetTableName(Type type) { var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>(); var result = pluralizationService.Pluralize(type.Name); result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); return result.ToLower(); }
注意:GetService 的泛型版本是命名空间 System.Data.Entity.Infrastructure.DependencyResolution 下的一个扩展方法
ToTable and Inheritance
ToTable 的另一个重要方面是如果你明确一个类型映射到给定的表,那么你可以改变 EF 使用的映射策略。如果你在继承层次中为每一个类型都调用此方法,像上面所做的那样 -- 把类型名当参数传递作为表名,那么你将改变默认的映射策略 Table-Per-Hierarchy (TPH) -- 使用 Table-Per-Type (TPT). 为了更好的说明举例如下
public class Employee { public int Id { get; set; } public string Name { get; set; } } public class Manager : Employee { public string SectionManaged { get; set; } }
默认情况下,Employee 和 Manager 都将映射成数据库中的同一张表(Employees),表中同时包含 employees 和 managers , 存储在每一行的实例类型将由一个标识列来决定,这就是 TPH 策略带来的结果 -- 对层级只有一张表。但是如果你对每一个类都使用 ToTable, 那么每一个类型都将各自映射成自己的表,这正如 TPT 策略所示的那样
modelBuilder.Types() .Configure(c=>c.ToTable(c.ClrType.Name));
上面代码映射成的表结构如下图
你可以通过如下几种方式来避免此问题并且维护默认的 TPH 映射
- 使用相同的表名为层级中的每一个类型调用 ToTable ;
- 只为层级中的基类调用ToTable (上例中为 Employee)
Execution Order
最后一个约定生效,这和 Fluent API 是一样的。这意味着如果在同一个属性上有两个约定,那最后一个起作用。
modelBuilder.Properties<string>() .Configure(c => c.HasMaxLength(500)); modelBuilder.Properties<string>() .Where(x => x.Name == "Name") .Configure(c => c.HasMaxLength(250));
由于最大长度250约定设置位于500后面,所以字符串的长度将会被限定在250。以这种方式可以实现约定的覆写(override)
在一些特殊的情况下,Fluent API 和 Data Annotations 也可被用来 override Conventions
Built-in Conventions
因为定制约定会受到默认 Code First Conventions 的影响,所以在一个约定运行之前或之后添加另一个约定是有意义的,为了实现这个,我们可以在约定集合中使用方法 AddBefore 和 AddAfter
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
内建约定列表请参考命名空间 System.Data.Entity.ModelConfiguration.Conventions Namespace
当然你也可以移除一个约定,示例如下
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); }
参考原文:http://msdn.microsoft.com/en-us/data/jj819164
(五)Fluent API - 配置关系
上一篇文章我们讲解了如何用 Fluent API 来配置/映射属性和类型,本文将把重点放在其是如何配置关系的。
文中所使用代码如下
public class Student { public int ID { get; set; } public string Name { get; set; } public DateTime EnrollmentDate { get; set; } // Navigation properties public virtual Address Address { get; set; } public virtual OtherInfo OtherInfo { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } } public class Department { public Department() { this.Courses = new HashSet<Course>(); } // Primary key public int DepartmentID { get; set; } public string Name { get; set; } public decimal Budget { get; set; } public System.DateTime StartDate { get; set; } public int? Administrator { get; set; } // Navigation property public virtual ICollection<Course> Courses { get; private set; } } public class Course { public int CourseID { get; set; } public string Title { get; set; } public int Credits { get; set; } // Foreign key public int DepartmentID { get; set; } public string DepartmentName { get; set; } public int SomeDepartmentID { get; set; } // Navigation properties public virtual Department Department { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } public virtual ICollection<Instructor> Instructors { get; set; } } public class Instructor { public int InstructorID { get; set; } public string Name { get; set; } public DateTime HireDate { get; set; } // Navigation properties public virtual ICollection<Course> Courses { get; set; } } public class Enrollment { public int EnrollmentID { get; set; } public int CourseID { get; set; } public int StudentID { get; set; } // Navigation property public virtual Course Course { get; set; } public virtual Student Student { get; set; } } public class Address { public int AddressId { get; set; } public string HomeAddress { get; set; } public string LiveAddress { get; set; } // Navigation property public virtual Student Student { get; set; } } public class OtherInfo { public int Id { get; set; } public string HomeAddress { get; set; } public string MailAddress { get; set; } public string PhoneNumber { get; set; } public string StudentID { get; set; } // Navigation property public virtual Student Student { get; set; } }
EntityTypeConfiguration<TEntityType>
上面是泛型类的部分方法截图,一般我们通过 Code First Fluent API 来配置实体间的关系时都是从此泛型类的实例开始,此泛型类为我们提供了大部分的关系处理方法,如必须的 HasRequired ,可选的 HasOptional ,多个的 HasMany .
上面所有方法(除了 HasMany, HasOptional, HasRequired )的返回值都是泛型类 EntityTypeConfiguration<TEntityType> 的实例本身,这给链式操作提供了极大地方便.
HasRequired, HasOptional, HasMany 这三个方法的参数都是一个 lambda expression, 只不过前两个用于表示实体关系(Relationship)间的导航属性(navigation property),后一个则是导航属性的集合(Collection)
HasRequired, HasOptional, HasMany 这三个方法的返回值都是可以继续进行配置的泛型类实例,虽然名称不同,方法名也略有不同,但方法主体所体现的思想是一致的
RequiredNavigationPropertyConfiguration<TEntityType, TTargetEntityType>
OptionalNavigationPropertyConfiguration<TEntityType, TTargetEntityType>
ManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType>
三个泛型类都有类似于如下三个方法 WithRequired, WithOptional, WithMany, 这三个方法都提供了重载的无参版本,有参方法的参数也都是一个 lambda expression, 前两个用于表示实体关系(Relationship)间的导航属性(navigation property),后一个则是导航属性的集合(Collection)
接下来,你可以使用方法 HasForeignKey 继续配置外键属性,方法的参数仍然是一个 lambda expression, 只不过代表一个属性
DependentNavigationPropertyConfiguration<TDependentEntityType>
public CascadableNavigationPropertyConfiguration HasForeignKey<TKey>(Expression<Func<TDependentEntityType, TKey>> foreignKeyExpression);
其它两个类主要包含方法 Map ,如下
ForeignKeyNavigationPropertyConfiguration
public CascadableNavigationPropertyConfiguration Map(Action<ForeignKeyAssociationMappingConfiguration> configurationAction);
ManyToManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType>
public ManyToManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> Map(Action<ManyToManyAssociationMappingConfiguration> configurationAction); public ManyToManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> MapToStoredProcedures(); public ManyToManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> MapToStoredProcedures(Action<ManyToManyModificationStoredProceduresConfiguration<TEntityType, TTargetEntityType>> modificationStoredProcedureMappingConfigurationAction);
我们可以继续在返回的实例上进行配置
CascadableNavigationPropertyConfiguration
public void WillCascadeOnDelete(); public void WillCascadeOnDelete(bool value);
我们看到了级联删除
Configuring Relationships
1:1,0 - Configuring a Required-to-Optional Relationship (One-to–Zero-or-One)
一个学生可以有一个或没有其它信息(包括邮件地址、手机号等)
// Map one-to-zero or one relationship modelBuilder.Entity<OtherInfo>() .HasRequired(t => t.Student) .WithOptional(t => t.OtherInfo);
1:1 - Configuring a Relationship Where Both Ends Are Required (One-to-One)
一个学生肯定有一个地址信息(包含家庭住址、居住地址)
// Map one-to-one relationship modelBuilder.Entity<Address>() .HasRequired(t => t.Student) .WithRequiredPrincipal(t => t.Address);
1:N - Configuring a Required-to-Many Relationship (One-to-Many)
一个学生可以参加多门课程
// Map one-to-many relationship modelBuilder.Entity<Student>() .HasMany(t => t.Enrollments) .WithRequired(t => t.Student);
N:N - Configuring a Many-to-Many Relationship (Many-to-Many)
一个老师可以教授多门课程,一门课程也可以由多名老师教授
// Map one-to-many relationship modelBuilder.Entity<Course>() .HasMany(t => t.Instructors) .WithMany(t => t.Courses);
你还可以进一步指定中间连接表(数据库将会创建中间连接表)
// Map one-to-many relationship modelBuilder.Entity<Course>() .HasMany(t => t.Instructors) .WithMany(t => t.Courses) .Map(m => { m.ToTable("CourseInstructor"); m.MapLeftKey("CourseID"); m.MapRightKey("InstructorID"); });
Configuring a Relationship with One Navigation Property - 基于导航属性配置关系
如果关系是单向的,即两个实体间只在一个实体上定义导航属性。Code First Conventions 能够自动推断这种关系为 one-to-many .
例如你想在 Student 和 Address 两个实体间建立 one-to-one 关系,而且只在 Address 实体上包含导航属性,此时你就需要用 Code First Fluent API 配置这种关系
// Map one-to-one relationship modelBuilder.Entity<Address>() .HasRequired(t => t.Student) .WithRequiredPrincipal();
WillCascadeOnDelete - Enabling Cascade Delete (级联删除)
你可以使用 WillCascadeOnDelete 来级联删除关系,如果从属主体上的外键是 not nullable, 那么 Code First 将设置级联删除,否则将不会设置级联删除,而只是仅仅把外键设置成 null
在 Code First Conventions 下是这样移除级联删除的
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>() modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>()
Code First Fluent API 的方式如下
// Cascade Delete modelBuilder.Entity<Course>() .HasRequired(t => t.Department) .WithMany(t => t.Courses) .HasForeignKey(d => d.DepartmentID) .WillCascadeOnDelete(false);
Configuring a Composite Foreign Key - 配置组合外键
如果设置 Department 的主键为组合主键 DepartmentID, Name,则可以通过 Fluent API 为 Course 指定组合外键
// Composite primary key modelBuilder.Entity<Department>() .HasKey(d => new { d.DepartmentID, d.Name }); // Composite foreign key modelBuilder.Entity<Course>() .HasRequired(c => c.Department) .WithMany(d => d.Courses) .HasForeignKey(d => new { d.DepartmentID, d.DepartmentName });
Renaming a Foreign Key That Is Not Defined in the Model - 重命名外键
可以重命名外键名
// Renaming a Foreign Key That Is Not Defined in the Model modelBuilder.Entity<Course>() .HasRequired(c => c.Department) .WithMany(t => t.Courses) .Map(m => m.MapKey("ChangedDepartmentID"));
Configuring a Foreign Key Name That Does Not Follow the Code First Convention
如果从属实体上的外键属性名不符合 Code First Conventions 的规范,意即从属实体上没有外键,你可以通过如下方式来指定
// Configuring a Foreign Key Name That Does Not Follow the Code First Convention modelBuilder.Entity<Course>() .HasRequired(c => c.Department) .WithMany(d => d.Courses) .HasForeignKey(c => c.SomeDepartmentID);
原文链接:http://msdn.microsoft.com/en-us/data/jj591620
(六)存储过程
声明:本文只针对 EF6+
默认情况下,Code First 对实体进行插入、更新、删除操作是直接在表上进行的,从 EF6 开始你可以选择使用存储过程(Stored Procedures)
简单实体映射 Basic Entity Mapping
注意:本文将使用 Fluent API 来配置使用存储过程
public class Blog { public int BlogId { get; set; } public string Name { get; set; } public string Url { get; set; } [Timestamp] public byte[] Timestamp { get; set; } public virtual ICollection<Post> Posts { get; set; } }
modelBuilder.Entity<Blog>() .MapToStoredProcedures();
上面的代码执行后,Code First 将利用某些约定在数据库中生成一些存储过程:
- 生成三个存储过程,名称分别为<type_name>_Insert, <type_name>_Update, <type_name>_Delete (本例为 Blog_Insert, Blog_Update, Blog_Delete);
- 参数名对应于属性名 (注意:如果在 property上使用 HasColumnName() 或者 Column attribute 来重命名,那么参数也将使用这个重命名过的名称 );
- The insert stored procedure 为每一个属性都有一个参数,除了那些标记为数据库产生的(identity or computed),返回结果为那些标记为数据库产生的属性列;
- The update stored procedure 为每一个属性都有一个参数,除了那些标记为数据库产生且模式为 computed 的。一些并发标记的需要一个代表原始值的参数(更多信息请参考 Concurrency Tokens section)。返回值为那些 computed property 的列;
- The delete stored procedure 参数为实体主键(或者组合主键),此外也需要为每一个独立关联的外键准备一个参数(指那些没有在实体上定义相应外键属性的关系),一些并发标记的需要一个代表原始值的参数(更多信息请参考 Concurrency Tokens section)
存储过程的具体内容如下
CREATE PROCEDURE [dbo].[Blog_Insert] @Name [nvarchar](max), @Url [nvarchar](max) AS BEGIN INSERT [dbo].[Blog]([Name], [Url]) VALUES (@Name, @Url) DECLARE @BlogId int SELECT @BlogId = [BlogId] FROM [dbo].[Blog] WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity() SELECT t0.[BlogId], t0.[Timestamp] FROM [dbo].[Blog] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[BlogId] = @BlogId END CREATE PROCEDURE [dbo].[Blog_Update] @BlogId [int], @Name [nvarchar](max), @Url [nvarchar](max), @Timestamp_Original [rowversion] AS BEGIN UPDATE [dbo].[Blog] SET [Name] = @Name, [Url] = @Url WHERE (([BlogId] = @BlogId) AND (([Timestamp] = @Timestamp_Original) OR ([Timestamp] IS NULL AND @Timestamp_Original IS NULL))) SELECT t0.[Timestamp] FROM [dbo].[Blog] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[BlogId] = @BlogId END CREATE PROCEDURE [dbo].[Blog_Delete] @BlogId [int], @Timestamp_Original [rowversion] AS BEGIN DELETE [dbo].[Blog] WHERE (([BlogId] = @BlogId) AND (([Timestamp] = @Timestamp_Original) OR ([Timestamp] IS NULL AND @Timestamp_Original IS NULL))) END
Overriding the Defaults
你可以重写部分或全部的默认配置
重写存储过程名
重写 update 存储过程名
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.HasName("modify_blog")));
重写所有的存储过程名
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.HasName("modify_blog")) .Delete(d => d.HasName("delete_blog")) .Insert(i => i.HasName("insert_blog")));
效果与下面使用一样 lambda block syntax
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => { s.Update(u => u.HasName("modify_blog")); s.Delete(d => d.HasName("delete_blog")); s.Insert(i => i.HasName("insert_blog")); });
重写存储过程参数名
modelBuilder .Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.Parameter(b => b.BlogId, "blog_id")));
上面所有的操作都是可组合的和链式的,如如下示例重命名所有的存储过程及其参数名
modelBuilder .Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.HasName("modify_blog") .Parameter(b => b.BlogId, "blog_id") .Parameter(b => b.Name, "blog_name") .Parameter(b => b.Url, "blog_url")) .Delete(d => d.HasName("delete_blog") .Parameter(b => b.BlogId, "blog_id")) .Insert(i => i.HasName("insert_blog") .Parameter(b => b.Name, "blog_name") .Parameter(b => b.Url, "blog_url")));
重命名数据库产生的返回列名
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Insert(i => i.Result(b => b.BlogId, "generated_blog_identity")));
CREATE PROCEDURE [dbo].[Blog_Insert] @Name [nvarchar](max), @Url [nvarchar](max) AS BEGIN INSERT [dbo].[Blog]([Name], [Url]) VALUES (@Name, @Url) DECLARE @BlogId int SELECT @BlogId = [BlogId] FROM [dbo].[Blog] WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity() SELECT t0.[BlogId] AS generated_blog_identity, t0.[Timestamp] FROM [dbo].[Blog] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[BlogId] = @BlogId END
无外键关系 Relationships Without a Foreign Key in the Class
如果实体上有定义外键属性,那么其重命名操作与其它属性无异。如果实体间的关系存在,但是并没定义外键属性,那么默认的参数名为 <navigation_property_name>_<primary_key_name>
public class Blog { public int BlogId { get; set; } public string Name { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
如上类定义将会导致在 Insert 和 Update Post 存储过程中产生参数 Blog_BlogId
CREATE PROCEDURE [dbo].[Post_Insert] @Title [nvarchar](max), @Content [nvarchar](max), @Blog_BlogId [int] AS BEGIN INSERT [dbo].[Post]([Title], [Content], [Blog_BlogId]) VALUES (@Title, @Content, @Blog_BlogId) DECLARE @PostId int SELECT @PostId = [PostId] FROM [dbo].[Post] WHERE @@ROWCOUNT > 0 AND [PostId] = scope_identity() SELECT t0.[PostId] FROM [dbo].[Post] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[PostId] = @PostId END CREATE PROCEDURE [dbo].[Post_Update] @PostId [int], @Title [nvarchar](max), @Content [nvarchar](max), @Blog_BlogId [int] AS BEGIN UPDATE [dbo].[Post] SET [Title] = @Title, [Content] = @Content, [Blog_BlogId] = @Blog_BlogId WHERE ([PostId] = @PostId) END
Overriding the Defaults
通过提供主键属性给 Parameter 方法,你可以重命名在类中没有包含的外键参数名
modelBuilder.Entity<Post>() .MapToStoredProcedures(s => s.Insert(i => i.Parameter(p => p.Blog.BlogId, "blog_id")));
生成的 Insert 存储过程如下
CREATE PROCEDURE [dbo].[Post_Insert] @Title [nvarchar](max), @Content [nvarchar](max), @blog_id [int] AS BEGIN INSERT [dbo].[Post]([Title], [Content], [Blog_BlogId]) VALUES (@Title, @Content, @blog_id) DECLARE @PostId int SELECT @PostId = [PostId] FROM [dbo].[Post] WHERE @@ROWCOUNT > 0 AND [PostId] = scope_identity() SELECT t0.[PostId] FROM [dbo].[Post] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[PostId] = @PostId END
如果在从属实体(dependent entity)上没有导航属性(navigation property)(例如 Post.Blog),你可以使用 Navigation(原文是 Association 方法,但笔者发现根本没有此方法) 方法来确定另一端的关系然后为相应的主键(或组合主键)配置参数
public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } }
modelBuilder.Entity<Post>() .MapToStoredProcedures(s => s.Insert(i => i.Navigation<Blog>( b => b.Posts, c => c.Parameter(b => b.BlogId, "blog_id"))));
产生的存储过程如下
CREATE PROCEDURE [dbo].[Post_Insert] @Title [nvarchar](max), @Content [nvarchar](max), @blog_id [int] AS BEGIN INSERT [dbo].[Post]([Title], [Content], [Blog_BlogId]) VALUES (@Title, @Content, @blog_id) DECLARE @PostId int SELECT @PostId = [PostId] FROM [dbo].[Post] WHERE @@ROWCOUNT > 0 AND [PostId] = scope_identity() SELECT t0.[PostId] FROM [dbo].[Post] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[PostId] = @PostId END
并发标记 Concurrency Tokens
Update 和 Delete 存储过程也需要处理并发问题:
- 如果实体包含并发标记,存储过程可选择地有一个 Output 参数用于返回更新/删除的列的数目,这样一个参数必须通过方法 RowsAffectedParameter 配置(注意:EF 默认使用 ExecuteNonQuery 的返回值来确定有多少行受影响,如果你在存储过程中执行逻辑操作将导致 ExecuteNonQuery 的返回值是错误的,此时指定一个行影响的 Output 参数是有必要的);
- 每一个并发标记,都有一个参数,命名为 <property_name>_Original (如 Timestamp_Original),这个参数将传递属性的原始值 - 从数据库查询的值
- 数据库计算(computed)的并发标记 - 例如 timestamp - 将有一个原始值参数;
- 非计算属性的并发标记在 Update 存储过程中也有一个更新值参数,只是使用之前讨论过的为新值的命名约定。此处的一个例子为用Blog 的 URL 作为并发标记,更新后的新值是必须的因为这个值可能被你的代码更新成另一个新值(不像 Timestamp 标记只能被数据库更新)
一个计算并发标记(timestamp)标记的例子
public class Blog { public int BlogId { get; set; } public string Name { get; set; } public string Url { get; set; } [Timestamp] public byte[] Timestamp { get; set; } }
modelBuilder.Entity<Blog>() .MapToStoredProcedures();
CREATE PROCEDURE [dbo].[Blog_Update] @BlogId [int], @Name [nvarchar](max), @Url [nvarchar](max), @Timestamp_Original [rowversion] AS BEGIN UPDATE [dbo].[Blog] SET [Name] = @Name, [Url] = @Url WHERE (([BlogId] = @BlogId) AND (([Timestamp] = @Timestamp_Original) OR ([Timestamp] IS NULL AND @Timestamp_Original IS NULL))) SELECT t0.[Timestamp] FROM [dbo].[Blog] AS t0 WHERE @@ROWCOUNT > 0 AND t0.[BlogId] = @BlogId END
一个非计算并发标记(URL)例子
public class Blog { public int BlogId { get; set; } public string Name { get; set; } [ConcurrencyCheck] public string Url { get; set; } }
CREATE PROCEDURE [dbo].[Blog_Update] @BlogId [int], @Name [nvarchar](max), @Url [nvarchar](max), @Url_Original [nvarchar](max) AS BEGIN UPDATE [dbo].[Blog] SET [Name] = @Name, [Url] = @Url WHERE (([BlogId] = @BlogId) AND (([Url] = @Url_Original) OR ([Url] IS NULL AND @Url_Original IS NULL))) END
Overriding the Defaults
使用 RowsAffectedParameter 方法
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.RowsAffectedParameter("rows_affected")));
CREATE PROCEDURE [dbo].[Blog_Update] @BlogId [int], @Name [nvarchar](max), @Url [nvarchar](max), @Url_Original [nvarchar](max), @rows_affected [int] OUT AS BEGIN UPDATE [dbo].[Blog] SET [Name] = @Name, [Url] = @Url WHERE (([BlogId] = @BlogId) AND (([Url] = @Url_Original) OR ([Url] IS NULL AND @Url_Original IS NULL))) SET @rows_affected = @@ROWCOUNT END
对于计算并发标记 - 只有原始值需要传递 - 我们可以使用标准的 Pameter 方法来重命名参数名
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.Parameter(b => b.Timestamp, "blog_timestamp")));
对于非计算并发标记 - 原始值和更新值都需传递 - 我们可以使用 Parameter 方法的重载版本来为每一个参数重命名
modelBuilder.Entity<Blog>() .MapToStoredProcedures(s => s.Update(u => u.Parameter(b => b.Url, "blog_url", "blog_original_url")));
N:N 关系 Many to Many Relationships
定义如下类
public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual ICollection<Tag> Tags { get; set; } } public class Tag { public int TagId { get; set; } public string TagName { get; set; } public virtual ICollection<Post> Posts { get; set; } }
映射到存储过程
modelBuilder.Entity<Post>() .HasMany(p => p.Tags) .WithMany(t => t.Posts) .MapToStoredProcedures();
默认生成的存储过程如下:
- 生成两个存储过程,命名为 <type_one><type_two>_Insert 和 <type_one><type_two>_Delete (本例中为PostTag_Insert 和PostTag_Delete);
- 参数为每一类型的主键(或组合主键),命名为 <type_name>_<property_name> (本例为 Post_PostId 和 Tag_TagId)
CREATE PROCEDURE [dbo].[PostTag_Insert] @Post_PostId [int], @Tag_TagId [int] AS BEGIN INSERT [dbo].[PostTag]([Post_PostId], [Tag_TagId]) VALUES (@Post_PostId, @Tag_TagId) END CREATE PROCEDURE [dbo].[PostTag_Delete] @Post_PostId [int], @Tag_TagId [int] AS BEGIN DELETE [dbo].[PostTag] WHERE (([Post_PostId] = @Post_PostId) AND ([Tag_TagId] = @Tag_TagId)) END
Overriding the Defaults
可以像配置实体存储过程一样来配置此存储过程和参数的名称
modelBuilder.Entity<Post>() .HasMany(p => p.Tags) .WithMany(t => t.Posts) .MapToStoredProcedures(s => s.Insert(i => i.HasName("add_post_tag") .LeftKeyParameter(p => p.PostId, "post_id") .RightKeyParameter(t => t.TagId, "tag_id")) .Delete(d => d.HasName("remove_post_tag") .LeftKeyParameter(p => p.PostId, "post_id") .RightKeyParameter(t => t.TagId, "tag_id")));
产生的存储过程如下
CREATE PROCEDURE [dbo].[add_post_tag] @post_id [int], @tag_id [int] AS BEGIN INSERT [dbo].[PostTag]([Post_PostId], [Tag_TagId]) VALUES (@post_id, @tag_id) END CREATE PROCEDURE [dbo].[remove_post_tag] @post_id [int], @tag_id [int] AS BEGIN DELETE [dbo].[PostTag] WHERE (([Post_PostId] = @post_id) AND ([Tag_TagId] = @tag_id)) END
原文链接:http://msdn.microsoft.com/en-us/data/dn468673
(七)空间数据类型 Spatial Data Types
声明:本文针对 EF5+, Visual Studio 2012+
空间数据类型(Spatial Data Types)是在 EF5 中引入的,空间数据类型表现有两种:
- Geography (地理学上的)- 存储的是椭圆形数据,如 GPS 的经纬度坐标;
- Geometry (几何学上的)- 代表欧氏几何(平面的)坐标系统。
下面示例该数据类型的一个应用程序
新建 C# 控制台应用程序
相信 Console 应该都会建,命名为 SpatialCodeFirst
使用 Code First 建立 Model
注意为 DbGeography 添加 System.Data.Entity.Spatial 命名空间
public class University { public int UniversityID { get; set; } public string Name { get; set; } public DbGeography Location { get; set; } }
定义DbContext 的派生类型
先要安装 Entity Framework (请参考 http://www.cnblogs.com/panchunting/p/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application.html)
注意添加命名空间 System.Data.Entity, System.Data.Entity.ModelConfiguration.Conventions
class UniversityContext : DbContext { public UniversityContext() : base("UniversityContext") { } public DbSet<University> Universities { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } }
定义UniversityInitializer
注意添加命名空间 System.Data.Entity, System.Data.Entity.Spatial
class UniversityInitializer : DropCreateDatabaseIfModelChanges<UniversityContext> { protected override void Seed(UniversityContext context) { var universities = new List<University>() { new University { Name = "Graphic Design Institute", Location = DbGeography.FromText("POINT(-122.336106 47.605049)") }, new University { Name = "School of Fine Art", Location = DbGeography.FromText("POINT(-122.335197 47.646711)") } }; universities.ForEach(s => context.Universities.Add(s)); context.SaveChanges(); } }
配置App.config
注意黄色高亮部分,第一部分为配置数据库连接(使用 LocalDB ),第二部分为配置说明使用的 DbContext 及其初始化器
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="UniversityContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=SpatialCodeFirst;Integrated Security=SSPI;" providerName="System.Data.SqlClient"/> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <entityFramework> <contexts> <context type="SpatialCodeFirst.UniversityContext, SpatialCodeFirst"> <databaseInitializer type="SpatialCodeFirst.UniversityInitializer, SpatialCodeFirst" /> </context> </contexts> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" /> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>
在 Program.cs 添加代码调用
class Program { static void Main(string[] args) { using (var context = new UniversityContext()) { var myLocation = DbGeography.FromText("POINT(-122.296623 47.640405)"); var university = (from u in context.Universities orderby u.Location.Distance(myLocation) select u).FirstOrDefault(); Console.WriteLine("The closest University to you is: {0}.", university.Name); Console.ReadKey(); } } }
运行查看结果
后台数据库定义
表中数据
(localdb)\v11.0不在列表中
右击新建 Connection 即可
原文:http://msdn.microsoft.com/en-us/data/hh859721
(八)迁移 Migrations
创建初始模型和数据库
在开始使用迁移(Migrations)之前,我们需要一个 Project 和一个 Code First Model, 对于本文将使用典型的 Blog 和 Post 模型
- 创建一个新的控制台应用程序 MigrationsDemo;
- 添加最新的 EntityFramework 到项目
- Tools –> Library Package Manager –> Package Manager Console;
- 运行命令 Install-Package EntityFramework
- 创建 Blog.cs 和 DbContext 的派生类 BlogContext.cs
public class Blog { public int BlogId { get; set; } public string Name { get; set; } }
public class BlogContext : DbContext { public DbSet<Blog> Blogs { get; set; } }
更改 Program.cs 以调用
static void Main(string[] args) { using (var db = new BlogContext()) { db.Blogs.Add(new Blog { Name = "Another Blog" }); db.SaveChanges(); foreach (var blog in db.Blogs) { Console.WriteLine(blog.Name); } } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); }
运行查看结果
发现如上错误"CREATE DATABSE permission denied in databse 'master'"
我们在 BlogContext 上的无参构造函数上添加诊断代码并设置调试断点
System.Diagnostics.Debug.Write(Database.Connection.ConnectionString);
再次运行
我们注意到 Data Scource 竟然是 .\\SQLEXPRESS 而不是我们想要的 localDB , 这是因为:
- 如果我们安装了 SQL Express,那么 database 将会安装在 local SQL Express instance,否则 Code First 才将尝试使用 localDB;
- SQL Express 总是具有优先权,只要安装了它
知道了原因我们就好解决了:
- 如果想继续使用 SQL Express,那么就配置相应地权限,请参考 http://odetocode.com/Blogs/scott/archive/2012/08/14/a-troubleshooting-guide-for-entity-framework-connections-amp-migrations.aspx;
- 如果想改用 localDB, 只需在.config 配置即可(放在 configSections 节点后面)
<connectionStrings> <add name="BlogContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=BlogContext;Integrated Security=SSPI;" providerName="System.Data.SqlClient"/> </connectionStrings>
再次运行就行了,让我们看一下后台生成的数据库
启用迁移
我们对模型 Blog 做一些更改:增加一个 Url 属性
public string Url { get; set; }
我们此时再次运行程序,发现如下错误
'InvalidOperationException' was unhandled. The model backing the 'BlogContext' context has changed since the database was created. Consider using Code First Migrations to update the database ( http://go.microsoft.com/fwlink/?LinkId=238269)
正如错误消息提示的那样,是时候使用 Code First Migrations,第一步是运行如下的命令:
- 在 Package Manager Console 下运行命令 Enable-Migrations
这个命令将在项目下创建文件夹 Migrations
- The Configuration class 这个类允许你去配置如何迁移,对于本文将使用默认的配置(在本文中因为只有一个 Context, Enable-Migrations 将自动对 context type 作出适配);
- An InitialCreate migration (本文为 201312240822431_InitialCreate.cs)这个迁移之所以存在是因为我们之前用 Code First 创建了数据库, 在启用迁移前,scaffolded migration 里的代码表示在数据库中已经创建的对象,本文中即为表 Blog (列 BlogId 和 Name). 文件名包含一个 timestamp 以便排序(如果之前数据库没有被创建,那么 InitialCreate migration 将不会被创建,相反,当我们第一次调用 Add-Migration 的时候所有表都将归集到一个新的 migration 中)
多个实体锁定同一数据库
当使用 EF6 之前的版本时,只会有一个 Code First Model 被用来生成/管理数据库的 Schema, 这将导致每个数据库只会有一张 __MigrationsHistory 表,从而无法辨别实体与模型的对应关系。
从 EF6 开始,Configuration 类将会包含一个 ContextKey 属性,它将作为每一个 Code First Model 的唯一标识符, __MigrationsHistory 表中一个相应地的列允许来自多个模型(multiple models)的实体共享表(entries),默认情况下这个属性被设置成 context 的完全限定名。
生成、运行迁移
Code First Migrations 有两个你需要熟悉的命令:
- Add-Migration 将 scaffold 创建下一次基于上一次迁移以来的更改的迁移;
- Update-Databse 将任何挂起的迁移应用到数据库
我们需要脚手架(scaffold 直译)一个迁移,以上面的 Url 属性为例,命令 Add-Migration 允许我们对迁移命名,我们姑且称之为 AddBlogUrl
- 在 Package Manager Console 中运行命令 Add-Migration AddBlogUrl;
- 一个新的迁移(名称包含 timestamp 前缀)在目录 Migrations 中创建成功
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddBlogUrl : DbMigration { public override void Up() { AddColumn("dbo.Blogs", "Url", c => c.String()); } public override void Down() { DropColumn("dbo.Blogs", "Url"); } } }
我们现在可以对这个迁移进行编辑或者增加,但似乎看起来还不错,那我们就直接用 Update-Database 来应用到数据库吧
- 在 Package Manager Console 中运行命令 Update-Database ;
- AddBlogUrl 迁移将会被应用到数据库(表 Blogs 增加一列 Url)
定制化迁移
到目前为止我们生成并运行了一个迁移,但是没有对迁移做任何更改,下面我们将尝试做一些更改:在类 Bolg 上增加一属性 Rating
public int Rating { get; set; }
新建 Post
public class Post { public int PostId { get; set; } [MaxLength(200)] public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
在 Blog 中添加 Post 的集合
public virtual ICollection<Post> Posts { get; set; }
在 Package Manager Console 中运行命令 Add-Migration AddPostClass
生成的迁移如下
接下来我们对迁移做些更改:
- 在 Posts.Title 列上增加唯一索引;
- 使 Blogs.Rating 列非空,对于表中已经存在的数据,新列都会被赋值成 CLR 的默认数据类型(如 Rating 是整型,故默认值为0),但是我们想指定默认值为3,这样存在的记录将会有一个合理的评分。
更改后的代码如下
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddPostClass : DbMigration { public override void Up() { CreateTable( "dbo.Posts", c => new { PostId = c.Int(nullable: false, identity: true), Title = c.String(maxLength: 200), Content = c.String(), BlogId = c.Int(nullable: false), }) .PrimaryKey(t => t.PostId) .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true) .Index(t => t.BlogId) .Index(p => p.Title, unique: true); AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3)); } public override void Down() { DropIndex("dbo.Posts", new[] { "Title" }); DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs"); DropIndex("dbo.Posts", new[] { "BlogId" }); DropColumn("dbo.Blogs", "Rating"); DropTable("dbo.Posts"); } } }
在 Package Manager Console 中运行命令 Update-Database –Verbose
数据移动 / 定制SQL
迄今为止,迁移都没有更改或移动数据,现在让我们看一下需要移动数据的例子。虽然没有对数据移动的原生支持,但是我们可以随意运行 SQL脚本。
让我们在 Post 中增加一个属性 Abstract, 稍后我们使用列 Content 的开头来填充此列(数据库已有记录)
public string Abstract { get; set; }
- 在 Package Manager Console 中运行命令 Add-Migration AddPostAbstract ;
- 生成的迁移非常好,但是我们想使用 Content 的前 100 个字符来预填充 Abstract 列,我们可对迁移做如下更改
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddPostAbstract : DbMigration { public override void Up() { AddColumn("dbo.Posts", "Abstract", c => c.String()); Sql("UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL"); } public override void Down() { DropColumn("dbo.Posts", "Abstract"); } } }
在 Package Manager Console 中运行命令 Update-Database –Verbose
迁移至指定版本(包括后退)
迄今为止,我们总是升级至最新迁移,然而某些时候我们需要升级/降级至指定版本,例如我们想迁移数据库至运行 AddBlogUrl 迁移之后的状态,此时我们就可以使用 –TargetMigration 来降级到这个版本
在 Package Manager Console 中运行命令 Update-Database –TargetMigration: AddBlogUrl
这个命令将会运行 AddBlogAbstract and AddPostClass 的 Down 命令
如果你想回滚一切至空数据库,可以使用命令 Update-Database –TargetMigration: $InitialDatabase
得到SQL脚本
如果其它开发人员也希望在他们自己的机器上拥有这些更改,他们只需在我们 check in 代码至 source control 的时候做一次同步即可,一旦他们拥有了这些迁移,只需运行命令 Update-Database 就可以把这些更改应用于本地。但是如果我们想把这些更改推送至测试服务器或生产服务器,我们也许需要一份 SQL 脚本提供给 DBA
- 在运行 Update-Database 的时候指定 -Specify 标记,我们就能够使得这些更改被写入一个脚本中而不是被应用,我们同时也会为此脚本指定源迁移和目标迁移,例如我们希望产生的脚本是从一个空数据库($InitialDatabase)到最新的版本(AddPostAbstract 迁移);(注意:如果你没有指定目标迁移,那么迁移将始终更新至最新版本;如果你没有指定源迁移,那么迁移将以数据库目前状态为初始)
- 在 Package Manager Console 中运行命令 Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: AddPostAbstract
产生的 SQL 脚本如下
DECLARE @CurrentMigration [nvarchar](max) IF object_id('[dbo].[__MigrationHistory]') IS NOT NULL SELECT @CurrentMigration = (SELECT TOP (1) [Project1].[MigrationId] AS [MigrationId] FROM ( SELECT [Extent1].[MigrationId] AS [MigrationId] FROM [dbo].[__MigrationHistory] AS [Extent1] WHERE [Extent1].[ContextKey] = N'MigrationsDemo.BlogContext' ) AS [Project1] ORDER BY [Project1].[MigrationId] DESC) IF @CurrentMigration IS NULL SET @CurrentMigration = '0' IF @CurrentMigration < '201312240822431_InitialCreate' BEGIN CREATE TABLE [dbo].[Blogs] ( [BlogId] [int] NOT NULL IDENTITY, [Name] [nvarchar](max), CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY ([BlogId]) ) CREATE TABLE [dbo].[__MigrationHistory] ( [MigrationId] [nvarchar](150) NOT NULL, [ContextKey] [nvarchar](300) NOT NULL, [Model] [varbinary](max) NOT NULL, [ProductVersion] [nvarchar](32) NOT NULL, CONSTRAINT [PK_dbo.__MigrationHistory] PRIMARY KEY ([MigrationId], [ContextKey]) ) INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion]) VALUES (N'201312240822431_InitialCreate', N'MigrationsDemo.BlogContext', 0x1F8B0800000000000400CD574B4FDB4010BE57EA7FB0F6D41EC826E1D22207040954511B4018B86FEC4958751FAE778D92DFD6437F52FF4267FD8E4D1E200EBD44F178E69B996FE7B1FEFBFB8F7FB692C27B86C470AD4664D0EB130F54A823AE962392DAC5D1177276FAF1837F19C995F758EA1D3B3DB45466449EAC8D4F2835E11348667A928789367A617BA19694459A0EFBFDAF7430A0801004B13CCFBF4B95E512B2077C1C6B15426C5326663A02610A39BE093254EF9A4930310B6144667C99308B519809484DBC73C11946118058108F29A56DF6F6E4C1406013AD96418C0226EED731A0DE82090345EC27B5FAA169F4872E0D5A1BBE8906522588295E221576EDC2CBD21C910BA1974D0DD4F90EEB0D018A6E131D4362D777B068D84D23E2D14D5BDA36AE4C5B762E8411992A7B3C24DE752A049B0BA8186B501B589DC03750800701D12DB3161264701A41964A2782963FF75B7AC323C24A23DE8CAD7E805ADA273C60B622DE155F41544A8A081E14C7C244239BA4B0CFC94322DED9874FEB93EA9E1F96B0651C296990EA64B0B2ADB3CCF503B00D4D43BC1A3C2FE65E56062F4650F9AAFB84E68D523614DDD251FE8CC53192D1E8B042E205797B8D8F82D797B4CC3168685EA8EC2ADACA13960F5B42EB2DBAC648AF7862EC84593667EE38C691ECA81DC06CE9A94970BB7F6ABE4B6DF73FB7D89C31BD6E433699BBC26424567E96175431BCD4C785611032C1922D5D38D622956A5747EF42C97BAB89914B0E47C81AA7099009BAF63E6DE5DF269876186ECDA5F671EDAAF4B64AE5BDAAF85665FB4595ED5F289DB2CB558887C43CF3C8955CB0361664CF29F4825F622C38E65B2BCC98E20B30F65EFF049C83C3FE60D8DA4B6FD811D49848FCCF8B823B0AF6AE82CE1E397C37A86796844F2CF924D9EA7313E995F3FF1538AF9BF1DD81B57FD26F1DF479D58D4834C74BCD7D1E60B11ADEB803BA1DE0D3E6C5CB9F80E1CB1AC25DC314846EEED5A0A5CE542D74493166D48CA854699DC00C2C8B9098F3C4F2050B2DBE0EC1986C153F3291A2CAA59C43345537A98D537B6E0CC8B9D8B83EF874B7FF6CD16DC6ECDFC4D9E47E8F14304C8E29C08DBA48B988AAB8AFBAA5BC0DC2D548D11F18155E45106EB9AE90AEB53A10A8A06F023128D75DF720638160E64605EC19B6C7B69FC34DC6FC0967B8FEA429306A7BF71D40DD87C0E93F1D45E2A93A0C0000 , N'6.0.2-21211') END IF @CurrentMigration < '201312310618077_AddBlogUrl' BEGIN ALTER TABLE [dbo].[Blogs] ADD [Url] [nvarchar](max) INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion]) VALUES (N'201312310618077_AddBlogUrl', N'MigrationsDemo.BlogContext', 0x1F8B0800000000000400CD574B4FDB4010BE57EA7FB0F6D41EC826E1D22207040954511B4018B86FEC4958751FAE778D92DFD6437F52FF4267FD8E4D1E200EBD44F178E69B996FE7B1FEFBFB8F7FB692C27B86C470AD4664D0EB130F54A823AE962392DAC5D1177276FAF1837F19C995F758EA1D3B3DB45466449EAC8D4F2835E11348667A928789367A617BA19694459A0EFBFDAF7430A0801004B13CCFBF4B95E512B2077C1C6B15426C5326663A02610A39BE093254EF9A4930310B6144667C99308B519809484DBC73C11946118058108F29A56DF6F6E4C1406013AD96418C0226EED731A0DE82090345EC27B5FAA169F4872E0D5A1BBE8906522588295E221576EDC2CBD21C910BA1974D0DD4F90EEB0D018A6E131D4362D777B068D84D23E2D14D5BDA36AE4C5B762E8411992A7B3C24DE752A049B0BA8186B501B589DC03750800701D12DB3161264701A41964A2782963FF75B7AC323C24A23DE8CAD7E805ADA273C60B622DE155F41544A8A081E14C7C244239BA4B0CFC94322DED9874FEB93EA9E1F96B0651C296990EA64B0B2ADB3CCF503B00D4D43BC1A3C2FE65E56062F4650F9AAFB84E68D523614DDD251FE8CC53192D1E8B042E205797B8D8F82D797B4CC3168685EA8EC2ADACA13960F5B42EB2DBAC648AF7862EC84593667EE38C691ECA81DC06CE9A94970BB7F6ABE4B6DF73FB7D89C31BD6E433699BBC26424567E96175431BCD4C785611032C1922D5D38D622956A5747EF42C97BAB89914B0E47C81AA7099009BAF63E6DE5DF269876186ECDA5F671EDAAF4B64AE5BDAAF85665FB4595ED5F289DB2CB558887C43CF3C8955CB0361664CF29F4825F622C38E65B2BCC98E20B30F65EFF049C83C3FE60D8DA4B6FD811D49848FCCF8B823B0AF6AE82CE1E397C37A86796844F2CF924D9EA7313E995F3FF1538AF9BF1DD81B57FD26F1DF479D58D4834C74BCD7D1E60B11ADEB803BA1DE0D3E6C5CB9F80E1CB1AC25DC314846EEED5A0A5CE542D74493166D48CA854699DC00C2C8B9098F3C4F2050B2DBE0EC1986C153F3291A2CAA59C43345537A98D537B6E0CC8B9D8B83EF874B7FF6CD16DC6ECDFC4D9E47E8F14304C8E29C08DBA48B988AAB8AFBAA5BC0DC2D548D11F18155E45106EB9AE90AEB53A10A8A06F023128D75DF720638160E64605EC19B6C7B69FC34DC6FC0967B8FEA429306A7BF71D40DD87C0E93F1D45E2A93A0C0000 , N'6.0.2-21211') END IF @CurrentMigration < '201312310648099_AddPostClass' BEGIN CREATE TABLE [dbo].[Posts] ( [PostId] [int] NOT NULL IDENTITY, [Title] [nvarchar](200), [Content] [nvarchar](max), [BlogId] [int] NOT NULL, CONSTRAINT [PK_dbo.Posts] PRIMARY KEY ([PostId]) ) CREATE INDEX [IX_BlogId] ON [dbo].[Posts]([BlogId]) CREATE UNIQUE INDEX [IX_Title] ON [dbo].[Posts]([Title]) ALTER TABLE [dbo].[Blogs] ADD [Rating] [int] NOT NULL DEFAULT 3 ALTER TABLE [dbo].[Posts] ADD CONSTRAINT [FK_dbo.Posts_dbo.Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [dbo].[Blogs] ([BlogId]) ON DELETE CASCADE INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion]) VALUES (N'201312310648099_AddPostClass', N'MigrationsDemo.BlogContext', 0x1F8B0800000000000400D558CD6EE33610BE17E83B083AB505D64AB29736B07791759222681D0751B2D78091C60E518A54452AB09F6D0F7DA4BE4287FA33454AB6ECAC17DD4B1093331F6786DF0C67F4EF977FC61F5709F35E219354F0897F3A3AF13DE09188295F4EFC5C2DDEFDEA7FFCF0E30FE3AB3859799F6BB9F75A0E35B99CF82F4AA5E74120A31748881C2534CA84140B358A4412905804672727BF05A7A70120848F589E37BECFB9A209143FF0E754F0085295133613313059ADE34E58A07AB7240199920826FE8C2E33A2D00A790989F0BD0B46095A11025BF81EE15CA862F7FC5142A832C197618A0B843DAC5340B90561122ADBCF37E243DD3839D36E041BC583C2E0370EA28B57180AB5D6E6156E4EFC4F4C2C4D0994F903D6AD055CBACB440A995ADFC3C2D0BB897D2F68EB06B672A36AE9691326FE0D57EFCF7CEF36678C3C336822668436542283DF81035E04C4774429C83082373114AE381658E7E9BFF5697845C834DF9B91D59FC097EA052F98AC7CEF9AAE20AE572A0B1E394562A292CA72D875C863C68E7EC63D8643036F0F9C85714B5EE9B288A3857627A492BE770FACA4F70B4D4B5A8FF4CE53C10AEF3A13C9BD609578B1F8F440B225283443D83BA1C8B3C832611C6C08B795861AE6101A6ABD436858EB7D2B1A3E50C5B6F110F3F52B70044B9B427B8ECEC561493C948B25D9F6A7624D38978A35498750F1424A11D1E268831B4F1D75F18AC75EAF0D65C06BAB31E63953346534C22327FE2F4E34BAC09ADCDA8095DEB7C14E7D3B2FE6FC121828F02EA2F27D99121991D8BD038C41DC5EC154824C739830648F5419A15CB97947794453C2FA4CB614F67831B4510DBCBD730929709D617D717FDBB90DBC15A45D3119070669DCB2A6B3103520330C2832736597B8523E0465486255DE10B5C50287416DE5AAA43BCA251F2D65C3813642956FC6BE938C761CB7E5456364639F730DDB32C150AF626313BAED4847BE37B7B1E9F382B2D1AB1BC2A0A7231CCF489A6201353AC46AC50BCBF670FA2EDCBF254B4A8C20921D9D59636D7312BE3B6409D6AE2EF5315CD34CAA4BA2C833D1F5761A278ED800EED527991474EFA826552DADFF2F35DA3DF2A88B229BC85DA333894EE8E23DB5AED7552B9A72C248D6F3004D05CB13BE3DCFFB51CADED0C428578623148D9F09502C0CD7AF9B3A13A25E7351C6811545A7A239F7E494FFF6A50FA24499B86FA084DBDC0DA244B75A5F28EB8ECE0C657777B80DA5EAD34C906A693846D3859928CDE2709CFD487E547238C5D416694E6F8AAA553CC75521DB3D733B95AD14F13D0CCF2B8D75550BD7524132D202A3F06F3665B4086D2D30239C2E40AA07F11770DD5B9F9E59A3FB0163742065CCFECFB334D521D839A6ECEAD2B78CCFFC9564D10BC97E4AC8EA671369CF11F94D38ED31B8F079AB4BDFC9047A94CB6B0D9D75D4DDB173BF0BB066CC375D6607810FBACC4366B8B2CFFD96A356BBD13D60663C6856EBE9C78E369F7D1F3399DB3EEF9ECC7A07B3F2819AF8F1B3C05B2DD9DB3DAE74CD6CBD235B176CE71075A469CEF479D3A3EF1AE09C99EF38339BDB4E20478C0FFDC84D49971B08FDD99F43D462472373C317A226A965512D6255AE19281223752E3245172452B81D8194C5D3F499B01C45AE9267886FF83C5769AED065489E59EB3BA126FBB6F38BC1B46DF3789E166DF5D77001CDA4E802CCF9A79CB2B8B1FBBAA3EEF640E82CAADE2B7D974ABF5BCB7583742BF840A02A7C4DF23F4092320493731E9257E8B76D770CDB111B5F5282B349222B8C8D3EFE44FAC5C9EAC37FEC5F9589AA1A0000 , N'6.0.2-21211') END IF @CurrentMigration < '201312310729575_AddPostAbstract' BEGIN ALTER TABLE [dbo].[Posts] ADD [Abstract] [nvarchar](max) UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion]) VALUES (N'201312310729575_AddPostAbstract', N'MigrationsDemo.BlogContext', 0x1F8B0800000000000400D559CD6EE33610BE17E83B083AB505D64AB29736B077E1759222689D0451B2D78096C636518A54452AB09F6D0F7DA4BE4287FA1729D9B2B35E742F414CCE7C9C197E339CB1FFFDF2CFF8E32662CE2B24920A3E71CF4767AE033C1021E5AB899BAAE5BB5FDD8F1F7EFC617C1D461BE77329F75ECBA126971377AD547CE97932584344E428A24122A458AA5120228F84C2BB383BFBCD3B3FF700215CC4729CF163CA158D20FB801F67820710AB94B0B90881C9621D77FC0CD5B92311C898043071E77495108556C82B8884EB4C192568850F6CE93A8473A1B2DDCB6709BE4A045FF9312E10F6B48D01E5968449286CBFACC587BA7176A1DDF06AC5A3C2E0560EA28BD7180AB5D5E6656E4EDC4F4CAC9A1228F3076C5B0BB8F490881812B57D846543EF36741DAFADEB99CA95AAA1A74D98B8B75CBDBF709DBB9431B2605045AC115A5F89047E070E7811103E10A520C108DE8690B96259609CA7FF96A7E11521D35C674E367F025FA9355E30D9B8CE0DDD4058AE14163C738AC4442595A4B0EF90E7849DFC8C470C8706DE1D3803E38EBCD255164703ED4148255DE711584EEF358D735A8FF4CE4BC60AE72611D1A3608578B6F8F2449215283443983BBE4893C03061ECD584DB49430D730C0DB5DE31342CF5BE150D9FA862BB7888F9FA153882A54DA13D27E7E27421554282D31F34AC5A0C257DCEEAC3395F32DBE67C990D43383F955204343BBA41C2978E027CCD43A7D7863CE0A5D518F394291A331AE09113F7172B1A5D605512D760B9F76DB073D74CC07B7E050C1438D3207FC866440624B4EF006310B657306721D1C94218D254F38772652738E5018D09EB33D95038E069D24655F0E6CE15C4C0752AF7C5FD6DE756F04690F6C564EC354863D74F9DEEA80149C380AC046CCC5A9ACBFBA01A9258FE6BA2B6586031A8AD5CBC1D9672CE4743B9E1401BA1C8B7C6BE958C661C77E5456564659F750DBB32A1A15EC4C62474DB918E7CAF6EA36E28BDBCA32C3B4FAFA7F51CCF491C63016DB4A2C58AE3E77DE8EC9D7F78EF17E5185E203B5AC0CADAEA247CE0C80A8C5DFDA684704313A9AE88220BA2EBED2C8C2CB101DC2B4F6A52D0BEA39254A5B4FE3FD76837E3A32E8AD491BB4167229DD0D9C36D5CAFAD9675FF8491A4E7019A0996467C779EF7A3E44D6813235F198E9075984D806C61B87ED93D3621CA351B65EC1951B42A9A754F56F96F5FFA204AE489FB064AD85DE4204A74ABF585B26C1D9BA1EC6E4377A1140D6113A4581A8E51B57B4D946A71384EDDCE3581EAD5E14887A5CB496966956553A43ABD2ACF46191E172571FFD704568DCC455C07C3F34A435D1FFDAD54108DB4C0C8FF9BCD18CD2EA91498134E9720D593F80BB81E07CE2F8C6F1B8E98FC3D2943F67F1EFFA90EC1DEC96A5FBFBF63E2E7AF2409D624F929229B9F9B48074EF56FC2694FEE99CF3B5DFA4E86E6935C5E6B4E2EA36E4FCA875D803116BFE932CDD1F74D601DD97014338E192DF3F6FB5B4E80EDFEFB8851F6A811B2A74D3CD9D8F87D8C8A7657BF7F60EC9D17F3D76EE2860B81B79AB3B77B8AEA1A257B27C92ED8CED9EE444366D3E77A74D837575AA3E8694649BB37418E347EE8406E4ABAAA21F4CF1E1C82163B2A995BBE1425490D8B4A11A372CD419110A9334D145D6239C4ED00A4CCDEB9CF84A528721D2D20BCE5F7A98A53852E43B460ADEF4935D9779D9FCDCB6D9BC7F771D6ED7F0D17D04C8A2EC03DFF9452165676DF74D4DD1E089D45C5E3A7EF52E94770B5AD90EE041F085484AF4AFE2788628660F29EFBE415FA6DDB1FC376C4C65794E0C814C902A3D6C78F48BF30DA7CF80FF7E621BAAA1B0000 , N'6.0.2-21211') END
产生幂等脚本(EF6+)
从 EF6 开始,如果你使用 –SourceMigration $InitialDatabase, 产生的脚本将是幂等的,幂等脚本意味着无论数据库当前处于什么版本/状态,都能升级至最新版本或指定版本(指定 –TargetMigration),生成的脚本包括检查表 __MigrationsHistory 的逻辑以及只更新之前从未更新的
在应用程序启动时自动升级(MigrateDatabaseToLatestVersion初始化器)
当你发布部署应用程序的时候,可能希望当程序启动的时候它自动更新数据库(更新应用任何未更新的迁移),你可以通过注册 MigrateDatabaseToLatestVersion 数据库初始化器来实现这一点,数据库初始化器只包含一些逻辑检查用于确保数据库被正确设置,这个逻辑检查将会在AppDomain 的 context 第一次被使用的时候执行。
当我们创建一个初始化器的实例时,需要指定 context type(BlogContext)以及 migrations configuration (Configuration)- 这个迁移配置类是在我们启用迁移时生成的 Migrations 目录下增加的
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Text; using System.Threading.Tasks; using MigrationsDemo.Migrations; namespace MigrationsDemo { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<BlogContext, Configuration>()); using (var db = new BlogContext()) { db.Blogs.Add(new Blog { Name = "Another Blog " }); db.SaveChanges(); foreach (var blog in db.Blogs) { Console.WriteLine(blog.Name); } } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }