EntityFramework
http://blog.csdn.net/bitfan/article/details/13001935转载
EntityFramework走马观花之
CRUD(中)
上一篇文章中,介绍了在EF中如何执行查询的问题。本篇文章讨论数据的更新。
如果是独立的实体对象,在底层数据库中它对应一张独立的表,那么,对它进行新建、删除和修改没有任何难度,实在不值浪费笔墨在它上头。
在现实项目中,完全独立的对象少之又少,绝大多数情况都是对象之间有着紧密的关联。这种关联主要分为三种类型:一对一、一对多和多对多。
如果对EF浅尝辄止,则我几乎可以肯定你一定会在实际开发中被对象间的关联弄得焦头烂额。下面就和大家聊聊EF是如何处理不同对象关联类型数据更新问题的。
一对一关联
在面向对象的世界中,使用对象组合实现一对一关联,这种关联具有方向性。比如A与B对象间是一对一关联,如果得到A就能顺藤摸瓜得到B,就说A可导航到B,这是单向一对一关联,如果从A能得到B,从B也能得到A,这就是双向一对一关联。
以下代码中,Teacher与Office间是一对一双向关联:
public partialclass Teacher
{
public int TeacherId { get; set; }
public string Name { get; set; }
public virtual Office Office { get; set; }
}
public partialclass Office
{
public int OfficeId { get; set; }
public string Address { get; set; }
public virtual Teacher Teacher { get; set; }
}
User与Office类间的关联在数据库中体现为Teacher和Office两个表间的一对一关联。
这里要注意:面向对象世界中实体类的一对一双向关联,双方的地位是“平等”的,但在关系数据库领域中,每个关联一定有主有次,下图为SQL Server中创建关联的窗体,可以看到一对一关联两端分别是主键表和外键表。
在数据库中创建一对一关联,有两个很要命的东西,不注意铁定出问题:
(1)在外键表一端,其Key不能是 自增标识字段。比如Office作为从表,其主键OfficeId就不能是自增的,否则EF会在向Office添加记录时报告:Adependent property in a ReferentialConstraint is mapped to a store-generatedcolumn.
(2)应该把一对一关联设置为“级联删除”。否则在删除主键表端记录时如果外键表端记录还存在,则会报错。
还有另外一个小问题:
数 据库中的一对一关联,总被EF识别为1 ~ 0..1关联,即使你使用ModelFirst方式显式定义Teacher和Office类之间为1 ~ 1关联,生成数据库后,再用Database First方式逆向生成数据模型,你会发现Teacher和Office类的关联变成1 ~ 0..1 !
换句话说,EF实体对象的1对1关联,总是被张冠李戴为1对0..1关联。
好了,下面具体说说新建记录的问题。
要在数据库中创建一对一关联实体对象的相关记录,可以使用以下三种方式:
(1)new 一个Teacher对象,new一个Office对象,用代码关联两者,之后SaveChanges。只要OfficeId列不是自增标识列就能成功。
(2)new 一个Teacher对象,SaveChanges,再new一个Office对象,将其关联上这个“老”的己存在数据库中的Teacher对象,SaveChanges,也能成功。
(3)new一个Office对象,直接SaveChanges,这时会报错。因为Office是从对象,它没有关联主对象时,是无法保存到数据库的。
下面看看删除对象的情形:
(1)删除Teacher对象,SaveChanges,则它关联的Office也会被同时删除,注意这要求启用“级联删除”特性,否则,必须先从DbSet<Office>中移除Office对象,再删除Teacher,操作才能成功。
(2)直接删除Office对象,SaveChanges:没有任何问题。
轮到修改一对一关联的情形了。
假设我们需要给一个Teacher对象“换”另一个“Office”对象(即换办公室,估计这位老师升官了)。
这有两种方式实现:
第一种方式很简单,直接找到Teacher对象所关联的Office对象,修改它的属性为新值,之后SaveChanges即可,这本质上是“旧房改造”。
另一种方式是“直接分配新房”: 先new一个Office对象代表新办公室,再把它关联上Teacher,如下所示:
using (varcontext = new EFModelFirstDbEntities())
{
//查找要换办公室的Teacher对象
Teacher teacher = context.Teachers.Include("Office").First();
teacher.Office = new Office
{
Address = "Bit" +ran.Next(1, 100)
};
context.SaveChanges();
}
这里有一个细节很重要,在提取Teacher对象时,一定要使用Include方法把它相关联的“Office”对象也装入进来。否则,在SaveChanges时,EF报告:
违反了 PRIMARY KEY 约束“PK_Office”。不能在对象“dbo.Office”中插入重复键。
如果确实不想Include,则可以显式指示EF把老的Office对象状态设置为“删除”:
Teacher teacher =context.Teachers.First();
context.Entry(teacher.Office).State = EntityState.Deleted;
teacher.Office = new Office
{
Address = "Bit" +ran.Next(1, 100)
};
context.SaveChanges();
有的朋友可能会推测EF会生成两条SQL命令,一条是Delete,另一条是Insert,但我最终测试的结果可能会让你吃惊:
上述两种方式,最后生成的都是一条Update命令。
这就是说,这两种实现方式没有本质差别。
发发感触:开发中有很多细节,光看资料不动手是发现不了的。光看书不实践能真正掌握技术?那是神话!开发实践是学习软件技术,提升自身能力的硬道理。
一对多关联
有了一对一关联的基础,把握一对多关联就简单多了。
我选取“Book(书)”和“BookReview(书评)”作为一对多关联的示例,并在数据库中为关联启用了“级联删除”特性:
如果使用Db First方式,可以得到以下实体类代码:
public partial class Book
{
public Book()
{
this.BookReviews = new HashSet<BookReview>();
}
public int BookId { get; set; }
public string BookName { get; set; }
public virtual ICollection<BookReview> BookReviews { get; set; }
}
public partial class BookReview
{
public int BookReviewId { get; set; }
public string Reader { get; set; }
public string Content { get; set; }
public int BookId { get; set; }
public virtual Book Book { get; set; }
}
上述代码中,最值得注意的就是Book类的BookReviews属性是HashSet。
先看新建记录的情况:
对于一对多关联,new一个Book对象,SaveChanage,之后,就可以向其BookReviews集合属性中添加新书评对象。
这很简单,不用多说。删除记录的情况就有点复杂。
如果要删除单个的Book对象,由于启用了级联删除,干掉一个Book,它所关联的所有BookReview也一并删除了。
如 果想删除单个书评,如果使用DB_First方式,Visual Studio生成的实体对象集合其类型为ICollection<T>,实际上是一个普通的HashSet<T>集合对象,不具 备跟踪对象状态的功能。因此,在删除单个对象时,需要显式设置其状态为EntityState.Deleted,否则,删除将失败:
using(var context=newEFModelFirstDbEntities())
{
Book book =context.Books.First();
BookReview reviewToBeDelete = book.BookReviews.FirstOrDefault();
context.Entry(reviewToBeDelete).State =EntityState.Deleted;
book.BookReviews.Remove(reviewToBeDelete);
context.SaveChanges();
}
更简单的方式是直接从DbSet中移除,这是推荐的方式
Book book = context.Books.First();
BookReview reviewToBeDelete =book.BookReviews.FirstOrDefault();
context.BookReviews.Remove(reviewToBeDelete);
context.SaveChanges();
另一个在实践中遇到的问题是”批量删除”。
如果想删除某本书的全部书评,像下面这么干是不行的,运行时将抛出异常:
foreach (var review in book.Reviews)
{
context.BookReviews.Remove(review);
}
这是因为循环迭代访问集合的过程中不允许修改集合。
然而,可以使用ToList()方法将DbSet()中的实体集合提取为内存集合,然后foreach访问时,从“原始对象集合”中移除:
var reviewList = book.Reviews.ToList();
foreach (var review in reviewList)
{
context.BookReviews.Remove(review);
}
这里reviewList和context.BookReviews是两个不同的集合对象,在foreach访问reviewList时,可以方便地移除BookReviews中的对象。
上述两个“不同”的集合,实际引用“相同的”一堆BookReview对象,不信?
设断点让程序暂停,以下在VisualStudio“立即窗口(Immediate Windows)”测试可以证明我没有说谎:
?reviewList==book.Reviews
false
?reviewList[0]==book.Reviews[0]
True
关于批量删除,默认情况下,EF会为每个删除的实体对象生成一条Delete命令,当删除大量实体时,这有可能带来性能问题。在这种情况下,最好的解决方案是直接向数据库发送SQL命令:
Book book = context.Books.First();
context.Database.ExecuteSqlCommand( "Delete from BookReview where BookId={0}" , book.BookId);
一个SQL命令搞掂,简单高效。可以收工,回家睡觉去也!
关于一对多关联,还有一个问题需要说说,那就是“如何在两个实体对象间移动子对象”。实现起来也简单:
先从源实体对象中先Remove掉子对象,再Add到目标对象中即可。
以下示例代码把第一本书的第一条书评移到第二本书下:
Book book1 = context.Books.First();
Book book2 = context.Books.OrderBy(b =>b.BookId).Skip(1).First();
BookReview review = book1.BookReviews.First();
book1.BookReviews.Remove(review);
book2.BookReviews.Add(review);
context.SaveChanges();
与一对一关联的情况类似,上述代码将只生成一个update命令。
多对多关联
多对多关联的最典型实例就是软件权限管理系统中的“用户(User)”与“角色(Role)”。一个用户可以拥有多种角色,一个角色可以包容多个用户。
对于实体对象间的多对多关联,EF code First自动地在数据库层将其拆成两个一对多关联,变成以下这个模样,并为这两个关联启用“级联删除”:
多对多关联对象的新建和修改,可以参照一对多的情形来处理。比如要删除单个的User或Role对象,很简单,直接从DbSet中移除它们,再SaveChange即可。
不再多说,大家自己回去编写实验代码。
比较麻烦的是涉及到多对多关联本身的添加与删除问题。典型的例子是:
把某个User加入到特定的Role的User集合中,或者从Role的User集合中移除某个User。
对于上述这些情况,EF将不动User表和Role表,只在中间表RoleUsers中动手脚。
比如要从User与Role多对多关联任一方的对象集合导航属性中移除某个(或某些)User或Role对象,然后SaveChanges,对于这种操作,EF默认不会修改User与Role实体对象的状态,只是在底层数据库的中间表RoleUsers中删除了相应的记录,使它们“断开关联”。
理解这点很关键。
这里还有一个细节:
如 果在SaveChange之后,涉及到的相关实体对象(User和Role)都还需要继续使用,则一定要注意:你仅从一方集合导航属性中移除了某对象,另 一方的集合导航属性中对此对象的引用是还在的,这有可能带来不一致的问题。只有等到下一次从数据库中装入数据时,才能重回一致。
请看以下代码:
using (varcontext = new EFCodeFirstDbContext())
{
User u =context.Users.Find(userId);
Role r =context.Roles.Find(roleId);
if (u != null && r !=null)
{
//以下两句,任一种方式均可行
r.Users.Remove(u);
//u.Roles.Remove(r);
int result =context.SaveChanges();
Console.WriteLine("将用户从角色中移除操作完毕。返回值:" + result);
}
}
上 述加粗的两句,不管写哪一句,SaveChanges之后,都能达到在底层数据库的RoleUsers表中删除相应记录的功能。但如果只写了一句,则 Role对象的Users集合中确实移除了指定的User对象,但此User对象的Roles属性中还包容有这个Role对象,这就是数据不一致的情况。 因此,最安全的写法是两句都写。