Entity Framework在数据库与对象映射上做了很多工作,除了将数据库里的表映射成相应的对象以外,它还能够自动处理表之间的外键关系,并且可以用导航属性(Navigation Property)的方式在对象层面上表示这些关系。
一般来说,当你插入一个对象时,Entity Framework默认会自动将对象通过导航属性关联的对象也插入到数据库里面去,大部分情况下,这是我们想要的结果。当然,如果关联的对象已经存在于数据库当中时,Entity Framework会避免重复插入对象。但问题是,这个检查对象已经存在避免重复插入数据的功能,好像只在一个Context(环境)下有效,即下面的代码是可以正常执行的:
using (var context = new TestContext())
{
var milestone = new Milestone()
{
Title = "测试里程碑",
StartDate = DateTime.Now,
DueDate = DateTime.Now + TimeSpan.FromDays(30)
};
context.Milestones.Add(milestone);
context.SaveChanges();
id = milestone.Id;
var project = new Project()
{
Title = "测试项目",
StartDate = DateTime.Now,
DueDate = DateTime.Now + TimeSpan.FromDays(30),
Owner = "测试用户"
};
project.Children.Add(milestone);
context.Project.Add(project);
context.SaveChanges();
}
而如果对象是跨Context(环境)的话,或者基于现有对象复制的对象(包括主键也复制的情况),这就会产生重复插入的问题,因为新复制的对象,Entity Framework没有办法跟踪对象的状态,“误以为”对象是一个全新的对象,比如,下面这段代码就会导致Entity Framework抛出一个异常,异常根据Entity对象的数据库约束不同,可能会报告不同的错误信息—这个问题一开始让我迷惑了好几天:
public static void Main(string[] args)
{
int id = 0;
using (var context = new TestContext())
{
var milestone = new Milestone()
{
Title = "测试里程碑",
StartDate = DateTime.Now,
DueDate = DateTime.Now + TimeSpan.FromDays(30)
};
context.Milestones.Add(milestone);
context.SaveChanges();
id = milestone.Id;
}
using ( var context = new TestContext())
{
var project = new Project()
{
Title = "测试项目",
StartDate = DateTime.Now,
DueDate = DateTime.Now + TimeSpan.FromDays(30),
Owner = "测试用户"
};
var child = new Milestone() {
Id = id
};
project.Children.Add(child);
context.Project.Add(project);
context.SaveChanges();
}
}
执行上面这段代码,Entity Framework会在最后一个context.SaveChanges()上面抛出DbUpdateException,详细信息是:“{"The conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value.\r\nThe statement has been terminated."}”。这个异常一开始看上去太怪异了, 明明我将要保存的Project对象的所有DateTime类型都已经赋值(而且赋值都在范围内)了,为什么还说超出赋值范围呢?
后面才发现,这是因为,Entity Framework在插入project对象是,看到它的关联对象列表Children里,有一个Milestone对象,而Milestone对象是重新复制的(只复制了ID)—这个场景是因为用户在网页上创建一个项目时,可以从里程碑列表里选择一个事先创建好了的里程碑。由于Entity Framework没有办法跟踪这个新复制的Milestone对象的状态,所以它“误认为”这个对象是一个新的对象,因此重新插入这个对象,而这个对象又没有设置一些必要的日期属性,导致了前面那个异常。
既然搞明白了道理,修复起来也很简单,就是显式告诉Entity Framework跟踪这个对象—通过把第二个using段改成下面这样:
using ( var context = new TestContext())
{
var project = new Project()
{
Title = "测试项目",
StartDate = DateTime.Now,
DueDate = DateTime.Now + TimeSpan.FromDays(30),
Owner = "测试用户"
};
var child = new Milestone() {
Id = id
};
project.Children.Add(child);
var adapter = context as IObjectContextAdapter;
adapter.ObjectContext.AttachTo("Milestones", child);
context.Project.Add(project);
context.SaveChanges();
}
注意:我用的是Entity Framework CTP 5,采用的是代码优先(code first)的方式创建的数据库,但是本文提到的问题在数据库优先和模型优先的情况里都是一样的。
因为在网上找了好多文章都没有提到这个问题,所以在这里记录下来。