0

I have the following, I could make it work as I want to but I think i'm doing it the wrong way, could you please explain how this could be done in a more efficient way ? While also looping on Categories and doing the same as with Districts within the same Insert() Method.

Thanks in advance.

    #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;
        }
    }

EDIT

Is is wrong to achieve the above by doing the following ?

            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();
            } 
user1027620
  • 2,745
  • 5
  • 37
  • 65
  • Please note that I am aware that some stuff are missing from the method above like StoreID and the fact that the loop keeps appending the INSERT statement. – user1027620 Jul 07 '12 at 16:01
  • 3
    You could e.g. create a single XML that contains all your `DistrictID` as a list, and then call your `INSERT` only once (and extract the individual `ID` values from the XML structure using a XQuery statement) – marc_s Jul 07 '12 at 16:04

5 Answers5

4

The first obvious thing is to move the invariant part of the sqlCommand out of the loop

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

But this doesn't answer your fundamental problem. How to avoid hitting the database multiple times.
You could build a query with multiple inserts like this

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

But string concatenation is really a bad practice and I present this solution just as an example and I don't want to suggest this kind of approach.

The last possibility are Table-Valued Parameters (Only from SqlServer 2008).

First you need to create a Sql Type for the table you will pass in

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

and a StoredProcedure that will insert the data from the datatable passed in

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

then, back to your code you pass the district into the storedprocedure

(Probably you need to convert your List in a 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();

Well, if you look back at the link above, you will find other ways to pass your data in a single step to the database backend.... (Scroll to the end and you will find also a method that doesn't require a stored procedure on the database)

Steve
  • 213,761
  • 22
  • 232
  • 286
  • what is this line here? `sqlCommand.Parameters.AddWithValue("@DistrictID", 0); // as dummy value` since you already added a .Parameters[""].Value within the loop – user1027620 Jul 07 '12 at 16:13
  • @user1027620 You cannot set the value of a parameter which does not exist, you need to create the parameter first. There's a `.Add` though that doesn't need a dummy value, which might be nicer. –  Jul 07 '12 at 16:16
  • 1
    @Steve could you please be more specific as to why is the concatenation a bad practice? In this specific case, I see no real downside - the concatenation is not based on user input, so there is no possibility of SQL injection. – Nikola Anusev Jul 07 '12 at 16:56
  • @NikolaAnusev, In this case (insert only integers?) I suppose that one could be less restrictive but only if the OP confirms your assert 'not based on user input'. – Steve Jul 07 '12 at 17:01
  • @Steve I am also passing in a list of Categories in my method parameters, could you please show me how this is added to the solution you provided? Thanks again. – user1027620 Jul 07 '12 at 17:02
  • Here you have another problem. (I have overlooked it till now). You need to get back the StoreID used in the subsequent inserts. Your actual code doesn't give you this value. (And, no Table Values Parameters can't be used as output parameters) – Steve Jul 07 '12 at 17:07
  • @Steve could you please take a look at my edit above? Thanks. – user1027620 Jul 07 '12 at 17:17
  • The line inside the loop AddWithValue is wrong. At the second loop you will get an exception sayint that you have already defined the parameter inside your collection. Look at my first example, put the AddWithValue outside the loop, while inside the loop update the parameter value with the current item – Steve Jul 07 '12 at 17:22
2

Assuming Stores has an identity column, in SQL Server, create a table type and a table-valued parameter to take advantage of it:

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

Then in C#, you can pass your List in directly without any looping:

  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();
  }
Aaron Bertrand
  • 272,866
  • 37
  • 466
  • 490
  • Is there another way of doing that without a stored procedure? Thanks – user1027620 Jul 07 '12 at 16:33
  • @user1027620 Can you explain why? – Aaron Bertrand Jul 07 '12 at 16:33
  • I'm really not familiar with Stored Procedures or how they really work. It would require some time to understand what you shared above. I'm sure there might be benefits for using Stored Procedures instead but I don't don't what they are. – user1027620 Jul 07 '12 at 16:36
  • 1
    @user1027620 If you want an answer that doesn't use stored procedures, you need to make that part of your question. You shouldn't just reject answers because you're not familiar with stored procedures. Please read http://stackoverflow.com/questions/2934634/ad-hoc-queries-vs-stored-procedures-vs-dynamic-sql and http://stackoverflow.com/questions/2734007/when-is-it-better-to-write-ad-hoc-sql-vs-stored-procedures and http://stackoverflow.com/questions/462978/when-should-you-use-stored-procedures and http://www.codinghorror.com/blog/2005/05/stored-procedures-vs-ad-hoc-sql.html – Aaron Bertrand Jul 07 '12 at 16:41
  • See the main issue that I have zero stored procedures in my database at the moment and I think that working on the code you provided would make my work a bit inconsistent because of what I've already written, don't you agree? – user1027620 Jul 07 '12 at 16:45
  • No, I don't. You can't learn to swim if you don't jump in the water. It's not an excuse to never use stored procedures because you've never used them before. How will you ever use a stored procedure? Please read up on the pros and cons and use *real* arguments to either accept or reject a solution that uses a stored procedure. (The answer you up-voted because it "directly answers you question" also used a stored procedure. How was it different other than the fact that someone had down-voted it?) – Aaron Bertrand Jul 07 '12 at 16:47
  • I've also upvoted your answer since it also provides a solution to my problem. But it's probably my mistake for not making it clear in my question that I wasn't familiar with Stored Procedures even tho I provided my code that does not use Stored Procedures. Anyway, I am checking the links you and Steve provided and will work on implementing the answers. Thank you for the help. – user1027620 Jul 07 '12 at 16:51
1

Recently in my project i used XML as a data type in my stored proc and did insert update and delete in just one shot instead of hitting the database multiple times .

Sample Stored proc

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

Similarly you can write for update and delete .In C# u need to create the 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();
praveen
  • 12,083
  • 1
  • 41
  • 49
  • Well it wasn't me so I don't know :/ – user1027620 Jul 07 '12 at 16:37
  • 2
    I agree, I'm not sure, though I find TVPs a heck of a lot easier to use than XML. I also find it puzzling that someone would upvote an answer that says "Use a TVP, here's a link" but not upvote an answer that says "Use a TVP" and actually shows some code to demonstrate how it can be done in this particular scenario. StackOverflow user behavior is quite puzzling. – Aaron Bertrand Jul 07 '12 at 16:37
  • Anyway i just presented my view which might help others . – praveen Jul 07 '12 at 16:39
  • Thank you praveen. +1 since this is a possible solution that answers my question directly. – user1027620 Jul 07 '12 at 16:41
  • @user1027620 to be fair, I think everyone here has answered your question directly. Your question was "how can I do this more efficiently?" and/or "how do I avoid hitting the database multiple times?", right? Every single answer has addressed that. – Aaron Bertrand Jul 07 '12 at 16:41
  • @AaronBertrand: In XML we can frame different tags for insert ,update and delete and use the same proc to perform these DML operations .Is it possible to achieve this using TVP ? – praveen Jul 07 '12 at 16:56
  • Yes, why wouldn't it be possible to perform a variety of operations with the data that is passed into a TVP? – Aaron Bertrand Jul 07 '12 at 17:09
0

Personally, I would create a stored procedure for the insert and pass in a Table-Valued param, which would allow you to do

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

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

Unless you're using SQL 2005, then I would use an XML param in my stored proc and Serialize a collection to be inserted.

Chris Gessler
  • 22,727
  • 7
  • 57
  • 83
0

Think about your system design. Where is the data that you need to insert coming from? If it's already in the database, or another database, or some other kind of data store, you should be able to achieve a more bulk kind of transfer, simply inserting from one database to the other in a loop in stored procedure.

If the data is coming from a user, or some incompatible data store, like say an export from some third party program, then you basically have to realize that to get it into the database will involve quite of few round-trips to the database. You can use some tables, or XML or such , but those are actually closer to doing a bulk insert using other methods.

The bottom line is that SQL databases are designed to do inserts one at a time. This is 99% of the time OK because you are never asking users using the UI to type in thousands of things at one time.

Andyz Smith
  • 698
  • 5
  • 20
  • There is a difference between "doing inserts one a time" and using a completely separate command and all the scaffolding required. And I question your 99% figure. A lot of apps let you pick multiple items from a drop-down or check multiple checkboxes. I would bet that 99% of *those* don't submit every checkbox value as a completely separate command. – Aaron Bertrand Jul 07 '12 at 18:25