-2

I work on project that was created about 8 years ago it use ADO.NET technology.

The data that I want to save in the table is List of objects:

Here is custom class:

public class ReportTrafficDepartment
{
    public int id { get; set; }
    public System.Nullable<int> siteNum { get; set; }
    public System.Nullable<System.DateTime> dateReport { get; set; }
    public string siteName { get; set; }
    public System.Nullable<int> prog1 { get; set; }
    public System.Nullable<int> progLayout1 { get; set; }
    public System.Nullable<int> prog2 { get; set; }
    public System.Nullable<int> progLayout2 { get; set; }
    public System.Nullable<bool> isLOZ { get; set; }
    public System.Nullable<System.DateTime> start { get; set; }
    public System.Nullable<System.DateTime> end { get; set; }
    public System.Nullable<System.TimeSpan> time { get; set; }
    public System.Nullable<float> Prog1ToProg2Check { get; set; }
    public string comment { get; set; }
}

And here is the function that called when I want to save data:

    public void saveReport(IEnumerable<ReportTrafficDepartment> report)
    {
        try
        {

            SqlConnection conn = new SqlConnection("connetcion string");

            foreach (var record in report)
            {
                SqlCommand cmd = new SqlCommand("SaveEcxelReport", conn);
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add("@siteNum", SqlDbType.Int).Value = record.siteNum;
                cmd.Parameters.Add("@dateReport", SqlDbType.DateTime).Value = record.dateReport;
                cmd.Parameters.Add("@siteName", SqlDbType.NVarChar).Value = record.siteName;
                cmd.Parameters.Add("@prog1", SqlDbType.Int).Value = record.prog1;
                cmd.Parameters.Add("@progLayout1", SqlDbType.Int).Value = record.progLayout1;
                cmd.Parameters.Add("@prog2", SqlDbType.NVarChar).Value = record.prog2;
                cmd.Parameters.Add("@progLayout2", SqlDbType.Int).Value = record.progLayout2;
                cmd.Parameters.Add("@isLOZ", SqlDbType.Bit).Value = record.isLOZ;
                cmd.Parameters.Add("@start", SqlDbType.DateTime).Value = record.start;
                cmd.Parameters.Add("@end", SqlDbType.DateTime).Value = record.end;
                cmd.Parameters.Add("@Prog1ToProg2Check", SqlDbType.Real).Value = record.Prog1ToProg2Check;
                cmd.Parameters.Add("@comment", SqlDbType.NVarChar).Value = record.comment;

                conn.Open();
                cmd.ExecuteNonQuery();
                conn.Close();
            }
        }
        catch
        {
            throw;
        }
    }

And here is stored procedure that I use to insert record to the table:

      ALTER PROCEDURE [dbo].[SaveEcxelReport]
         @siteNum INT = NULL,
         @dateReport DATETIME = NULL,
         @siteName NVARCHAR(255) = NULL,
         @prog1 INT = NULL,
         @progLayout1 INT = NULL,
         @prog2 INT = NULL,
         @progLayout2 INT = NULL,
         @isLOZ BIT = NULL,
         @start DATETIME = NULL,
         @end DATETIME = NULL,
         @time DATETIME = NULL,
         @Prog1ToProg2Check REAL = NULL,
         @comment NVARCHAR(255) = NULL

        AS   
        BEGIN
          SET NOCOUNT ON;
          insert into dbo.ReportTrafficDepartment(siteNum, dateReport, siteName, prog1, progLayout1, prog2, progLayout2, isLOZ, [start], [end], [time], Prog1ToProg2Check, comment) 
                 values (@siteNum, @dateReport, @siteName, @prog1,@progLayout1, @prog2, @progLayout2, @isLOZ, @start, @end, @time, @Prog1ToProg2Check, @comment) 
        END

As you can see in SaveReport function in foreach method I open connection I pass item to the stored procedure then I close connection.

But, I think my attitude is wrong (i.e. it's not good idea to open and close connection inside foreach loop).

Any idea should I change the attitude to store collection to the database if yes how should I do it?

halfer
  • 19,824
  • 17
  • 99
  • 186
Michael
  • 13,950
  • 57
  • 145
  • 288

1 Answers1

2

You should add a using statement around the creation of the SqlConnection and open it before entering the loop. The using statement will close automatically the connection when you exit from that using block. Another little step is to create the parameters outside the loop just one time and set the values inside the loop. No need to create all that parameter set at each loop, you can reuse them. But the most important fix is to add a transaction around your code to prevent partial inserts of your enumerable in case of exceptions.

public void saveReport(IEnumerable<ReportTrafficDepartment> report)
{
    try
    {

        using(SqlConnection conn = new SqlConnection("connetcion string"))
        {
            conn.Open();
            using(SqlTransaction tr = conn.BeginTransaction())
            using(SqlCommand cmd = new SqlCommand("SaveEcxelReport", conn, tr))
            {
                 cmd.CommandType = CommandType.StoredProcedure; 
                 cmd.Parameters.Add("@siteNum", SqlDbType.Int);
                 cmd.Parameters.Add("@dateReport", SqlDbType.DateTime);
                 .... other parameters ....
                foreach (var record in report)
                {
                    cmd.Parameters["@siteNum"].Value = record.siteNum;
                    cmd.Parameters["@dateReport"].Value = record.dateReport;

                    ... set the values for other parameters

                    cmd.ExecuteNonQuery();
               }
               tr.Commit();
            }
        }
    }
    catch
    {
        // If something goes wrong the rollback is executed automatically 
        // when you exit from the using block above
        //if(tr != null) 
        //{
        //   tr.RollBack();
        //}

        // ------
        // Here you should log the error before throwing. 
        // It is a good habit and often a lifesaver to log the errors 
        // ------

        // But if you don't log or you don't do anything else 
        // but just throw then the whole try catch is useless 
        // and you can completely remove it 
        throw;
    }
}

After these simple fixes you should look at using an ORM that can just take the enumerable and store it to the database without further coding on your part

Evk
  • 98,527
  • 8
  • 141
  • 191
Steve
  • 213,761
  • 22
  • 232
  • 286
  • Since SqlTransaction also implements IDisposable - worth wrapping it in using block too. – Evk Dec 26 '17 at 19:11
  • Yes I know, but then how to rollback in the catch block? We could add a direct call to tr.Dispose(); when needed (Or a finally with a tr.Dispose there) – Steve Dec 26 '17 at 19:17
  • You don't actually need to explicitly rollback in catch block (while you can of course). When dispose is called - transaction will be rolled back if not commited already. So you can just do `using (var tr = conn.BeginTransaction()) {... tr.Commit();}` – Evk Dec 26 '17 at 19:29
  • Sorry to be dense, but are you sure about that because I have not found evidences of this. Do you have some actual link that explains this behavior? – Steve Dec 26 '17 at 19:31
  • OK, I have found something here https://stackoverflow.com/questions/641660/will-a-using-statement-rollback-a-database-transaction-if-an-error-occurs So it seems that it rollbacks automatically for SqlConnection but it is something provider specific – Steve Dec 26 '17 at 19:38
  • 1
    I was doing this for years, and just verified this again (with experiment), but I'm not sure I have any links confirming this. Documentation of DbTransaction.Dispose says that "Dispose should rollback the transaction. However, the behavior of Dispose is provider specific". Not sure though what else disposing of transaction can do except rolling back if not commited, seems there are no other sensible options. `TransactionScope` also does the same (though "officially" this time). – Evk Dec 26 '17 at 19:42
  • Not sure why someone downvoted your answer - but that's of course not me, since such minor detail (even arguable detail) cannot be the reason for downvote. – Evk Dec 26 '17 at 19:54
  • Not a thing to look at for just more than a second. Sometime you receive downvotes just because.... – Steve Dec 26 '17 at 19:55
  • Well, If you're using a transaction anyway you would probably get better performance sending a data table as a table valued parameter and executing just a single statement instead of executing multiple statements one by one. – Zohar Peled Jan 10 '18 at 16:24