通过实体框架 Code First,可以使用您自己的域类表示 EF 执行查询、更改跟踪和更新函数所依赖的模型。Code First 利用称为“约定先于配置”的编程模式。这意味着 Code First 将假设类遵循 EF 用于概念模型的架构约定。在这种情况下,EF 将能够找出自己工作所需的详细信息。但是,如果您的类不遵守这些约定,则可以向类中添加配置,以向 EF 提供它需要的信息。
Code First 为您提供了两种方法来向类中添加这些配置。一种方法是使用名为 DataAnnotations 的简单特性,另一种方法是使用 Code First 的 Fluent API,该 API 向您提供了在代码中以命令方式描述配置的方法。
本文将重点讨论如何在 Fluent API 中优化关系。Code First 约定非常适合根据指向子集合或单个类的属性识别类之间的常见关系。当类不使用外键时,Code First 可以推断数据库外键。但有时类提供的信息不足,Code First 无法正常处理这些关系。
介绍模型
我将从两个简单的类 Blog 和 Post 开始,这里 Blog 与 Post 有一对多关系。
public class Blog { public int Id { get; set; } public string Title { get; set; } public string BloggerName { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public DateTime DateCreated { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
了解一对多关系的约定
在类中定义一对多关系的一种常见方法是在一个类中包含一个子集合,然后在子类中包含一个外键属性和一个导航属性。在上面的示例中,Blog 有一个 Posts 属性,它是 Post 类型的 ICollection。Post 有一个外键 BlogID 和一个导航属性 Blog,该导航属性指回其父 Blog。
此设置符合 Code First 约定的预期,因此,Code First 将创建以下数据库表:
图 1
请注意,Code First 将 BlogId 用作数据库外键(Posts.BlogId 和 Blogs.Id 之间定义了主键/外键约束),该值不可为 Null。这是 Code First 约定根据类确定的。
没有外键属性时使用 HasRequired 提供帮助
如果 Post 类中没有 BlogId 属性但有导航属性 Blog,该怎么办呢?Code first 仍然能够创建关系,因为它知道这一 Blog 属性指回 Blog 实体。因此它仍然会创建图 2 中所示的数据库外键 Posts.Blog_Id,以及将其链接到 Blog.Id 的约束。
图 2
但有一个重要区别,Blog_Id 可以为 Null。可以添加与 Blog 绑定的 Posts。Code First 约定就是这样解释类的,但这可能并不符合您的意图。您可以使用 Fluent API 进行修复。
Code First 从类中构建模型时,将应用 Fluent API 配置。通过重写这里显示的 DbContext 类的 OnModelCreating 方法,可以注入配置。
public class BlogContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { //使用 Fluent API 配置模型 }
DbModelBuilder 提供了一个配置挂钩。在这里,我们可以告诉模型生成器我们需要影响其中一个实体,您可以使用泛型指定是哪个实体,这里是 Post。访问它后,可以使用 HasRequired 方法(特定于关系)指定需要一个导航属性,在本例中为 Blog 属性。
modelBuilder.Entity<Post>().HasRequired(p => p.Blog);
这会产生两方面影响。首先,数据库中的 Blog_Id 将再次变为不可为 Null。实体框架仍将根据需要或在将更改保存回数据库前执行验证,以确保满足此要求。
配置非常规外键名称
类中有外键时,属性名称必须始终符合 Code First 约定。约定是:键是它所指向的类的名称(例如 Blog)和 _Id 或 Id 的组合。这就是 Code First 能够使用类 BlogId 中的原始属性的原因。
如果属性不符合约定,会怎么样呢?也许您使用了“FK”+ 父级 +“Id”?
public int FKBlogId { get; set; }
Code first 根本不知道 FKBlogId 应是外键。它将创建一个标准列来显示该属性,还将创建 Blog_Id 外键,因为它根据 Blog 属性确定需要这个外键。
图 3
此外,在代码中使用 Blog 和 Post 时,根本不会将 FKBlogId 识别为指回 Blog 的外键。如果有修复双向关系的代码,它将不会使用 FKBlogId 属性执行其任务。
可以使用 Fluent API 修复此问题,告诉 Code First 您的真实意图,使用 FKBlogId 作为与 Blog 的关系中的外键属性。可以从现成的配置开始。
在此配置中,将定义关系的两端:Blog 中指向多关系 (Posts) 的属性以及 Post 中指回父级 (Blog) 的属性。
首先需要添加 WithMany 方法,使用该方法可以指示 Blog 中的哪个属性包含 Many 关系。
modelBuilder.Entity<Post>().HasRequired(p => p.Blog) .WithMany(b => b.Posts)
然后可以向其添加 HasForeignKey 方法,指示 Post 的哪个属性是指回 Blog 的外键。最后,完整的映射如下:
modelBuilder.Entity<Post>().HasRequired(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey(p => p.FKBlogId);
现在,Code First 获得了需要的信息,能够创建正确的数据库架构(或正确映射到现有数据库),可以在与双向关系有关的应用程序中提供预期的行为。
图 4
在多对多关系中定义联接表架构
在类中,可以通过指向彼此的属性方便地描述多对多关系。例如,如果在模型中添加了一个 Tag 类来跟踪文章的标记,您需要它们之间有多对多关系。
下面是新的 Tag 类:
public class Tag { public int TagId{ get; set; } public string Name { get; set; } public ICollection<Post> Posts { get; set; } }
下面是为 Post 类添加的新属性:
public ICollection<Tag> Tags { get; set; }
Code first 需要联接表的命名是将这两个类的名称组合在一起并包含外键属性,其中每个属性都是类名称和键属性的组合。在本例中,我使用 Post.Id 和 Tag.TagId。如果使用 Code First 创建数据库,使用 Code First 约定的表应类似下面这样:
图 5
使用 Code First 构建数据库时,这可能不是问题。但如果映射到现有数据库,这样的命名可能根本无法对齐表。可以使用 Fluent API 描述表名和两个列名,可以显式命名一个、两个或全部三个。
下面介绍如何实现此映射。我们使用示例,其中需要定义所有三个名称。表应为 PostJoinTag,列应为 TagId 和 PostId。
您需要从 Entity 映射方法开始,可以选择从 Post 或 Tag 开始。这里我将使用 Post。然后需要指定关系的两端。与在上例中指定一对多关系的方式类似,可以使用 HasMany 和 WithMany 进行。在这里,我通过 Post 实体的 Tags 属性指示它具有“多”关系,因此,Tag 通过其 Posts 属性与其“多”关系有关系。
modelBuilder.Entity<Post>() .HasMany(p => p.Tags) .WithMany(t => t.Posts) .Map(mc => { mc.ToTable("PostJoinTag"); mc.MapLeftKey("PostId"); mc.MapRightKey("TagId"); });
图 6
指定哪个是 MapLeftKey 和哪个是 MapRightKey 时需要小心。左键应为所指向的第一个类的键,即 Post,右键为关系的另一侧。如果搞反了方向,数据将不能正确存储,用户会很迷惑。
摘要
您已经了解了使用 Code First 的关系 Fluent API 可以描述的关系映射的一些可能情况。这里我用一对多关系和多对多关系修复了约定对我意图的误解。您还可以使用其他映射,可以单独使用或组合使用。尽管它们开始看起来可能令人迷惑和多余,例如 IsRequired 和 HasRequired 或 WithMany 和 HasMany。但现在您已经了解到这些映射有非常明确的作用,它们的不同是有原因的。
请查阅实体框架 4.1 MSDN 文档 http://msdn.microsoft.com/library/gg696172(v=VS.10).aspx 和实体框架团队博客 ( http://blogs.msdn.com/adonet ),了解可以使用 Fluent API 实现的更多关系映射。