在上篇文章中,我们理了一下基于外键关联的单向一对一关系。在这篇文章中,我们理一理“基于共享主键的单向一对一关系”,找出Entity Framework中正确的映射关系。
一、明确需求,也就是Entity Framework正确映射之后要达到的效果
1)数据库结构要符合要求——共享主键。所下图所示,数据库中表A与表B的主键都是AID。
2)实体关系要符合要求——单向一对一关系。我们通过下面的UML类图来表达:
上图中只有A到B的关联箭头,这就是“单向”,这个箭头也表示A依赖B,在代码中的表现就是A有一个导航属性A.B。
上图中箭头两头的两个1就是“一对一”,存在一个A,必然存在一个对应的B;存在一个B,必然存在一个对应的A。
EDM中的实体关系要与UML类图中的关系一致。
实体A的定义:
public class A
{
public int AID { get; set; }
public string Title { get; set; }
public B B { get; set; }
}
实体B的定义:
public class B
{
public int AID { get; set; }
public string Body { get; set; }
}
3)持久化操作要符合要求
只允许A与B一起进行持久化,测试代码如下:
var a = new A();
a.Title = "title test";
a.B = new B();
a.B.Body = "body test";
using (EfUnitOfWork ef = new EfUnitOfWork())
{
ef.Set<A>().Add(a);
ef.SaveChanges();
}
不允许A与B各自单独的持久化,测试代码如下:
//不允许的持久化操作
var a = new A();
a.Title = "title a";
using (EfUnitOfWork ef = new EfUnitOfWork())
{
ef.Set<A>().Add(a);
ef.SaveChanges();
}
var b = new B();
b.Body = "body b";
using (EfUnitOfWork ef = new EfUnitOfWork())
{
ef.Set<B>().Add(b);
ef.SaveChanges();
}
4)生成的SQL查询语句要符合要求
比如这样的查询:
using (EfUnitOfWork ef = new EfUnitOfWork())
{
ef.Set<A>()
.Include(a => a.B)
.Where(a => a.AID == 1)
.ToList();
}
生成的SQL查询语句应该是:
SELECT
[Extent1].[AID] AS [AID],
[Extent1].[Title] AS [Title],
[Extent2].[AID] AS [AID1],
[Extent2].[Body] AS [Body]
FROM [dbo].[A] AS [Extent1]
INNER JOIN [dbo].[B] AS [Extent2] ON [Extent1].[B_AID] = [Extent2].[AID]
WHERE 1 = [Extent1].[AID]
二、用最笨的方法找出答案
这个最笨的方法是,对四种映射关系逐一进行测试,看哪个与我们想要的效果最一致。
下面我们分别来看看在不同的映射关系配置下Entity Framework的行为。
1).HasRequired(a => a.B).WithMany();
FluentAPI:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<B>().HasKey(b => b.AID);
modelBuilder.Entity<A>().HasRequired(a => a.B).WithMany();
}
a) EF生成的数据库结构:
表A多了一个字段B_AID,并通过B_AID关联至表B的主键AID。数据库结构不一致,不符合要求。
b) EF生成的EDM图:
EDM与UML中的关系定义不一致,这里是一对多关系,我要的是一对一关系,不符合要求。
c) 不允许实体A的单独持久化,但允许实体B的单独持久化,不符合要求。
d) 生成的SQL查询语句符合要求。
【小结】数据库结构不符合要求,实体关系不符合要求,持久化不符合要求,生成的SQL查询语句符合要求。
2).HasRequired(a => a.B).WithOptional();
FluentAPI:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<B>().HasKey(b => b.AID);
modelBuilder.Entity<A>().HasRequired(a => a.B).WithOptional();
}
a) EF生成的数据库结构:
数据库结构符合要求。
b) EF生成的EDM图:
关联的一端是0..1,也就是允许“存在一个B,不存在一个A”的情况,实体关系不符合要求。
c) 不允许实体A的单独持久化,但允许实体B的单独持久化,不符合要求。
d) 生成的SQL查询语句符合要求。
【小结】数据库结构符合要求,实体关系不符合要求,持久化不符合要求,SQL查询语句符合要求。
3).HasRequired(a => a.B).WithRequiredDependent();
FluentAPI:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<B>().HasKey(b => b.AID);
modelBuilder.Entity<A>().HasRequired(a => a.B).WithRequiredDependent();
}
a) 生成的数据库结构与.WithOptional();一样,符合要求。
b) EF生成的EDM图:
实体关系是“单向一对一”关系,与UML类图一致。但类的摆放位置不一致,在EDM中,B在A的前面,也就是B是Principal,我们希望A是Pricipal,有点不一致。
c) 不允许实体A的单独持久化,但允许实体B的单独持久化,不符合要求。
d) 生成的SQL查询语句符合要求。
【小结】数据库结构符合要求,实体关系有点不符合要求(Pricipal不一致),持久化不符合要求,SQL查询语句符合要求。
4).HasRequired(a => a.B).WithRequiredPrincipal();
FluentAPI:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<B>().HasKey(b => b.AID);
modelBuilder.Entity<A>().HasRequired(a => a.B).WithRequiredPrincipal();
}
a) 生成的数据库结构与.WithOptional();一样,符合要求。
b) EF生成的EDM图:
实体关系是“单向一对一”关系,与UML类图一致,A是Pricipal,符合要求。
c) 允许实体A的单独持久化,不允许实体B的单独持久化,不符合要求。
d) 生成的SQL查询语句:
SELECT
[Extent1].[AID] AS [AID],
[Extent1].[Title] AS [Title],
[Extent3].[AID] AS [AID1],
[Extent3].[Body] AS [Body]
FROM [dbo].[A] AS [Extent1]
LEFT OUTER JOIN [dbo].[B] AS [Extent2] ON [Extent1].[AID] = [Extent2].[AID]
LEFT OUTER JOIN [dbo].[B] AS [Extent3] ON [Extent2].[AID] = [Extent3].[AID]
WHERE 1 = [Extent1].[AID]
我们想要的是INNER JOIN,这里却是两个LEFT OUTER JOIN,不符合要求。
等等。。。我们改一下查询的LINQ语句试试,改为:
using (EfUnitOfWork ef = new EfUnitOfWork())
{
ef.Set<A>()
.Include(a => a.B)
.Where(a => a.B.AID == 1)//原来是a => a.AID == 1
.ToList();
}
改过之后,生成的SQL查询语句符合要求:
[Extent1].[AID] AS [AID],
[Extent1].[Title] AS [Title],
[Extent2].[AID] AS [AID1],
[Extent2].[Body] AS [Body]
FROM [dbo].[A] AS [Extent1]
INNER JOIN [dbo].[B] AS [Extent2] ON [Extent1].[AID] = [Extent2].[AID]
WHERE 1 = [Extent2].[AID]
【小结】数据库结构符合要求,实体关系符合要求,持久化不符合要求,SQL查询语句符合要求。
三、总结与分析
数据库结构 | 实体关系 | 持久化 | SQL查询语句 | |
WithMany() | 不符合 | 不符合 | 不符合 | 符合 |
WithOptional() | 符合 | 不符合 | 不符合 | 符合 |
WithRequiredDependent() | 符合 | 不符合 | 不符合 | 符合 |
WithRequiredPrincipal() | 符合 | 符合 | 不符合 | 符合 |
从上面的表中可以出,成绩最好的是WithRequiredPrincipal(),但它有一个地方不符合要求, 就是允许实体A的单独持久化。
为什么实体B不能单独持久化?看数据库的外键关系就知道答案(A_B外键约束的功劳):
那我们只要解决“不允许实体A的单独持久化”的问题,就能完成“基于共享主键的单向一对一关系”的完美映射。
既然数据库中不好下手,那就从实体类下手吧。给实体A的导航属性A.B加一个[Required]属性,在实体验证时就要求A.B必须有一个对应的实体B的实例。修改后的实体A的代码如下:
public class A
{
public int AID { get; set; }
public string Title { get; set; }
[Required]
public B B { get; set; }
}
经过努力,我们终于找到了最佳答案——
对于“基于共享主键的单向一对一”关系,Entity Framework中正确的映射关系定义是:
modelBuilder.Entity<A>().HasRequired(a => a.B).WithRequiredPrincipal();