Providermodellen i ADO.NET 2.0  

ADO.NET 2.0 indeholder mange nyheder for databaseudviklere – men blandt de vigtigste er uden tvivl ændringerne i den basale dataprovidermodel. Med den nye model indfrier Microsoft et stort ønske hos de udviklere, der ønsker at udvikle dataprovider-uafhængige applikationer. Det er ikke svært at implementere en sådan løsning selv – og der har da også længe eksisteret adskillige glimrende løsninger på problemstillingen – men nu er løsningen altså indbygget i version 2.0 af .NET frameworket. Løsningen er iøvrigt et glimrende eksempel på, hvordan design patterns kan give en løst koblet og fleksibel arkitektur.

Providermodellen i klassisk ADO.NET

Providermodellen i ADO.NET 1.0 og 1.1 har som udgangspunkt været rettet mod at al databaseudvikling sker mod de specifikke providere som for eksempel SqlClient, OleDb, Odbc og OracleClient.

Ønsker man at lave provider-uafhængig kode, har disse providere dog heldigvis nogle fælles træk i form af fælles interfaces og en enkelt fælles basisklasse. Så ønsker man eksempelvis at tilgå alle disse provideres Connections på en fælles måde, kan det gøres ved at tilgå deres IDbConnection-interfaces. På tilsvarende måde kan Command-objekterne tilgås via IDbCommand-interfacet og så videre for DataReaders, Parameters, ParameterCollections etc. DataAdapterne skiller sig ud fra de øvrige typer, idet de både nedarver fra DbDataAdapter-basisklassen og implementerer IDbDataAdapter-interfacet.

Et eksempel på kode der udnytter SqlClient-klassernes implementationer af de fælles interfaces kunne se ud som følger (koden kan varieres på et utal af måder):

Dim connString As String = "..."
Dim sql As String = "SELECT ..."

Dim conn As System.Data.IDbConnection
conn = New System.Data.SqlClient.SqlConnection()
conn.ConnectionString = connString

Dim cmd As System.Data.IDbCommand
cmd = conn.CreateCommand()
cmd.CommandText = sql

Dim adap As System.Data.IDbDataAdapter
adap = New System.Data.SqlClient.SqlDataAdapter()
adap.SelectCommand = cmd

Dim ds As New System.Data.DataSet()
adap.Fill(ds)
Dim tbl As New System.Data.DataTable()
tbl = ds.Tables(0)

conn.Close()
string connString = "...";
string sql = "SELECT ...";

System.Data.IDbConnection conn;
conn = new System.Data.SqlClient.SqlConnection();
conn.ConnectionString = connString;

System.Data.IDbCommand cmd;
cmd = conn.CreateCommand();
cmd.CommandText = sql;

System.Data.IDbDataAdapter adap;
adap = new System.Data.SqlClient.SqlDataAdapter();
adap.SelectCommand = cmd;

System.Data.DataSet ds = new System.Data.DataSet();
adap.Fill(ds);
System.Data.DataTable tbl = new System.Data.DataTable();
tbl = ds.Tables[0];

conn.Close();

Med undtagelse af instansieringen af SqlConnection- og SqlDataAdapter-objekterne er denne kode uafhængig af den konkrete dataprovider, og som sådan ganske lige til at skrive for en applikationsudvikler.

Polymorfismen for dataprovidere er altså først og fremmest baseret på implementation af fælles interfaces. Interfaces er en kraftfuld mekanisme til at sikre en kontrakt mellem server og klienter. Hvis man skal være meget stringent, kan et interface per definition aldrig ændres; et krav der skyldes, at en klient ved implementation af et interface lover at implementere samtlige metoder i interfacet – det er netop heri det rent kontraktslige ligger. Definerer, implementerer og kalder man et interface internt i en mindre organisation, kan man måske nok vælge at se stort på, at interfacet per definition ikke må ændre sig. Men lige så snart man offentliggør et interface ud af til, og andre koder op imod interfacet, hænger man derimod på det! Dette gælder naturligvis i særlig grad, hvis man hedder Microsoft, idet tusinder og atter tusinder af udviklere koder op imod Microsofts interfaces – og det forpligter.

Fra interfaces til basisklasser

For at skabe en højere grad af fleksibilitet i den fremtidige udvikling af .NETs data API’er har Microsoft i ADO.NET 2.0 ændret de fælles træk fra at basere sig på interfaces til basisklasser – basisklassers virtuelle metoder kræver jo ikke nogen implementation af subklasserne (de konkrete providere). Microsoft har derfor mulighed for at udvide mængden af fælles træk for dataprovidere uden derved at pålægge applikationsudviklerne en vedligeholdelsesbyrde. Skiftet fra interfaces til basisklasser er naturligvis ikke uden potentielle problemer, idet kontrakten ikke længere er helt så ren, som den var med interfaces; man risikerer for eksempel, at en dataprovider-leverandør har defineret metoder på en konkret dataprovider-klasse som senere karambolerer med nye metoder, som Microsoft måtte introducere i basisklasserne. Alt i alt giver det dog en større fleksibilitet for fremtidens data-API.

De fra ADO.NET 1.0 og 1.1 kendte interfaces fortsætter naturligvis med at være understøttede således, at eksisterende kode fortsat vil kunne eksekveres.

Nedenfor ses et udvalg af de mest brugte data-klasser. SqlClient er anvendt som eksempel, men skemaet gælder også for de andre dataprovidere.

SqlClient klasse

Abstrakt basis klasse

Interface

SqlConnection

DbConnection

IDbConnection

SqlCommand

DbCommand

IDbCommand

SqlDataReader

DbDataReader

IDataReader
    IDataRecord

SqlTransaction

DbTransaction

IDbTransaction

SqlParameter

DbParameter

IDbDataParameter
    IDataParameter

SqlParameterCollection

DbParameterCollection

IDataParameterCollection
    IList

SqlDataAdapter

DbDataAdapter

IDbDataAdapter
    IDataAdapter

SqlCommandBuilder

DbCommandBuilder

 

SqlException

DbException

 

SqlConnectionStringBuilder

DbConnectionStringBuilder

 

DataAdapter-klasserne har iøvrigt ved samme lejlighed (heldigvis) fået en renere implementation, idet DbDataAdapter-basisklassen implementerer IDbDataAdapter-interfacet.

Som en bibemærkning bør det nok lige nævnes, at de enkelte abstrakte basisklasser ved samme lejlighed er blevet udvidet med nye funktioner i forhold til de allerede eksisterende interfaces – eksempelvis har DbConnection fået tilføjet GetSchema-metoderne som kan bruges til at indhente schema-definitioner for en datasource.

Samme eksempel som før, men nu hvor der kodes mod de nye fælles basisklasser i ADO.NET 2.0:

Dim connString As String = "..."
Dim sql As String = "SELECT ..."

Dim conn As System.Data.Common.DbConnection
conn = New System.Data.SqlClient.SqlConnection()
conn.ConnectionString = connString

Dim cmd As System.Data.Common.DbCommand
cmd = conn.CreateCommand()
cmd.CommandText = sql

Dim adap As System.Data.Common.DbDataAdapter
adap = New System.Data.SqlClient.SqlDataAdapter()
adap.SelectCommand = cmd

Dim tbl As New System.Data.DataTable()
adap.Fill(tbl)

conn.Close()
string connString = "...";
string sql = "SELECT ...";

System.Data.Common.DbConnection conn;
conn = new System.Data.SqlClient.SqlConnection();
conn.ConnectionString = connString;

System.Data.Common.DbCommand cmd;
cmd = conn.CreateCommand();
cmd.CommandText = sql;

System.Data.Common.DbDataAdapter adap;
adap = new System.Data.SqlClient.SqlDataAdapter();
adap.SelectCommand = cmd;

System.Data.DataTable tbl = new System.Data.DataTable();
adap.Fill(tbl);

conn.Close();

En central fabrik

For at denne kode kan være helt uafhængig af den konkrete dataprovider, kræver det dog, at vi får gjort noget ved instansieringen af SqlConnection- og SqlDataAdapter-klasserne. Mange udviklere med fokus på provider-uafhængig databasekode har håndteret det ved hjælp af en factory til instansiering af de konkrete provider-objekter. Men nu behøver man ikke kigge længere end til .NET frameworket for at få adgang til en passende factory. Der findes én provider-factory for hver dataprovider: SqlClientFactory, OleDbFactory og så videre; og alle disse provider-factories er specialiseringer af den abstrakte basisklasse System.Data.Common.DbProviderFactory

DbProviderFactory-klassen har følgende instans-members:

Function CreateCommand() As DbCommand
Function CreateCommandBuilder() As DbCommandBuilder
Function CreateConnection() As DbConnection
Function CreateConnectionStringBuilder() As DbConnectionStringBuilder
Function CreateDataAdapter() As DbDataAdapter
Function CreateDataSourceEnumerator() As DbDataSourceEnumerator
Function CreateParameter() As DbParameter
Function CreatePermission( _
           ByVal state As System.Security.Permissions.PermissionState) _
           As System.Security.CodeAccessPermission
ReadOnly Property CanCreateDataSourceEnumerator() As Boolean
DbCommand CreateCommand()
DbCommandBuilder CreateCommandBuilder()
DbConnection CreateConnection()
DbConnectionStringBuilder CreateConnectionStringBuilder()
DbDataAdapter CreateDataAdapter()
DbDataSourceEnumerator CreateDataSourceEnumerator()
DbParameter CreateParameter()
System.Security.CodeAccessPermission CreatePermission
            (System.Security.Permissions.PermissionState state)
bool CanCreateDataSourceEnumerator { get; }

En fabrik at skabe

Brug af de konkrete provider-factories mindsker afhængigheden af den konkrete provider, men allerbedst ville det være, hvis vi også kunne gøre vores kode uafhængig af disse konkrete provider-factories; og det kan vi lige netop ved brug af System.Data.Common.DbProviderFactories-klassen - en meget overskuelig utility-klasse, der kun består af tre statiske (C#) / shared (VB) metoder:

Shared Function GetFactory(ByVal providerInvariantName As String) _
           As DbProviderFactory
Shared Function GetFactory(ByVal providerRow As System.Data.DataRow) _
           As DbProviderFactory
Shared Function GetFactoryClasses() As System.Data.DataTable
static DbProviderFactory GetFactory(System.Data.DataRow providerRow)
static DbProviderFactory GetFactory(string providerInvariantName)
static System.Data.DataTable GetFactoryClasses()

GetFactoryClasses-metoden returnerer en DataTable med en liste over de aktuelt tilgængelige provider-factories. Den returnerede DataTable indeholder fire felter, der med SqlClient som eksempel indeholder følgende data:

  • Name: ”SqlClient Data Provider”
  • Description: “.Net Framework Data Provider for SqlServer”
  • InvariantName: “System.Data.SqlClient”
  • AssemblyQualifiedName: “System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”

GetFactoryClasses-metoden henter som udgangspunkt listen over tilgængelige dataprovider-factories i machine.config – men listen kan modificeres ved at add’e og remove provider-factories i de konkrete applikationers web.config og app.config.

InvariantName-værdien kan fodres til GetFactory-metoden med en tilsvarende konkret DbProviderFactory som resultat. Alternativt kan man sende en given DataRow returneret fra GetFactoryClasses-metoden med til GetFactory-metoden, som så selv vil slå InvariantName-feltet op i DataRow’en. Uanset den valgte fremgangsmåde får man altså returneret en konkret DbProviderFactory, og man kan nu formulere den tidligere kode som:

Dim connString As String = "..."
Dim sql As String = "SELECT ..."
Dim providerInvariantName As String = "System.Data.SqlClient"

Dim factory As System.Data.Common.DbProviderFactory
factory = System.Data.Common.DbProviderFactories.GetFactory(providerInvariantName)

Dim conn As System.Data.Common.DbConnection
conn = factory.CreateConnection()
conn.ConnectionString = connString

Dim cmd As System.Data.Common.DbCommand
cmd = conn.CreateCommand()
cmd.CommandText = sql

Dim adap As System.Data.Common.DbDataAdapter
adap = factory.CreateDataAdapter()
adap.SelectCommand = cmd

Dim tbl As New System.Data.DataTable()
adap.Fill(tbl)

conn.Close()
string connString = "...";
string sql = "SELECT ...";
string providerInvariantName = "System.Data.SqlClient";

System.Data.Common.DbProviderFactory factory;
factory = System.Data.Common.DbProviderFactories.GetFactory(providerInvariantName);

System.Data.Common.DbConnection conn;
conn = factory.CreateConnection();
conn.ConnectionString = connString;

System.Data.Common.DbCommand cmd;
cmd = conn.CreateCommand();
cmd.CommandText = sql;

System.Data.Common.DbDataAdapter adap;
adap = factory.CreateDataAdapter();
adap.SelectCommand = cmd;

System.Data.DataTable tbl = new System.Data.DataTable();
adap.Fill(tbl);

conn.Close();

Close but no cigar

Med ændringerne i dataprovider-modellen har ADO.NET 2.0 gjort det simpelt at lave applikationer, hvor dataprovider koden er uafhængig af den konkrete dataprovider. Det betyder dog ikke, at det dermed er trivielt at lave applikationer, der er uafhængige af konkrete databaseprodukter. Så snart man laver andet end helt trivielle udviklingsprojekter vil SQL-syntaks, SQL-semantik og mange fundamentale forskelle med hensyn til eksempelvis autocounters, timestamps, datatyper og meget, meget andet være vigtige faktorer under udviklingen. Det kræver planlægning såvel som smarte database-frameworks at opnå fuldstændig uafhængighed af – og dermed fuld portabilitet mellem – konkrete databaser, men det kan lade sig gøre, og den nye dataprovidermodel i ADO.NET 2.0 er et første skridt på vejen.

Der følger et lille kodeeksempel med til denne artikel. Eksemplet findes både til VB og C#, og det kan downloades her.