Tag Archives: ADO.NET

System.Transactions – dobrý sluha, ale špatný pán

.NET Framework 2.0 zavádí nový namespace System.Transactions, který umožňuje velmi programátorsky pohodlnou práci s transakcemi, a to jako transakcemi ADO.NET/SQL Serveru, tak i MSMQ (Message Queues) a MSDTC (Distributed Transaction Coordinator).

Můžeme tak například celkem transparentně obalit kus kódu transakcí, aniž bychom museli do kódu zasahovat a transakce explicitně nastavovat.

using (TransactionScope scope = new TransactionScope())
{
   using (SqlConnection connection = new SqlConnection(connectionString))
   {
      SqlCommand command = connection.CreateCommand();
      command.CommandText = "Insert....";
      command.Connection = connection;

      SqlCommand command2 = connection.CreateCommand();
      command2.CommandText = "Update....";
      command2.Connection = connection;

      connection.Open();
      command.ExecuteNonQuery();
      command2.ExecuteNonQuery();
      connection.Close();
   }
   scope.Complete();
}

…vše vypadá krásně a opravdu to může i krásně fungovat, můžeme si ale i pěkně naběhnout.

V první řadě, pokud výše uvedený kód běží vůči SQL2000 serveru, pak se namísto běžné SQL-transakce vytvoří distribuovaná transakce spravovaná MSDTC, Distributed Transaction Coordinatorem – což bude mít velmi nepříjemný dopad na výkon naší aplikace. Při použití s SQL2000 totiž nejsou podporovány tzv. „promotable transactions“.

Pokud používáme SQL2005 server, tento problém odpadá, transakce bude realizována prostřednictvím SqlTransaction.

Dalším problémem však je, že explicitně neurčujeme, co vše je součástí transakce, takže veškeré transakční zdroje (resources), které v rámci transaction-scope používáme, se automaticky zaregistrují jako součást transakce a snadno tak opět skončíme na distribuované transakci spravované MSDTC.

Závěr

Osobně raději pro transakční zpracování SQL používám klasickou SqlTransaction, navíc pokud si vytvoříme malou pomůcku, pak můžeme i SqlTransaction řešit obdobně pohodlným způsobem:

int myID = 5;
object result;

SqlDataAccess.ExecuteTransaction(
   delegate(SqlTransaction transaction)
   {
      // uvnitř lze používat i lokální proměnné (samozřejmě i parametry, statické fieldy atp.)

      SqlCommand cmd1 = new SqlCommand("command string");
      cmd1.Transaction = transaction;
      cmd1.Connection = transaction.Connection;
      cmd1.Parameters.AddWithValue("@MyID", myID);
      cmd1.ExecuteNonQuery();

      SqlCommand cmd2 = new SqlCommand("another command");
      cmd2.Transaction = transaction;
      cmd2.Connection = transaction.Connection;
      result = cmd2.ExecuteScalar();
   });
Související články

Konverze SqlDataReaderu na DataSet

Ač to není příliš šťastná situace, může se nám někdy přihodit, že potřebujeme konvertovat SqlDataReader na DataSet.
V .NET Frameworku 1.1 jsou v podstatě dvě základní cesty – buď to udělat ručně, prvek po prvku, nebo si uvědomit, že DataAdapter dělá v podstatě totéž, a už by to tedy mohlo být někde řešeno (v .NET Frameworku 2.0 již je tato funkčnost exponováno prostřednictvím metody DataTable.Load(), viz níže).
Ručně to vypadá např. takto (kód není můj, tak to berte s rezervou):

public static DataSet DataReaderToDataSet( SqlDataReader rd )
{
    DataSet ds = new DataSet();
    do
    {
       DataTable st = rd.GetSchemaTable();
       DataTable Dt = new DataTable();
 
       if (st != null)
       {
          for (int i = 0 ; i < st.Rows.Count ; i++)
          {
             DataRow dr = st.Rows[i];
             string columnName = (string)dr["ColumnName"];
             DataColumn column = new DataColumn(columnName, (Type)dr["DataType"]);
             dt.Columns.Add(column);
          }
 
          ds.Tables.Add(dt);
 
          while (rd.Read())
          {
             DataRow dr = dt.NewRow();
 
             for (int i = 0; i < rd.FieldCount; i++)
                dr[i] = rd.GetValue(i);
 
             dt.Rows.Add(dr);
          }
       }
       else
       {
          DataColumn column = new DataColumn("RowsAffected");
          dt.Columns.Add(column);
          ds.Tables.Add(dt);
          DataRow dr = dt.NewRow();
          dr[0] = rd.RecordsAffected;
          dt.Rows.Add(dr);
       }
    }
    while (rd.NextResult());
 
    return ds;
}

Mnohem lepší fígl přes DataAdapter spočívá ve zjištění, že třída DbDataAdapter, z které jsou odvozeny všechny specifické DataAdaptery obsahuje metodu protected Fill(DataTable, IDataReader).

Můžeme tedy vytvořit vlastní DataAdapter odvozený od DbDataAdapteru, který tuto metodu použije ke zbudování příslušné tabulky:

public class DataReaderAdapter : DbDataAdapter 
{ 
   public int FillFromReader(DataTable dataTable, IDataReader dataReader) 
   { 
      return this.Fill(dataTable, dataReader); 
   } 
   protected override RowUpdatedEventArgs CreateRowUpdatedEvent( 
      DataRow dataRow, 
      IDbCommand command, 
      StatementType statementType, 
      DataTableMapping tableMapping)
   {
      return null;
   } 
   protected override RowUpdatingEventArgs CreateRowUpdatingEvent( 
      DataRow dataRow, 
      IDbCommand command, 
      StatementType statementType, 
      DataTableMapping tableMapping)
   {
      return null;
   } 
   protected override void OnRowUpdated( 
      RowUpdatedEventArgs value)
   {
   } 
   protected override void OnRowUpdating( 
      RowUpdatingEventArgs value)
   {
   } 
}

…a dát DataTable do DataSetu už není problém, případně escalovat SqlDataReader na další rowset a vytáhnout další tabulku. Ostatně DbDataAdapter obsahuje i mnoho další overloadů metody Fill() a můžeme si tak udělat mnohem chytřejší DataReaderAdapter.

Update pro .NET Framework 2.0

.NET Framework 2.0 již tuto problematiku řeší metodou DataTable.Load();

RAISERROR a jeho efekt v ADO.NET

RAISERROR

Z SQL stored procedur můžeme oznamovat vnější aplikaci chyby prostřednictvím příkazu RAISERROR, nejběžněji:

RAISERROR(message_str, severity_int, state_int, arg1, arg2)

přičemž

  • message_str představuje textový popis chyby v like-PRINTF podobě s procentama (argn pak představují hodnoty k dosazení),
  • severity_int udává závažnost chyby (viz níže),
  • state_int je číslo od 1 do 127 a předává se tím stavová hodnota (obvykle prostě 1)

např. tedy

RAISERROR ('The level for job_id:%d should be between %d and %d.',
      16, 1, @@JOB_ID, @@MIN_LVL, @@MAX_LVL)
ADO.NET

A teď k efektu RAISERROR v ADO.NET – vše závisý na parametry severity. Pro uživatelské chyby jsou určeny severity od 1 do 18, severity od 19 do 25 jsou určeny pro sysadmina.

Výsledký efekt v ADO.NET je tedy následující:

  • severity <= 10 nevyvolá výjimku v .NET, nýbrž event InfoMessage související SqlConnection (k odchycení musíme navěsit event-handler),
  • severity > 10 vyvolá výjimku SqlException, ale nezavře spojení,
  • severity > 16 vyvolá výjimky SqlException a uzavře spojení.

POZOR!!! Samotné RAISERROR nezajistí ROLLBACK transakce (musí se provést explicitně), ani nepřeruší běh T-SQL dávky (nutno třeba RETURN).