7

我想保存一些具有双向关系的实体(两端的导航属性)。这是通过 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()出现此错误:

连接对象不能在事务范围内登记。

这个问题这篇 MSDN 文章建议我明确地登记交易......

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();
}

错误使我大吃一惊:

“无法确定相关操作的有效顺序。由于外键约束、模型要求或存储生成的值,可能存在相关性。”

这是一个先有鸡还是先有蛋的问题。我无法使用不可为空的外键保存对象,但为了填充外键,我需要先保存对象。

看着这个问题,我似乎不得不放松我的模型,并且:

  • 在关系的至少一侧具有可为空的 FK
  • 先保存那些对象
  • 建立关系
  • 再次保存。

像这样:

public class TransferItem: Operation
{
    public Nullable<long> TransferId { get; set; }
    [etc]
}

另外,这里是映射。Morteza Manavi关于 EF 1:1 关系的文章非常帮助。基本上,我需要与指定的 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)
            ;

用于保存实体的更新代码位于问题的开头。

4

2 回答 2

4

为了完成这项工作,我必须添加更流畅的映射,以显式创建类TransferItem上的可选映射Transfer,以及使 FK 在TransferItem.

一旦映射被修复,我就没有问题将这一切包装在单个 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();
         }

      }
   }

运行时生成此数据库:

在此处输入图像描述

这个输出:

在此处输入图像描述

于 2012-09-06T01:27:38.703 回答
0

这可能是因为两次调用SaveChanges导致连接被打开和关闭两次。对于某些版本的 SQL Server,这会导致事务被提升为分布式事务,如果服务未运行,该事务将失败。

最简单的解决方案是通过在创建TransactionScope. 然后 EF 不会尝试打开或关闭连接本身,直到它在释放上下文时关闭它。

请参阅此答案以获取代码示例以及博客文章。

于 2012-09-12T17:58:51.613 回答