在 sql 查询期间获取 Id 并用于另一个插入语句

Posted

技术标签:

【中文标题】在 sql 查询期间获取 Id 并用于另一个插入语句【英文标题】:Getting Id during sql query and using for another Insert Statment 【发布时间】:2017-03-08 07:30:21 【问题描述】:

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

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

假设我有一个“订单”表,我有一个“交易”表和一个“订单行”表。 我想在插入时获取表“order”的增量 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

【问题讨论】:

我不认为该代码可以编译。我在 MSDN 中找不到 SqlConnection 的任何 Execute 方法。 我正在使用一个名为 dapper.net 的库,*** 实际上正在使用它。它只是使将对象映射到数据库变得容易。它被认为是一种微观形式。就像一个迷你版的实体框架。这在我的问题范围内应该无关紧要。 我使用 storprocs,您正在寻找的是输出参数,我对 dapper 了解不多,但快速的谷歌让我明白了这一点:***.com/questions/28652987/… 答案有一个关于如何使用的例子dapper 中的输出参数 【参考方案1】:

可以做到吗...是的...我们是否应该自己尝试这样做...我不会:)但是让我们尝试任何方式。

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

定义辅助接口并强制数据类实现它们或使用属性声明来指定 id 字段和外键引用 研究注入或代码生成技术,以便您可以在编译时而不是运行时执行一些“动态”编码和查找。

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

这就是后面sn-ps要走下去的旅程

    准备生成的 SQL 以捕获 Id 字段 保存更改时输出 Id 值 遍历数组中所有剩余的对象并设置外键值

注意:这些代码更改未经测试或生产异常处理,我也不会称其为“最佳实践”,它只是为了证明这一概念并帮助其他编码人员:)

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

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

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

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

    在Save Changes中,实现输出参数来捕获创建的Id,如果Id被捕获,则将其保存回命令关联的对象中。

    注意:此代码 sn-p 显示了对第 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;
            
        
    
    

    所以现在您的对象在插入时会更新其 ID。 现在是有趣的部分...更新 Id 后,您现在应该遍历其他对象并更新它们的外键字段。

您在现实中如何处理这件事在很大程度上取决于您准备对正在更新的数据实施什么样的假设/惯例。为简单起见,假设我们需要更新的所有外键都使用约定 ParentClassName_Id 命名。

这意味着如果在我们的示例中我们只是插入了一个新的“Widget”,那么我们可以尝试强制更新此事务范围内具有字段“Widget_Id”(或“Widget_AutoId”)的所有其他对象

    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 数据持久性库中更新外键(或引用)。 您可能在现实中观察到关系键字段很少使用任何约定一致地命名,但即使遵循约定,我的简单约定也不支持对同一类型的父级有多个引用的表,例如一个清单我客户的应用中有 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 的任何交互,这意味着要测试或维护的代码要少得多。 (= 更少的错误)

【讨论】:

谢谢。顺便说一句,如果您不知道,*** 会使用和维护 Dapper。如果你还没有使用 Dapper,它会非常酷,它可以节省很多工作。我创建这个库的原因是因为实体框架和 n-hibernate 都加载了包袱,我想创建一个具有简单功能的库,例如实体框架,而不是所有包袱。它只会通过简单的操作来节省时间,对于更复杂的问题,您可以使用 dapper 或 ado.net 是的,很抱歉对 dapper 有点消极,已经很晚了。 (我将编辑帖子并将其调低)我的意思是,一旦你开始按照你描述的方式做这些事情,那么现在你要么最终得到这些其他 ORM 的包袱,要么更糟,你最终会降低性能。大部分包袱都在于维护参照完整性和支持复杂的批处理场景。在这样做了 15 年后,当我看到你编写的代码量现在必须维护时,你的行李比他们的要贵。【参考方案2】:

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

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

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

【讨论】:

是的,代码确实是正确的。如果您阅读了我的问题,就会意识到这不是我所追求的。因为我不知道表格甚至列,所以我需要收集这个。我的问题是,如果您在哪里使用我的库,您将如何实现我上面发布的场景?

以上是关于在 sql 查询期间获取 Id 并用于另一个插入语句的主要内容,如果未能解决你的问题,请参考以下文章

如果需要,插入更新 pdo mysql 查询并获取最后插入的 id

用于获取 ID1 的最新条目的 SQL 查询

SQL将数据插入临时表并将结果另存为视图

MySQL:如果存在则获取 ID,否则插入并返回 ID

VB.Net/Access SQL 插入子查询问题

插入行并获取其id并将该id插入另一个表的存储过程