ASPNL logo (1 kb)
Saturday, February 04, 2012




Microsoft MVP

.NET Codewise Community
<< vorige | overzicht | volgende >>

.NET 2.0: Werken met data in ADO.NET 2.0

Door Michiel van Otegem
1 augustus 2008

ADO.NET is het onderdeel van het .NET Framework voor het benaderen van databases en andere gegevensbronnen. Voor versie 2.0 is veel werk verricht om de snelheid en efficiency te verbeteren en ontwikkelaars minder code te laten schrijven.

ADO.NET 1.x heeft zich in de afgelopen jaren bewezen als een goede technologie voor het werken met gegevensbronnen. De stap van ADO naar ADO.NET (zie kader “Van ADO naar ADO.NET”) was weliswaar een grote, maar de voordelen zijn enorm. Dat wil echter niet zeggen dat ADO.NET 1.x perfect is. Vooral op het gebied van snelheid en efficiency viel nog wel wat winst te behalen, te beginnen bij situaties waarin een DataSet een rol speelt (en dat zijn er veel).

De meest gebruikte manier om met gegevens te werken in ADO.NET is door een DataSet te laden met een DataAdapter, waarna de verbinding met de gegevensbron gesloten worden. Wijzigingen aan de DataSet worden later weer doorgevoerd naar de gegevensbron via (dezelfde) DataAdapter. De DataAdapter zorgt daarbij zelf dat de verbinding met de database geopend en gesloten wordt, tenzij je dit zelf doet. In feite is de DataSet een representatie van (een gedeelte) van de gegevensbron die in het geheugen bewaard wordt, en eventueel zelfs over een netwerk verstuurd kan worden of in een bestand opgeslagen kan worden. De DataSet bevat tabellen en daartussen kunnen zonodig relaties en andere constraints gedefinieerd worden, die al dan niet gelijk zijn aan hoe deze in de gegevensbron gedefinieerd zijn. Per tabel gebruik je een DataAdapter voor het laden en weer wegschrijven van gegevens. Omdat er geen constante verbinding met de gegevensbron is komt hierbij wel de volgende vraag naar voren: “Wat gebeurt er als iemand anders gegevens wijzigt tussen het lezen en schrijven van gegevens?”. Dat probleem is op meerdere manieren op te lossen en hangt eigenlijk af van de functionaliteit die men wil implementeren. Zo is het mogelijk om de wijzigingen allemaal door te voeren, of allemaal niet. Ook is het mogelijk om alle wijzigingen die goed gaan door te voeren. In al die gevallen kan de gebruiker op de hoogte gesteld worden, maar is het ook mogelijk om door te gaan alsof er niets aan de hand is. Een nadeel aan de manier waarop de DataAdapter in ADO.NET 1.x werkt is dat alle wijzigingen aan een DataSet één voor éen verwerkt worden, niet in batch. Vooral met veel wijzigingen kan dat de prestatie aardig beïnvloeden. In ADO.NET 2.0 is de DataAdapter uitgebreid met de UpdateBatchSize eigenschap, waarmee ingesteld kan worden hoeveel rijen tegelijk bijgewerkt moeten worden. Het bijwerken van een gegevensbron met 10 rijen tegelijk zou dan kunnen zoals in de code in afbeelding 1.

   1:  'benodigde objecten aanmaken
   2:  Dim connection As New SqlConnection(connString)
   3:  Dim adapter As New SqlDataAdapter(sql, connection)
   4:  Dim ds As New DataSet()
   5:   
   6:  'DataSet vullen
   7:  adapter.Fill(ds)
   8:   
   9:  'DataSet wijzigen (code weggelaten)
  10:    11:   
  12:  'Wijzig operaties aanmaken op basis van sql string
  13:  Dim commandBuilder As New SqlCommandBuilder(adapter)
  14:   
  15:  'Gegevensbron bijwerken (in batch)
  16:  adapter.UpdateBatchSize = 10
  17:  adapter.Update(ds)

Afbeelding 1, Gegevens lezen en schrijven met een DataAdapter in VB.NET

De code in Afbeelding 1 begint met het initialiseren (niet openen) van de database verbinding en het aanmaken van de DataAdapter. Merk op dat er gebruik gemaakt wordt van connString en sql, waarvan aangenomen wordt dat deze al gedefinieerd zijn. De enige regel code die in Afbeelding 1 anders is dan in ADO.NET 1.x is regel 16, want daar wordt de batch grootte ingesteld. Die ene regel maakt behoorlijk wat verschil in prestatie. Afbeelding 1 is overigens specifiek voor SQL Server. Voor andere databases gebruik je andere objecten, bijvoorbeeld OracleConnection voor Oracle en OleDbConnection voor alle OleDb ondersteunende gegevensbronnen. Omdat de verschillende classes niet erven van een gemeenschappelijke Connection class is het lastig om code te schrijven die onafhankelijk is van de gegevensbron. Om dit verhelpen is in ADO.NET 2.0 het principe van de Provder Factory toegevoegd. Hiermee is het wel mogelijk om gegevenbron onafhankelijke code te schrijven, en die is eventueel configureerbaar, zodat je niet opnieuw hoeft te compileren wanneer je een ander type gegevensbron wilt gebruiken. Afbeelding 2laat zien hoe dit werkt. Op regel 2 wordt aangegeven welke provider gebruikt moet worden (in dit geval SQL Server). Voor OleDb zou dit System.Data.OleDb zijn, en voor Oracle System.Data.OracleClient.

   1:  'factory aanmaken
   2:  Dim providerName As String = "System.Data.SqlClient"
   3:  Dim provider As DbProviderFactory = _
   4:     DbProviderFactories.GetFactory(providerName)
   5:   
   6:  'generieke objecten definieren
   7:  Dim connection As DbConnection
   8:  Dim adapter As DbDataAdapter
   9:   
  10:  'Objecten maken met provider
  11:  connection = provider.CreateConnection()
  12:  adapter = provider.CreateAdapter()

Afbeelding 2, Werken met een ProviderFactory

Gegevens kopiëren

Hoewel de mogelijkheid tot batch updates van een DataSet erg handig zijn, dekken ze niet de situatie waarin je grote hoeveelheden gegevens wilt kopiëren van één tabel naar een andere (mogelijk in verschillende gegevensbronnen). Op zo’n moment kun je eigenlijk beter gebruik maken van een DataReader waarmee je, zolang de verbinding met de gegevensbron geopend is, rijen één voor één kunt uitlezen. In ADO.NET 1.x moet je dan vervolgens voor elke rij een INSERT operatie uitvoeren om de gegevens in de andere gegevensbron te krijgen. Dat vergt ten eerst wat programmeerwerk, en ten tweede is ook dit niet bepaald efficiënt. In het geval dat je met SQL Server werkt kun je in ADO.NET 2.0 echter gebruik maken van het SqlBulkCopy object dat de uitgelezen gegevens kopieert naar de opgegeven tabel. Eventueel kun je daarbij een batch grootte opgeven. Een simpel voorbeeld zie je in Afbeelding 3.

   1:  'Database verbinding met bron maken en openen
   2:  Dim source As New SqlConnection(sourceString)
   3:  source.Open()
   4:   
   5:  'Database verbinding met bestemming maken en openen
   6:  Dim destination As New SqlConnection(destinationString)
   7:  destination.Open()
   8:  'DataReader maken
   9:  Dim command As New SqlCommand(sql, source)
  10:  Dim reader As SqlDataReader = command.ExecuteReader()
  11:  'Kopieer actie instellen en uitvoeren
  12:  Dim bulkCopy As New SqlBulkCopy(destination)
  13:  bulkCopy.DestinationTableName = "MijnTabel"
  14:  bulkCopy.WriteToServer(reader)
  15:   
  16:  'Verbindingen sluiten
  17:  reader.Close()
  18:  bulkCopy.Close()
  19:  source.Close()
  20:  destination.Close()

Afbeelding 3, Bulk copy met SQL Server

De bulk copy operatie voor SQL Server is bijna net zo snel als via Data Transfomation Services (DTS), en behoorlijk veel sneller dan dezelfde soort operatie via een batch update. Dat dit in code kan betekent echter dat dit soort operaties ook toegankelijk gemaakt kunnen worden voor gebruikers via een applicatie. Van reguliere gebruikers kun je niet verwachten dat ze even een DTS package maken, als dat überhaupt al toegestaan zou worden door de systeembeheerder.

Asynchrone operaties

Het kan zijn dat operatie sop de database lang duren. In ADO.NET 1.x betekent dit dat de client moet wachten totdat er een resultaat is. In ADO.NET 2.0 kun je echter ook operaties asynchroon uitvoeren, zodat de client verder kan met andere zaken terwijl de database operaties uitgevoerd worden. Zolang verschillende opdrachten niet van elkaar afhankelijk zijn, kun je zelfs een hele serie opdrachten asynchroon opstarten, en vervolgens wachten op het resultaat. Dit alles kan met alle geneugten die reguliere aanroepen ook hebben. ADO.NET 2.0 bevat drie verschillende mogelijkheden voor asynchrone afhandeling: een "polling model" waarbij in de code periodiek gekeken wordt of de operatie al klaar is, een "callback model" waarbij er een functie is die wordt aangeroepen als de operatie klaar is, en een "wait model" waarbij er na het opstarten van meerdere asynchrone operaties gewoon gewacht wordt tot ze klaar zijn. Hoewel asynchrone operaties heel handig zijn, moet er wel op gelet worden dat foutafhandeling niet even triviaal is als bij synchrone operaties.

Serializeren van gegevens

Serializatie is een operatie waarbij een object omgezet wordt in een representatie die opgeslagen kan worden of verstuurd kan worden. In .NET kun je de meeste objecten op twee manieren serializeren, als XML of binair. Binair is veel efficiënter, maar is niet uitwisselbaar met niet .NET systemen. Hoewel de DataSet impliciet te serializeren is naar XML, kun je ook de WriteXml methode gebruiken (of ReadXml om weer in te lezen). Een probleem met ADO.NET 1.x is dat dit niet kan met een DataTable, hoewel een DataSet in principe één of meer DataTable objecten bevat. Als je maar één tabel nodig hebt, is een DataSet dus eigenlijk een beetje overbodig. In ADO.NET 2.0 kan een DataTable object daarom nu ook opgeslagen worden als XML. Een ander probleem is dat een DataSet niet binair geserializeerd kan worden, zodat je altijd aangewezen bent op XML, ook als aan de andere kant van de lijn ook een .NET applicatie staat. Gelukkig is ook dit in ADO.NET 2.0 een zaak van het verleden.

Transacties

Strikt genomen geen onderdeel van ADO.NET 2.0, maar iets wat hier niet mag ontbreken is een nieuwe manier om met transacties om te gaan. Het .NET Framework bevat in versie 2.0 de System.Transactions namespace, met eigen transactie manager. Dit betekent dat je voor transacties niet meer gebonden bent aan Enterprise Services (ook wel Component Services genoemd) met de Distributed Transaction Co-ordinator (DTC) of een verbindingsspecifieke transactie. Die twee zijn namelijk niet verenigbaar en hebben allebei voor- en nadelen. De DTC heeft een behoorlijke overhead, maar is onmisbaar als je werkt met meerdere databases, terwijl een verbindingsspecifieke transactie relatief lichtgewicht is, maar alleen werkt met de database waarvoor de transactie is opgezet. De transactie manager van System.Transactions gebruikt een lichtgewicht transactie wanneer er maar met één database gewerkt wordt, maar doet een “upgrade” naar de DTC als er meerdere database in het spel komen. Een klein addertje onder het gras is dat dit vooralsnog alleen werkt met SQL Server 2005, dus als je een andere database server gebruikt, stapt System.Transactions meteen over naar de DTC. Het is echter aannemelijk dat meer database servers deze mogelijkheid zullen gaan ondersteunen.

MARS

ADO.NET heeft nog een feature die vooralsnog alleen werkt met SQL Server 2005, genaamd MARS (Multiple Active Result Sets). SQL Server 2005 is in staat om over dezelfde database verbinding meerdere DataReaders open te hebben en om zonodig tegelijkertijd INSERT, DELETE, en andere operaties uit te voeren. In ADO.NET 1.x is het zo dat je een DataReader kunt openen om gegevens te lezen, maar als je op basis van die gegevens een operatie uit wilt voeren, moet je of de DataReader afsluiten, of een nieuwe databaseverbinding aanmaken naast de al bestaande verbinding. Het is evident dat hierdoor niet erg efficiënt omgegaan wordt met de beschikbare resources. In het geval dat er een connection pool is met een beperkt aantal database verbindingen betekent dit dat er snel niet genoeg beschikbare verbindingen zijn. Bovendien wordt de server er ook niet vrolijk van als er onnodig veel verbindingen open staan. Overigens is MARS niet de oplossing der oplossingen voor dit probleem, want MARS heeft zo z’n eigen nadelen. MARS kan redelijk wat (server) resources opslokken, afhankelijk van de situatie, en wanneer er gewerkt wordt met transacties is de kans op deadlock groter. Bovendien kunnen alle operaties binnen MARS alleen maar deel uitmaken van dezelfde transactie. Het is niet mogelijk om meerdere transacties te definiëren voor verschillende operaties die met MARS uitgevoerd worden.

Statistieken

Een aardigheidje van ADO.NET 2.0 is de mogelijkheid om statistieken op te vragen van een database verbinding. Het nut ervan is beperkt tot monitoring en het debuggen van applicaties, en vanwege de overhead is het ook verstandig om het alleen in die situaties te gebruiken. Heb gebruik ervan is overigens wel heel gemakkelijk, zoals Afbeelding 4 laat zien.

   1:  'Statistieken aanzetten op bestaande verbinding
   2:  connection.StatisticsEnabled = True
   3:   
   4:  'Operaties uitvoeren (code weggelaten)
   5:     6:   
   7:  'Statistieken opvragen
   8:  Dim ht As HashTable = connection.RetrieveStatistics()
   9:   
  10:  'Statistieken weergeven
  11:  Console.WriteLine("Bytes verstuurd " & ht("BytesSent"))
  12:  Console.WriteLine("Bytes ontvangen " & ht("BytesOntvangen"))
Afbeelding 4, Statistieken opvragen

Afbeelding 4 laat slechts twee van de op te vragen statistieken zien. Er zijn er nog veel meer, zoals bijvoorbeeld de tijd die gespendeerd is aan het uitvoeren van opdrachten, hoeveel rijen er geselecteerd zijn, hoeveel transacties actief waren, enzovoorts.

Rijk aan features

ADO.NET 2.0 is rijk aan features. Dat was ADO.NET 1.x ook al, maar er zijn zeer veel uitbreidingen. Dit artikel beschrijft slechts de meest in het oog springende elementen. Vooral in combinatie met SQL Server 2005 is ADO.NET 2.0 zeer krachtig. Dit geldt ook omdat ADO.NET 2.0 aangepast is om binnen de context van SQL Server 2005 zelf te werken.

Van ADO naar ADO.NET

Voordat het .NET Framework er was, was ADO (ActiveX Data Objects) de methode om met gegevens uit een gegevensbron te werken. Met de komst van ADO.NET werd de manier waarop je met gegevens werkt radicaal anders, zo anders dat ADO.NET een hele andere naam had moeten hebben. Zeker als je bedenkt dat ActiveX synoniem is aan COM en ADO.NET niets met COM te maken heeft.. Het enige voordeel van de naam ADO.NET is dat ontwikkelaars die met ADO gewerkt hebben weten waar ADO.NET voor dient. Het centrale ontwikkelmodel van ADO is echter client-server, waarbij het gebruikelijk is dat er een verbinding met de database is. Pas in latere versies is er functionaliteit toegevoegd die het toestaat om (tijdelijk) de verbinding met de database te verbreken, maar client-server is altijd het centrale model geweest. In het wereldbeeld van ADO.NET is client-server slechts een van de mogelijkheden en is het centrale programmeermodel anders. Belangrijk is dat ADO.NET tracht de gegevensuitwisseling zo efficiënt mogelijk te maken. In de meeste gevallen wordt de database verbinding zo snel mogelijk verbroken en wordt data uit de gegevensbron in het geheugen van de client opgeslagen, meestal in een DataSet object. Wanneer records toegevoegd, gewijzigd of verwijderd worden, wordt dit eerst doorgevoerd op de DataSet. Pas in een later stadium worden de wijzigingen aan de DataSet doorgevoerd naar de gegevensbron. Dit biedt onder andere de mogelijkheid om de gegevens ergens tijdelijk op te slaan als er geen netwerk verbinding is, of om gegevens via het internet naar een andere locatie te sturen (waar geen database verbinding is), en later de wijzigingen terug te krijgen. Verder is het veel eenvoudiger om een schaalbare oplossing te maken, omdat het aantal open database verbindingen beperkt blijft.

Dit artikel is eerder verschenen in NetOpus, juni 2005.

<< vorige | ^ naar boven | overzicht | volgende >>
copyright 2000-2007 ASPNL