【问题标题】:I need to do Multiple Breeze SaveChanges in one transaction我需要在一个事务中执行多个 Breeze SaveChanges
【发布时间】:2020-01-06 20:27:53
【问题描述】:

我有一个接受费用支付的网页,它必须插入 6 行,分布在 4 个表格中。我不得不将 INSERTS 分成两个单独的 SaveChanges,但我需要它们都在同一个数据库事务中,以便在出现问题时所有 INSERTS 和 UPDATES 都会回滚。

我在 SPA 模板中在 Entity Framework 6.2 和 Oracle Mgd Data Access 12.2 之上使用 Breeze 1.6。

这 4 个表是 A、B、C 和 D。表 B、C 和 D 是 A 的子表,每个表都携带 A 的 PK 作为外键。我最初按照我的应用程序 A1、B1、C1、C2、C3、D1 的要求按此顺序对 INSERTS 进行编码,然后是单个 Breeze SaveChanges。 C3 具有 Oracle 触发器,可在插入 C3 时更新 A1 和 B1 中的几列。我的问题是实体框架没有按我编码的序列插入(我知道我无法控制序列)。我实际上得到了这个序列:A1、C1、C2、C3、B1、D1,由于 C3 有一个更新 A 和 B 的触发器,它遇到了问题,因为 B 还没有插入。

所以我发现了这个问题:What logic determines the insert order of Entity Framework 6,它建议使用多个 SaveChanges 来获得一些控制。得到的工作如下:

  • A1、B1 保存更改
  • C1、C2、C3、D1 保存更改 包括 A 和 B 的 C3 更新在内的所有触发器现在都运行良好。

BUT Breeze/Entity Framework 将每个 SaveChanges 视为一个事务,因此现在我无法回滚这两个部分,以防第二个 SaveChanges 出现错误。如果第 2 部分失败,我是否必须自己编写第 1 部分回滚代码?还是有什么我不知道的秘密?

我熟悉 BeginTransaction、Commit 和 Rollback,但不确定如何/在何处将其与 Breeze 和 Entity Framework 混合使用。 Breeze 抓取每个 SaveChanges 并自动将其包装在事务中。

我需要将多个 SaveChanges 分组到一个事务中。

感谢您的帮助。

【问题讨论】:

  • 更新:这是 Visual Studio 2017,.Net FW 4.6.2,对于数据库,我在 SPA 中的 Entity Framework 6.2 之上使用 Breeze 1.6,在 Oracle Mgd Data Access 19.3 之上模板。

标签: oracle entity-framework transactions breeze savechanges


【解决方案1】:

我认为在这种情况下最好的做法是让 Breezeclient 将其视为单个保存,而 server 将保存包分开保存以正确的顺序。

让 Breeze ContextProvider 反序列化保存包,然后使用 BeforeSaveEntities 挂钩手动进行更改。最后,将实体标记为Unchanged,这样 ContextProvider 就不会再次尝试保存它们。

这是一个示例实现。

服务器

在服务器上,在您的 Breeze Controller 中,创建一个新的保存方法,该方法添加一个 BeforeSaveEntitiesDelegate,即 仅用于此特殊保存:

[HttpPost]
public SaveResult SaveFeePayment(JObject saveBundle) {
{
    contextProvider.BeforeSaveEntitiesDelegate += BeforeSaveFeePayment;
    return contextProvider.SaveChanges(saveBundle);
}

private Dictionary<Type, List<EntityInfo>> BeforeSaveFeePayment(Dictionary<Type, List<EntityInfo>> entities)
{
    var context = contextProvider.Context;

    // Get the list of EntityInfo for each type.  Throws exception if not found.
    // A fee payment save bundle must have A, B, C, and D or it is invalid.
    var ainfos = entities[typeof(A)];
    var binfos = entities[typeof(B)];
    var cinfos = entities[typeof(C)];
    var dinfos = entities[typeof(D)];

    using (var tx = context.Database.BeginTransaction()) 
    {               
        // Save A and B
        Attach(context, ainfos);
        Attach(context, binfos);
        context.SaveChanges();

        // Save C and D
        Attach(context, cinfos);
        Attach(context, dinfos);
        context.SaveChanges();

        tx.Commit();
    }

    // Set all states to Unchanged, so Breeze won't try to save them again
    ainfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    binfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    cinfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    dinfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);

    // Return the entities, so Breeze will return them back to the client
    return entities;
}

// Map Breeze EntityState to EF EntityState
private static Dictionary<Breeze.ContextProvider.EntityState, System.Data.Entity.EntityState> entityStateMap =
    new Dictionary<Breeze.ContextProvider.EntityState, System.Data.Entity.EntityState> {
      { Breeze.ContextProvider.EntityState.Added, System.Data.Entity.EntityState.Added },
      { Breeze.ContextProvider.EntityState.Deleted, System.Data.Entity.EntityState.Deleted },
      { Breeze.ContextProvider.EntityState.Detached, System.Data.Entity.EntityState.Detached },
      { Breeze.ContextProvider.EntityState.Modified, System.Data.Entity.EntityState.Modified },
      { Breeze.ContextProvider.EntityState.Unchanged, System.Data.Entity.EntityState.Unchanged }
    };

// Attach entities to the DbContext in the correct entity state 
private static void Attach(DbContext context, List<EntityInfo> infos)
{
    foreach(var info in infos)
    {
        var efState = entityStateMap[info.EntityState];
        context.Entry(info.Entity).State = efState;
    }
}

客户

在客户端上,您需要使用named save 调用SaveFeePayment 端点。 只有在保存费用时,您才会这样做。继续使用普通的SaveChanges 端点进行其他保存。

通过指定resourceName调用特殊端点:

var saveOptions = new SaveOptions({ resourceName: "SaveFeePayment" });
return myEntityManager.saveChanges(null, saveOptions);

交易?

我还没有测试这个例子是 100% 的交易行为。我不确定我们是应该使用现有的contextProvider.Context 还是在方法开始时创建一个新的 DbContext。区别在于如何处理数据库连接。请参阅Microsoft's guidance,并希望它与 Oracle 一样。

希望您之前的事务管理解决方案可以应用于上面的BeforeSaveFeePayment 方法。

希望这会有所帮助。

【讨论】:

  • 哇,谢谢,史蒂夫。我刚看到这个。有趣的是,我只是在与我们的开发架构师交谈,他说我需要反序列化服务器上​​的保存包,并将其放入各种类型的集合中。然后构建包装在事务中的多个 SaveChanges。这不是你在做的吗? (我需要一段时间来学习和理解你在这里的所有内容)感谢您抽出时间来帮助我。
  • BeginTransaction 和 Commit/Rollback 在哪里适合这个?
  • 是的,我们让 Breeze 将保存包放入各种类型的集合中,然后执行多个 SaveChanges。我在示例中添加了一个事务块,但我没有对其进行测试,我不确定我们是应该使用现有的contextProvider.Context 还是在方法开始时创建一个新的 DbContext。
  • 对了,需要将删除状态返回给客户端。我认为应该是deletedInfos.ForEach(info =&gt; info.EntityState = Breeze.ContextProvider.EntityState.Detached);,但也许应该是EntityState.Deleted。请同时尝试。
  • 太好了,很高兴你解决了这个问题。很高兴我能帮上忙。
【解决方案2】:

就我而言,在 .NET core 3.1 上使用 Breeze,我必须设置 IsTemporary=true,如以下代码示例所示:

private static void Attach(BreezeContext context, List<EntityInfo> infos)
{
  foreach (var info in infos)
  {
    var efState = entityStateMap[info.EntityState];

    context.Entry(info.Entity).Property("Id").IsTemporary = true; //Explicitly set to true
    context.Entry(info.Entity).State = efState;
  }
}

默认设置为false,因此SaveChanges() 抛出以下错误:

“无法为标识列插入显式值”

这可能是由于 Breeze 中的错误造成的。同时,将IsTemporary 设置为true 至少可以解决问题。

【讨论】:

    猜你喜欢
    • 2015-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-26
    • 2012-01-22
    • 1970-01-01
    • 2011-06-19
    相关资源
    最近更新 更多