【问题标题】:A better way to achieve INSERT without hitting the database multiple times在不多次访问数据库的情况下实现 INSERT 的更好方法
【发布时间】:2012-07-07 15:58:38
【问题描述】:

我有以下内容,我可以让它按我的意愿工作,但我认为我做错了,你能解释一下如何以更有效的方式完成吗?同时也在 Categories 上循环,并在同一 Insert() 方法中执行与 Districts 相同的操作。

提前致谢。

    #region Methods
    public int Insert(List<District> Districts, List<Category> Categories)
    {
        StringBuilder sqlString = new StringBuilder("INSERT INTO Stores (name, image) VALUES (@Name, @Image);");

        using (SqlConnection sqlConnection = new
           SqlConnection(ConfigurationManager.ConnectionStrings["OahuDB"].ConnectionString))
        {
            SqlCommand sqlCommand = new SqlCommand(sqlString.ToString(), sqlConnection);
            sqlCommand.Parameters.AddWithValue("@Name", this.Name);
            sqlCommand.Parameters.AddWithValue("@Image", this.Image);

            sqlConnection.Open();
            int x = (int)sqlCommand.ExecuteScalar();

            sqlString.Clear();
            sqlCommand.Parameters.Clear();

            foreach (District item in Districts)
            {
                sqlString.AppendLine("INSERT INTO districts_has_stores (district_id, store_id) VALUES (@DistrictID, @StoreID);");
                sqlCommand.CommandText = sqlString.ToString();
                sqlCommand.Parameters.AddWithValue("@DistrictID", item.ID);
                sqlCommand.ExecuteNonQuery();
            }

            return x;
        }
    }

编辑

通过以下方式实现上述目标是错误的吗?

            sqlString.Clear();
            sqlCommand.Parameters.Clear();
            sqlString.AppendLine("INSERT INTO districts_has_stores (district_id, store_id) VALUES (@DistrictID, @StoreID);");
            sqlCommand.CommandText = sqlString.ToString();
            sqlCommand.Parameters.AddWithValue("@StoreID", x);
            foreach (District item in Districts)
            {
                sqlCommand.Parameters.AddWithValue("@DistrictID", item.ID);
                sqlCommand.ExecuteNonQuery();
            } 
            sqlString.Clear();
            sqlCommand.Parameters.Clear();
            sqlString.AppendLine("INSERT INTO categories_has_stores (category_id, store_id) VALUES (@CategoryID, @StoreID);");
            sqlCommand.CommandText = sqlString.ToString();
            sqlCommand.Parameters.AddWithValue("@StoreID", x);
            foreach (Category item in Categories)
            {
                sqlCommand.Parameters.AddWithValue("@CategoryID", item.ID);
                sqlCommand.ExecuteNonQuery();
            } 

【问题讨论】:

  • 请注意,我知道上面的方法中缺少一些东西,例如 StoreID 以及循环不断附加 INSERT 语句的事实。
  • 你可以例如创建一个包含所有DistrictID 列表的XML,然后只调用一次INSERT(并使用XQuery 语句从XML 结构中提取单个ID 值)

标签: c# asp.net sql sql-server iterator


【解决方案1】:

第一个明显的事情是将 sqlCommand 的不变部分移出循环

sqlCommand.Parameters.Clear(); 
sqlString.Clear();
sqlString.AppendLine("INSERT INTO districts_has_stores (district_id, store_id) VALUES (@DistrictID, @StoreID);"); 
sqlCommand.CommandText = sqlString.ToString(); 
sqlCommand.Parameters.AddWithValue("@DistrictID", 0);  // as dummy value
sqlCommand.Parameters.AddWithValue("@StoreID", x);  // invariant
foreach (District item in Districts) 
{ 
    sqlCommand.Parameters["@DistrictID"].Value = item.ID; 
    sqlCommand.ExecuteNonQuery(); 
} 

但这并不能解决您的根本问题。如何避免多次访问数据库。
您可以像这样构建一个包含多个插入的查询

sqlString.Clear();
sqlString.Append("INSERT INTO districts_has_stores (district_id, store_id) VALUES (");
foreach(District item in Districts)
{
    sqlString.Append(item.ID.ToString);
    sqlString.Append(", ")
    sqlString.Append(x.ToString()); 
    sqlString.Append("),"); 
}
sqlString.Length--;
sqlCommand.CommandText = sqlString.ToString()

但是字符串连接确实是一种不好的做法,我提出这个解决方案只是作为一个例子,我不想建议这种方法。

最后一种可能性是Table-Valued Parameters(仅来自 SqlServer 2008)。

首先你需要为你要传入的表创建一个Sql类型

CREATE TYPE dbo.DistrictsType AS TABLE
    ( DistrictID int, StoreID int )

还有一个StoredProcedure,它将插入传入的数据表中的数据

CREATE PROCEDURE usp_InsertDistricts 
(@tvpNewDistricts dbo.DistrictsType READONLY)
AS
BEGIN
    INSERT INTO dbo.Districts (DistrictID, StoreID)
    SELECT dt.DistrictID, dt.StoreID FROM @tvpNewDistricts AS dt;
END

然后,回到您的代码,您将区传递到存储过程中

(可能您需要将列表转换为 DataTable)

DataTable dtDistricts = ConvertListToDataTable(Districts);
SqlCommand insertCommand = new SqlCommand("usp_InsertDistricts", sqlConnection);
SqlParameter p1 = insertCommand.Parameters.AddWithValue("@tvpNewDistricts", dtDistricts);
p1.SqlDbType = SqlDbType.Structured;
p1.TypeName = "dbo.DistrictsType";
insertCommand.ExecuteNonQuery();

好吧,如果您回头看上面的链接,您会发现其他方法可以将您的数据一步传递到数据库后端......(滚动到最后,您还会发现一种方法不需要数据库上的存储过程)

【讨论】:

  • 这里的这一行是什么? sqlCommand.Parameters.AddWithValue("@DistrictID", 0); // as dummy value 因为您已经在循环中添加了 .Parameters[""].Value
  • @user1027620 不存在的参数不能设置值,需要先创建参数。有一个 .Add 虽然它不需要一个虚拟值,这可能会更好。
  • @Steve 您能否更具体地说明为什么串联是一种不好的做法?在这种特定情况下,我没有看到真正的缺点 - 连接不是基于用户输入,因此不存在 SQL 注入的可能性。
  • @NikolaAnusev,在这种情况下(仅插入整数?)我想限制性可能会小一些,但前提是 OP 确认您的断言“不基于用户输入”。
  • @Steve 我还在我的方法参数中传递了一个类别列表,您能否告诉我如何将其添加到您提供的解决方案中?再次感谢。
【解决方案2】:

假设 Stores 有一个标识列,在 SQL Server 中,创建一个表类型和一个表值参数以利用它:

CREATE TYPE dbo.DistrictsTVP AS TABLE
(
  DistrictID INT -- PRIMARY KEY? I hope so.
);
GO

CREATE PROCEDURE dbo.InsertStoreAndDistricts
  @Name NVARCHAR(255),
  @Image <some data type???>,
  @Districts dbo.DistrictsTVP READONLY
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @StoreID INT;

  INSERT dbo.Stores(name, [image]) SELECT @Name, @Image;

  SET @StoreID = SCOPE_IDENTITY();

  INSERT dbo.district_has_stores(district_id, store_id)
    SELECT DistrictID, @StoreID
      FROM @Districts;
END
GO

然后在 C# 中,您可以直接将您的 List 传入,而无需任何循环:

  using (...)
  {
    SqlCommand cmd       = new SqlCommand("dbo.InsertStoreAndDistricts", sqlConnection);
    cmd.CommandType      = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@Districts", Districts);
    tvparam.SqlDbType    = SqlDbType.Structured;

    // other params here - name and image

    cmd.ExecuteNonQuery();
  }

【讨论】:

  • 在没有存储过程的情况下还有其他方法吗?谢谢
  • @user1027620 你能解释一下原因吗?
  • 我真的不熟悉存储过程或它们的实际工作方式。需要一些时间来理解您在上面分享的内容。我确信使用存储过程可能会有好处,但我不知道它们是什么。
  • @user1027620 如果您想要一个不使用存储过程的答案,您需要将这部分作为您的问题。您不应该仅仅因为您不熟悉存储过程而拒绝答案。请阅读stackoverflow.com/questions/2934634/…stackoverflow.com/questions/2734007/…stackoverflow.com/questions/462978/…codinghorror.com/blog/2005/05/…
  • 查看我的数据库中存储过程为零的主要问题,我认为处理您提供的代码会使我的工作有点不一致,因为我已经写过,你不同意吗?
【解决方案3】:

最近在我的项目中,我在存储过程中使用 XML 作为数据类型,并且一次插入更新和删除,而不是多次访问数据库。

示例存储过程

ALTER PROCEDURE [dbo].[insertStore]
@XMLDATA xml,
@name varchar(50),
@image datatype
 AS
 Begin
  INSERT INTO Store
  (name
   ,image
  )
Select XMLDATA.item.value('@name[1]', 'varchar(10)') AS Name,   
XMLDATA.item.value('@image[1]', 'yourData type') AS Image
FROM @XMLDATA.nodes('//Stores/InsertList/Store') AS XMLDATA(item)
END

同样你可以写更新和删除。在 C# 中你需要创建 xml

public  string GenerateXML(List<District> Districts)
 var xml = new StringBuilder();
 var insertxml = new StringBuilder();
 xml.Append("<Stores>");
 for (var i = 0; i < Districts.Count; i++)
        { var obj = Districts[i];
          insertxml.Append("<Store");
          insertxml.Append(" Name=\"" + obj.Name  + "\" ");
          insertxml.Append(" Image=\"" + obj.Image + "\" ");
          insertxml.Append(" />");
        }
xml.Append("<InsertList>");
xml.Append(insertxml.ToString());
xml.Append("</InsertList>");

SqlCommand cmd= new SqlCommand("insertStore",connectionString);
cmd.CommandType=CommandType.StoredProcedure;
SqlParameter param = new SqlParameter ();
param.ParameterName ="@XMLData";
param.value=xml;
paramter.Add(param);
cmd.ExecuteNonQuery();

【讨论】:

  • 好吧,不是我,所以我不知道:/
  • 我同意,我不确定,尽管我发现 TVP 比 XML 更容易使用。我还觉得令人费解的是,有人会支持“使用 TVP,这是一个链接”的答案,但不会支持“使用 TVP”的答案,并且实际上显示了一些代码来演示如何在这种特定情况下完成它. StackOverflow 的用户行为非常令人费解。
  • 不管怎样,我只是提出了我的观点,可能对其他人有所帮助。
  • 谢谢你。 +1,因为这是直接回答我的问题的可能解决方案。
  • @user1027620 公平地说,我想这里的每个人都直接回答了你的问题。您的问题是“我怎样才能更有效地做到这一点?”和/或“我如何避免多次访问数据库?”,对吧?每个答案都解决了这个问题。
【解决方案4】:

就个人而言,我会为插入创建一个存储过程并传入一个表值参数,这将允许您这样做

INSERT tbl (f1, f2, ... fN)
SELECT * FROM @TVP

http://msdn.microsoft.com/en-us/library/bb510489.aspx

除非您使用的是 SQL 2005,否则我将在我的存储过程中使用 XML 参数并序列化要插入的集合。

【讨论】:

    【解决方案5】:

    考虑一下您的系统设计。您需要插入的数据来自哪里?如果它已经在数据库、另一个数据库或某种其他类型的数据存储中,您应该能够实现更批量的传输,只需在存储过程的循环中从一个数据库插入到另一个数据库。

    如果数据来自用户或某些不兼容的数据存储,例如从某个第三方程序导出,那么您基本上必须意识到将其导入数据库将涉及相当多的往返数据库。您可以使用一些表或 XML 等,但这些实际上更接近于使用其他方法进行批量插入。

    底线是 SQL 数据库被设计为一次插入一个。这在 99% 的情况下都可以,因为您永远不会要求使用 UI 的用户一次输入数千个内容。

    【讨论】:

    • “一次插入一个”与使用完全独立的命令以及所需的所有脚手架之间存在差异。我质疑你 99% 的数字。许多应用程序允许您从下拉列表中选择多个项目或选中多个复选框。我敢打赌,99% 的 那些 不会将每个复选框值作为一个完全独立的命令提交。
    猜你喜欢
    • 1970-01-01
    • 2016-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-19
    • 2023-01-12
    相关资源
    最近更新 更多