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:以原子方式保存双向引用的主要内容,如果未能解决你的问题,请参考以下文章

EF4 CTP5 Code First 方法忽略表属性

MVC3+EF4.1学习系列-------创建EF4.1 code first的第一个实例

EF4 Code First & SQLCE 4.0 - 生成数据库时出现异常

EF 4.1 Code-First 项目上的 MvcMiniProfiler 不分析 SQL

ef4.4 code first数据库新增时 初始化表数据不成功

EF 4.1:使用 Fluent 映射从 Code First 中查找关键属性类型