Entity Framework - 理清关系 - 基于外键关联的单向一对一关系

注:本文针对的是 Entity Framework Code First 场景。

之前写过三篇文章试图理清Entity Framework中的一对一关系(单相思(单向一对一), 两情相悦(双向一对一), 两情相悦-续),但当时理得不够清,新的一年重新理一理。

当时“一对一”的实体关系,对应的数据库关系是外键关联(实际上是一种“一对多”关系,所以映射时用了WithMany)。而数据库中的“一对一”关系是共享主键(这是我个人的理解,不妥之处,欢迎指出),下篇文章将要理的就是这个关系。

由于双向“一对一”关系很少用到,而且不推荐使用,为了更清楚地理解,我们这里只谈单向一对一关系,也就是“基于外键关联的单向一对一关系(One-to-one Unidirectional relationships)”,对应的之前的文章是单相思(单向一对一)

1. 类图

2. 类的定义

public class BlogSite
{
public int BlogID { get; set; }
public string BlogApp { get; set; }
public bool IsActive { get; set; }
public Guid UserID { get; set; }
public virtual BlogUser BlogUser { get; set; }
}
public class BlogUser
{
public Guid UserID { get; set; }
public string Author { get; set; }
public int BlogID { get; set; }
}

3. 数据库结构

4. Enitity Framework映射关系定义

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<BlogSite>()
.HasRequired(b => b.BlogUser)
.WithMany();
}

怎么理解这里的HasRequired与WithMany呢?

我的理解是:HasRequired是针对BlogSite与BlogUser的关系,WithMany是针对BlogUser与BlogSite的关系。.HasRequired(b => b.BlogUser).WithMany()表示BlogSite与BlogUser存在Required关联关系(One-To-One, 每一个BlogSite都有一个对应的BlogUser),而这个关联对BlogUser来说是One-To-Many(一个BlogUser可以有多个BlogSite)。

你也许会疑惑?明明是单向一对一的实体关系,这里怎么弄出个一对多的关系?

如果有这样的疑惑,属正常现象,我从去年7月份写那篇文章开始疑惑,一直疑惑到现在,写篇文章时才有点搞明白。

注意,“单向一对一”是什么?是实体关系;Entity Framework是什么?是O/RM,是用来映射实体关系与数据库关系;还少了什么?数据库关系。

从上面的图中的数据库结构可以看出,BlogSite与BlogUser之间是外键关联关系,下面的图可以更清楚地看出这一点。

这个外键关联表示的就是BlogUser与BlogSite之间是一对多的数据库关系。

总结一下:

实体关系 —— BlogSite与BlogUser之间的单向一对一

数据库关系 —— BlogUser与BlogSite之间的一对多

是不是这样呢?Entity Framework是不是也是这样认为的呢?我们来验证一下。

 

怎么知道Entity Framework的想法呢?

通过EDM。

可这里是Code First?

不管什么First,都有EDM,因为这是Entity Framework的地图,没有它,Entity Framework就会晕头转向。Code First的EDM是在EF运行时生成的,不是没有地图,只是在Entity Framework的心中,我们看不到而已。

怎么让Entity Framework说出心里话呢?

从Morteza Manavi大师那学到一招,代码如下:

using (var context = new Context())
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;

using (XmlWriter writer = XmlWriter.Create(@"Model.edmx", settings))
{
EdmxWriter.WriteEdmx(context, writer);
}
}

通过上面的代码,你就可以拿到EF心中的地图 —— edmx文件。请看地图:

果然,EF心中的地图就是BlogUser与BlogSite的一对多关系。地图的作用是什么?是让EF通过地图在数据库找到对应的数据。地图是如何产生的?是我们通过FluentAPI告诉Entity Framework:.HasRequired(b => b.BlogUser).WithMany()。

我们再来剖析一下.HasRequired(b => b.BlogUser).WithMany()。

之前,我一直被困扰,是因为总是把这里的定义当作实体的关系的定义。错!这里虽然用的是实体进行定义,但定义的是实体与数据库中的数据之间的映射关系(这本来就是常识,竟然被忽略了),更多的是告诉EF这些实体在数据库中的数据关系。EF最终是根据这个定义生成相应的SQL。

那我们从生成查询SQL的角度来理解一下:

.HasRequired(b => b.BlogUser)告诉EF,这是一个INNER JOIN查询(BlogSite INNER JOIN BlogUser);但INNER JOIN还需要条件,WithMany()告诉EF这是一个外键关联,EF据此进行推断,从BlogUser中找到主键UserID,并检查BlogSite中是否存在名为UserID的属性,如果存在,就以此为外键进行查询。而我们的BlogSite中有UserID,于是生成下面的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]

 

小结

理清“基于外键关联的单向一对一关系”,关键在于对modelBuilder.Entity<A>().HasRequired(A => A.B).WithMany()的理解。

我再来理解一次:

.HasRequired(A => A.B) 表示:1)实体A与实体B是一对一关系,实体A有一个导航属性A.B;2)在数据库中表A与表B存在一对一关联(INNER JOIN)。

.WithMany() 表示:1) 实体B与实体A可以没有关系,也可以是一对多关系;2)在数据库中表A与表B存在外键关联。

上面全是我的个人理解,真正理清Entity Framework中的关系需要大家的力量,我只是抛个砖。

 

除了“基于外键关联的单向一对一关系”,还有“基于共享主键的单向一对一关系”,这也是我们开发中经常碰到的一种关系,比如博客文章(BlogPost)与文章内容(PostBody),新闻(NewsItem)与新闻内容(NewsBody)。下一篇文章将会理理这个关系。

posted @ 2012-01-04 15:03 dudu 阅读(1724) 评论(13) 编辑 收藏

 回复 引用 查看   
#1楼 2012-01-04 15:14 今昭      
沙发
 回复 引用 查看   
#2楼 2012-01-04 15:23 assiwe      
为啥要博客文章(BlogPost)与文章内容(PostBody)一对一, 而不是BlogPost里有一个PostBody属性呢?
 回复 引用 查看   
#3楼[楼主] 2012-01-04 15:26 dudu      
@assiwe
PostBody在数据库中是一张单独的表,PostBody只在需要的时候加载。

 回复 引用 查看   
#4楼 2012-01-04 15:26 john23.net      
楼下的沙发
 回复 引用 查看   
#5楼 2012-01-04 15:31 贺臣      
dudu 的语言幽默
 回复 引用 查看   
#6楼 2012-01-04 15:38 assiwe      
那把PostBody设成延迟加载不就好了. 为啥要另来一个表啊?
这样你要读取BlogPost不是要查询两次吗?

 回复 引用 查看   
#7楼[楼主] 2012-01-04 15:49 dudu      
@assiwe
BlogPost是频繁查询的,95%以上的情况下查询BlogPost时不需要查询PostBody。

 回复 引用 查看   
#8楼 2012-01-04 16:51 assiwe      
不明白, 不需要查询PostBody你只要不读它就好了. EF默认就是lazy load的啊.
我想问的是, 为什么你在设计这个一对一关系的时候, 用BlogPost和PostBody两张表,而不是用一张表呢?

 回复 引用 查看   
#9楼 2012-01-04 17:46 leeolevis      
.HasRequired(b => b.BlogUser).WithMany();
这两句代码只是Fluent API的写法,不加这两句用Data Annotations的方式生成的数据库关系也是一样的。

我个人比较喜欢先建数据库,然后用EntitiesToDTOs生成模型和DTO

 回复 引用 查看   
#10楼 2012-01-04 21:49 denli      
对什么时候使用HasRequired(..).WithMany(..)
什么时候使用WithRequired(..).HasMany(..)没有概念,
不得要领,没能发现他们之间具体的区别.
楼主有空能解释下就太多谢了.

 回复 引用 查看   
#11楼 2012-01-04 22:09 slive      
dudu加油,支持EF
 回复 引用 查看   
#12楼 2012-01-06 15:11 leeolevis      
想做一下内容补充,可惜不能回复自己的内容:)
刚才做项目时突然发现的,复杂的关系还是需要用Fluent API的写法,比如我刚才碰到了一个Follower对象中有两个外键都是UserId,这个时候不写EntityTypeConfiguration调用User时就会得不到子对象
            // Relationships
            this.HasRequired(t => t.User)
                .WithMany(t => t.Star)
                .HasForeignKey(d => d.UserId);

            this.HasRequired(t => t.Target)
                .WithMany(t => t.Fans)
                .HasForeignKey(d => d.TargetId);


另外想问DUDU这种写法用Fluent API的方式可以写吗

[StringLength(100, ErrorMessageResourceName = "validation_EmployeeFirstNameLength", ErrorMessageResourceType = typeof(HRMessages))]
        public string Name { get; set; }

 回复 引用 查看   
#13楼[楼主] 2012-01-06 22:27 dudu      
@leeolevis
Fluent API只能写成这样的:
modelBuilder.Entity<User>()
.Property(a => a.Name)
.HasMaxLength(100);