在SQL查询期间获取Id并使用另一个Insert Statment

时间:2017-03-08 07:30:21

标签: c# .net sql-server transactions dapper

所以我一直在创建一个使用dapper的库,并允许用户操作数据库。

我需要一些帮助来找到实现以下目标的最佳方法。

假设我有一个“订单”表,我有一个“交易”表和一个“order_line”表。 我想在插入时使用表“order”的Increment Id并使用它将它存储在“transaction”和“order_line”表中的一列中,我希望所有这些都在SQL事务中完成,这样我就可以回滚任何问题的情况。

既然我的图书馆对任何类型和动作都是动态的,我不知道如何处理这样的事情。

以下是有关如何插入的代码: 我有2个全局变量

  private string connectionString { get; set; }

        public void newConnection(string connection)
        {
            if (string.IsNullOrWhiteSpace(connectionString))
            {
                connectionString = connection;
            }
        }

        private List<KeyValuePair<string, object>> transactions = new List<KeyValuePair<string, object>>();

以下是调用将类保存到数据库的方法:

public void Add(object item)
{
    string propertyNames = "";
    string propertyParamaters = "";
    Type itemType = item.GetType();

    System.Reflection.PropertyInfo[] properties = itemType.GetProperties();

    for (int I = 0; I < properties.Count(); I++)
    {
        if (properties[I].Name.Equals("Id", StringComparison.CurrentCultureIgnoreCase) || properties[I].Name.Equals("AutoId", StringComparison.CurrentCultureIgnoreCase))
        {
            continue;
        }

        if (I == properties.Count() - 1)
        {
            propertyNames += "[" + properties[I].Name + "]";
            propertyParamaters += "@" + properties[I].Name;
        }
        else
        {
            propertyNames += "[" + properties[I].Name + "],";
            propertyParamaters += "@" + properties[I].Name + ",";
        }
    }

    string itemName = itemType.Name;
    KeyValuePair<string, object> command = new KeyValuePair<string, object>($"Insert Into[{ itemName}] ({ propertyNames}) Values({ propertyParamaters})", item);
    transactions.Add(command);
}

还有更多方法,例如编辑,删除,编辑列表,删除列表等,但在这种情况下不相关。

如果要提交对数据库的更改,请调用:

public void SaveChanges()
        {
            using (SqlConnection sqlConnection = new SqlConnection(connectionString))
            {
                sqlConnection.Open();
                using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
                {
                    try
                    {
                        foreach (KeyValuePair<string, object> command in transactions)
                        {
                            sqlConnection.Execute(command.Key, command.Value, sqlTransaction);
                        }

                        sqlTransaction.Commit();
                    }
                    catch
                    {
                        sqlTransaction.Rollback();
                        throw;
                    }
                    finally
                    {
                        sqlConnection.Close();
                        transactions.Clear();
                    }
                }
                sqlConnection.Close();
            }

            transactions.Clear();
        }

您可以在github.com找到我的图书馆 https://github.com/pietercdevries/Bamboo.Net

2 个答案:

答案 0 :(得分:1)

可以做到吗......是的......我们应该自己尝试这样做......我不会:)但是让我们尝试一下。

可以使这段代码更简单的一些想法:

  • 定义帮助程序接口并强制数据类实现它们或使用属性声明来指定id字段和外键引用
  • 调查注入或代码生成技术,以便您可以获得一些动态的&#39;编译和查找在编译时执行,而不是运行时。
  

我没有使用Dapper,你的SqlConnection.Execute()是我不熟悉的扩展方法,但我假设它从传入的对象生成DbParameters并在执行时将它们应用于SqlCommand。希望dapper有一些函数来提取参数,以便它们可以在这个代码示例中使用,或者你可以使用其中的一些概念并使它们适应你的dapper代码。我只是想承认这一点,我在这里省略了任何代码示例,在执行命令时参数化对象。

这是以下片段将会消失的旅程

  1. 准备生成的SQL以捕获Id字段
  2. 保存更改时输出Id值
  3. 迭代数组中的所有剩余对象并设置外键值
  4.   

    注意:这些代码更改未经过生产测试或异常处理,我也称之为“#34;最佳实践&#34;它只是为了证明这个概念并帮助一个编码人员:)

    1. 您已经为Id字段跟踪设计了一个约定,让我们通过准备sql语句来设置输出参数的值来扩展这个想法:

        

      注意:在MS SQL中,请使用SCOPE_IDENTITY()优先于@@ Identity   What is the difference between Scope_Identity(), Identity(), @@Identity, and Ident_Current?

           

      注意:因为生成的语句是使用参数,而我们还没有读取参数值,所以在找到要插入其他对象的Id值后,我们不需要重新生成保存的SQL语句... phew ......

      public void Add(object item)
      {
          List<string> propertyNames = new List<string>();
          Type itemType = item.GetType();
      
          System.Reflection.PropertyInfo[] properties = itemType.GetProperties();
      
          for (int I = 0; I < properties.Count(); I++)
          {
              if (properties[I].Name.Equals("Id", StringComparison.CurrentCultureIgnoreCase) || properties[I].Name.Equals("AutoId", StringComparison.CurrentCultureIgnoreCase))
              {
                  continue;
              }
              propertyNames.Add(properties[I].Name);
          }
      
          string itemName = itemType.Name;
          KeyValuePair<string, object> command = new KeyValuePair<string, object>
              ($"Insert Into[{itemName}] ({String.Join(",", propertyNames.Select(p => $"[{p}]"))}) Values({String.Join(",", propertyNames.Select(p => $"@{p}"))}); SET @OutId = SCOPE_IDENTITY();", item);
          transactions.Add(command);
          // Simply append your statement with a set command on an @id parameter we will add in SaveChanges()
      }
      
    2. 在Save Changes中,实现输出参数以捕获创建的Id,如果捕获了Id,则将其保存回与命令关联的对象中。

        

      注意:此代码段显示了第3项中对解决方案的引用。   并且foreach被替换为for,因此我们可以从当前索引进行前向迭代

      public void SaveChanges()
      {
          using (SqlConnection sqlConnection = new SqlConnection(connectionString))
          {
              sqlConnection.Open();
              using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
              {
                  try
                  {
                      for (int i = 0; i < transactions.Count; i++)
                      {
                          KeyValuePair<string, object> command = transactions[i];
                          // 1. Execute the command, but use an output parameter to capture the generated id
                          var cmd = sqlConnection.CreateCommand();
                          cmd.Transaction = sqlTransaction;
                          cmd.CommandText = command.Key;
                          SqlParameter p = new SqlParameter()
                          {
                              ParameterName = "@OutId",
                              Size = 4,
                              Direction = ParameterDirection.Output
                          };
                          cmd.Parameters.Add(p);
                          cmd.ExecuteNonQuery();
      
                          // Check if the value was set, non insert operations wil not set this parameter
                          // Could optimise by not preparing for the parameter at all if this is not an 
                          // insert operation.
                          if (p.Value != DBNull.Value)
                          {
                              int idOut = (int)p.Value;
      
                              // 2. Stuff the value of Id back into the Id field.
                              string foreignKeyName = null;
                              SetIdValue(command.Value, idOut, out foreignKeyName);
                              // 3. Update foreign keys, but only in commands that we haven't execcuted yet
                              UpdateForeignKeys(foreignKeyName, idOut, transactions.Skip(i + 1));
                          }
                      }
      
                      sqlTransaction.Commit();
                  }
                  catch
                  {
                      sqlTransaction.Rollback();
                      throw;
                  }
                  finally
                  {
                      sqlConnection.Close();
                      transactions.Clear();
                  }
              }
              sqlConnection.Close();
          }
      
          transactions.Clear();
      }
      
      
      /// <summary>
      /// Update the Id field of the specified object with the provided value
      /// </summary>
      /// <param name="item">Object that we want to set the Id for</param>
      /// <param name="idValue">Value of the Id that we want to push into the item</param>
      /// <param name="foreignKeyName">Name of the expected foreign key fields</param>
      private void SetIdValue(object item, int idValue, out string foreignKeyName)
      {
          // NOTE: There are better ways of doing this, including using interfaces to define the key field expectations.
          // This logic is consistant with existing code so that you are familiar with the concepts
          Type itemType = item.GetType();
          foreignKeyName = null;
      
          System.Reflection.PropertyInfo[] properties = itemType.GetProperties();
      
          for (int I = 0; I < properties.Count(); I++)
          {
              if (properties[I].Name.Equals("Id", StringComparison.CurrentCultureIgnoreCase) || properties[I].Name.Equals("AutoId", StringComparison.CurrentCultureIgnoreCase))
              {
                  properties[I].SetValue(item, idValue);
                  foreignKeyName = $"{item.GetType().Name}_{properties[I].Name}";
                  break;
              }
          }
      }
      
    3. 现在,您的对象在插入时会更新其ID。 现在为有趣的部分...更新Id后,您现在应该遍历其他对象并更新其外键字段。

    4. 在现实中如何解决这个问题很大程度上取决于您准备对要更新的数据执行什么样的假设/约定。为简单起见,假设我们需要更新的所有外键都以约定{ParentClassName} _ {Id}命名。

      这意味着,如果在我们的示例中我们刚刚插入了一个新的小部件,那么我们可以尝试强制更新此交易范围内具有字段&#39; Widget_Id&#39;的所有其他对象。 (或&#39; Widget_AutoId&#39;)

          private void UpdateForeignKeys(string foreignKeyName, int idValue, IEnumerable<KeyValuePair<string, object>> commands)
          {
              foreach(var command in commands)
              {
                  Type itemType = command.Value.GetType();
                  var keyProp = itemType.GetProperty(foreignKeyName);
                  if(keyProp != null)
                  {
                      keyProp.SetValue(command.Value, idValue);
                  }
              }
          }
      

      这是一个非常简单的示例,说明如何更新OP数据持久性库中的外键(或引用)键。 您可能已经在实际中观察到关系键字段很少使用任何约定一致地命名,但即使遵循约定,我的简单约定也不支持对同一类型的父类具有多个引用的表,例如一个Manifest我的客户的应用程序有3个链接回用户表:

      public class Manifest
      {
          ...
          Driver_UserId { get; set; }
          Sender_UserId { get; set; }
          Receiver_UserId { get; set; }
          ...
      }
      

      您需要发展一些非常先进的逻辑来处理所有可能的链接组合。

      某些ORM通过将值设置为负数来执行此操作,并将每种类型的数字减少一个新类型添加到命令集合中。然后在插入后,您只需要更新带有更新数字的伪造负数的关键字段。您仍然需要知道哪些字段是关键字段,但至少您不需要跟踪构成每个关系结束的精确字段,我们可以跟踪这些字段。

      我喜欢Entity Framework如何处理它,尝试使用属性上的属性注入关于字段的链接信息。您可能必须创建自己的,但它是一个干净的声明性概念,迫使您在数据模型类中预先描述这些关系,以便各种逻辑可以在以后利用,而不仅仅是生成SQL语句。

        

      我不想过于批评Dapper,但是一旦你开始沿着这条路走下去或手动管理这样的参照完整性,你就应该考虑更像企业就绪的ORM,比如Entity Framework或者nHibernate 。当然,他们带来了一些包袱,但这些ORM已经真正发展成为社区优化的成熟产品。我现在只有很少的手动编写或脚本代码来定制与RDBMS的任何交互,这意味着要测试或维护的代码要少得多。 (=更少的错误)

答案 1 :(得分:0)

它没有说明您使用的是哪个数据库。如果它是MSSQL你可以做

var id =  connection.Query<int?>("SELECT @@IDENTITY").SingleOrDefault();

执行插入后。这会给你最后一次插入的id。