ADO.NET w praktyce


Opcje wywołania SqlCommand różnią się tylko tym, co jest pobierane przez klienta. Nic nie stoi na przeszkodzie, by wykonać polecenie select name, gid from Customer, używając ExecuteScalar - tyle że w tym wypadku klient otrzyma tylko wartość pola name pierwszego rekordu generowanego przez dany select.

Aby zobaczyć w jaki sposób używana jest struktura DataReader, warto przeanalizować konkretne zastosowanie.

W poniższym przykładzie wykonujemy proste przejście po kolejnych rekordach tabeli Customer. Najpierw definiowane jest połączenie. Potem polecenie, które będzie wybierać elementy z tabeli. Następnie polecenie jest uruchamiane i zwraca SqlDataReader - czyli jednokierunkowy kursor tylko do odczytu. W konsekwencji pętla wykonywana jest tyle razy, ile jest rekordów. A dzięki opcji CommandBehavior.CloseConnection, po zakończeniu iteracji, automatycznie zamykane jest połączenie z bazą. Ponadto blok using powoduje, że zasoby niezarządzane są zwalniane:

using (SqlConnection cnn = new SqlConnection(@"Data Source=NIEZAPOMINAJKA2\SQL2005; Initial Catalog=PCWKVS2005; Integrated Security=True")) {

SqlCommand cmd;

cmd = cnn.CreateCommand();

cmd.CommandText = "select * from Customer";

cnn.Open();

SqlDataReader dr;

dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

while (dr.Read()) {

System.Diagnostics.Debug.WriteLine(dr["name"]);

}

}

Najwięcej problemów może się pojawić przy poleceniach typu dr["name"]. DataReader nic nie wie o schemacie bazy. Aby odczytać wartość pola, trzeba po prostu przekazać nazwę - ale dopiero w czasie działania aplikacji okaże się czy programista nie popełnił np. literówki.

DataReader ma jeszcze jedno zastosowanie - pomaga w przypadku pracy z tzw. obiektami BLOB, czyli dużymi obiektami w bazie danych (np. obraz zapisany w polu). Można wtedy odczytywać taką informację "porcjami" (a nie pobierać w całości, co czasami może spowodować, że aplikacja zużyłaby za dużo pamięci).

W CommandText można też przekazać ciąg poleceń (oddzielonych średnikiem). Wtedy można się przesuwać do następnego zestawu wyników, używając NextResult(). Oczywiście nie trzeba zakończyć pracy z poprzednim zestawem rekordów, by przesunąć się do następnego.

Transakcje w ADO.NET są obsługiwane w trochę dziwny sposób (przynajmniej na pierwszy rzut oka). Najpierw z połączenia pobierany jest obiekt SqlTransaction (zwraca go metoda BeginTransaction z SqlConnection). Następnie obiekt ten przypisywany jest konkretnemu poleceniu SqlCommand, do właściwości Transaction) i wtedy dane polecenie wykonywane jest w danym kontekście transakcyjnym (czyli np. całość operacji może być potwierdzona (tr.Commit()) albo wycofana (tr.Rollback()). Transakcja jest związana z konkretnym połączeniem. Warto też dodać, że jeżeli chcemy wykonać operację przy użyciu TableAdapter w ramach transakcji, to wystarczy obiekt Transaction przypisać poszczególnym SqlCommand definiującym DataAdapter. Wtedy operacja Fill czy Update będzie wykonana w ramach kontekstu transakcyjnego.

Providery niezależne od bazy

W ADO.NET 2.0 wprowadzono też inny sposób pracy z bazą danych, gdzie można posługiwać się typami niezależnymi od danej bazy danych (dokładniej - od biblioteki obsługującej dany typ bazy). W pliku konfiguracyjnym .NET Framework machine.config zdefiniowane są pewne struktury definiujące providera do różnych baz danych. Taki provider udostępnia fabrykę (factory), która pośredniczy przy tworzeniu odpowiednich obiektów podobnych do znanych z np. SqlClient. Warto przeanalizować analogiczny przykład jak poprzednio, ale napisany w taki sposób, by łatwo można było go przenieść np. na platformę Access:

  1. DbProviderFactory f = DbProviderFactories.GetFactory("System.Data.SqlClient");

  2. DbConnectionStringBuilder csb = f.CreateConnectionStringBuilder();

  3. csb.Add("Data Source", @"NIEZAPOMINAJKA2\SQL2005");

  4. csb.Add("Initial Catalog", "PCWKVS2005");

  5. csb.Add("Integrated Security", "SSPI");

  6. using (

    DbConnection dcnn = f.CreateConnection()) {

  7. dcnn.ConnectionString = csb.ConnectionString;

  8. DbCommand dcmd = dcnn.CreateCommand();

  9. dcmd.CommandText = "select * from Customer";

  10. dcnn.Open();

  11. DbDataReader ddr = dcmd.ExecuteReader();

    while (ddr.Read()) {System.Diagnostics.Debug.WriteLine(ddr["name"]);

    }

    }

W pierwszej linii pobieramy fabrykę do SQL Servera. Następnie przy jej użyciu tworzymy obiekt pozwalający zdefiniować łańcuch połączeń; używając Add, definiujemy poszczególne składniki - analogicznie jak w poprzednim wypadku. Potem z fabryki pobieramy połączenie (linia 6) i inicjujemy łańcuch połączenia. Pozostałe operacje wykonywane są tak samo jak w poprzednim przykładzie - tworzymy polecenie, wykonujemy je pobierając DBDataReader, a później w pętli przechodzimy po odczytanych rekordach).

Na pytanie, czy warto w ten sposób programować, nie można udzielić jednoznacznej odpowiedzi. Z jednej strony takie odizolowanie od konkretnego dostawcy pozwala łatwo przenosić kod kliencki pomiędzy bazami danych. Czasami - gdy np. dane są tylko pobierane z różnych źródeł - może to być bardzo wygodne (można nawet podać listę dostępnych dostawców, którzy są w stanie wykonać dane operacje). Ale w przypadku rozbudowanej aplikacji bardziej istotna dla jej pracy jest ta reszta - procedury przechowywane, widoki, subtelności składni danego dialektu SQL itp. Trzeba także pamiętać, że jeśli np. dynamicznie budujemy CommandText, to nie wystarczy, że kod wywołujący będzie przenośny jeżeli użyte wyrażenia będą specyficzne dla danej bazy.

Warto też pamiętać o tym, że jeżeli mamy funkcję, która ma przyjąć jako parametr jakiś DataReader, to możemy określić jej parametr jako typu IDataReader. Każdy z obiektów ADO.NET implementuje bowiem odpowiedni interfejs - ICommand, IDataAdapter, IConnection itp.

Osoby zainteresowane sposobem programowania z wykorzystaniem fabryki mogą zerknąć do artykułu w MSDN "Generic Coding with the ADO.NET 2.0 Base Classes and Factories", gdzie dokładnie wyjaśnione są różne aspekty programowania z użyciem System.Data.Common.

Transakcje rozproszone

Jeżeli chcemy uzyskać transakcję obejmującą wiele połączeń (albo nawet wiele połączeń z różnymi serwerami), musimy uciec się do pomocy tzw. transakcji rozproszonych - a w świecie .NET najwygodniej jest wtedy użyć mechanizmu Enterprise Services, czyli napisać komponent COM+).

W .NET 2.0 wprowadzono także specjalną przestrzeń nazw System.Transaction. Pozwala ona zamknąć pewien ciąg wywołań w bloku "transakcyjnym" (ale bez formalnego konstruowania komponentów Enterprise Services):

try

{

using(TransactionScope scope = new TransactionScope())

{

/* Tu coś wykonywanego w ramach transakcji */

//Zatwierdzam transakcję

scope.Complete();

}

}

catch(TransactionAbortedException e) { ... }

catch{

//z innych powodów nie da się zakończyć

}