问题描述
我有一种方法,通过将接收到的实体与相应的数据库行进行比较,将实体图的更改同步到数据库,然后用每个对象正确的
ChangeTracker
更新State
。
这通常工作得很好,但是当存在唯一索引时,由于语句的顺序变得很重要,所以一切都会崩溃。
在这个例子中,我首先要删除唯一:“A”的行
DeleteUpdateOrders
,然后修改具有唯一:“A”的另一行。如果 DELETE
在 UPDATE
之前执行,这将起作用,但是语句将以相反的方式执行。
有没有办法控制 Entity Framework 6 发出语句的顺序?
数据库脚本(SQL Server):
CREATE DATABASE ChangeTracker
GO
USE ChangeTracker
CREATE TABLE DeleteUpdateOrders
(
DeleteUpdateOrderId INT NOT NULL PRIMARY KEY,
[Unique] CHAR NOT NULL
)
CREATE UNIQUE INDEX IX_DeleteUpdateOrders_Unique
ON DeleteUpdateOrders ([Unique])
INSERT INTO DeleteUpdateOrders VALUES (1, 'A')
INSERT INTO DeleteUpdateOrders VALUES (2, 'B')
最小重现:
public void Main()
{
var context = new TestContext();
context.Database.Log = Console.Write;
var databaseRows = context.DeleteUpdateOrders.AsNoTracking().ToArray();
// Delete the row with Unqiue = "A"
var rowToDelete = databaseRows.Single(r => r.DeleteUpdateOrderId == 1);
context.Entry(rowToDelete).State = EntityState.Deleted;
// Get the existing row with Unique = "B" and set Unique = "A"
var rowToModify = databaseRows.Single(r => r.DeleteUpdateOrderId == 2);
rowToModify.Unique = "A";
context.Entry(rowToModify).State = EntityState.Modified;
// context.ChangeTracker at this stage contains:
// Modified { DeleteUpdateOrderId: 2, Unique: "A" }
// Deleted { DeleteUpdateOrderId: 1, Unique: "A" }
// Console output indicates that the UPDATE was executed before
// the DELETE statement, and the innermost SqlException thrown on
// context.SaveChanges is:
//
// Cannot insert duplicate key row in object 'dbo.DeleteUpdateOrders' with
// unique index 'IX_DeleteUpdateOrders_Unique'.The duplicate key value is (A).
context.SaveChanges();
}
public class TestContext : DbContext
{
public IDbSet<DeleteUpdateOrder> DeleteUpdateOrders { get; set; }
public TestContext() :
base("Server=.; Database=ChangeTracker; Integrated Security=SSPI")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer<TestContext>(null);
// Informing EF6 about the index does not seem to make a difference ...
modelBuilder
.Entity<DeleteUpdateOrder>()
.HasIndex(entity => entity.Unique)
.IsUnique();
}
}
public class DeleteUpdateOrder
{
public int DeleteUpdateOrderId { get; set; }
public string Unique { get; set; }
}
据我所知,没有办法告诉 EF 明确的操作顺序。强制执行的明显方法是使用 2x
SaveChanges
调用,但这需要显式事务来确保两者一起提交或回滚。
每当涉及导航属性时,EF 将强制执行操作顺序。根据这个唯一值的性质,如果该值本身有意义,您可以引入表格和阴影导航属性。
例如,如果我们有一个 UniqueValues 表,将此 UniqueValue 作为 PK,那么我们可以将关系配置为:
modelBuilder
.Entity<DeleteUpdateOrder>()
.HasOne<UniqueValue>("UniqueValue")
.WithOne()
.HasForeignKey(entity => entity.Unique)
.IsRequired();
现在,当您运行该场景时,您可以:
var rowToDelete = context.DeleteUpdateOrders.Single(r => r.DeleteUpdateOrderId == 1);
context.DeleteUpdateOrders.Remove(rowToDelete);
// Get the existing row with Unique = "B" and set Unique = "A"
var rowToModify = context.DeleteUpdateOrders.Single(r => r.DeleteUpdateOrderId == 2);
rowToModify.Unique = "A";
context.SaveChanges();
我使用删除/插入、插入/更新等进行了几次不同的运行测试。操作顺序并不重要,EF 在知道存在依赖关系时会正确解析顺序。
此方法的警告是,它确实需要唯一值的 FK 和关联表,因此如果您只想要一个可以检查和插入(如果不存在)的唯一值,则此方法将不起作用。在插入新订单之前,需要将新值插入到 UniqueValues 表中。