C#开发者DLinq概述(4):实体生命周期

4. 实体生命周期

DLinq 不只是实现了针对关系数据库的标准查询操作。DLinq不但可以将标准查询操作转化为SQL语句,还可以管理实体的整个生命周期,维持数据完整性以及自动完成数据的存储。

一个典型的应用场景是,通过一个或者多个查询返回实体的集合,然后通过这样或者那样的方式修改实体的数据,最后将这些修改保存到服务器中。一般的应用程序将重复上述过程,直到不再使用这些数据。就这点来说,实体和一般的对象每什么不一样,被运行时所管理。不同的是,实体的数据被保存在了数据库中。这些实体即便被运行时所回收,它们的数据还是被保存到了服务器中。所以说,实体的真实生命周期并不是由运行时来决定的。

在这章中,我们将重点讨论实体生命周期-特定运行时环境实体的生命周期,从DataContext的创建到实体或者DataContext的终结。

4.1 跟踪实体变化

您可以任意地处理从数据库中获取到的实体。这些实体是您们的,您们想怎么处理都可以。同时,DLinq将跟踪这些实体变化,当您调用SubmitChanges()时,这些实体的变化将被持久化到数据库中。

一旦从数据库中获取到了实体,DLinq便开始跟踪它们,即便您没有对它们进行任何操作。事实上,我们前面提到的“标识管理服务”已经说明了这点。实体变化跟踪开销很小,直到实体发生变化Dlinq才真正进行跟踪。

Customer cust = db.Customers.Single(c => c.CustomerID == "ALFKI");
cust.CompanyName = “Dr. Frogg’s Croakers”;

在上面的例子中,CompanyName一旦被赋值,DLinq就能感知实体的这一变化,并能将这些变化记录下来。实体成员的原始值将被“变化跟踪服务”保留,所以您可以调用DataContextRejectChanges()方法撤销对实体的修改。

db.RejectChanges();

// Show the original name
Console.WriteLine(cust.CompanyName);

变化跟踪服务也可以跟踪实体关系属性的变化。我们通过使用关系属性来实现实体之间的关联。党您修改了某个关联属性的时候,您完全没必要去修改与实体相关联的实体的标识为主键的成员的值。在您提交之前,DLinq会自动同步这些变化。

Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);

foreach (Order o in db.Orders.Where(o => o.CustomerID == custId2)) {
   o.Customer = cust1;
}

在上面的例子中,我们通过设置OrderCustomer属性从而将某个CustomerOrder转移给了另外一个Customer。因为CustomerOrder之间存在关联关系,所以您能够修改任何一方来修改实体之间的关联。如下所示,您可以很方便地将Ordercust2 Orders移除,然后加到cust1Orders集合中。

Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
Customer cust2 = db.Customers.Single(c => c.CustomerID == custId2);

// Pick some order
Order o = cust2.Orders[0];

// Remove from one, add to the other
cust2.Orders.Remove(o);
cust1.Orders.Add(o);

// Displays ‘true’
Console.WriteLine(o.Customer == cust1);

当然,如果您给关系属性赋值为null,那就意味着您删除了实体之间的关联。设置OrderCustomer属性为null,相当于将Order从某个CustomerOrder集合中删除。

Customer cust = db.Customers.Single(c => c.CustomerID == custId1);

// Pick some order
Order o = cust.Orders[0];

// Assign null value
o.Customer = null;

// Displays ‘false’
Console.WriteLine(cust.Orders.Contains(o));

为了维护实体之间的一致性,我们需要自动更新实体之间的关联。不同于普通对象的是,实体之间的关联往往是双向的。DLinq可以让您使用特性来指定实体之间的关联关系,尽管如此,DLinq并不是总能自动地同步实体之间的这种双向关系。这个就需要在您的类定义中进行显示声明。使用代码自动生成工具生成的实体类就已经有了这样的声明。在下一章中,我们将告诉您怎样使用代码自动生成工具。

需要重点注意的是,删除实体之间的关联并不代表将实体从数据库中删除。请记住,删除数据库表格中的某一行才能真正将实体从数据库中删除。所以,彻底删除实体的方法就是将某个实体从某个Table中移除。

Customer cust = db.Customers.Single(c => c.CustomerID == custId1);

// Pick some order
Order o = cust.Orders[0];

// Remove it directly from the table (I want it gone!)
db.Orders.Remove(o);

// Displays ‘false’.. gone from customer’s Orders
Console.WriteLine(cust.Orders.Contains(o));

// Displays ‘true’.. order is detached from its customer
Console.WriteLine(o.Customer == null);

同实体的其他变化一样,我们并没有真正地删除实体Order。我们只是将实体从某个Table中移除并将之同其他实体分离。当我们将Order实体从Order表中删除时,变化跟踪服务会将这个实体标记为删除。当您调用SubmitChanges()的时候,实体才真正地从数据库中删除。运行时管理着对象实例的生命周期,只要它还存在引用,它就不会被删除。然而,实体一旦从某个Table中删除并且提交给了数据库,变化跟踪服务将不再跟踪这个实体。

DataContext创建之前出现的其他实体都不被DataContext跟踪。这种对象在应用程序中到处可见。在某个应用程序中,您完全可以不需要从数据库中获取数据,就可以任意地使用实体类的实例。变化跟踪和标识管理仅仅发生在DataContex可以识别实体的地方。因此,除非您把新建的实体添加DataContext中,否则这两个服务都不会作用于这个实体。

DataContext的变化跟踪和标识管理服务可以有两种方式。您可以显式地调用Table集合的Add()方法。

Customer cust =
   new Customer {
            CustomerID = “ABCDE”,
            ContactName = “Frond Smooty”,
            CompanyTitle = “Eggbert’s Eduware”,
            Phone = “888-925-6000”
   };

// Add new customer to Customers table
db.Customers.Add(cust);

或者您可以将一个新建的实体附加到DataContext可以识别的对象上。

// Add an order to a customer’s Orders
cust.Orders.Add(
   new Order { OrderDate = DateTime.Now }
);

即便新建的被附加到其他新建的实体上,DataContext还是可以识别它们。

// Add an order and details to a customer’s Orders
Cust.Orders.Add(
   new Order {
            OrderDate = DateTime.Now,
            OrderDetails = {
                     new OrderDetail {
                               Quantity = 1,
                               UnitPrice = 1.25M,
                               Product = someProduct
                     }
            }
   }
);

事实上,尽管通过这种方式您没有调用Add()方法,DataContext还是会将目前没有被追踪到的对象集合中的实体视为一个新的实例。

4.2 提交变化

不管您对实体做了多少改变,这些变化也只是作用于内存中的副本。没有任何数据被保存到数据库中。您需要显式地调用DataContextSubmitChanges()方法来提交这些变化给数据库服务器。

Northwind db = new Northwind("c://northwind//northwnd.mdf");

// make changes here

db.SubmitChanges();

当您调用DataContextSubmitChanges()方法时,DLinq会试图将所有的变化转化为等价的SQL命令,然后交给数据库服务器执行,最终实现插入、更新或者删除相应数据库表格中的行。您也可以使用自定义的处理逻辑来覆盖这些行为来满足您的需求,但是这些行为提交的顺序都由DataContext的“变化处理”服务来进行管理。

首先,当您调用SubmitChanges()的时候,DataContext需要找出附加到它上面的所有的新实体。这些新实体然后被添加到一个被跟踪的实体的集合中。被挂起修改的所有的实体将根据彼此之间的依赖关系进行排序。被依赖的实体排在所依赖的实体之前。数据库中的外键约束和唯一性约束在很大程度上决定了这个实体集合的排序。然后当我们真正提交这些变化的时候,Dlinq会启动一个事务来管理这一系列的数据库操作。最后,一个一个的实体的变化将被转化为相应的SQL命令让数据库服务器去执行。

在提交被执行的时候,数据库检测到的任何错误都将导致整个提交过程被中断,然后会导致一个异常被抛出。然后所有的变化都将被回滚,就好像没有发生任何变化一样。但DataContext仍旧保留所有的变化记录,所以您就有可能试图修改出现的问题,然后再次调用SubmitChanges()重新提交所有的变化。

Northwind db = new Northwind("c://northwind//northwnd.mdf");

// make changes here

try {
   db.SubmitChanges();
}
catch (Exception e) {
   // make some adjustments
  
...
   // try again
  
db.SubmitChanges();
}

一旦提交成功,DataContext便会简单地删除掉这些跟踪到的变化,从而接受(Accept)了这些实体的变化。您也可以在任何时候显式地调用AcceptChanges()方法。但是,事务提交失败并不会导致RejectChanges()被调用。SQL命令的回滚并不会使得DLinq隐式地回滚本地实体发生的所有的变化。如上所示,重新提交这些变化是很有必要的。

4.3 同步变化

SubmitChanges()失败有很多原因。您或许创建了一个拥有非法主键的实体,这个主键在数据库中已经存在,或者这个值违反了数据库的检查约束。因为这些检查需要完全了解整个数据库的状态,所以很难再回到原来的业务逻辑中去。尽管如此,提交失败发生的可能性最大的原因是,别人在您之前修改了这个实体。

当然,如果您是用了隔离级别为完全序列化的事务,这些失败是不可能会发生了。但是,在实际应用中我们很少使用这种隔离级别(悲观式并发控制),因为它的代价很高尽管很少会发生提交冲突。同步变化最受欢迎的方式是使用乐观式并发控制。在乐观式并发控制中,没有任何行锁,而是直到您调用SubmitChanges()才会开始一个事务。这就意味着,在您从返回实体和提交变化之间,数据库的状态已经发生了变化。

所以,除非您采用最后一次更新有效的策略,即覆盖前面的任何变化,但是您可能还需要在数据被修改的时候得到一个警告。

DataContext默认采用乐观式并发控制。如果首次从数据库中返回实体的数据库状态和您提交时的数据库状态一致,数据库更新才会成功。

当定义您的实体类的时,您可以控制DataContext并发控制的程度。每个Column特性都有一个UpdateCheck属性,可分为三种:Always, Never, WhenChanged。如果设置Column特性的UpdateCheck属性的值为Always这就意味着这个成员将一直被用于进行并发冲突检测,除非我们显示声明了一个版本标识。Column特性有一个IsVersion属性可以让您指定数据库是否会为这个成员的数据值保留一个版本标识。如果这个成员存在一个版本标识,那么这个版本标识可替代用于决定并发冲突。

如果乐观提交的时候出现了并发冲突,将会抛出一个异常来说明发生了某个错误。提交过程的事务将会被取消,当时DataContext仍旧保持所有的变化,这就允许您解决了提交错误之后然后再次提交。

while (retries < maxRetries) {
   Northwind db = new Northwind("c://northwind//northwnd.mdf");

   //
在这里,获取并修改实体

   try {
            db.SubmitChanges();
            break;
   }
   catch (OptimisticConcurrencyException e) {
            retries++;
   }
}

如果您是在一个中间层或者服务器上修改了数据,那么重做事务的最简单的方案就是开始一个新事务然后再尝试提交,也就是说重新创建一个DataContext然后提交这些变化。7.7节中我们描述了一些其他的细节。

4.4 事务

事务是数据库或者其他资源管理器提供的服务,用于保证一系列单独的操作可以被原子地执行。也就是说,如果失败的话这些操作都可以被自动回滚,否则可以保证它们被顺利地执行。如果在某个范围内没有事务,DataContext会自动开始一个新的数据库事务,从而保证您在调用SubmitChanges()时数据库更新可以被顺利地执行。

您或许需要选择控制使用的事务的类型,事务的隔离级别或者定义事务的范围。DataContext默认使用的事务隔离级别是ReadCommitted

Product prod = q.Single(p => p.ProductId == 15);

if
(prod.UnitsInStock > 0)
   prod.UnitsInStock--;

using
(TransactionScope ts = new TransactionScope()) {
   db.SubmitChanges();
   ts.Complete();
}

在上面的例子中,我们通过创建一个TransactionScope对象来开始一个隔离级别为完全序列化的事务。这个事务保证事务范围内执行的任何数据库命令都会被正确地执行。

Product prod = q.Single(p => p.ProductId == 15);

if
(prod.UnitsInStock > 0)
   prod.UnitsInStock--;

using
(TransactionScope ts = new TransactionScope()) {
   db.ExecuteCommand(“exec sp_BeforeSubmit”);
   db.SubmitChanges();
   ts.Complete();
}

上面的例子中,在提交变化之前,我们使用了DataContextExecuteCommand()方法来执行一个数据库存储过程。无论这个存储过程怎样作用于这个数据库,我们可以肯定的是这个操作也在这个事务中执行。

同样地,您也可以将查询和悲观式锁定数据库的操作加入到一个事务中。

using(TransactionScope ts = new TransactionScope()) {
   Product prod = q.Single(p => p.ProductId == 15);

   if
(prod.UnitsInStock > 0)
            prod.UnitsInStock--;

   db.SubmitChanges();
   ts.Complete();
}

如果事务执行成功,那么DataContext将自动接受它所跟踪的实体的所有变化。但是如果事务执行失败,DataContext将回滚所有的变化。这样可以让您在处理变化提交的时候能够有最大的灵活性。

您也有可能使用一个本地SQL事务来替代TransactionScopeDLinq允许使用本地事务,这样您就可以很容易地将新的LINQ特性添加到遗留的ADO.NET应用中。但是如果您真打算这么做,您就需要为此做更多的事情。

Product prod = q.Single(p => p.ProductId == 15);

if
(prod.UnitsInStock > 0)
   prod.UnitsInStock--;

db.LocalTransaction = db.Connection.BeginTransaction();
try {
   db.SubmitChanges();
   db.LocalTransaction.Commit();
   db.AcceptChanges();
}
catch {
   db.LocalTransaction.Abort();
   throw;
}
finally {
   db.LocalTransaction = null;
}

您可以看到,手动使用数据库事务需要做更多的工作。不仅如此,您还不得不启动一个事务,然后需要显式地指定DataContext LocalTransaction属性。然后您必须使用try-catch块来环绕您的提交逻辑,显式地提交修改,显示地接受变化,还需要在事务在执行失败之后取消事务。而且,当您执行完这个事务之后,您需要将LocalTransaction属性设会null

4.5 存储过程

当您调用SubmitChanges()方法时,DLinq生成并执行相应的SQL命令来执行数据库行的插入、更新和删除。您可以使用自定义逻辑来覆盖这些操作。在这种处理方式中,“变化处理”将自动使用一些可选的技术如数据库存储过程。

我们考虑在Northwind示例数据库中使用存储过程来批量更新Products表格中的单位。这个存储过程的定义如下:

create proc UpdateProductStock
   @id                                       int,
   @originalUnits          int,
   @decrement                        int
as

您可以使用存储过程,而没必要去使用强类型的DataContextUpdate命令定义的方法。即便DLinq代码自动生成工具已经帮您实现了这样的方法,但您也还是可以在partial class中重新定义这些方法。

public partial class Northwind : DataContext
{
   ...

   [UpdateMethod]
   public void OnProductUpdate(Product original, Product current) {
            // Execute the stored procedure for UnitsInStock update
            if (original.UnitsInStock != current.UnitsInStock) {
                     int rowCount = this.ExecuteCommand(
                               "exec UpdateProductStock " +
                               "@id={0}, @originalUnits={1}, @decrement={2}",
                               original.ProductID,
                               original.UnitsInStock,
                               (original.UnitsInStock - current.UnitsInStock)
                     );
                     if (rowCount < 1)
                               throw new OptimisticConcurrencyException();
            }
            ...
   }
}

UpdateMethod特性告诉DataContext使用这个方法来替代自动生成的更新语句。传递的originalcurrent参数为指定类型实体的拷贝。这两个参数都可用于并发冲突检测。请注意,因为您重写了默认的更新逻辑,所以您也需要加入对冲突检测的处理。

使用DataContextExecuteCommand来执行UpdateProductStock存储过程,返回了被影响到的行数,ExecuteCommand方法的签名如下:

public int ExecuteCommand(string command, params object[] parameters);

作为参数传递的对象数组用于执行命令

update方法,我们也可以使用InsertMethodDeleteMethod特性来指定insertdelete方法。insertdelete方法只有一个参数,那就是需要更新的实体。例如,更新和删除一个Product实例的代码如下:

[InsertMethod]
public void OnProductInsert(Product prod) { ... }

[DeleteMethod]
public void OnProductDelete(Product prod) { ... }

您可以任意指定这些方法的名称,但是我们需要指定方法的特性,而且方法的签名必须符合特定的规则。


你可能感兴趣的:(C#开发者DLinq概述(4):实体生命周期)