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:

image

其中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或并发访问的情况的下会怎么样呢?这是我们接下来要讨论的。

 

链接

LINQ那些事(总)

1、 LINQ那些事儿(1)- 定义从关系数据库到Entity Class的映射

2、 LINQ那些事儿(2)- 简单对象的CRUD操作和Association的级联操作

3、 LINQ那些事儿(3)- 事务和并发冲突处理

4、 LINQ那些事儿(4)- Query Expression和Query Operator

5、 LINQ那些事儿(5)- 动态查询

6、 LINQ那些事儿(6)- DataContext的对象生命周期管理

7、 LINQ那些事儿(7)- 通过自定义IEnumerable<T>来扩展LINQ

8、LINQ那些事儿(8)- 通过自定义IQueryable<T>和IQueryableProvider来扩展LINQ

posted @ 2009-12-31 23:44  海南K.K  阅读(1356)  评论(0编辑  收藏  举报