LINQ to SQL 中的级联删除
本文将讨论使用 LINQ to SQL 执行级联删除的替代方法。
引言
本文将讨论使用 LINQ to SQL 执行级联删除的替代方法。级联删除是指删除一个记录时,同时删除与该记录通过外键关联的其他记录的操作。LINQ to SQL 不会专门处理级联删除,开发者需要自行决定是否需要执行此操作,以及如何来实现级联删除。
问题
执行级联删除的问题并非 LINQ to SQL 才出现,在处理这类问题时,基本上有相同的替代方法。问题在于,当一个记录被标记为删除时,需要确定如何处理与之通过外键关联(特别是那些非可空字段)的其他表中的相关记录——是删除还是保留它们。
例如,考虑 Northwind 数据库中的 Customer 表。Customer 表与 Orders 表之间存在外键关系(Orders 表又与 Order_Details 表存在外键关系)。要删除一个有相关订单的客户,就需要处理 Orders 表和 Order_Details 表中的相关记录。在 LINQ to SQL 的术语中,相关的表被称为实体集。
LINQ to SQL 不会违反外键关系。如果应用程序尝试删除具有此类关系的记录,执行的代码将抛出异常。以 Northwind 数据库为例,如果尝试删除一个有相关订单的客户,就会发生异常。这并非问题,而是应有的行为,否则外键关系还有何意义?关键在于,你需要确定是否真的要删除包含相关实体集的记录,如果决定删除,又该如何处理——是保留相关记录,还是与目标记录一起删除它们?
解决方案
有几种可行的替代方案。你可以在代码中使用 LINQ to SQL 来处理级联删除,也可以在 SQL Server 中处理外键关系。
如果将此代码执行到 Northwind 数据库,它将创建一个带有相关订单和订单明细的客户。
try
{
Customer c = new Customer();
c.CustomerID = "AAAAA";
c.Address = "554 Westwind Avenue";
c.City = "Wichita";
c.CompanyName = "Holy Toledo";
c.ContactName = "Frederick Flintstone";
c.ContactTitle = "Boss";
c.Country = "USA";
c.Fax = "316-335-5933";
c.Phone = "316-225-4934";
c.PostalCode = "67214";
c.Region = "EA";
Order_Detail od = new Order_Detail();
od.Discount = .25f;
od.ProductID = 1;
od.Quantity = 25;
od.UnitPrice = 25.00M;
Order o = new Order();
o.Order_Details.Add(od);
o.Freight = 25.50M;
o.EmployeeID = 1;
o.CustomerID = "AAAAA";
c.Orders.Add(o);
using (NWindDataContext dc = new NWindDataContext())
{
var table = dc.GetTable();
table.InsertOnSubmit(c);
dc.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
但是,如果你尝试删除客户而不处理实体集,像这样:
using (NWindDataContext dc = new NWindDataContext())
{
var q =
(from c in dc.GetTable<Customer>()
where c.CustomerID == "AAAAA"
select c).Single<Customer>();
dc.GetTable<Customer>().DeleteOnSubmit(q);
dc.SubmitChanges();
}
将会导致错误,数据库不会发生任何更改。
解决方案替代方案 1 – 使用 LINQ to SQL 处理删除
你可以通过手动删除关联实体集中的所有相关实体来处理级联删除;这是一种简单的实现方式:
try
{
using (NWindDataContext dc = new NWindDataContext())
{
var q =
(from c in dc.GetTable<Customer>()
where c.CustomerID == "AAAAA"
select c).Single<Customer>();
foreach (Order ord in q.Orders)
{
dc.GetTable<Order>().DeleteOnSubmit(ord);
foreach (Order_Detail od in ord.Order_Details)
{
dc.GetTable<Order_Detail>().DeleteOnSubmit(od);
}
}
dc.GetTable<Customer>().DeleteOnSubmit(q);
dc.SubmitChanges();
}
UpdateDataGrid();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
从这个例子来看,要删除客户及其相关的订单和订单明细,代码首先根据客户 ID(主键)选择匹配的客户。一旦找到匹配项,代码会遍历与每个客户相关的订单,并使用 DeleteOnSubmit
调用将其标记为删除。
此外,由于订单和订单明细之间还存在另一个关系,代码会遍历与订单相关的所有订单明细,并将它们也标记为删除。最后,将客户本身标记为删除,然后对数据上下文调用 SubmitChanges。实体被标记为删除的顺序无关紧要,LINQ to SQL 会在执行 SubmitChanges 调用时根据外键的配置进行排序。
解决方案替代方案 2 – 在 SQL Server 中处理级联删除
可以在 SQL Server 中完全管理级联删除。为此,只需将外键关系删除规则设置为“级联”即可。
如果你已经构建了数据库图,最简单的设置删除规则的方法是打开图,点击图中的外键关系,然后打开 INSERT 和 UPDATE 属性以显示 Delete Rule 属性,然后将 Delete Rule 属性设置为 Cascade,如图 3 所示。
重复删除相关订单的客户的示例,如果我们为所有约束的删除规则都设置为级联,我们可以使用这段代码删除客户:
try
{
using (NWindDataContext dc = new NWindDataContext())
{
var q =
(from c in dc.GetTable<Customer>()
where c.CustomerID == "AAAAA"
select c).Single<Customer>();
dc.GetTable<Customer>().DeleteOnSubmit(q);
dc.SubmitChanges();
}
UpdateDataGrid();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
正如你在此示例代码中看到的,无需像上一个方案那样麻烦地标记实体集的每个成员为删除,因为在这种情况下,SQL Server 已被告知如何处理客户或订单记录的删除。因此,删除客户也会导致 Order 和 Order Details 表中包含的相关记录被删除。
解决方案替代方案 3 – 在 SQL Server 中处理级联删除
也可以将实体集中的外键字段设置为可空,然后将该字段的删除规则设置为“Set Null”。也可以为字段设置一个默认值,并将删除规则设置为“Set Default”。如果需要删除(在此示例中)一个客户记录但保留订单和订单明细记录,这两种方法都可能有用。这两种方法都可以以类似于前一个解决方案替代方案的方式处理。将外键值设置为可空可能不是最佳选择,但它是一个可行的替代方案。
解决方案替代方案 4 – 使用存储过程处理级联删除
可以创建或添加一个存储过程来完成级联删除,并使用 LINQ to SQL 调用该存储过程。添加到设计器中的存储过程可以直接从数据上下文调用,例如,如果我们有一个名为 DeleteCustomer
的存储过程,它接受客户 ID 作为参数并处理级联删除,我们可以这样做:
Using(NwindDataContext dc = new NwindDataContext())
{
dc.DeleteCustomer("AAAAA");
}
摘要
级联删除对 LINQ to SQL 来说并非新问题,它一直是同样的问题。在本文中,我描述了几种在代码内部和 SQL Server 端处理级联删除的方法,但正如 .NET 中的许多事物一样,还有其他几种方法可以在 LINQ to SQL 中实现此操作。