EF4 Code First + SQL Server CE:以原子方式保存双向引用
Posted
技术标签:
【中文标题】EF4 Code First + SQL Server CE:以原子方式保存双向引用【英文标题】:EF4 Code First + SQL Server CE: save bidirectional reference atomically 【发布时间】:2012-08-31 09:20:12 【问题描述】:我想保存一些具有双向关系的实体(两端的导航属性)。这是通过 2 次调用 context.SaveChanges()
来完成的。
[折叠后有关我的模型、映射以及我如何到达那里的完整详细信息。]
public void Save()
var t = new Transfer();
var ti1 = new TransferItem();
var ti2 = new TransferItem();
//deal with the types with nullable FKs first
t.TransferIncomeItem = ti1;
t.TransferExpenseItem = ti2;
context.Transfers.Add(t);
context.Operations.Add(ti1);
context.Operations.Add(ti2);
//save, so all objects get assigned their Ids
context.SaveChanges();
//set up the "optional" half of the relationship
ti1.Transfer = t;
ti2.Transfer = t;
context.SaveChanges();
一切都很好,但是如果在两次调用SaveChanges()
之间发生雷击,如何确保数据库不会不一致?
输入TransactionScope
...
public void Save()
using (var tt = new TransactionScope())
[...same as above...]
tt.Complete();
...但是在第一次调用 context.SaveChanges()
时失败并出现此错误:
连接对象不能加入事务范围。
This question 和 this MSDN article 建议我明确登记交易...
public void Save()
using (var tt = new TransactionScope())
context.Database.Connection.EnlistTransaction(Transaction.Current);
[...same as above...]
tt.Complete();
...同样的错误:
连接对象不能加入事务范围。
死路一条……让我们换一种方法——使用显式事务。
public void Save()
using (var transaction = context.Database.Connection.BeginTransaction())
try
[...same as above...]
transaction.Commit();
catch
transaction.Rollback();
throw;
仍然没有运气。这一次,错误信息是:
BeginTransaction 需要一个打开且可用的连接。连接的当前状态是关闭。
我该如何解决这个问题?
TL;DR 详细信息
这是我的简化模型:一个引用两个操作 (TransferItem) 的事务,该操作引用回事务。 这是 Transfer 与其两个项目之间的 1:1 映射。
我想要的是确保在添加新的Transfer
时自动保存。
这是我走过的路,也是我卡住的地方。
型号:
public class Transfer
public long Id get; set;
public long TransferIncomeItemId get; set;
public long TransferExpenseItemId get; set;
public TransferItem TransferIncomeItem get; set;
public TransferItem TransferExpenseItem get; set;
public class Operation
public long Id;
public decimal Sum get; set;
public class TransferItem: Operation
public long TransferId get; set;
public Transfer Transfer get; set;
我想将此映射保存到数据库 (SQL CE)。
public void Save()
var t = new Transfer();
var ti1 = new TransferItem();
var ti2 = new TransferItem();
t.TransferIncomeItem = ti1;
t.TransferExpenseItem = ti2;
context.Transfers.Add(t);
context.Operations.Add(ti1);
context.Operations.Add(ti2);
context.SaveChanges();
这个错误让我大吃一惊:
“无法确定相关操作的有效顺序。 由于外键约束,模型可能存在依赖关系 需求或存储生成的值。”
这是一个先有鸡还是先有蛋的问题。我无法使用不可为空的外键保存对象,但为了填充外键,我需要先保存对象。
看this question好像要放松一下我的模型了,还有:
至少在关系的一侧具有可为空的 FK 先保存这些对象 建立关系 再次保存。像这样:
public class TransferItem: Operation
public Nullable<long> TransferId get; set;
[etc]
另外,这里是映射。 Morteza Manavi 的article on EF 1:1 relationships 真的很有帮助。基本上,我需要与指定的 FK 列创建一对多关系。 'CascadeOnDelete(false)' 处理有关多个级联路径的错误。 (数据库可能会尝试删除 Transfer 两次,每个关系一次)
modelBuilder.Entity<Transfer>()
.HasRequired<TransferItem>(transfer => transfer.TransferIncomeItem)
.WithMany()
.HasForeignKey(x => x.TransferIncomeItemId)
.WillCascadeOnDelete(false)
;
modelBuilder.Entity<Transfer>()
.HasRequired<TransferItem>(transfer => transfer.TransferExpenseItem)
.WithMany()
.HasForeignKey(x => x.TransferExpenseItemId)
.WillCascadeOnDelete(false)
;
保存实体的更新代码在问题的开头。
【问题讨论】:
【参考方案1】:为了完成这项工作,我必须添加更流畅的映射,以在 Transfer
类上显式创建 TransferItem
的可选映射,并使 TransferItem
上的 FK 可以为空。
一旦映射被修复,我将这一切包装在单个 TransactionScope 中没有问题。
这是整个控制台应用程序:
public class Transfer
public long Id get; set;
public long TransferIncomeItemId get; set;
public long TransferExpenseItemId get; set;
public TransferItem TransferIncomeItem get; set;
public TransferItem TransferExpenseItem get; set;
public class Operation
public long Id get; set;
public decimal Sum get; set;
public class TransferItem : Operation
public long? TransferId get; set;
public Transfer Transfer get; set;
public class Model : DbContext
public DbSet<Transfer> Transfers get; set;
public DbSet<Operation> Operations get; set;
public DbSet<TransferItem> TransferItems get; set;
protected override void OnModelCreating( DbModelBuilder modelBuilder )
modelBuilder.Entity<Transfer>()
.HasRequired( t => t.TransferIncomeItem )
.WithMany()
.HasForeignKey( x => x.TransferIncomeItemId )
.WillCascadeOnDelete( false );
modelBuilder.Entity<Transfer>()
.HasRequired( t => t.TransferExpenseItem )
.WithMany()
.HasForeignKey( x => x.TransferExpenseItemId )
.WillCascadeOnDelete( false );
modelBuilder.Entity<TransferItem>()
.HasOptional( t => t.Transfer )
.WithMany()
.HasForeignKey( ti => ti.TransferId );
class Program
static void Main( string[] args )
using( var scope = new TransactionScope() )
var context = new Model();
var ti1 = new TransferItem();
var ti2 = new TransferItem();
//deal with the types with nullable FKs first
context.Operations.Add( ti1 );
context.Operations.Add( ti2 );
var t = new Transfer();
context.Transfers.Add( t );
t.TransferIncomeItem = ti1;
t.TransferExpenseItem = ti2;
//save, so all objects get assigned their Ids
context.SaveChanges();
//set up the "optional" half of the relationship
ti1.Transfer = t;
ti2.Transfer = t;
context.SaveChanges();
scope.Complete();
运行时生成此数据库:
这个输出:
【讨论】:
【参考方案2】:这可能是因为对SaveChanges
的两次调用导致连接被打开和关闭两次。对于某些版本的 SQL Server,这会导致事务被提升为分布式事务,如果服务未运行,则会失败。
最简单的解决方案是通过在创建TransactionScope
之前显式打开连接来手动管理连接。然后 EF 不会尝试打开或关闭连接本身,直到它在释放上下文时关闭它。
请参阅 this answer 获取代码示例以及 this 和 this 博客文章。
【讨论】:
以上是关于EF4 Code First + SQL Server CE:以原子方式保存双向引用的主要内容,如果未能解决你的问题,请参考以下文章
MVC3+EF4.1学习系列-------创建EF4.1 code first的第一个实例
EF4 Code First & SQLCE 4.0 - 生成数据库时出现异常
EF 4.1 Code-First 项目上的 MvcMiniProfiler 不分析 SQL