原以为躲入代码世界,就可以不用搞关系,哪知“关系无处不在”。写代码多年之后,终于明白“面向对象的关键是搞好对象之间的关系”。而Entity Framework作为ORM中的明日之星,首当其冲的使命就是配合对象搞好关系。
博客园开发团队在使用Entit Framework的过程中,被困扰最多的就是实体类之间的关系处理以及这种关系与数据库之间的映射。所以,希望通过这个系列文章将我们的困惑、理解与实践经验拿出来与大家分享。知识与经验只有拿出来分享与传播,才会变得更有价值;藏在那,只会慢慢变质,最终随风而去。
关系分三种:一对一、一对多、多对多。我们就从“一对一”开始吧。“一对一”说简单也简单,说复杂也复杂;在现实世界,关键在于是否找对了那个“一”,“找一个人,过一辈子”;而在代码世界,又该如何“一对一”,让我们一起探索吧。
“一对一”也分两种情况:一种是单相思(单向),一种是两情相悦(双向)。单相思容易,两情相悦难啊;但一见就能两情相悦毕竟可遇而不可求,多数时候两情相悦来自于一方的单相思。
那我们就从最简单的单相思开始吧 —— “我的眼中只有你,你的眼中会不会有我”。
场景描述:园子里的每一个“博客”(BlogSite)都对应着一个对技术充满激情的“人”(BlogUser)。我想参观一下园子,看看所有这些博客,看看这些博客背后的主人(GetAllBlogSites)。
类图如下:
数据库表结构如下:
VS2010中的项目文件结构(基于博客园最新架构):
开始我们的旅程:
1. 第一步当然是测试,TDD可不是用来炫耀的专业名词,它是一个轮子,可以让你在开发的道路上走得更快,所以叫测试驱动。测试代码如下:
[TestMethod] public void GetAllBlogSites_Test() { _aggBlogSiteService.GetAllBlogSites().ToList() .ForEach( b => { Console.WriteLine("BlogApp:" + b.BlogApp + ", Author:" + b.BlogUser.Author); } ); }
测试代码很简单,就是调用应用层的服务接口IAggBlogSiteService.GetAllBlogSites();
通过测试代码我们可以明确需求:获取一个包含所有BlogSite的列表,每个BlogSite包含BlogUser信息。这也是一个很常见的查询需求。
2. 看一下应用层的服务实现:
public IEnumerable<BlogSite> GetAllBlogSites() { return _blogSiteReposiotry.Entities .Include(b => b.BlogUser) .Where(b => b.IsActive == true); }
很简单的LINQ查询,需要注意的地方是Include,只有用了Include,在执行时Entity Framework才会生成BlogSite与BlogUser两张表的INNER JOIN查询。否则,就会使用延迟加载(LazyLoading),在每次访问BlogSite.BlogUser属性时进行查询;如果返回100个BlogSite,就会产生100次对BlogUser表的查询。
3. 进入关键环节,如何针对这种单向的一对一关系对Entity Framework进行设置(这里用的是Code First)。
有两种方式可以实现:一种是通过FluentAPI,在OnModelCreating中实现;一种是通过DataAnnotations直接在实体类上设置。
a) FluentAPI设置的具体代码如下:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<BlogUser>().ToTable("BlogUser"); modelBuilder.Entity<BlogUser>().HasKey(u => u.UserID); modelBuilder.Entity<BlogSite>().ToTable("BlogSite"); modelBuilder.Entity<BlogSite>().HasKey(b => b.BlogID); //针对“一对一”关系的设置 modelBuilder.Entity<BlogSite>().HasRequired(b => b.BlogUser). WithMany().HasForeignKey(b => b.UserID); modelBuilder.Conventions.Remove<IncludeMetadataConvention>(); base.OnModelCreating(modelBuilder); }
只需看“一对一”关系设置部分的代码。HasRequired(b => b.BlogUser)很好理解就是定义“一对一”的关系;WithMany()目前是个迷,Many与One-to-One关系看起来驴头不对马嘴,但就得这样,这可是我们经过很多尝试,最后试出来的解决方法。否则会生成让人哭笑不得的SQL语句。下面我们来欣赏一下EF生成的让人哭笑不得的SQL。
开始我们是用FluentAPI这样进行定义的:
//针对“一对一”关系的设置 modelBuilder.Entity<BlogSite>().HasRequired(b => b.BlogUser). WithRequiredDependent().Map(conf => conf.MapKey("UserID"));
注:这样定义时,需要注释掉BlogSite的UserID属性。
这种方式,能够得到正确的结果,但生成的SQL很奇怪:
SELECT [Extent1].[BlogID] AS [BlogID], [Extent1].[BlogApp] AS [BlogApp], [Extent1].[IsActive] AS [IsActive], [Join1].[UserID1] AS [UserID], [Join1].[Author] AS [Author], [Join3].[BlogID] AS [BlogID1] FROM [dbo].[BlogSite] AS [Extent1] LEFT OUTER JOIN (SELECT [Extent2].[UserID] AS [UserID1], [Extent2].[Author] AS [Author] FROM [dbo].[BlogUser] AS [Extent2] LEFT OUTER JOIN [dbo].[BlogSite] AS [Extent3] ON [Extent2].[UserID] = [Extent3].[UserID] ) AS [Join1] ON [Extent1].[UserID] = [Join1].[UserID1] LEFT OUTER JOIN (SELECT [Extent4].[UserID] AS [UserID2], [Extent5].[BlogID] AS [BlogID] FROM [dbo].[BlogUser] AS [Extent4] LEFT OUTER JOIN [dbo].[BlogSite] AS [Extent5] ON [Extent4].[UserID] = [Extent5].[UserID] ) AS [Join3] ON [Extent1].[UserID] = [Join3].[UserID2] WHERE 1 = [Extent1].[IsActive]
无需多说,看一眼你就无法忍受这样的SQL。
再看看使用WithMany()生成的SQL:
SELECT [Extent1].[BlogID] AS [BlogID], [Extent1].[BlogApp] AS [BlogApp], [Extent1].[IsActive] AS [IsActive], [Extent1].[UserID] AS [UserID], [Extent2].[UserID] AS [UserID1], [Extent2].[Author] AS [Author] FROM [dbo].[BlogSite] AS [Extent1] INNER JOIN [dbo].[BlogUser] AS [Extent2] ON [Extent1].[UserID] = [Extent2].[UserID] WHERE 1 = [Extent1].[IsActive]
多干净,这才是我们想要的。
b) 还有一种简单的方法,通过DataAnnotations在BlogSite的UserID属性上设置[ForeignKey("BlogUser")],代码如下:
public class BlogSite { public int BlogID { get; set; } public string BlogApp { get; set; } public bool IsActive { get; set; } [ForeignKey("BlogUser")] public Guid UserID { get; set; } public BlogUser BlogUser { get; set; } }
这是Entity Framework 4.0开始引入的新特性,欲知详情,请看Foreign key vs. Independent associations in Entity Framework 4。
一个ForeignKey可以达到WithMany同样的效果,够简单。
写到这,单向一对一关系搞好咧。接着干吗呢?这还用问,运行测试,收工!