LINQ那些事儿(2)- 简单对象的CRUD操作和Association的级联操作
从(1)我们看到,当生成entity class定义时,entity class或xml mapping文件中都已经完整的包含了entity和关系数据库的映射信息了,LINQ2SQL会根据这些信息来把CRUD操作转化为SQL提交给数据库,并且把数据库的返回DataTable封装成我们想要的对象。
所谓简单对象,就是数据表定义中没有Foreign-key的entity class,在操作这类对象时不会涉及级联的操作。
简单对象的CRUD操作,可参考MSDN:http://msdn.microsoft.com/zh-cn/library/bb399349.aspx
有一点很方便,在插入数据时,LINQ2SQL不但生成了Insert的SQL语句,而且还生成语句把ColumnAttribute标记IsDbGenerated=true的数据列取回。这点当我们用数据库生成的uniqueidentifier列或自增id做主键时尤其方便。
下面让我们来一起讨论级联操作以及相关问题,为了方便示例我定义了两张数据表Publishers和Books:
其中PublisherID和BookID都是RowGuid,而且默认值为newid(),以下代码都是基于SqlMetal生成的xml mapping和entity class。
添加
下面代码示例了在添加Publisher记录时,同时添加两个关联Book记录
var context = GenerateContext(); Publisher publisher = new Publisher { Name = "Microsoft" }; publisher.Books.Add(new Book { Title = "Expert F#" }); publisher.Books.Add(new Book { Title = "Beautiful code" }); context.Publishers.InsertOnSubmit(publisher); context.SubmitChanges();
提交成功,关联对象的添加就好像是集合操作。但是,好像缺了点什么?我们好像没有给Book.PublisherID赋值,作为外键没有赋值为什么会没抛出异常呢?这都是生成代码的功劳,我们看看Publisher.Books属性的定义
private EntitySet<Book> _Books; public Publisher() { this._Books = new EntitySet<Book>( new Action<Book>(this.attach_Books), new Action<Book>(this.detach_Books)); OnCreated(); }
当向Books集合中添加元素时,会调用attach_Books让Book.Publisher指向Publisher对象
private void attach_Books(Book entity) { this.SendPropertyChanging(); entity.Publisher = this; }
而Book.Publisher的赋值又触发事件使Book.PublisherId=Book.Publisher.PublisherID
public Publisher Publisher { get { … } set { Publisher previousValue = this._Publisher.Entity; if (((previousValue != value) || (this._Publisher.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Publisher.Entity = null; previousValue.Books.Remove(this); } this._Publisher.Entity = value; if ((value != null)) { value.Books.Add(this); this._PublisherID = value.PublisherID; } else { this._PublisherID = default(System.Guid); } this.SendPropertyChanged("Publisher"); } } }
更新
下面代码示例了在更新Publisher记录时,同时更新相关的Book记录
Publisher publisher = context.Publishers.Where( p => p.PublisherID == new Guid("ae825c5f-465d-4eb5-a2bb-cc1aeb5edb7d")).Single(); publisher.Name = "Updated Publisher"; var book = publisher.Books.First(); book.Title = "Updated book"; context.SubmitChanges();
这样的操作我们称之为什么?级联更新?事实上LINQ2SQL的实现里不存在所谓的“级联更新“。在生命周期内,每一个DataContext对象都维护着每一个查询获得的对象的引用,并且跟踪对象的修改,所有发生了修改的对象,在调用DataContext.SubmitChanges的时候,都会被保存到数据库,这方面的内容在”LINQ那些事(6)“里会详细讨论。所以,在这里不是级联更新,而是Publisher和Book对象都发生了更改,所以在调用SubmitChanges都被保存了。
涉及Association的更新,有时还会引发异常,我们来看下面这段代码:
var context = GenerateContext(); Publisher publisher = context.Publishers.Where( p => p.PublisherID == new Guid("ae825c5f-465d-4eb5-a2bb-cc1aeb5edb7d")).Single(); var book = publisher.Books.First(); publisher.Books.Remove(book); context.SubmitChanges();
这段代码的意图是删除Publisher对象相关的某Book对象,看起来是很漂亮的集合操作,但是运行时抛出异常:
“An attempt was made to remove a relationship between a Publisher and a Book. However, one of the relationship's foreign keys (Book.PublisherID) cannot be set to null.“
还记得我们在调用Publisher.Books.Add时候,EntitySet的会调用attach_Books来修改Book.PublisherID吗?在调用Publisher.Books.Remove的时候,EntitySet会调用detach_Books来让Book.PublisherID = default(Guid)
而对于LINQ2SQL,只跟踪到了Book.PublisherID属性发生了更改,所以在调用SubmitChanges会尝试更新Book记录而产生上述外键异常。解决这个问题的方法是:在Book.PublisherID的ColumnAttribute定义中,把DeleteOnNull设为true。这样当Context发现Book.PublisherID为空(default(Guid)相当于Guid.Empty)时,不是执行Update而是Delete操作。
删除
根据msdn,目前版本的LINQ是不支持级联删除的,需要级联删除只能依赖数据库的级联删除,否则也可以考虑下面的方式:
context.Books.DeleteAllOnSubmit(publisher.Books.AsEnumerable()); context.Publishers.DeleteOnSubmit(publisher); context.SubmitChanges();
你不需要担心事务的问题,context.SubmitChanges在执行时会自动创建本地数据库事务,来保证操作的完整。关于事务的问题,我们在“LINQ那些事(3)”中会有详细讨论。
查询
先看看下面这段代码
var context = GenerateContext(); context.Log = Console.Out; // 查询publisher对象 Console.WriteLine("Querying publisher"); Publisher publisher = context.Publishers.Where( p => p.PublisherID == new Guid("ae825c5f-465d-4eb5-a2bb-cc1aeb5edb7d")).Single(); Console.WriteLine("Querying books") // 当调用publisher.Books.GetEnumerator()时,执行book对象的查询 foreach (Book book in publisher.Books) { Console.WriteLine(book.Title); }
在默认情况下,DataContext并不会加载Assocation对象(EntitySet<T>或EntityRef<T>),当Assocation对象需要被访问时才会执行数据库查询,这就是所谓的lazy-loading。LINQ2SQL的Layz-loading的实现与IEnumerable<T>的deferred query execution是一样的,有兴趣可以看看EntitySet<T>.GetEnumerator或EntityRef<T>.Entity.Getter代码。
Lazy-loading的好处是避免了不必要的查询,但是在某些场合确定Assocation对象都应该加载时,我们可以设置DataContext. LoadOption来指定Assocation对象的加载:
DataLoadOptions option = new DataLoadOptions(); option.LoadWith<Publisher>(p => p.Books); context.LoadOptions = option;
总结:本节讨论了如何使用LINQ2SQL来进行简单对象和涉及Association的对象的CRUD操作。示例代码段在只涉及一个DataContext对象或在单线程的情况下都可以正确运行,但是当涉及多个DataContext或并发访问的情况的下会怎么样呢?这是我们接下来要讨论的。
链接
1、 LINQ那些事儿(1)- 定义从关系数据库到Entity Class的映射
2、 LINQ那些事儿(2)- 简单对象的CRUD操作和Association的级联操作
4、 LINQ那些事儿(4)- Query Expression和Query Operator
6、 LINQ那些事儿(6)- DataContext的对象生命周期管理
7、 LINQ那些事儿(7)- 通过自定义IEnumerable<T>来扩展LINQ
8、LINQ那些事儿(8)- 通过自定义IQueryable<T>和IQueryableProvider来扩展LINQ
All the posts in this blog are provided "AS IS" with no warranties, and confer no rights. Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 2.5 China Mainland License.