Intro till ADO.NET för ADO programmerare
Förord
Detta är en kortfattad genomgång av några likheter och skillnader melllan ADO och ADO.NET. Artikeln lämpar sig bäst för dig som kan ADO men ännu inte tittat på / kommit igång med ADO.NET.
Bakgrund
När ADO dök upp var det en befrielse med den enkelhet man kan sköta åtkomst till databaser. Tidigare objektmodeller (DAO, RDO) fyllde sitt syfte men var inget man kom igång med på en eftermiddag. Namnsättningen på efterträdaren ADO.NET kan missleda oss att tro att arbetet med databasåtkomst fortlöper som inget hade hänt. Vi som valde den snabba vägen
Set rs = CreateObject("ADODB.Recordset")
rs.Open "select * from Customer", "DSN=OrderDB"
tvingas nu skriva några rader till. Misströsta ej, planerar du dina projekt rätt så kommer du förmodligen skriva färre rader dataåtkomstkod än tidigare. Se bara till att separera dataåtkomsten i egna klasser och förmodligen i ett eget projekt. Jag kommer inte fördjupa mig i arkitektoniska spörsmål i denna artikel utan visa på några likheter och skillnader mellan ADO och ADO.NET.
Exempel för ADO är skrivna i VBScript för ODBC och förutsätter att konstanter är definierade (adovbs.inc i ASP). ADO.NET kod är skriven i VB.NET för SQL Server och förutsätter:
Imports System.Data
Imports System.Data.SqlClient
Använder du inte SQL Server (eller MSDE) kommer du förmodligen ansluta till databasen med OleDB. Då skall du importera System.Data.OleDb istället för System.Data.SqlClient och klassnamn som börjar med "Sql" anges istället "OleDb". Övriga skillnader som finns mellan klasserna har jag ej kontrollerat.
Mappning av klasserna
I det första exemplet visade jag hur vi kan göra vårt arbete mot databasen på två rader i ADO. Nu har inte alla alltid gjort det så enkelt för sig. Ibland behöver man trots kontrollera lite mer, t ex skapa disconnected recordset, anropa lagrade procedurer med parametrar etc. Då har vi ägnat mer tid åt ADO:s objektmodell och denna förhåller sig förenklat mot ADO.NET enligt följande:ADO ADO.NET Förklaring
Connection SqlConnection Anslutning (kommunikation) till databasen.
Command SqlCommand SQL kommando, t ex anrop till lagrad procedur.
Recordset DataSet, SqlDataReader Leverar informationen som vi använder i vårt program.
finns ej SqlDataAdapter Kopplar ihop DataSet mot Command och Connection objekten.
Dessa är de mest grundläggande klasserna, det finns fler i både ADO och ADO.NET. En stor skillnad är att överlappning av metoder mellan klasserna tagits bort i ADO.NET. Till exempel kan vi i ADO exekvera en SQL-fråga med Connection, Command och Recordset objekten. I ADO.NET är kan endast SqlCommand objekt utföra den uppgiften.
Exekvera utan tabellresultat
ADO
Det räcker med anslutning för att utföra en SQL sats som inte returnerar ett Recordset.
Dim cn
Set cn = CreateObject("ADODB.Connection")
cn.Open "DSN=Northwind"
cn.Execute "delete Customers where CustomerID = 'xxxx'"
cn.Close
ADO.NET
För att utföra något mot databasen behöver vi en anslutning och ett kommando.
Dim cn As New SqlConnection()
cn.ConnectionString = "Data Source = (local);InitialCatalog= Northwind;UserID=sa;Password=bad"
Dim cmd As New SqlCommand()
cmd.Connection = cn
cmd.CommandText = "delete Customers where CustomerID = 'xxxx'"
cmd.CommandType = CommandType.Text
cmd.Connection.Open()
cmd.ExecuteNonQuery()
cmd.Connection.Close()
Begränsningen är att vi inte kan hämta någon tabelldata med flera rader. Vi kan bara få reda på hur många rader som påverkades av frågan samt få resultatet av output parametrar om vi anropar en lagrad procedur (med SqlParameter objekt).
Hämta information för läsning framåt
Exemplet visar hur vi kör en fråga som returnerar ett resultat som endast kan läsas framåt.ADO
Nu behöver vi ett recordset objekt som ställs in för att bara kan läsa framåt.
Dim cn Set cn = CreateObject("ADODB.Connection")
cn.Open "DSN=Northwind"
Dim rs
Set rs = CreateObject("ADODB.Recordset")
Set rs.ActiveConnection = cn
rs.Source = "select CompanyName from Customers"
rs.CursorType = adOpenForwardOnly
rs.LockType = adLockReadOnly
rs.Open
Do Until rs.EOF
WScript.Echo rs(0)
rs.MoveNext
Loop
rs.Close
cn.Close
ADO.NET
För denna uppgift lägger vi till en DataReader som inte kan göra annat än att läsa resultatet framåt.
Dim cn As New SqlConnection()
cn.ConnectionString = "Data Source=(local);Initial Catalog= Northwind;UserID=sa;Password=bad"
Dim cmd As New SqlCommand()
cmd.Connection = cn
cmd.CommandText = "select CompanyName from Customers"
cmd.CommandType = CommandType.Text
cmd.Connection.Open()
Dim reader As SqlDataReader = cmd.ExecuteReader()
While reader.Read
Console.WriteLine(reader.GetString(0))
End While
reader.Close()
cmd.Connection.Close()
Hämta information frånkopplad från källan
Detta är förmodligen (förhoppningsvis) den vanligaste metoden för att hämta ett resultat från databasen. Den tar mindre databasresurser i anspråk än föregående exempel då vi kan frigöra databasanslutningen så fort informationen lästs in i vårt Recordset/DataSet.ADO
Vi skapar ett recordset objekt där anslutningen frikopplas efter att resultatet lästs in.
Dim cn
Set cn = CreateObject("ADODB.Connection")
cn.Open "DSN=Northwind"
Dim rs
Set rs = CreateObject("ADODB.Recordset")
Set rs.ActiveConnection = cn
rs.Source = "select CompanyName from Customers"
rs.CursorLocation = adUseClient
rs.CursorType = adOpenStatic
rs.LockType = adLockBatchOptimistic
rs.Open
Set rs.ActiveConnection = Nothing
cn.Close
Set cn = Nothing
Do Until rs.EOF
WScript.Echo rs(0)
rs.MoveNext
Loop
rs.Close
Set rs = Nothing
ADO.NET
DataSet är till sin natur frånkopplad från källan och kan inte kommunicera med databasen själv.
För att prata med databasen behöver vi en SqlDataAdapter. SqlDataAdaptern använder i sin tur en SqlConnection för anslutningen, SqlCommand för frågan och en DataReader objekt för att fylla DataSet objektet med information. I exemplet nedan skapas och hanteras dessa objekt "under huven", vi behöver inte bekymra oss.
Dim ds As New DataSet()
Dim da As New SqlDataAdapter("select CompanyName from Customers", "Data Source=(local);Initial Catalog= Northwind;UserID=sa;Password=xxxx")
da.Fill(ds)
da = Nothing
Dim row As DataRow
For Each row In ds.Tables(0).Rows
Console.WriteLine(row(0))
Next
ds = Nothing
När vi skapar SqlDataAdapter objektet använder vi en alternativ konstruktor som anger SQL frågan och anslutningssträngen. Det är den information objektet behöver för att skapa SqlConnection och SqlCommand objekten.
En uppenbar skillnad jämfört med ADO är att vi inte har någon motsvarighet till rs.MoveNext. I DataSet så ligger raderna (och tabellerna) i collection objekt. Något ovant i början men logiskt när man använt det en tid.
Klasserna
De tidigare exemplen har ställt ADO mot ADO.NET. De har likheter men också mycket stora skillnader. Resterande del av artikeln är inriktad på att beskriva klassernas arbetsuppgifter, hur de hänger ihop och hur man använder dem. Det dyker också upp några nya klasser som tidigare inte nämnts.
SqlConnection
Klassen är renare är dess föregångare, det går t ex inte att köra någon fråga med ett SqlConnection objekt. De uppgifter som det har är att sköta anslutning till databasen samt initiera manuella transaktioner.
SqlTransaction
SqlTranscation objekt används för att hantera manuella transaktioner. Dessa skall inte förväxlas med COM+/MSDTC transaktioner. En manuell transaktion kan bara gå inom en datakälla och man måste själv koda commit och rollback regler. Exempel på hur vi kan använda en manuell transaktion:
Try
Dim cn As New SqlConnection("Data Source=(local);Initial Catalog= Northwind;UserID=sa;Password=xxxx")
Dim tran As SqlTransaction
cn.Open()
Try
tran = cn.BeginTransaction()
Dim cmd As New SqlCommand()
Dim records As Integer
With cmd
.CommandText = "delete Customers where CustomerID = 'VINET'"
.CommandType = CommandType.Text
.Connection = cn
.Transaction = tran
records = .ExecuteNonQuery()
End With
tran.Commit()
Console.WriteLine(String.Concat(records, " poster raderade."))
Catch ex As Exception
tran.Rollback()
Console.WriteLine(ex.Message)
Finally
cn.Close()
End Try
Catch
Console.WriteLine("Kunde ej ansluta till databasen.")
End Try
Exemplet visar inte på någon meningsfull transaktion. Vi kör en delete fråga och bara bryr oss om den inte fungerar, vilket i så fall ändå resulterar i att inget ändras i databasen.
SqlCommand
Denna klass skiljer sig inte väsentligt från ADO. Den är till för att ställa in frågan mot databasen. Detta kan vara SQL i text form eller en lagrad procedur med parametrar mm. Ett exempel på hur man skapar ett SqlCommand objekt som skall anropa lagrad procedur med två in- och en utparameter.
Dim cn As New SqlConnection(connectionString)
Dim param As SqlParameter
Dim cmd As New SqlCommand()
With cmd
.Connection = cn
.CommandText = "GetOrderValue"
.CommandType = CommandType.StoredProcedure
With .Parameters
' cmd skapar SqlParameter objektet åt oss.
.Add("@OrderID", 1)
' Vi skapar paremeter objektet själva
.Add(New SqlParameter("@CurrencyID", 752))
' Vi behöver ställa in mer än vad konstruktorn medger
param = New SqlParameter()
param.ParameterName = "@OrderValue"
param.Direction = ParameterDirection.Output
param.SqlDbType = SqlDbType.Int
.Add(param)
End With
cn.Open()
.ExecuteNonQuery()
cn.Close()
End With
Console.WriteLine(String.Concat("Ordervärdet är ", param.Value.ToString))
Det går även att låta ett SqlCommand nyttja XML funktionaliteten i SQL Server (... for xml...). Man gör det på ett mer intuitivt sätt än i ADO med ExecuteXMLReader metoden. I ADO fick man använda Command.Properties.Item("Output Stream") och någon parameter på Execute metoden.
SqlDataReader
Detta är en enklare form av "Recordset" som optimerats för prestanda, vissa benchmark tester visar väsentligt bättre prestanda än vid använding av DataSet.Skall man alltid använda SqlDataReader? Finns det då ingen hake? Det gör det. Så länge man har SqlDataReadern öppen så är databasanslutningen låst. Klassen ärver från MarshalByRefObject vilket gör det kostsamt att skyffla mellan datorer. Bygger man en ASP.NET applikation så bör alla komponenter vara installerade på webbservern. Det går inte att utnyttja data-cachening i ASP.NET.
Det finns naturligtvis tillfällen då man av prestandaskäl bör använda SqlDataReader, bara man är införstådd med dess natur och förstår när man skall göra det.
Klassens metoder består i princip av möjligheten att flytta sig till nästa rad, nästa tabell samt att läsa värden i kolumner.
DataSet
Det finns tyvärr ingen möjlighet att på några rader beskriva alla möjligheter med DataSet. De är i princip en mini-databas som kan ha nycklar, relationer, vyer mm. Grundläggande är att ett DataSet alltid är frånkopplad från källan och att det inte förutsätts ha bara en tabell. Även i Recordset kan man hantera flera tabeller men detta med begränsningar såsom att dessa inte kan frikopplas från källan.Som tidigare nämnts finns inte metoder såsom MoveNext som sitter i ryggmärgen när man programmerar ADO. Istället så ligger både tabeller och rader i collection objekt vilket innebär att navigeringen blir annorlunda. Nedan följer korta exempel på vanliga navigeringssätt och metoder för att ändra data.
Dim ds As New DataSet()
... kod för att fylla ds med flera tabeller ...
Dim table As DataTable
' Första tabllen i ds
table = ds.Tables(0)
' Tabellen som heter "Customers"
table = ds.Tables("Customers")
' Antalet rader i en tabell
Dim rowCount As Integer = table.Rows.Count
Dim row As DataRow
' Första raden i tabellen
row = table.Rows(0) ' eller
row = ds.Tables(0).Rows(0)
' Sista raden i tabellen
row = table.Rows(table.Rows.Count - 1)
' Gå igenom varje rad i tabellen
For Each row In table.Rows
' Läsa värde från kolumnen CustomerName
Console.WriteLine(row("CustomerName"))
' Ändra värde i kolumn nr 6
row(5) = "Superkund"
Next
' Lägga till en rad
row = table.NewRow()
row("CustomerID") = 52
row("CustomerName") = "Stures Verkstad"
table.Rows.Add(row)
' eller
Dim values() As Object = {52, "Stures Verkstad"}
table.Rows.Add(values)
' Radera en rad
row.Delete()
' Commit på ändringar
ds.AcceptChanges() ' Alla ändringar i DataSetet
table.AcceptChanges() ' Ändringar i tabellen
row.AcceptChanges() ' Radens ändringar
' Rollback på ändringar
ds.RejectChanges() ' Alla ändringar i DataSetet
table.RejectChanges() ' Ändringar i tabellen
row.RejectChanges() ' Radens ändringar
Observera att commit/rollback metodanrop på dessa objekt gör att ändringen inte längre existerar. Om du gör en ändring, anropar AcceptChanges och sist försöker spara det till din "riktiga" databas med en DataAdapter så sparas inget. I det fallet är DataAdaptern som skall anropa AcceptChanges efter att ändringarna sparats till databasen.
Detta var det mest grundläggande man behöver veta för att använda DataSet. Är Ni intresserade av mer så rösta på artikeln så att det blir uppenbart att intresse finns. Det finns utrymme för att skriva åtskilliga artiklar om enbart DataSet.
SqlDataAdapter
Detta är bryggan som gör att DataSet kan komminicera med databasen med hjälp av SqlConnection, SqlCommand, SqlDataReader m fl objekt. Det finns alternativa konstruktorer på klassen så att inte själva behöver skapa SqlCommand och SqlConnection objekt.DataAdaptern används för att både läsa och skriva till databasen. Skall man bara läsa information räcker det med ett SqlCommand objekt för SelectCommand egenskapen. Skall vi även kunna utföra skrivning till databasen behövs SqlCommand objekt för InsertCommand, UpdateComman och DeleteCommand egenskaperna.
I de fall vi bara hämtar information från en tabell kan DML frågorna byggas automatiskt av ett SqlCommandBuilder objekt. Primärnyckeln måste då vara med i select frågan.
Dim sqlSelect As String
sqlSelect = "select OrderID, Freight "
sqlSelect += "from Orders "
sqlSelect += "where CustomerID in (select CustomerID from Customers where Country = 'Venezuela') "
' Hämta information
Dim ds As New DataSet()
Dim da As New SqlDataAdapter(sqlSelect, connectionString)
da.Fill(ds)
' Ändra värden
Dim row As DataRow
For Each row In ds.Tables(0).Rows
row("Freight") = 1.01 * Convert.ToDouble(row("Freight"))
Next
' Bygg DML kommandon automatiskt.
Dim cmdBuilder As New SqlCommandBuilder(da)
' Skriv till databasen.
da.Update(ds, "Table")
Om vårt data innehåller information från flera tabeller i databasen eller vi vill ha bättre kontroll på ändringar kan vi använda lagrade procedurer för ändamålet. Vi kan då mappa kolumner mot parametrar till de lagrade procedurerna. I exemplet skapar jag bara ett SqlCommand objekt för delete, vill man kunna göra insert och update så måste SqlCommand objekt skapas för dem också.
Dim connectionString As String = "Data Source=(local); Initial Catalog= Lab;UserID=sa;Password=xxxx"
Dim cn As New SqlConnection(connectionString)
Dim sqlSelect As String
sqlSelect = "select CustomerID, CustomerName from Customer"
' Hämta information
Dim ds As New DataSet()
Dim da As New SqlDataAdapter(sqlSelect, cn)
da.Fill(ds, "Customer")
' Ta bort dumma kunder
Dim row As DataRow
For Each row In ds.Tables("Customer").Rows
If row("CustomerName").ToString = "Dum kund" Then
row.Delete()
End If
Next
' Bygg fråga för delete.
Dim cmdDelete As New SqlCommand()
With cmdDelete
.Connection = cn
.CommandText = "delCustomer"
.CommandType = CommandType.StoredProcedure
.Parameters.Add(New SqlParameter("@CustomerID", SqlDbType.Int, 4, "CustomerID"))
End With
' Koppla delete frågan
da.DeleteCommand = cmdDelete
' Skriv till databasen.
da.Update(ds, "Customer")
SqlParameter objektet mappas med den användna konstruktorn mot ett kolumnnamn. För varje rad som tagits bort med Delete metoden kommer DataAdapter objektet köra kommandot.
Som någon kanske märkt så behöver man inte öppna och stänga databasanslutningen. Är den stängd när man anropar Fill eller Update metoderna så kommer DataAdaptern öppna för att utföra kommandona och därefter stänga anslutningen. Är anslutningen öppen så lämnas den öppen efter utförda kommandon.
De olika kommandona på DataAdapterna kan ha helt olika datakällor. Om vi t ex använder replikering i SQL Server kanske vi vill läsa från en server men uppdatera till en annan.
Gäst Konto
Richard Ohlsson
Det här är en riktigt bra sammanfattning. Den har jag haft mycket hjälp av.
Mattis Kvarnström
Utmärkt genomgång och förklaring utav ADO.NET, var till stor hjälp för mig att få ökad förståelse för b.la DataSet