2

I have to perform a bulk operation bool AddEntitiesX(List<X>):

  • For each insert of X into X_Table (with X_UID as auto-increment ID), I have to insert k-times another entity Y into Y_Table (with X_UID as FK and Y_UID as auto-increment ID), since X contains a list of k-Y entities.
  • Then, for each inserted X, I need to insert also a Z entity in Z_Table (with X_UID as FK and Z_UID as auto-increment ID).

My pseudo-code will look like this:

// cannot use 'TransactionScope' since there are problems with Oracle
// so, prepare SqlTransaction
foreach (X)
{
 //1. call 'InsertX' SP

 foreach (Y in x.ListY)
   //2. call 'InsertY' SP

 //3. call 'InsertZ' SP
}
// commit transaction

How can I retrieve the X_UID from InsertX SP to pass to the next stored procs?

If there is no way, then since I cannot have this big transaction in one stored procedure, how should I model it?

I would like to know best practices to handle this kind of operations from business to data layer using transactions.

Thank you... and please let me know if my question is not clear enough.

Pankaj
  • 9,749
  • 32
  • 139
  • 283
Learner
  • 3,297
  • 4
  • 37
  • 62
  • What I have understood so far is that you have in the list of x with values(1,2,3,4) and in the y list you have (5,6,7,8). Now you will insert (1,2,3,4) in tableX and the pairs (1,5), (2,5), (3,5), (4,5), (1,6), (2,6) like this. Right? – Pankaj Jun 09 '11 at 10:22
  • yes, C# is ok. So, for example... X1 comes with Y11,Y12,Y13 ; X2 comes with Y21, Y22; X3 comes with none. X:Y=1:n relation. You could also consider, eg X:Z=1:1 – Learner Jun 09 '11 at 10:30

3 Answers3

1

You can use Bulk Copy. Using this, first bulk insert all the records of x in a new table and bulk insert all records of Y in a new table.

Now you can use Cross joins in between these two new tables..like below

Select X.TableXVal, Y.TableYVal from NewTableX X
Cross Join NewTableY Y

This query can be written in the Stored Procedure and stored procedure can be called like below

using (System.Data.SqlClient.SqlConnection con = new SqlConnection("YourConnection string")) { 
    con.Open(); 
    SqlCommand cmd = new SqlCommand(); 
    string expression = "Parameter value"; 
    cmd.CommandType = CommandType.StoredProcedure; 
    cmd.CommandText = "Your Stored Procedure"; 
    cmd.Parameters.Add("Your Parameter Name", 
                SqlDbType.VarChar).Value = expression;    
    cmd.Connection = con; 
    using (IDataReader dr = cmd.ExecuteReader()) 
    { 
        if (dr.Read()) 
        { 
        } 
    } 
}

You can create the sample tables like below

create table NewTableX
(
    ID int Primary Identity(1,1),
    TableXVal int
)

create table NewTableY
(
    ID int Primary Identity(1,1),
    TableYVal int
)

In this way, you can skip inserting the records one by one. hope this will help you.

For more information about using BulkCopy. Below is the code.

private void CreateDataTableFromList()
        {
            //supose you have list for X like below
            List<int> x = new List<int>();
            x.Add(1);
            x.Add(2);
            x.Add(3);
            x.Add(4);

            //supose you have list for Y like below
            List<int> y = new List<int>();
            y.Add(1);
            y.Add(2);
            y.Add(3);
            y.Add(4);


            DataTable dt = new DataTable();
            DataColumn dc;
            DataRow dr;

            dc = new DataColumn();
            dc.DataType = System.Type.GetType("System.Int32");
            dc.ColumnName = "TableXVal";
            dt.Columns.Add(dc);

            dr = dt.NewRow();
            dr["TableXVal"] = 1;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableXVal"] = 2;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableXVal"] = 3;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableXVal"] = 4;
            dt.Rows.Add(dr);


            SqlBulkCopy copy = new SqlBulkCopy("Your Connection String");
            copy.DestinationTableName = "NewTableX";
            copy.WriteToServer(dt);


            dt = new DataTable();

            dc = new DataColumn();
            dc.DataType = System.Type.GetType("System.Int32");
            dc.ColumnName = "TableYVal";
            dt.Columns.Add(dc);

            dr = dt.NewRow();
            dr["TableYVal"] = 1;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableYVal"] = 2;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableYVal"] = 3;
            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["TableYVal"] = 4;
            dt.Rows.Add(dr);

            copy = new SqlBulkCopy("Your Connection String");
            copy.DestinationTableName = "NewTableY";
            copy.WriteToServer(dt); 
        }

Step1 - Use CreateDataTableFromList function

Step 2 - Call stored procedure as mentioned above

Your stored procedure must have the select statement as mentioned above.

Pankaj
  • 9,749
  • 32
  • 139
  • 283
  • You mean temp tables ? ... Sorry, I cannot really follow you. My problem is more: how to use transactions for multiple insert operations where I would need to take out the auto-incremented ids from one insert and send to the next. – Learner Jun 09 '11 at 10:51
  • 1. It will be user defined tables. 2. You should not insert the records one by one in transaction. You should use transaction but this can be done in stored procedure as described above – Pankaj Jun 09 '11 at 10:57
  • There is no sign of 1:n relation in this example... Am I missing something? How would I know that inserted Y belong to a certain X? – Learner Jun 09 '11 at 11:59
  • Please go through the code again in teh select statement we are inserting the records like (X1, Y1), (X1, Y2) (X1, Y3) (X1, Y4) (X2, Y1 ....) – Pankaj Jun 09 '11 at 12:01
1

Have your InsertX sp structured like this:

ALTER PROCEDURE dbo.InsertX
(
    -- other parameters
    @ID int = null OUTPUT,
    @ErrMsg nvarchar(512) = null OUTPUT
)
AS
SET NOCOUNT ON
DECLARE @ret as int
SET @ret = 1
BEGIN TRY
    INSERT INTO [dbo].[XTable] 
        ([Column1]) 
    VALUES 
        (null)
    SET @ID = SCOPE_IDENTITY()
    SET @ErrMsg = 'OK'
END TRY
BEGIN CATCH
    SET @ErrMsg = ERROR_MESSAGE()
    SET @ret = -1
END CATCH
RETURN @ret

After the call you get the ID and feed it into InsertY

cdel
  • 717
  • 7
  • 14
  • They have, but since I am still in the transaction scope and which not commited yet, will I get anything back? I thought NO. Am I wrong? – Learner Jun 09 '11 at 12:12
  • 1
    You'll get the right ID (let say ID=14). If the transaction is rolled back the next insert will get next ID (15). It works even (new SqlCommand("SELECT @@IDENTITY", cnn, tran)).ExecuteScalar() from your code. – cdel Jun 09 '11 at 12:16
  • So, inside my SqlTransaction I can have this call to "SELECT @@IDENTITY" in between 2 INSERT calls and would return the right id? I guess this '@@IDENTITY' is also thread-safe right? I would not get another ID from somewhere else? – Learner Jun 09 '11 at 12:22
  • Thanks for your suggestion, I will give it a try and let you know, but I think I saw somewhere that you cannot get this ID since the transaction is not commited yet... but anyway, will try now. Thank you. – Learner Jun 09 '11 at 12:24
  • So my assumption was wrong, that's why I was so confused. You could still get the right auto-incremented ID, even inside transaction. I tested and it worked. I guess I could solve my problems now. Thanks a lot! – Learner Jun 09 '11 at 13:16
  • I am still not getting that why should you insert the rows one by one? You can insert all records in one query as well. Otherwise you can face performance issues if records increases in future for insertion!!! – Pankaj Jun 09 '11 at 14:43
1

One way is to use SCOPE_IDENTITY(), like cdel already suggested. Another way is to use the OUTPUT clause of INSERT

INSERT INTO table (field, field, ...) 
OUTPUT INSERTED.ID
VALUES (@value, @value, ...);

This inserts the record and also produces a result set, which contains the inserted row generated identity value. In C# you read this result set just as if you'd have executed a SELECT, ie. you use ExecuteReader().

One advantage of using OUTPUT clause is that is the only way it can reliable return multiple row IDs. Say you insert not one, but 100, you can get back the IDs of all 100 inserted rows in a single result set. In case you wonder how to insert 100 rows in one swoop, see Arrays and Lists in SQL Server 2008: Using Table-Valued Parameters.

Another advantage of using OUTPUT clause is that you can chain two statements into a single one, see Chained Updates.

Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • Hi Remus... very nice to hear from you. Thanks a lot for your answer. Very useful points to keep in mind for later. I was thinking now... Is it a good technique to have SqlDataAdapter.Fill inside a SqlTransaction? Shall I create a new question with this? – Learner Jun 09 '11 at 15:18
  • Just wanted to say that I wanted to keep it as simple as possible in order to have compatibility to Oracle. The new question is here: http://stackoverflow.com/questions/6295596/c-sqldataadapter-fill-within-a-sqltransaction-is-this-a-good-practice – Learner Jun 09 '11 at 15:56