Skip to content

9. Toegang tot de databases

9.1. Connector ADO.NET

Laten we nog eens kijken naar de gelaagde architectuur die al meerdere keren is gebruikt

In de bestudeerde voorbeelden heeft de laag [dao] tot nu toe gebruikgemaakt van twee soorten gegevensbronnen:

  • gegevens die hard in de code zijn gecodeerd
  • gegevens uit tekstbestanden

In dit hoofdstuk bekijken we het geval waarin de gegevens uit een database komen. De drielaagse architectuur evolueert dan naar een meerlaagse architectuur. Er bestaan verschillende varianten. We zullen de basisconcepten bestuderen aan de hand van de volgende:

In het bovenstaande schema communiceert de laag [dao] [1] met de laag SGBD [3] viaeen klassenbibliotheek die specifiek is voor de gebruikte SGBD en die bij deze wordt geleverd. Deze laag implementeert standaardfunctionaliteiten die worden samengevat onder de term ADO (Active X Data Objects). Een dergelijke laag wordt een provider (in dit geval een leverancier van toegang tot een database) of connector genoemd. De meeste SGBD-bestanden beschikken nu over een ADO.NET-connector, wat in het begin van het .NET-platform nog niet het geval was. De .NET-connectoren bieden geen standaardinterface aan de [dao]-laag, waardoor deze in de code de namen van de connectorklassen bevat. Als men van SGBD verandert, verandert men van connector en van klassen en moet men dan de laag [dao] aanpassen. Dit is zowel een efficiënte als een rigide architectuur, omdat de connector .NET is geschreven voor een specifieke SGBD en deze optimaal weet te benutten, en een starre architectuur omdat het wijzigen van SGBD betekent dat de laag [dao] moet worden aangepast. Dit tweede argument moet echter in het juiste perspectief worden geplaatst: bedrijven wisselen niet erg vaak van SGBD. Bovendien zullen we later zien dat er sinds versie 2.0 van .NET een generieke connector bestaat die flexibiliteit biedt zonder in te boeten aan prestaties.

9.2. De twee manieren om een gegevensbron te gebruiken

Het .NET-platform biedt twee verschillende manieren om een gegevensbron te gebruiken:

  1. verbonden modus
  2. offline modus

In de verbonden modus opent de applicatie

  1. een verbinding met de gegevensbron
  2. werkt met de gegevensbron in lees-/schrijfmodus
  3. sluit de verbinding af

In de offline-modus zal de applicatie

  1. opent de applicatie een verbinding met de gegevensbron
  2. haalt een kopie in het geheugen op van alle of een deel van de gegevens uit de bron
  3. sluit de verbinding
  4. werkt met de kopie in het geheugen van de gegevens (lezen/schrijven)
  5. wanneer het werk klaar is, opent de app een verbinding, stuurt de gewijzigde gegevens naar de gegevensbron zodat deze ze verwerkt, en sluit de verbinding

We behandelen hier alleen de modus met verbinding.

9.3. De basisbegrippen van het werken met een database

We zullen de belangrijkste concepten van het gebruik van een database toelichten aan de hand van een SQL Server Compact 3.5-database. Deze SGBD wordt meegeleverd met Visual Studio Express. Het is een lichte SGBD die slechts één gebruiker tegelijk kan verwerken. Hij volstaat echter om kennis te maken met het programmeren met databases. Later zullen we andere SGBD-databases bespreken.

De gebruikte architectuur is als volgt:

Een console-applicatie [1] zal gebruikmaken van een database van het type SqlServer Compact [3,4] via de connector Ado.Net van deze SGBD [2].

9.3.1. De voorbeeld -database

We gaan de database rechtstreeks in Visual Studio Express opbouwen. Hiervoor maken we een nieuw project van het type console aan.

  • [1]: het project
  • [2]: we openen het venster "Database-verkenner"
  • [3]: we maken een nieuwe verbinding aan
  • [4]: het type van SGBD wordt geselecteerd
  • [5,6]: we kiezen de SGBD SQL Server Compact
  • [7]: de database wordt aangemaakt
  • [8]: een SQL Server Compact-database wordt ingekapseld in één enkel bestand met de extensie .sdf. Geef aan waar deze moet worden aangemaakt, in dit geval in de map van het C#-project.
  • [9]: de nieuwe database heeft de naam [dbarticles.sdf] gekregen
  • [10]: we selecteren de Franse taal. Dit heeft gevolgen voor de sorteerbewerkingen.
  • [11,12]: de database kan met een wachtwoord worden beveiligd. Hier "dbarticles".
  • [13]: de informatiepagina wordt bevestigd. De database wordt nu fysiek aangemaakt:
  • [14]: de naam van de zojuist aangemaakte database
  • [15]: vink de optie "Save my password" aan, zodat je het wachtwoord niet elke keer opnieuw hoeft in te voeren
  • [16]: we controleren de verbinding
  • [17]: alles is in orde
  • [18]: we bevestigen de informatiepagina
  • [19]: de verbinding verschijnt in de database-explorer
  • [20]: op dit moment bevat de database nog geen tabellen. We maken er een aan. Een artikel heeft de volgende velden:
    • id: een unieke identificatiecode – primaire sleutel
    • nom: naam van het artikel – uniek
    • prix: prijs van het artikel
    • stockactuel: de huidige voorraad
    • stockminimum: de minimumvoorraad waaronder het artikel moet worden bijgevuld
  • [21]: het veld [id] is van het type geheel getal en is de primaire sleutel [22] van de tabel.
  • [23]: deze primaire sleutel is van het type Identity. Dit concept, dat specifiek is voor de SGBD SQL-server, geeft aan dat de primaire sleutel door de SGBD zelf wordt gegenereerd. Hier is de primaire sleutel een geheel getal dat begint bij 1 en bij elke nieuwe sleutel met 1 wordt verhoogd.
  • [24]: de overige velden worden aangemaakt. Merk op dat het veld [nom] een uniekheids beperking [25] heeft.
  • [26]: de tabel krijgt een naam
  • [27]: nadat de structuur van de tabel is gevalideerd, verschijnt deze in de database.
  • [28]: de inhoud van de tabel wordt opgevraagd
  • [29]: deze is momenteel leeg
  • [30]: we vullen de tabel met enkele gegevens. Een rij wordt gevalideerd zodra we doorgaan naar de invoer van de volgende rij. Het veld [id] wordt niet ingevuld: het wordt automatisch gegenereerd wanneer de rij wordt gevalideerd.

Nu moeten we het project nog zo configureren dat deze database, die zich momenteel in de hoofdmap van het project bevindt, automatisch wordt gekopieerd naar de uitvoermap van het project:

  • [1]: we vragen om alle bestanden te tonen
  • [2]: de database [dbarticles.sdf] verschijnt
  • [3]: deze wordt in het project opgenomen
  • [4]: het toevoegen van een gegevensbron aan een project start een wizard die we hier niet nodig hebben [5].
  • [6]: de database maakt nu deel uit van het project. We keren terug naar de normale modus [7].
  • [8]: het project met de bijbehorende database
  • [9]: in de eigenschappen van de database is te zien ([10]) dat deze automatisch naar de uitvoermap van het project wordt gekopieerd. Daar zal het programma dat we gaan schrijven de database ophalen.

Nu we een database tot onze beschikking hebben, kunnen we deze gaan gebruiken. Maar eerst nog even een paar herhalingen SQL.

9.3.2. De vier basisopdrachten van de taal SQL

SQL (Structured Language Query) is een gedeeltelijk gestandaardiseerde taal voor het opvragen en bijwerken van databases. Alle SGBD-talen voldoen aan het gestandaardiseerde deel van SQL, maar voegen eigen uitbreidingen toe aan de taal die gebruikmaken van bepaalde bijzonderheden van SGBD. We zijn al twee voorbeelden tegengekomen: het automatisch genereren van primaire sleutels en de toegestane typen voor de kolommen van een tabel zijn vaak afhankelijk van SGBD.

De vier basisopdrachten van de taal SQL die we hier presenteren, zijn gestandaardiseerd en worden door alle SGBD-versies geaccepteerd:

select col1, col2,... 
from table1, table2,...
where condition
order by expression
...
De query waarmee de gegevens uit een database kunnen worden opgehaald. Alleen de trefwoorden in de eerste regel zijn verplicht, de overige zijn optioneel. Er zijn nog andere trefwoorden die hier niet worden weergegeven.
  1. Er wordt een join uitgevoerd met alle tabellen die achter het trefwoord from staan
  2. Alleen de kolommen die achter het trefwoord `select` staan, worden behouden
  3. Alleen de rijen die voldoen aan de voorwaarde van het trefwoord `where` worden behouden
  4. De resulterende rijen, gesorteerd volgens de uitdrukking in het trefwoord `order by`, vormen het resultaat van de query. Dit resultaat is een tabel.
insert into table(col1,col2, ...)
values (val1,val2, ...)
Voegt een rij toe aan de tabel. (col1, col2, ...) geeft de kolommen aan van de rij die moeten worden gevuld met de waarden (val1, val2, ...).
update table
set col1=val1, col2=val2
where condition
Werkt de rijen van de tabel bij die aan de voorwaarde voldoen (alle rijen als er geen WHERE-voorwaarde is). Voor deze rijen krijgt de kolom coli de waarde vali
delete from table
where condition
Verwijdert alle tabelrijen die aan de voorwaarde voldoen

We gaan een console-applicatie schrijven waarmee SQL-opdrachten kunnen worden verzonden naar de database [dbarticles] die we eerder hebben aangemaakt. Hier is een voorbeeld van de uitvoering. De lezer wordt uitgenodigd om de verzonden SQL-opdrachten en hun resultaten te begrijpen.

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 100 10 1

Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 110 10 1

Requête SQL (rien pour arrêter) : delete from articles where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
  • regel 1: de zogenaamde verbindingsstring: deze bevat alle parameters die nodig zijn om verbinding te maken met de database.
  • regel 3: de inhoud van de tabel [articles] wordt opgevraagd
  • regel 16: er wordt een nieuwe regel ingevoegd. Merk op dat het veld id bij deze bewerking niet wordt geïnitialiseerd, omdat het veld SGBD de waarde van dit veld zal genereren.
  • regel 19: controle. Regel 28: de regel is inderdaad toegevoegd.
  • regel 30: de prijs van het zojuist toegevoegde artikel wordt met 10% verhoogd.
  • regel 33: we controleren
  • regel 42: de prijsverhoging heeft inderdaad plaatsgevonden
  • regel 44: we verwijderen het artikel dat we eerder hebben toegevoegd
  • regel 47: we controleren
  • regels 53-55: het artikel is niet meer aanwezig.

9.3.3. De basisinterfaces van ADO.NET voor de verbonden modus

Laten we terugkeren naar het schema van een applicatie die een database gebruikt via een ADO.NET-connector:

In de verbonden modus doet de applicatie het volgende:

  1. een verbinding met de gegevensbron
  2. werkt met de gegevensbron in lees-/schrijfmodus
  3. sluit de verbinding af

Drie ADO.NET-interfaces zijn voornamelijk betrokken bij deze bewerkingen:

  • IDbConnection, die de eigenschappen en methoden van de verbinding omvat.
  • IDbCommand, dat de eigenschappen en methoden van de uitgevoerde opdracht SQL omvat.
  • IDataReader, dat de eigenschappen en methoden van het resultaat van een SQL Select-opdracht omvat.

De interface IDbConnection

Wordt gebruikt om de verbinding met de database te beheren. De methoden M en eigenschappen P van deze interface die we zullen gebruiken, zijn de volgende:

Naam
Type
Rol
ConnectionString
P
verbindingsstring naar de database. Deze bevat alle parameters die nodig zijn om verbinding te maken met een specifieke database.
Open
M
opent de verbinding met de database die is gedefinieerd door ConnectionString
Close
M
sluit de verbinding
BeginTransaction
M
start een transactie.
State
P
verbindingsstatus: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Als Connection een klasse is die de interface IDbConnection implementeert, kan de verbinding als volgt tot stand worden gebracht:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

De interface IDbCommand

Wordt gebruikt om een opdracht SQL of een opgeslagen procedure uit te voeren. De methoden M en eigenschappen P van deze interface die we zullen gebruiken, zijn de volgende:

Naam
Type
Rol
CommandType
P
geeft aan wat er moet worden uitgevoerd – haalt zijn waarden uit een opsomming:
- CommandType.Text: voert de opdracht SQL uit die is gedefinieerd in de eigenschap CommandText. Dit is de standaardwaarde.
- CommandType.StoredProcedure: voert een opgeslagen procedure uit in de database
CommandText
P
- de tekst van de opdracht SQL die moet worden uitgevoerd als CommandType = CommandType.Text
- de naam van de opgeslagen procedure die moet worden uitgevoerd als CommandType = CommandType.StoredProcedure
Connection
P
de verbinding IDbConnection die moet worden gebruikt om de opdracht SQL uit te voeren
Transaction
P
de transactie IDbTransaction waarin de opdracht SQL moet worden uitgevoerd
Parameters
P
de lijst met parameters van een geconfigureerde opdracht SQL. De opdracht update articles set price=price*1.1 where id=@id heeft de parameter @id.
ExecuteReader
M
om een opdracht SQL Select uit te voeren. Dit levert een object IDataReader op dat het resultaat van Select weergeeft.
ExecuteNonQuery
M
om een opdracht SQL Update, Insert, Delete uit te voeren. Hiermee wordt het aantal rijen weergegeven dat door de bewerking is beïnvloed (bijgewerkt, ingevoegd, verwijderd).
ExecuteScalar
M
om een opdracht SQL uit te voeren; Select levert slechts één resultaat op, zoals in: select count(*) from articles.
CreateParameter
M
om de parameters IDbParameter aan te maken voor een geconfigureerde opdracht SQL.
Prepare
M
maakt het mogelijk de uitvoering van een geparametriseerde query te optimaliseren wanneer deze meerdere keren met verschillende parameters wordt uitgevoerd.

Als Command een klasse is die de interface IDbCommand implementeert, ziet de uitvoering van een opdracht SQL zonder transactie er als volgt uit:

// verbinding openen 
IDbConnection connexion=...
connexion.Open();
// opdracht voorbereiden
IDbCommand commande=new Command();
commande.Connection=connexion;
// select-opdracht uitvoeren
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// uitvoering van de opdracht update, insert, delete
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// verbinding verbreken
connexion.Close();

De interface IDataReader

Wordt gebruikt om de resultaten van een opdracht SQL Select in te kapselen. Een object IDataReader vertegenwoordigt een tabel met rijen en kolommen, die achtereenvolgens worden verwerkt: eerst de eerste rij, dan de tweede, ... De methoden M en eigenschappen P van deze interface die we zullen gebruiken, zijn de volgende:

Naam
Type
Rol
FieldCount
P
het aantal kolommen in de tabel IDataReader
GetName
M
GetName(i) geeft de naam weer van kolom nr. i van de tabel IDataReader.
Item
P
Item[i] vertegenwoordigt kolom nr. i van de huidige rij in de tabel IDataReader.
Read
M
gaat naar de volgende regel van de tabel IDataReader. Geeft de booleaanse waarde True terug als het lezen is gelukt, False anders.
Close
M
sluit de tabel IDataReader.
GetBoolean
M
GetBoolean(i): geeft de booleaanse waarde weer van kolom nr. i van de huidige rij in de tabel IDataReader. De andere, vergelijkbare methoden zijn: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): geeft de waarde van kolom nr. i van de huidige rij van de tabel IDataReader weer als type object.
IsDBNull
M
IsDBNull(i) levert True op als kolom nr. i van de huidige rij in de tabel IDataReader geen waarde heeft, wat wordt aangeduid met de waarde SQL NULL.

De verwerking van een object IDataReader ziet er vaak als volgt uit:

// verbinding openen 
IDbConnection connexion=...
connexion.Open();
// opdracht voorbereiden
IDbCommand commande=new Command();
commande.Connection=connexion;
// uitvoering select-opdracht
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// resultaten verwerken
while(reader.Read()){
     // huidige regel verwerken
        ...
}
// reader afsluiten
reader.Close();
// verbinding afsluiten
connexion.Close();

9.3.4. Foutbeheer

Laten we nog eens kijken naar de architectuur van een databaseapplicatie:

De laag [dao] kan tijdens het gebruik van de database talrijke fouten tegenkomen. Deze worden doorgegeven als uitzonderingen die worden gegenereerd door de connector ADO.NET. De code van de laag [dao] moet deze afhandelen. Elke bewerking met de database moet plaatsvinden binnen een try/catch/finally-blok om een eventuele uitzondering op te vangen en af te handelen, en de benodigde bronnen vrij te geven. De hierboven getoonde code voor het verwerken van het resultaat van een opdracht Select ziet er dan als volgt uit:

// verbinding initialiseren
IDbConnection connexion=...
// verbinding verwerken
try{
     // open
    connexion.Open();
     // opdrachtvoorbereiding
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // selectieopdracht uitvoeren
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // verwerking van resultaten
    try{
        while(reader.Read()){
         // huidige regel verwerken
        ...
    }finally{
         // reader sluiten
        reader.Close();
    }
}catch(Exception ex){
     // uitzonderingsafhandeling
    ...
}finally{
     // verbinding afsluiten
    connexion.Close();
}
...

Wat er ook gebeurt, de objecten IDataReader en IDbConnection moeten worden gesloten. Daarom vindt deze sluiting plaats in de clausules finally.

Het afsluiten van de verbinding en dat van het object IDataReader kunnen worden geautomatiseerd met een using-clausule:

// verbinding verwerken
try{
    using(IDbConnection connexion=...){
         // open
        connexion.Open();
         // opdrachtvoorbereiding
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // selectieopdracht uitvoeren
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // resultaten verwerken
            while(reader.Read()){
             // huidige regel verwerken
            ...
        }// met behulp van IData
    }//met behulp van IDbConnection
}catch(Exception ex){
     // uitzonderingsbeheer
    ...
}
..
  • Regel 3: de using-clausule zorgt ervoor dat de verbinding die in het using(...){...}-blok is geopend, buiten dit blok wordt gesloten, ongeacht de manier waarop het blok wordt verlaten: normaal of door het optreden van een uitzondering. We besparen één finally, maar het voordeel zit niet in deze kleine besparing. Door een using te gebruiken, hoeft de ontwikkelaar de verbinding niet zelf te sluiten. Het vergeten van het afsluiten van een verbinding kan echter onopgemerkt blijven en de applicatie op een schijnbaar willekeurige manier laten „crashen”, telkens wanneer het SGBD het maximale aantal openstaande verbindingen bereikt dat het aankan.
  • Regel 11: op dezelfde manier sluiten we het object IDataReader.

9.3.5. Configuratie van het voorbeeldproject

Het uiteindelijke project ziet er als volgt uit:

  • [1]: het project zal een configuratiebestand [App.config] bevatten
  • [2]: dit maakt gebruik van klassen uit twee DLL-bestanden die standaard niet zijn gerefereerd en die daarom moeten worden toegevoegd aan de projectreferenties:
    • [System.Configuration] om het configuratiebestand [App.config] te gebruiken
    • [System.Data.SqlServerCe] om de Sql Server Compact-database te gebruiken
  • [3, 4]: hierin wordt uitgelegd hoe u verwijzingen aan een project toevoegt.
  • [5, 6]: een overzicht van hoe je het bestand [App.config] aan een project toevoegt.

Het configuratiebestand [App.config] ziet er als volgt uit:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
 </connectionStrings>
</configuration>
  • regels 3-5: de tag <connectionStrings> in het meervoud definieert verbindingsstrings voor databases. Een verbindingsstring heeft de vorm "parameter1=waarde1;parameter2=waarde2;...". Hierin worden alle parameters gedefinieerd die nodig zijn om een verbinding met een specifieke database tot stand te brengen. Deze verbindingsstrings veranderen per SGBD. Op de website [http://www.connectionstrings.com/] wordt de vorm hiervan weergegeven voor de belangrijkste SGBD.
  • regel 4: definieert een specifieke verbindingsstring, in dit geval die van de SQL Server Compact-database dbarticles.sdf die we eerder hebben aangemaakt:
    • name = naam van de verbindingsstring. Via deze naam wordt een verbindingsstring door het C#-programma opgehaald
    • connectionString: de verbindingsstring voor een SQL Server Compact-database
    • DataSource: geeft het pad naar de database aan. De syntaxis |DataDirectory| verwijst naar de uitvoermap van het project.
    • Wachtwoord: het wachtwoord van de database. Deze parameter ontbreekt als er geen wachtwoord is.

De C#-code om de vorige verbindingsstring op te halen is als volgt:


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager is de klasse van de DLL [System.Configuration] waarmee het bestand [App.config] kan worden gebruikt.
  • ConnectionsStrings["nom"].ConnectionString: verwijst naar het attribuut connectionString van de tag < add name="naam" connectionString="..."> in de sectie <connectionStrings> van [App.config]

Het project is nu geconfigureerd. We gaan nu de klasse [Program.cs] bekijken, waarvan we eerder een uitvoervoorbeeld hebben gezien.

9.3.6. Het voorbeeldprogramma

Het programma [program.cs] ziet er als volgt uit:


using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // console-applicatie – voert SQL-verzoeken uit die via het toetsenbord zijn ingevoerd
            // in een database waarvan de verbindingsstring uit een configuratiebestand wordt gehaald

            // verwerking van het configuratiebestand [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // weergave van de verbindingsstring
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // er wordt een woordenboek samengesteld van de geaccepteerde SQL-opdrachten
            string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
            Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
            for (int i = 0; i < commandesSQL.Length; i++) {
                dicoCommandes.Add(commandesSQL[i], true);
            }

            // lezen en uitvoeren van de via het toetsenbord ingevoerde opdrachten SQL
            string requête = null;    // tekst van de query SQL
            string[] champs;            // de velden van de query    
            Regex modèle = new Regex(@"\s+");    // reeks spaties

            // lus voor het invoeren en uitvoeren van de via het toetsenbord ingevoerde opdrachten SQL
            while (true) {
                // verzoek van de query
                Console.Write("\nRequête SQL (rien pour arrêter) : ");
                requête = Console.ReadLine().Trim().ToLower();
                // klaar?
                if (requête == "")
                    break;
                // de aanvraag wordt opgesplitst in velden
                champs = modèle.Split(requête);
                // geldige aanvraag?
                if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
                    // foutmelding
                    Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
                    // volgende verzoek
                    continue;
                }
                // het verzoek wordt uitgevoerd
                if (champs[0] == "select") {
                    ExecuteSelect(connectionString, requête);
                } else
                    ExecuteUpdate(connectionString, requête);
            }
        }

        // uitvoering van een updateverzoek
        static void ExecuteUpdate(string connectionString, string requête) {
    ...
        }

        // uitvoering van een Select-query
        static void ExecuteSelect(string connectionString, string requête) {
....
        }
    }
}
  • regels 1-6: de naamruimten die in de toepassing worden gebruikt. Voor het beheer van een SQL Server Compact-database is de naamruimte [System.Data.SqlServerCe] uit regel 3 vereist. Hier is sprake van een afhankelijkheid van een eigen naamruimte van SGBD. Hieruit kan worden afgeleid dat het programma moet worden aangepast als de SGBD wordt gewijzigd.
  • regel 18: de verbindingsstring naar de database wordt uit het bestand [App.config] gelezen en op regel 25 weergegeven. Deze wordt gebruikt voor het tot stand brengen van een verbinding met de database.
  • regels 28-32: een woordenboek waarin de namen van de vier toegestane SQL-opdrachten zijn opgeslagen: select, insert, update, delete.
  • regels 40-62: de lus voor het invoeren van de via het toetsenbord ingevoerde SQL-opdrachten en de uitvoering ervan in de database
  • regel 48: de via het toetsenbord ingevoerde regel wordt opgesplitst in velden om te bepalen wat het eerste onderdeel moet zijn: select, insert, update, delete
  • regels 50-55: als de query ongeldig is, wordt er een foutmelding weergegeven en gaat men verder met de volgende query.
  • regels 57-61: de ingevoerde opdracht SQL wordt uitgevoerd. Deze uitvoering verloopt anders naargelang het gaat om een opdracht select of om een opdracht insert, update, delete. In het eerste geval haalt de opdracht gegevens uit de database zonder deze te wijzigen, in het tweede geval werkt de opdracht de database bij zonder gegevens op te halen. In beide gevallen wordt de uitvoering gedelegeerd aan een methode die twee parameters nodig heeft:
    • de verbindingsstring waarmee verbinding met de database kan worden gemaakt
    • de opdracht SQL die via deze verbinding moet worden uitgevoerd

9.3.7. Uitvoering van een query SELECT

Voor het uitvoeren van opdrachten van het type SQL zijn de volgende stappen nodig:

  1. Verbinding maken met de database
  2. Verzending van de opdrachten SQL naar de database
  3. Verwerking van de resultaten van de opdracht SQL
  4. De verbinding afsluiten

Stappen 2 en 3 worden herhaaldelijk uitgevoerd, waarbij de verbinding pas wordt verbroken aan het einde van de databaseverwerking. Openstaande verbindingen zijn beperkte hulpbronnen van een SGBD. Hiermee moet zuinig worden omgegaan. Daarom zal er altijd naar worden gestreefd de levensduur van een geopende verbinding te beperken. In het onderzochte voorbeeld wordt de verbinding na elke opdracht SQL gesloten. Voor de volgende opdracht SQL wordt een nieuwe verbinding geopend. Het openen en sluiten van een verbinding is kostbaar. Om deze kosten te verlagen, bieden sommige SGBD-systemen het concept van pools met geopende verbindingen: bij het opstarten van de applicatie worden N verbindingen geopend en aan de pool toegewezen. Deze blijven open tot het einde van de applicatie. Wanneer de applicatie een verbinding opent, ontvangt deze een van de N reeds geopende verbindingen uit de pool. Wanneer de verbinding wordt gesloten, wordt deze eenvoudigweg teruggeplaatst in de pool. Het voordeel van dit systeem is dat het transparant is voor de ontwikkelaar: het programma hoeft niet te worden aangepast om gebruik te maken van de verbindingspool. De configuratie van de verbindingspool is afhankelijk van SGBD.

We kijken eerst naar de uitvoering van de opdrachten SQL en Select. De methode ExecuteSelect van ons voorbeeldprogramma is als volgt:


// uitvoering van een SELECT-query
        static void ExecuteSelect(string connectionString, string requête) {
            // eventuele uitzonderingen worden afgehandeld
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // verbinding openen
                    connexion.Open();
                    // voert sqlCommand uit met een SELECT-query
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    SqlCeDataReader reader= sqlCommand.ExecuteReader();
                    // weergave van de resultaten
                    AfficheReader(reader);
                }
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // reader weergeven
        static void AfficheReader(IDataReader reader) {
...
        }
  • regel 2: de methode ontvangt twee parameters:
    • de verbindingsstring [connectionString], waarmee verbinding met de database kan worden gemaakt
    • de opdracht SQL Select [requête] die via deze verbinding moet worden uitgevoerd
  • regel 4: elke bewerking met een database kan een uitzondering genereren die men wellicht wil afhandelen. Dit is hier des te belangrijker omdat de door de gebruiker opgegeven opdrachten SQL syntactisch onjuist kunnen zijn. We moeten de gebruiker hiervan op de hoogte kunnen stellen. De volledige code staat daarom binnen een try/catch-blok.
  • regel 5: hier gebeuren verschillende dingen:
    • de verbinding met de database wordt geïnitialiseerd met de verbindingsstring [connectionString]. Deze is nog niet geopend. Dat gebeurt in regel 7.
    • De using-clausule (Ressource) {...} is een syntactische hulp die ervoor zorgt dat de resource Ressource, in dit geval een verbinding, wordt vrijgegeven bij het verlaten van het blok dat wordt bestuurd door using.
    • De verbinding is van een eigen type: SqlCeConnection, specifiek voor de SGBD SQL Server Compact.
  • regel 7: de verbinding is geopend. Op dit moment worden de parameters van de verbindingsstring gebruikt.
  • regel 9: er wordt een opdracht SQL verzonden via een eigen object SqlCeCommand. Regel 9 initialiseert dit object met twee gegevens: de te gebruiken verbinding en de opdracht SQL die eroverheen moet worden verzonden. Het object SqlCeCommand dient zowel voor het uitvoeren van een opdracht Select als van een opdracht Update, Insert of Delete. De eigenschappen en methoden ervan zijn beschreven in paragraaf 9.3.3.
  • regel 10: een opdracht SQL Select wordt uitgevoerd via de methode ExecuteReader van hetobject SqlCeCommand, dat een object IDataReader retourneert waarvan de methoden en eigenschappen in paragraaf 9.3.3 zijn beschreven.
  • regel 12: de weergave van de resultaten wordt toevertrouwd aan de volgende methode AfficheReader:

        // weergave reader
        static void AfficheReader(IDataReader reader) {
            using (reader) {
                // verwerking van de resultaten
                // -- kolommen
                StringBuilder ligne = new StringBuilder();
                int i;
                for (i = 0; i < reader.FieldCount - 1; i++) {
                    ligne.Append(reader.GetName(i)).Append(",");
                }
                ligne.Append(reader.GetName(i));
                Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
                // -- gegevens
                while (reader.Read()) {
                    // verwerking van de huidige regel
                    ligne = new StringBuilder();
                    for (i = 0; i < reader.FieldCount; i++) {
                        ligne.Append(reader[i].ToString()).Append(" ");
                    }
                    Console.WriteLine(ligne);
                }
            }
}
  • regel 2: de methode ontvangt een object IDataReader. Merk op dat we hier een interface hebben gebruikt en geen specifieke klasse.
  • regel 3: de clausule using wordt gebruikt om het sluiten van het object IDataReader automatisch af te handelen.
  • regels 8-10: de kolomnamen van de resultatentabel van Select worden weergegeven. Dit zijn de kolommen coli van de query `select col1, col2, ... from table ...`
  • regels 14-21: de resultatentabel wordt doorlopen en de waarden van elke rij van de tabel worden weergegeven.
  • regel 18: het type van kolom nr. i in de resultaten is onbekend, omdat de opgevraagde tabel onbekend is. De syntaxis reader.GetXXX(i), waarbij XXX het type van kolom nr. i is, kan dus niet worden gebruikt, omdat dit type onbekend is. We gebruiken dan de syntaxis reader.Item[i].ToString() om de weergave van kolom nr. i in de vorm van een tekenreeks te verkrijgen. De syntaxis reader.Item[i].ToString() kan worden afgekort tot reader[i].ToString().

9.3.8. Een updateopdracht uitvoeren: INSERT, UPDATE, DELETE

De code van de methode ExecuteUpdate is als volgt:


        // uitvoering van een bijwerkingsverzoek
        static void ExecuteUpdate(string connectionString, string requête) {
            // eventuele uitzonderingen worden afgehandeld
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // verbinding openen
                    connexion.Open();
                    // sqlCommand uitvoeren met een updateverzoek
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // weergave van het resultaat
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
}

We hebben gezegd dat de uitvoering van een opvraagopdracht Select niet verschilt van die van een updateopdracht Update, Insert, Delete alleen verschilt in de gebruikte methode van het object SqlCeCommand: ExecuteReader voor Select, ExecuteNonQuery voor Update, Insert, Delete. We bespreken alleen deze laatste methode in de bovenstaande code:

  • regel 10: de opdracht Update, Insert, Delete wordt uitgevoerd door de methode ExecuteNonQuery van het object SqlCeCommand. Als deze methode slaagt, geeft ze het aantal bijgewerkte (update), ingevoegde (insert) of verwijderde (delete) regels terug.
  • regel 12: dit aantal regels wordt op het scherm weergegeven

De lezer wordt verzocht een voorbeeld van de uitvoering van deze code te bekijken in paragraaf 9.3.2.

9.4. Andere connectoren ADO.NET

De code die we hebben bestudeerd is propriëtair: deze is afhankelijk van de naamruimte [System.Data.SqlServerCe], bedoeld voor de SGBD SQL Server Compact. We gaan nu hetzelfde programma bouwen met verschillende .NET-connectoren en kijken wat er verandert.

9.4.1. Connector SQL Server 2005

De gebruikte architectuur is als volgt:

De installatie van SQL Server 2005 wordt beschreven in de bijlagen bij paragraaf 1.1.

We maken een tweede project aan in dezelfde oplossing als eerder en vervolgens maken we de database SQL Server 2005 aan. De SGBD SQL Server 2005 moet worden gestart voordat de volgende handelingen worden uitgevoerd:

  • [1]: maak een nieuw project aan in de huidige oplossing en stel dit in als het huidige project.
  • [2]: maak een nieuwe verbinding aan
  • [3]: het type verbinding selecteren
  • [4]: de SGBD SQL-server selecteren
  • [5]: resultaat van de vorige keuze
  • [6]: gebruik de knop [Browse] om aan te geven waar de database SQL Server 2005 moet worden aangemaakt. De database is ingekapseld in een .mdf-bestand.
  • [7]: kies de hoofdmap van het nieuwe project en noem de database [dbarticles.mdf].
  • [8]: gebruik Windows-authenticatie.
  • [9]: de informatiepagina bevestigen
  • [11]: de database SQL Server
  • [12]: maak een tabel aan. Deze zal identiek zijn aan de eerder aangemaakte SQL Server Compact-database.
  • [13]: het veld [id]
  • [14]: het veld [id] is van het type Identity.
  • [15,16]: het veld [id] is de primaire sleutel
  • [17]: de overige velden van de tabel
  • [18]: geef de tabel de naam [articles] bij het opslaan (Ctrl+S).

Nu moeten we nog gegevens in de tabel invoeren:

We voegen de database toe aan het project:

De projectreferenties zijn als volgt:

Het configuratiebestand [App.config] is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
     <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
 </connectionStrings>
</configuration>
  • regel 4: de verbindingsstring naar de database [dbarticles.mdf] met Windows-authenticatie
  • regel 5: de verbindingsstring naar de database [dbarticles.mdf] met SQL Server-authenticatie. [sa,msde] is de combinatie (login, wachtwoord) van de beheerder van de SQL Server, zoals gedefinieerd in paragraaf 1.1.

Het programma [Program.cs] verloopt als volgt:


using System.Data.SqlClient;
...

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {
...
            // verwerking van het configuratiebestand [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
            } catch (Exception e) {
...
            }
...
            // lezen en uitvoeren van via het toetsenbord ingevoerde SQL-opdrachten
...
        }

        // uitvoering van een updateverzoek
        static void ExecuteUpdate(string connectionString, string requête) {
            // eventuele uitzonderingen worden afgehandeld
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // verbinding openen
                    connexion.Open();
                    // voert sqlCommand uit met een updateverzoek
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // weergave van het resultaat
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
....
            }
        }

        // uitvoering van een SELECT-query
        static void ExecuteSelect(string connectionString, string requête) {
            // eventuele uitzonderingen worden afgehandeld
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // verbinding openen
                    connexion.Open();
                    // voert sqlCommand uit met een SELECT-query
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    SqlDataReader reader = sqlCommand.ExecuteReader();
                    // verwerking van de resultaten
...
                }
            } catch (Exception ex) {
...
            }
        }
    }
}
  • regel 1: de naamruimte [System.Data.SqlClient] bevat de klassen waarmee een SQL Server 2005-database kan worden beheerd
  • regel 24: de verbinding is van het type SQLConnection
  • regel 28: het object dat de opdrachten SQL omvat, is van het type SQLCommand
  • regel 47: het object dat het resultaat van een SQL Select-opdracht omvat, is van het type SQLDataReader

De code is identiek aan die welke wordt gebruikt met de SGBD SQL Server Compact, op de namen van de klassen na. Om deze uit te voeren, kan men (regel 11) een van de twee verbindingsstrings gebruiken die zijn gedefinieerd in [App.config].

9.4.2. Connector MySQL5

De gebruikte architectuur is als volgt:

De installatie van MySQL5 wordt beschreven in de bijlagen bij paragraaf 1.2 en die van de connector Ado.Net in paragraaf 1.2.5.

We maken een derde project aan in dezelfde oplossing als eerder en voegen de benodigde verwijzingen toe:

  • [1]: het nieuwe project
  • [2]: waaraan we verwijzingen toevoegen
  • [3]: de DLL, [MySQL.Data] van de connector Ado.Net van MySql5, evenals die van [System.Configuration], [4].

We maken nu de database [dbarticles] en de bijbehorende tabel [articles] aan. De SGBD en MySQL5 moeten worden gestart. Daarnaast starten we de client [Query Browser] (zie paragraaf 1.2.3).

  • [1]: in [Query Browser], klik met de rechtermuisknop in het gebied [Schemata] [2] om [3], een nieuw schema (een term die verwijst naar een database), aan te maken.
  • [4]: de database krijgt de naam [dbarticles]. In [5] is deze zichtbaar. De database bevat momenteel nog geen tabellen. We gaan het volgende script SQL uitvoeren:
/* selectie van de huidige database */
USE dbarticles;
/* aanmaken van de artikeltabel */
CREATE TABLE ARTICLES (
    ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          DOUBLE PRECISION NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* gegevens invoegen in de tabel */
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
/* toevoegen van beperkingen */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • regel 1: de database [dbarticles] wordt de huidige database. De volgende opdrachten SQL worden hierop uitgevoerd.
  • regels 4-10: definitie van de tabel [ARTICLES]. Let op: SQL is de eigenaar van MySQL. De kolomtypen en de automatische generatie van de primaire sleutel (attribuut AUTO_INCREMENT) verschillen van wat we zagen bij de SGBD en SQL in Server Compact en Express.
  • regels 12-14: toevoeging van drie regels
  • regels 16-21: toevoeging van integriteitsbeperkingen op de kolommen.

Dit script wordt uitgevoerd in [MySQL Query Browser]:

  • in [MySQL Query Browser] [6] wordt het script [7] geladen. Dit is te zien in [8]. In [9] wordt het uitgevoerd.
  • in [10] is de tabel [articles] aangemaakt. We dubbelklikken erop. Hierdoor verschijnt het venster [11] met daarin de query [12], klaar om te worden uitgevoerd door [13]. In [14] zien we het resultaat van de uitvoering. We zien inderdaad de drie verwachte rijen. Merk op dat de waarden van het veld [ID] automatisch zijn gegenereerd (attribuut AUTO_INCREMENT van het veld).

Nu de database klaar is, kunnen we terugkeren naar de ontwikkeling van de applicatie in Visual Studio.

In [1] bevinden zich het programma [Program.cs] en het configuratiebestand [App.config]. Dit ziet er als volgt uit:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
 </connectionStrings>
</configuration>

Op regel 4 zijn de onderdelen van de verbindingsstring als volgt:

  • Server: de naam van de machine waarop SGBD, MySQL, hier localhost en c.a.d staan; de machine waarop het programma zal worden uitgevoerd.
  • Database: de naam van de beheerde database, hier dbarticles
  • Uid: de gebruikersnaam, hier root
  • Pwd: zijn wachtwoord, hier root. Deze twee gegevens verwijzen naar de beheerder die in paragraaf 1.2 is aangemaakt.

Het programma [Program.cs] is identiek aan dat van de vorige versies, op de volgende details na:

espace de noms
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

Het programma gebruikt de verbindingsstring met de naam dbArticlesMySql5 in het bestand [App.config]. De uitvoering levert de volgende resultaten op:

Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.3. Connector ODBC

De gebruikte architectuur is als volgt:

Het voordeel van de connectoren ODBC is dat ze een standaardinterface bieden aan de applicaties die er gebruik van maken. Zo zal de nieuwe applicatie, met één enkele code, kunnen communiceren met elke SGBD die een connector ODBC, c.a.d of de meeste SGBD heeft. De prestaties van de ODBC-connectoren zijn minder goed dan die van de „proprietaire” connectoren, die alle kenmerken van een specifieke SGBD kunnen benutten. Daar staat tegenover dat de applicatie zeer flexibel is: men kan van SGBD wisselen zonder de code te wijzigen.

We bekijken een voorbeeld waarbij de applicatie gebruikmaakt van een MySQL5-database of een SQL Server Express-database, afhankelijk van de verbindingsreeks die wordt opgegeven. Hieronder gaan we ervan uit dat:

  • de SGBD, SQL (Express-server) en MySQL5 zijn gestart
  • dat de driver van ODBC voor MySQL5 op de machine aanwezig is (zie paragraaf 1.2.6). Die van SQL Server 2005 is standaard aanwezig.
  • De gebruikte databases zijn die uit paragraaf 9.4.2 voor de database MySQL5, en die uit paragraaf 9.4.1 voor de database SQL Server Express.

Het nieuwe Visual Studio-project is als volgt:

Hierboven is de database SQL Server [dbarticles.mdf], die in paragraaf 9.4.1 is aangemaakt, gekopieerd naar de projectmap.

Het configuratiebestand [App.config] is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
     <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • regel 4: de verbindingsstring van de bron ODBC MySQL5. Dit is een string die we al eerder hebben besproken en waarin een nieuwe parameter Driver voorkomt die het te gebruiken stuurprogramma ODBC definieert.
  • regel 5: de verbindingsstring van de bron ODBC SQL Server Express. Dit is de string die al in een eerder voorbeeld werd gebruikt en waaraan de parameter Driver is toegevoegd.

Het programma [Program.cs] is identiek aan dat van de vorige versies, op de volgende details na:

espace de noms
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
OdbcDataReader

Het programma gebruikt een van de twee verbindingsstrings die zijn gedefinieerd in het bestand [App.config]. De uitvoering levert de volgende resultaten op:

Met de verbindingsstring [dbArticlesOdbcSqlServer2005]:

Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 500,0000 10 5
2 pompe 10,0000 10 2
3 arc 610,0000 4 1
4 flèches - lot de 6 100,0000 12 20
5 combinaison de plongée 300,0000 8 2
6 Bouteilles d'oxygène 120,0000 10 5

Met de verbindingsstring [dbArticlesOdbcMySql5]:

Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.4. Connector OLE DB

De gebruikte architectuur is als volgt:

Net als de connectoren ODBC bieden de connectoren OLE en DB (Object Linking and Embedding DataBase) een standaardinterface voor de toepassingen die er gebruik van maken. De stuurprogramma’s ODBC bieden toegang tot databases. De gegevensbronnen voor de stuurprogramma’s OLE en DB zijn gevarieerder: databases, e-mailsystemen, adresboeken, ... Elke gegevensbron kan het onderwerp zijn van een Ole-driver DB als een ontwikkelaar daarvoor kiest. Zo ontstaat er standaardtoegang tot een grote verscheidenheid aan gegevens.

We bekijken een voorbeeld waarbij de toepassing gebruikmaakt van een ACCESS-database of een SQL Server Express-database, afhankelijk van de verbindingsreeks die wordt opgegeven. Hieronder gaan we ervan uit dat de SGBD SQL Server Express is gestart en dat de gebruikte database dezelfde is als in het vorige voorbeeld.

Het nieuwe Visual Studio-project ziet er als volgt uit:

  • in [1]: de naamruimte die nodig is voor de connectoren OLE en DB is [System.Data.OleDb], die voorkomt in de bovenstaande verwijzing [System.Data]. De database SQL Server [dbarticles.mdf] is gekopieerd vanuit het vorige project. De database [dbarticles.mdb] is aangemaakt met Access.
  • In [2]: net als de database SQL Server heeft de database ACCESS de eigenschap [Copy to Output Directory=Copy Always], zodat deze automatisch wordt gekopieerd naar de uitvoermap van het project.

De database ACCESS [dbarticles.mdb] ziet er als volgt uit:

In [1] staat de structuur van de tabel [articles] en in [2] de inhoud ervan.

Het configuratiebestand [App.config] is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
     <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • regel 4: de verbindingsstring van de bron OLE DB ACCESS. Hierin staat de parameter Provider, die de te gebruiken driver OLE DB definieert, evenals het pad naar de database
  • regel 5: de verbindingsstring van de OLE DB Server Express-bron.

Het programma [Program.cs] is identiek aan dat van de vorige versies, op de volgende details na:

espace de noms
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
OleDbDataReader

Het programma gebruikt een van de twee verbindingsstrings die zijn gedefinieerd in het bestand [App.config]. De uitvoering levert de volgende resultaten op met de verbindingsstring [dbArticlesOleDbAccess]:

Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 1202 5 2
2 arc 5000 10 2
3 canoé 1502 12 6
4 fusil 3000 10 2
5 skis nautiques 1800 5 2
6 essai3 3 3 3
7 cachalot 200000 1 0
8 léopard 500000 1 1
9 panthère 800000 1 1

9.4.5. Generieke connector

De gebruikte architectuur is als volgt:

Net als de connectoren ODBC, OLE en DB biedt de generieke connector een standaardinterface aan de applicaties die er gebruik van maken, maar verbetert hij de prestaties zonder in te boeten aan flexibiliteit. De generieke connector is namelijk gebaseerd op de propriëtaire connectoren van de SGBD-serie. De applicatie maakt gebruik van klassen van de generieke connector. Deze klassen fungeren als tussenlaag tussen de applicatie en de propriëtaire connector.

Wanneer de toepassing bijvoorbeeld een verbinding met de generieke connector aanvraagt, retourneert deze een instantie van IDbConnection, de verbindingsinterface die in paragraaf 9.3.3 wordt beschreven, geïmplementeerd door een klasse MySQLConnection of SQLConnection, afhankelijk van de aard van het verzoek dat aan de connector is gedaan. Men zegt dat de generieke connector factory-klassen heeft: men gebruikt een factory-klasse om hem te vragen objecten aan te maken en er verwijzingen (pointers) naar te geven. Vandaar de naam (factory = fabriek, fabriek voor het produceren van objecten).

Er bestaat geen generieke connector voor alle SGBD (april 2008). Om te zien welke op een machine zijn geïnstalleerd, kan het volgende programma worden gebruikt:


using System;
using System.Data;
using System.Data.Common;

namespace Chap7 {
    class Providers {
        public static void Main() {
            DataTable dt = DbProviderFactories.GetFactoryClasses();
            foreach (DataColumn col in dt.Columns) {
                Console.Write("{0}|", col.ColumnName);
            }
            Console.WriteLine("\n".PadRight(40, '-'));
            foreach (DataRow row in dt.Rows) {
                foreach (object item in row.ItemArray) {
                    Console.Write("{0}|", item);
                }
                Console.WriteLine("\n".PadRight(40, '-'));
            }
        }
    }
}
  • regel 8: de statische methode [DbProviderFactories.GetFactoryClasses()] geeft de lijst met geïnstalleerde generieke connectoren weer, in de vorm van een databasetabel die in het geheugen is geplaatst (DataTable).
  • regels 9-11: geven de kolomnamen van de tabel dt weer:
    • dt.Columns is de lijst met kolommen van de tabel. Een kolom C is van het type DataColumn
    • [DataColumn]. ColumnName is de naam van de kolom
  • regels 13-18: geven de rijen van de tabel weer dt:
    • dt.Rows is de lijst met rijen van de tabel. Een rij L is van het type DataRow
    • [DataRow]. ItemArray is een array van objecten waarbij elk object een kolom van de rij vertegenwoordigt

Het resultaat van de uitvoering op mijn computer is als volgt:

Name|Description|InvariantName|AssemblyQualifiedName|
---------------------------------------
Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
---------------------------------------
MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • regel 1: de tabel heeft vier kolommen. De eerste drie zijn hier voor ons het nuttigst.

De volgende weergave laat zien dat we over de volgende generieke connectoren beschikken:

Naam
ID
Odbc Data Provider
System.Data.Odbc
OleDb Data Provider
System.Data.OleDb
OracleClient Data Provider
System.Data.OracleClient
SqlClient Data Provider
System.Data.SqlClient
Microsoft SQL Server Compact Data Provider
System.Data.SqlServerCe.3.5
MySQL Data Provider
MySql.Data.MySqlClient

Een generieke connector is in een C#-programma toegankelijk via zijn ID.

We bekijken een voorbeeld waarin de applicatie gebruikmaakt van de verschillende databases die we tot nu toe hebben opgebouwd. De applicatie ontvangt twee parameters:

  • de eerste parameter specificeert het type SGBD dat wordt gebruikt, zodat de juiste klassenbibliotheek wordt ingezet
  • de tweede parameter specificeert de te beheren database via een verbindingsstring.

Het nieuwe Visual Studio-project ziet er als volgt uit:

  • in [1]: de naamruimte die nodig is voor de generieke connectoren is [System.Data.common], aanwezig in de referentie [System.Data].

Het configuratiebestand [App.config] is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
        <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
        <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
        <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
    </connectionStrings>
    <appSettings>
        <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
        <add key="factoryMySql" value="MySql.Data.MySqlClient"/>
        <add key="factorySqlServer" value="System.Data.SqlClient"/>
        <add key="factoryOdbc" value="System.Data.Odbc"/>
        <add key="factoryOleDb" value="System.Data.OleDb"/>
    </appSettings>
</configuration>
  • regels 3-11: de verbindingsstrings van de verschillende gebruikte databases.
  • regels 13-17: de namen van de te gebruiken generieke connectoren

Het programma [Program.cs] ziet er als volgt uit:


...
using System.Data.Common;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // console-applicatie – voert SQL-query’s uit die via het toetsenbord worden ingevoerd
            // in een database waarvan de verbindingsstring wordt opgehaald uit een configuratiebestand, evenals de naam van de bijbehorende SGBD-connector

            // controle van de instellingen
            if (args.Length != 2) {
                Console.WriteLine("Syntaxe : pg factory connectionString");
                return;
            }

            // verwerking van het configuratiebestand
            string factory = null;
            string connectionString = null;
            DbProviderFactory connecteur = null;
            try {
                // factory
                factory = ConfigurationManager.AppSettings[args[0]];
                // verbindingsreeks
                connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
                // er wordt een generieke connector opgehaald voor de SGBD
                connecteur = DbProviderFactories.GetFactory(factory);
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // weergaven
            Console.WriteLine("Provider factory : [{0}]\n", factory);
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

...
                // uitvoering van de query
                if (champs[0] == "select") {
                    ExecuteSelect(connecteur,connectionString, requête);
                } else
                    ExecuteUpdate(connecteur, connectionString, requête);
            }
        }

        // uitvoering van een updateverzoek
        static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
            // eventuele uitzonderingen worden afgehandeld
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // verbinding configureren
                    connexion.ConnectionString = connectionString;
                    // verbinding tot stand brengen
                    connexion.Open();
                    // Configuratie van het commando 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // verzoek uitvoeren
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // weergave van het resultaat
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // uitvoering van een Select-query
        static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
            // eventuele uitzonderingen afhandelen
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // verbinding configureren
                    connexion.ConnectionString = connectionString;
                    // verbinding tot stand brengen
                    connexion.Open();
                    // Command-configuratie 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // query uitvoeren
                    DbDataReader reader = sqlCommand.ExecuteReader();
                    // weergave van de resultaten
...
                }
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
    }
}
  • regels 12-14: de toepassing ontvangt twee parameters: de naam van de generieke connector en de verbindingsstring naar de database in de vorm van sleutels uit het bestand [App.config].
  • regels 23, 25: in [App.config] worden de naam van de generieke connector en de verbindingsstring opgehaald
  • regel 27: de generieke connector wordt geïnstantieerd. Vanaf dat moment wordt deze gekoppeld aan een specifieke SGBD.
  • regels 39-43: de uitvoering van de via het toetsenbord ingevoerde opdracht SQL wordt gedelegeerd aan twee methoden waaraan het volgende wordt doorgegeven:
    • de uit te voeren query
    • de verbindingsstring die de database identificeert waarop de query zal worden uitgevoerd
    • de generieke connector die de klassen identificeert die moeten worden gebruikt om te communiceren met de SGBD die de database beheert.
  • regels 50-54: er wordt een verbinding tot stand gebracht met de methode CreateConnection (regel 50) van de generieke connector, waarna deze wordt geconfigureerd met de verbindingsstring van de te beheren database (regel 52). Vervolgens wordt de verbinding geopend (regel 54).
  • regels 56-58: het object Command dat nodig is voor de uitvoering van de opdracht SQL wordt aangemaakt met de methode CreateCommand van de generieke connector. Vervolgens wordt het geconfigureerd met de tekst van de uit te voeren opdracht SQL (regel 57) en de verbinding waarop deze moet worden uitgevoerd (regel 58).
  • regel 60: de updateopdracht SQL wordt uitgevoerd
  • regels 74-87: hier vinden we vergelijkbare code. De nieuwigheid bevindt zich op regel 84. Het object Reader, verkregen door de uitvoering van de opdracht Select, is van het type DbDataReader, dat op dezelfde manier wordt gebruikt als de objecten OleDbDataReader, OdbcDataReader, ... die we al eerder zijn tegengekomen.

Hier volgen enkele voorbeelden van uitvoeringen.

Met de basis MySQL5:

We openen de eigenschappenpagina van het project [1] en selecteren het tabblad [Debug] [2]. In [3]: de connector-ID van regel 14 van [App.config]. In [4] is de sleutel van de verbindingsreeks op regel 6 van [App.config]. De resultaten van de uitvoering zijn als volgt:

Provider factory : [MySql.Data.MySqlClient]
Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

Met de database SQL Server Compact:

In [1] is de connector-sleutel in regel 13 van [App.config]. In [2], de sleutel van de verbindingsstring uit regel 4 van [App.config]. De resultaten van de uitvoering zijn als volgt:

Provider factory : [System.Data.SqlServerCe.3.5]
Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

De lezer wordt verzocht de andere databases te testen.

9.4.6. Welke connector moet je kiezen?

Laten we terugkeren naar de architectuur van een applicatie met databases:

We hebben verschillende soorten connectoren bekeken ADO.NET:

  • proprietaire connectoren presteren het best, maar maken de laag [dao] afhankelijk van propriëtaire klassen. Het wijzigen van de SGBD houdt in dat de laag [dao] moet worden aangepast.
  • Met de connectoren ODBC, OLE of DB kan met meerdere databases worden gewerkt zonder de [dao]-laag te wijzigen. Ze presteren minder goed dan de propriëtaire connectoren.
  • De generieke connector is gebaseerd op de propriëtaire connectoren, maar biedt tegelijkertijd een standaardinterface naar de [dao]-laag.

Het lijkt er dus op dat de generieke connector de ideale connector is. In de praktijk slaagt de generieke connector er echter niet in om alle bijzonderheden van een SGBD achter een standaardinterface te verbergen. In de volgende paragraaf zullen we het begrip ‘geparametriseerde query’ bespreken. Met SQL Server heeft een geparametriseerde query de volgende vorm:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Met MySQL5 zou dezelfde query als volgt worden geschreven:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Er is dus een verschil in syntaxis. De eigenschap van de interface IDbCommand, beschreven in paragraaf 9.3.3, die betrekking heeft op de parameters, is als volgt:

Parameters
de lijst met parameters van een geconfigureerde opdracht SQL. De opdracht update articles set price=price*1.1 where id=@id heeft de parameter @id.

De eigenschap Parameters is van het type IDataParameterCollection, een interface. Deze vertegenwoordigt de verzameling parameters van de opdracht SQL CommandText. De eigenschap Parameters heeft een methode Add om parameters van het type IDataParameter toe te voegen, opnieuw een interface. Deze heeft de volgende eigenschappen:

  • ParameterName: naam van de parameter
  • DbType: het type SQL van de parameter
  • Value: de waarde die aan de parameter is toegewezen
  • ...

Het type IDataParameter is geschikt voor de parameters van de opdracht SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

omdat daar benoemde parameters in voorkomen. De eigenschap ParameterName kan worden gebruikt.

Het type IDataParameter is niet geschikt voor de opdracht SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

omdat de parameters geen naam hebben. Er wordt dan gekeken naar de volgorde waarin de parameters aan de verzameling [IDbCommand.Parameters] zijn toegevoegd. In dit voorbeeld moeten de 4 parameters worden ingevoegd in de volgorde nom, prix, stockactuel, stockminimum. Bij een verzoek met benoemde parameters maakt de volgorde waarin de parameters worden toegevoegd niet uit. Uiteindelijk kan de ontwikkelaar niet volledig voorbijgaan aan de SGBD die hij gebruikt bij het initialiseren van de parameters van een geparametriseerd verzoek. Dit is een van de huidige beperkingen van de generieke connector.

Er bestaan frameworks-codes die deze beperkingen omzeilen en bovendien nieuwe functionaliteiten toevoegen aan de [dao]-laag:

Een framework is een verzameling klassenbibliotheken die bedoeld is om een bepaalde manier van het ontwerpen van de applicatie te vergemakkelijken. Er bestaan er verschillende waarmee [dao]-lagen kunnen worden geschreven die zowel performant zijn als ongevoelig voor wijzigingen in SGBD:

  • Spring.Net [http://www.springframework.net/], dat al in dit document is besproken, biedt het equivalent van de onderzochte generieke connector, zonder de beperkingen daarvan, evenals diverse voorzieningen die de toegang tot gegevens vereenvoudigen. Er bestaat een Java-versie.
  • iBatis.Net [http://ibatis.apache.org] is ouder en uitgebreider dan Spring.Net. Er is een Java-versie beschikbaar.
  • NHibernate [http://www.hibernate.org/] is een port van de in de Java-wereld zeer bekende Java-versie van Hibernate. NHibernate stelt de laag [dao] in staat om te communiceren met SGBD zonder SQL-opdrachten te verzenden. De laag [dao] werkt met Hibernate-objecten. Met behulp van een querytaal HBL (Hibernate Query Language) kunnen query’s worden uitgevoerd op de door Hibernate beheerde objecten. Het zijn deze objecten die de SQL-opdrachten verzenden. Hibernate kan zich aanpassen aan de eigen SQL-opdrachten van de SGBD-opdrachten.
  • LINQ (Language INtegrated Query), geïntegreerd in versie 3.5 .NET en beschikbaar in C# 2008. LINQ volgt in de voetsporen van NHibernate, maar voorlopig (mei 2008) wordt alleen de SGBD SQL-server ondersteund. Dit zal in de loop van de tijd waarschijnlijk veranderen. LINQ gaat verder dan NHibernate: met de querytaal ervan kunnen op standaardwijze drie verschillende soorten gegevensbronnen worden doorzocht:
    • verzamelingen van objecten (LINQ to Objects)
    • een XML-bestand (LINQ to XML)
    • een database (LINQ to SQL)

Deze frameworks komen in dit document niet aan bod. Het wordt echter ten zeerste aangeraden om ze in professionele toepassingen te gebruiken.

9.5. Query's met parameters

In de vorige paragraaf hebben we het gehad over query's met parameters. We illustreren deze hier aan de hand van een voorbeeld voor de SGBD SQL Server Compact. Het project ziet er als volgt uit

  • in [1], het project. Alleen [App.config], [Article.cs] en [Parametres.cs] worden gebruikt. Let ook op de basis SQL Server en [dbarticles.sdf].
  • In [2] is het project geconfigureerd om [Parametres.cs] uit te voeren
  • in [3], de projectreferenties

Het configuratiebestand [App.config] definieert de verbindingsstring voor de database:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
</configuration>

Het bestand [Article.cs] definieert een klasse [Article]. Een object Article wordt gebruikt om de gegevens van een rij uit de tabel ARTICLES in de database [dbarticles.sdf] in te kapselen:


namespace Chap7 {
    class Article {
        // eigenschappen
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // constructors
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

    }
}

De toepassing [Parametres.cs] voert de geparametriseerde query's uit:


using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;

namespace Chap7 {
    class Parametres {
        static void Main(string[] args) {

            // verwerking van het configuratiebestand
            string connectionString = null;
            try {
                // verbindingsreeks
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // weergaven
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // aanmaken van een artikeltabel
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // eventuele uitzonderingen worden afgehandeld
            try {

                // bestaande artikelen uit de database verwijderen
                ExecuteUpdate(connectionString, "delete from articles");

                // de artikelen uit de tabel weergeven
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // de artikeltabel wordt in de database ingevoerd
                InsertArticles(connectionString, articles);

                // de artikelen uit de tabel worden weergegeven
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // een tabel met artikelen invoegen
        static void InsertArticles(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // verbinding openen
                connexion.Open();
                // configuratie van de opdracht
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // verwerking van de bestelling
                sqlCommand.Prepare();
                // regels invoegen
                for (int i = 0; i < articles.Length; i++) {
                    // initialisatie van parameters
                    sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                    sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                    sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                    sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                    // uitvoering van de query
                    sqlCommand.ExecuteNonQuery();
                }
            }
        }

        // uitvoering van een update-query
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // uitvoering van een SELECT-query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // weergave in de reader
        static void AfficheReader(IDataReader reader) {
...
    }
}

Nieuw ten opzichte van wat eerder is besproken, is de procedure [InsertArticles] in de regels 51-75:

  • regel 51: de procedure ontvangt twee parameters:
    • de verbindingsstring connectionString, waarmee de procedure verbinding kan maken met de database
    • een array met objecten Article die moeten worden toegevoegd aan de tabel Articles in de database
  • regel 56: de query voor het invoegen van een object [Article]. Deze heeft vier parameters:
    • @nom: de naam van het artikel
    • @prix: de prijs
    • @sa: de huidige voorraad
    • @sm: de minimumvoorraad

De syntaxis van deze verzoek met parameters is eigen aan SQL Server Compact. In de vorige paragraaf hebben we gezien dat met MySQL5 de syntaxis als volgt zou zijn:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Bij SQL Server Compact moet elke parameter worden voorafgegaan door het teken @. De naam van de parameters is vrij te kiezen.

  • regels 58-61: we definiëren de kenmerken van elk van de 4 parameters en voegen ze één voor één toe aan de lijst met parameters van het object SqlCeCommand, dat de opdracht SQL omvat die zal worden uitgevoerd.

Hier wordt de methode [SqlCeCommand].Parameters.Add gebruikt, die zes signaturen heeft. We gebruiken de volgende twee:

Add(string parameterName, SQLDbType type)

voegt de parameter met de naam parameterName toe en configureert deze. Deze naam moet een van de namen zijn uit de geconfigureerde geparametriseerde query: (@naam, ...). type verwijst naar het type SQL van de kolom waarop de parameter betrekking heeft. Er zijn talrijke typen beschikbaar, waaronder de volgende:

type SQL
type C#
opmerking
BigInt
Int64
 
DateTime
DateTime
 
Decimal
Decimaal
 
Float
Dubbel
 
Int
Int32
 
Money
Decimaal
 
NChar
String
string met vaste lengte
NVarChar
String
tekenreeks met variabele lengte
Real
Single
 

Add(string parameterName, SQLDbType type, int size)

de derde parameter size bepaalt de grootte van de kolom. Deze informatie is alleen nuttig voor bepaalde typen, zoals bijvoorbeeld het type NVarChar.

  • regel 63: de geparametriseerde query wordt gecompileerd. Men zegt ook dat deze wordt voorbereid, vandaar de naam van de methode. Deze bewerking is niet noodzakelijk. Ze is bedoeld om de prestaties te verbeteren. Wanneer een SGBD een opdracht SQL uitvoert, voert het eerst een aantal optimalisaties uit voordat de opdracht wordt uitgevoerd. Een geparametriseerde query is bedoeld om meerdere keren te worden uitgevoerd met verschillende parameters. De tekst van de query zelf verandert niet. Het optimalisatiewerk hoeft dan maar één keer te worden uitgevoerd. Sommige SGBD’en hebben de mogelijkheid om geparametriseerde query’s te „voorbereiden“ of te „compileren“. Er wordt dan een uitvoeringsplan voor deze query gedefinieerd. Dit is de optimalisatiefase waarover we het hadden. Eenmaal gecompileerd, wordt de query herhaaldelijk uitgevoerd, telkens met nieuwe, effectieve parameters maar met hetzelfde uitvoeringsplan.

Compilatie is niet het enige voordeel van parameterquery’s. Laten we de bestudeerde query nog eens bekijken:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Misschien wil men de tekst van de query programmatisch samenstellen:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

Als hierboven (naam,prijs,sa,sm) gelijk is aan ("artikel1",100,10,1), wordt de vorige query:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

Als (naam,prijs,sa,sm) nu gelijk is aan ("artikel1",100,10,1), wordt de vorige query:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

en wordt syntactisch onjuist vanwege de apostrof in de naam l'article1. Als nom afkomstig is van een invoer door de gebruiker, betekent dit dat we moeten controleren of de invoer geen apostrofs bevat en, indien dit wel het geval is, deze moeten neutraliseren. Deze neutralisatie is afhankelijk van SGBD. Het voordeel van de voorbereide query is dat deze dit werk zelf uitvoert. Deze mogelijkheid rechtvaardigt op zich al het gebruik van een voorbereide query.

  • regels 65-73: de rijen van de tabel worden één voor één ingevoegd
  • regels 67-70: elk van de vier parameters van de query krijgt zijn waarde via de eigenschap Value.
  • regel 72: de nu volledige invoegquery wordt op de gebruikelijke manier uitgevoerd.

Hier volgt een voorbeeld van de uitvoering:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------


------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

117 article1 100 10 1
118 article2 200 20 2
119 article3 300 30 3
120 article4 400 40 4
121 article5 500 50 5
  • regel 3: melding na het verwijderen van alle rijen uit de tabel
  • regels 5-7: laten zien dat de tabel leeg is
  • regels 10-18: tonen de tabel na het invoegen van de 5 artikelen

9.6. Transactions

9.6.1. Algemeen

Een transactie is een reeks opdrachten SQL die op "atomische" wijze wordt uitgevoerd:

  • ofwel slagen alle bewerkingen
  • ofwel mislukt er één, en dan worden alle voorgaande bewerkingen ongedaan gemaakt

Uiteindelijk zijn de bewerkingen van een transactie ofwel allemaal met succes uitgevoerd, ofwel is er geen enkele uitgevoerd. Wanneer de gebruiker zelf de controle heeft over de transactie, bevestigt hij een transactie met een opdracht COMMIT of annuleert hij deze met een opdracht ROLLBACK.

In onze voorgaande voorbeelden hebben we geen transactie gebruikt. Toch waren er wel transacties, want in een SGBD wordt een opdracht SQL altijd binnen een transactie uitgevoerd. Als de klant .NET zelf geen expliciete transactie start, maakt de SGBD gebruik van een impliciete transactie. Er zijn dan twee veelvoorkomende gevallen:

  1. elke afzonderlijke opdracht SQL maakt deel uit van een transactie, die door de opdracht SGBD vóór de opdracht wordt gestart en daarna wordt afgesloten. Men spreekt dan van de autocommit-modus. Het is dus alsof de client .NET voor elke opdracht SQL transacties uitvoert.
  2. SGBD bevindt zich niet in de autocommit-modus en start een impliciete transactie bij de eerste opdracht SQL die de klant .NET buiten een transactie om verstuurt, en laat de klant deze afsluiten. Alle opdrachten SQL die door de client .NET worden verzonden, maken dan deel uit van de impliciete transactie. Deze transactie kan door verschillende gebeurtenissen worden beëindigd: de klant verbreekt de verbinding, start een nieuwe transactie, ... maar dan bevinden we ons in een situatie die afhankelijk is van de SGBD. Dit is een modus die moet worden vermeden.

De standaardmodus wordt doorgaans bepaald door de configuratie van de SGBD. Sommige SGBD staan standaard in de autocommit-modus, andere niet. SQLServer Compact staat standaard in de autocommit-modus.

De SQL-opdrachten van verschillende gebruikers worden tegelijkertijd uitgevoerd in transacties die parallel lopen. De bewerkingen die door de ene transactie worden uitgevoerd, kunnen invloed hebben op die van een andere transactie. Er worden vier niveaus van isolatie tussen de transacties van verschillende gebruikers onderscheiden:

  • Uncommitted Read
  • Committed Read
  • Repeatable Read
  • Serializable

Uncommitted Read

Deze isolatiemodus wordt ook wel "Dirty Read" genoemd. Hier volgt een voorbeeld van wat er in deze modus kan gebeuren:

  1. een gebruiker U1 start een transactie op een tabel T
  2. een gebruiker U2 start een transactie op dezelfde tabel T
  3. gebruiker U1 wijzigt rijen in tabel T, maar bevestigt deze nog niet
  4. de gebruiker U2 „ziet“ deze wijzigingen en neemt beslissingen op basis van wat hij ziet
  5. de gebruiker annuleert zijn transactie met een ROLLBACK

We zien dat in stap 4 de gebruiker U2 een beslissing heeft genomen op basis van gegevens die later onjuist zullen blijken te zijn.

Committed Read

Deze isolatiemodus voorkomt het eerder genoemde probleem. In deze modus zal gebruiker U2 in stap 4 de wijzigingen die gebruiker U1 in tabel T heeft aangebracht, niet „zien”. Hij zal deze pas zien nadat U1 zijn transactie heeft vastgelegd (COMMIT).

In deze modus, ook wel "Unrepeatable Read" genoemd, kunnen zich echter de volgende situaties voordoen:

  1. een gebruiker U1 start een transactie op een tabel T
  2. een gebruiker U2 start een transactie op dezelfde tabel T
  3. de gebruiker U2 voert een SELECT uit om het gemiddelde te berekenen van kolom C van de rijen in T die aan een bepaalde voorwaarde voldoen
  4. gebruiker U1 wijzigt (UPDATE) bepaalde waarden in kolom C van T en bevestigt deze (COMMIT)
  5. de gebruiker U2 voert opnieuw dezelfde SELECT uit als in 3. Hij zal ontdekken dat het gemiddelde van kolom C is veranderd als gevolg van de wijzigingen die door U1 zijn aangebracht.

Nu ziet gebruiker U2 alleen de wijzigingen die door U1 zijn „gevalideerd”. Maar terwijl hij in dezelfde transactie blijft, leveren twee identieke bewerkingen (3 en 5) verschillende resultaten op. De term "Unrepeatable Read" verwijst naar deze situatie. Dit is vervelend voor iemand die een stabiel beeld van tabel T wil hebben.

Repeatable Read

In deze isolatiemodus is een gebruiker verzekerd van dezelfde resultaten bij het lezen van de database, zolang hij binnen dezelfde transactie blijft. Hij werkt met een momentopname waarin wijzigingen die door andere transacties zijn aangebracht – zelfs als deze zijn gevalideerd – nooit worden doorgevoerd. Hij zal deze wijzigingen pas zien wanneer hij zelf zijn transactie afsluit met een COMMIT of ROLLBACK.

Deze isolatiemodus is echter nog niet perfect. Na bewerking 3 hierboven worden de rijen die door gebruiker U2 zijn geraadpleegd, vergrendeld. Tijdens bewerking 4 kan gebruiker U1 de waarden in kolom C van deze rijen niet wijzigen (UPDATE). Hij kan echter wel rijen toevoegen (INSERT). Als sommige van de toegevoegde rijen voldoen aan de in stap 3 geteste voorwaarde, zal bewerking 5 een ander gemiddelde opleveren dan dat in stap 3, vanwege de toegevoegde rijen. Deze rijen worden soms ‘spookrijen’ genoemd.

Om dit nieuwe probleem op te lossen, moet worden overgeschakeld naar de isolatiemodus „Serializable“.

Serializable

In deze isolatiemodus zijn transacties volledig afgeschermd van elkaar. Dit zorgt ervoor dat het resultaat van twee gelijktijdig uitgevoerde transacties hetzelfde is als wanneer ze na elkaar zouden worden uitgevoerd. Om dit te bereiken, wordt de gebruiker U1 tijdens bewerking 4 – waarin hij regels wil toevoegen die het resultaat van de transactie SELECT van gebruiker U1 zouden wijzigen – hierin verhinderd. Een foutmelding zal aangeven dat het invoegen niet mogelijk is. Dit wordt pas mogelijk wanneer gebruiker U2 zijn transactie heeft bevestigd.

De vier isolatieniveaus voor transacties zijn niet in alle gevallen beschikbaar. Het standaardisolatieniveau is doorgaans het niveau ‘Committed Read’. Het gewenste isolatieniveau voor een transactie kan expliciet worden opgegeven bij het aanmaken van een expliciete transactie door een klant .NET.

9.6.2. Het transactiebeheer API

Een verbinding implementeert de interface IDbConnection die in paragraaf 9.3.3 wordt beschreven. Deze interface bevat de volgende methode:

BeginTransaction
M
start een transactie.

Deze methode heeft twee signaturen:

  1. IDbTransaction BeginTransaction(): start een transactie en retourneert het object IDbTransaction waarmee deze kan worden beheerd
  2. IDbTransaction BeginTransaction(IsolationLevel level): specificeert bovendien het gewenste beveiligingsniveau voor de transactie. level haalt zijn waarden uit de volgende opsomming:
ReadUncommitted
de transactie kan gegevens lezen die door een andere transactie zijn geschreven, maar die deze nog niet heeft gevalideerd – te vermijden
ReadCommitted
de transactie kan geen gegevens lezen die door een andere transactie zijn geschreven en die deze nog niet heeft gevalideerd. De gegevens die twee keer achter elkaar in de transactie worden gelezen, kunnen echter veranderen (not repeatable reads), omdat een andere transactie ze ondertussen mogelijk heeft gewijzigd (de gelezen rijen zijn niet vergrendeld – alleen de bijgewerkte rijen zijn dat wel). Bovendien kan een andere transactie regels hebben toegevoegd (spookregels) die bij de tweede lezing worden meegenomen.
RepeatableRead
De rijen die door de transactie worden gelezen, worden net als de bijgewerkte rijen vergrendeld. Dit voorkomt dat een andere transactie ze wijzigt. Het voorkomt echter niet dat er rijen worden toegevoegd.
Serializable
De tabellen die door de transactie worden gebruikt, worden vergrendeld, waardoor het toevoegen van nieuwe rijen door een andere transactie wordt verhinderd. Het is alsof de transactie de enige is. Dit vermindert de prestaties omdat transacties niet meer parallel werken.
Snapshot
De transactie werkt op een kopie van de gegevens die op tijdstip T is gemaakt. Wordt gebruikt wanneer de transactie alleen-lezen is. Levert hetzelfde resultaat op als serializable, maar vermijdt de bijbehorende kosten.

Zodra de transactie is gestart, wordt deze aangestuurd door het object van het type IDbTransaction, een interface waarvan we de volgende eigenschappen P en methoden M zullen gebruiken:

Naam
Type
Rol
Connection
P
de verbinding IDbConnection die de transactie ondersteunt
Commit
M
valideert de transactie – de resultaten van de opdrachten SQL die in de transactie zijn verzonden, worden naar de database gekopieerd.
Rollback
M
maakt de transactie ongeldig – de resultaten van de opdrachten SQL die in de transactie zijn gegeven, worden niet naar de database gekopieerd.

9.6.3. Het voorbeeldprogramma

We gaan verder met het vorige project en richten ons nu op het programma [Transactions.cs]:

  • in [1], het project.
  • in [2] is het project geconfigureerd om [Transactions.cs] uit te voeren

De code van [Transactions.cs] is als volgt:


using System;
using System.Configuration;
using System.Data;
using System.Data.SqlServerCe;
using System.Text;

namespace Chap7 {
    class Transactions {
        static void Main(string[] args) {

            // verwerking van het configuratiebestand
            string connectionString = null;
            try {
                // verbindingsreeks
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // weergaven
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // aanmaken van een tabel met 2 artikelen met dezelfde naam
            Article[] articles = new Article[2];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
            }
            // eventuele uitzonderingen worden afgehandeld
            try {
                Console.WriteLine("Insertion sans transaction...");
                // de artikeltabel wordt eerst zonder transactie in de database ingevoegd
                ExecuteUpdate(connectionString, "delete from articles");
                try {
                    InsertArticlesOutOfTransaction(connectionString, articles);
                } catch (Exception ex) {
                    // foutmelding
                    Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
                }
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // we doen hetzelfde nogmaals, maar dit keer binnen een transactie
                Console.WriteLine("\n\nInsertion dans une transaction...");
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // een artikeltabel invoegen zonder transactie
        static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
        }

        // een artikeltabel invoegen binnen een transactie
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
        }

        // uitvoering van een update-query
        static void ExecuteUpdate(string connectionString, string requête) {
....
        }

        // uitvoering van een SELECT-query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // Reader weergeven
        static void AfficheReader(IDataReader reader) {
...
            }
        }
    }
}
  • regels 12-19: de verbindingsstring naar de database SQLServer wordt gelezen in [App.config]
  • regels 25-28: er wordt een array met twee objecten Article aangemaakt. Deze twee artikelen hebben dezelfde naam "artikel". De database [dbarticles.sdf] heeft echter een uniekheidsbeperking op de kolom [nom] (zie paragraaf 9.3.1). Deze twee artikelen kunnen dus niet tegelijkertijd in de database aanwezig zijn. De twee artikelen met de naam „artikel“ worden toegevoegd aan de tabel articles. Er zal dus een probleem ontstaan: c.a.d. Een uitzondering die wordt gegenereerd door SGBD en doorgegeven door de bijbehorende connector ADO.NET. Om het effect van de transactie te laten zien, worden de twee artikelen in twee verschillende omgevingen ingevoegd:
    • eerst buiten elke transactie om. Hierbij moet worden bedacht dat SQLServer Compact in dit geval in de autocommit-modus werkt, c.a.d. Elke opdracht SQL wordt in een impliciete transactie geplaatst. Het eerste artikel wordt ingevoegd. Het tweede niet.
    • Vervolgens in een expliciete transactie die beide invoegingen omvat. Omdat de tweede invoeging zal mislukken, wordt de eerste ongedaan gemaakt. Uiteindelijk wordt er geen enkele invoeging uitgevoerd.
  • regel 33: de tabel articles wordt leeggemaakt
  • regel 35: het invoegen van de twee artikelen zonder expliciete transactie. Omdat we weten dat de tweede invoegactie een uitzondering zal veroorzaken, wordt deze afgehandeld door een try/catch
  • regel 46: weergave van de tabel articles
  • regels 44-46: we herhalen dezelfde reeks, maar deze keer wordt een expliciete transactie gebruikt om de invoegingen uit te voeren. De uitzondering die hier optreedt, wordt afgehandeld door de methode InsertArticlesInTransaction.
  • regels 54-56: de methode InsertArticlesOutOfTransaction is dezelfde als de methode InsertArticles uit het eerder besproken programma [Parametres.cs].
  • regels 64-66: de methode ExecuteUpdate is dezelfde als eerder. De opdracht SQL wordt uitgevoerd in een impliciete transactie. Dit is hier mogelijk omdat we weten dat SQLServer Compact in dit geval in de autocommit-modus werkt.
  • regels 69-71: hetzelfde geldt voor de methode ExecuteSelect.

De methode InsertArticlesInTransaction is als volgt:


// een artikeltabel invoegen in een transactie
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // verbinding openen
                connexion.Open();
                // opdracht configureren
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // verwerking van de opdracht
                sqlCommand.Prepare();
                // transactie
                SqlCeTransaction transaction = null;
                try {
                    // transactie starten
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // de opdracht SQL moet in deze transactie worden uitgevoerd
                    sqlCommand.Transaction = transaction;
                    // regels invoegen
                    for (int i = 0; i < articles.Length; i++) {
                        // initialisatie van parameters
                        sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                        sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                        sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                        sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                        // uitvoering van de query
                        sqlCommand.ExecuteNonQuery();
                    }
                    // de transactie wordt bevestigd
                    transaction.Commit();
                    Console.WriteLine("transaction validée...");
                } catch {
                    // de transactie ongedaan maken
                    if (transaction != null)transaction.Rollback();
                    Console.WriteLine("transaction invalidée...");
                }
            }
        }

We gaan alleen in op de verschillen met de methode InsertArticles van het eerder besproken programma [Parametres.cs]:

  • regel 16: er wordt een transactie SqlCeTransaction gedeclareerd.
  • regels 17, 35: de try/catch-constructie om de uitzondering af te vangen die na de tweede invoeging zal optreden
  • regel 19: de transactie wordt aangemaakt. Deze behoort tot de huidige verbinding.
  • regel 21: de geconfigureerde opdracht SQL wordt in de transactie geplaatst
  • regels 23-31: de invoegingen worden uitgevoerd
  • regel 33: alles is goed verlopen – de transactie is gevalideerd – de invoegingen worden definitief in de database opgenomen.
  • regel 37: er is een probleem opgetreden. De transactie wordt ongedaan gemaakt, indien deze bestond.

De uitvoering levert de volgende resultaten op:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Insertion sans transaction...
Il y a eu 0 ligne(s) modifiée(s)
Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

126 article 100 10 1


Insertion dans une transaction...
Il y a eu 1 ligne(s) modifiée(s)
transaction invalidée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------
  • regel 4: weergegeven door ExecuteUpdate("delete from articles") – er waren geen rijen in de tabel
  • regel 5: de uitzondering die werd veroorzaakt door de tweede invoeging. Het bericht geeft aan dat de beperking UQ__ARTICLES__0000000000000010 niet is gecontroleerd. Meer informatie hierover is te vinden in de eigenschappen van de database:
  • in [1] in de weergave [Database Explorer] van Visual Studio is een verbinding [2] gemaakt met de database [dbarticles.sdf]. Deze heeft een index UQ__ARTICLES__0000000000000010. Door met de rechtermuisknop op deze index te klikken, krijgt men toegang tot de eigenschappen ervan (Index properties)
  • in [3,4] zien we dat de index UQ__ARTICLES__0000000000000010 overeenkomt met een uniekheidsbeperking op de kolom [NOM]
  • regels 7-11: weergave van de tabel articles na de twee invoegingen. De tabel is niet leeg: het eerste artikel is ingevoegd.
  • regel 15: weergegeven door ExecuteUpdate("delete from articles") – er stond een rij in de tabel
  • regel 16: bericht weergegeven door InsertArticlesInTransaction wanneer de transactie mislukt.
  • regels 18-20: laten zien dat er geen invoegingen zijn gedaan. De Rollback van de transactie heeft de eerste invoeging ongedaan gemaakt.

9.7. De methode ExecuteScalar

9.7.1. Onder de methoden van de interface IDbCommand, beschreven in paragraaf 9.3.3, bevond zich de volgende methode:

ExecuteScalar
M
om een opdracht SQL uit te voeren, waarbij Select slechts één resultaat oplevert, zoals in: select count(*) from articles.

Hier tonen we een voorbeeld van het gebruik van deze methode. Laten we teruggaan naar het project:

  • in [1], het project.
  • In [2] is het project geconfigureerd om [ExecuteScalar.cs] uit te voeren

Het programma [ExecuteScalar.cs] ziet er als volgt uit:


...
namespace Chap7 {
    class Scalar {
        static void Main(string[] args) {

            // verwerking van het configuratiebestand
            string connectionString = null;
...

            // weergaven
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // aanmaken van een tabel met 5 artikelen
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // eventuele uitzonderingen worden afgehandeld
            try {
                // de artikeltabel wordt in een transactie ingevoegd
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
                // het gemiddelde van de artikelprijzen berekenen
                decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
                Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
                // of het aantal artikelen
                int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
                Console.WriteLine("Nombre d'articles={0}", nbArticles);
            } catch (Exception ex) {
                // foutmelding
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // een artikeltabel in een transactie invoegen
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
        }


        // uitvoering van een bijwerkingsopdracht
        static object ExecuteScalar(string connectionString, string requête) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // verbinding openen
                connexion.Open();
                // uitvoering van een query
                return new SqlCeCommand(requête, connexion).ExecuteScalar();
            }
        }

        // uitvoering van een update-query
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // uitvoering van een SELECT-query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // weergave in de reader
        static void AfficheReader(IDataReader reader) {
...
        }
    }
}
  • regels 14-17: aanmaken van een tabel met 5 artikelen
  • regel 22: de tabel articles wordt leeggemaakt
  • regel 23: deze wordt gevuld met de 5 artikelen
  • regel 24: de tabel wordt weergegeven
  • regel 26: vraagt de gemiddelde prijs van de artikelen op
  • regel 29: vraagt het aantal artikelen op
  • regel 49: gebruik van de methode [IDbCommand].ExecuteScalar() om elk van deze waarden te berekenen.

De resultaten van de uitvoering zijn als volgt:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)
transaction validée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

145 article1 100 10 1
146 article2 200 20 2
147 article3 300 30 3
148 article4 400 40 4
149 article5 500 50 5
Prix moyen des articles=300
Nombre d'articles=5

Regels 15 en 16 tonen de twee waarden die door de methode ExecuteScalar worden geretourneerd.

9.8. Voorbeeldtoepassing - versie 7

We nemen de voorbeeldtoepassing IMPOTS opnieuw onder de loep. De laatste versie is besproken in paragraaf 7.6. Het betrof de volgende drielaagse toepassing:

  • de laag [ui] was een grafische interface [A] en de laag [dao] haalde zijn gegevens uit een tekstbestand [B].
  • Het instantiëren van de lagen en hun integratie in de applicatie werd verzorgd door Spring.

We passen de laag [dao] aan, zodat deze zijn gegevens uit een database haalt.

9.8.1. De database-

De inhoud van het vorige tekstbestand [B] wordt in een database MySQL5 geplaatst. We laten zien hoe dit in zijn werk gaat:

  • [1]: MySQL Administrator is gestart
  • [2,3]: klik met de rechtermuisknop in het veld [Schemata] en kies de optie [Create Schema] om een nieuwe database aan te maken
  • [4]: de database krijgt de naam [bdimpots]
  • [5]: deze is toegevoegd aan de databases in het gebied [Schemata].
  • [6,7]: klik met de rechtermuisknop op de tabel en kies de optie [Create New Table] om een tabel aan te maken
  • [8]: de tabel krijgt de naam [tranches]. Deze bevat de kolommen van [id, limite, coeffR, coeffN].
  • [9,10]: [id] is de primaire sleutel van het type INTEGER en heeft het attribuut AUTO_INCREMENT [10]: het is de SGBD die deze kolom zal invullen bij het toevoegen van rijen.
  • De kolommen [limite, coeffR, coeffN] zijn van het type DOUBLE.
  • [11,12]: de nieuwe tabel verschijnt in het tabblad [Schema Tables] van de database.
  • [13,14]: om gegevens in de tabel in te voeren
  • [15]: [Query Browser] is gestart
  • [16]: de gegevens zijn ingevoerd en gevalideerd voor de kolommen van [limite, coeffR, coeffN]. De kolom [id] is ingevuld door SGBD. De validatie heeft plaatsgevonden met [17].
  • nog steeds in [Query Browser] [18], wordt [20] uitgevoerd, de query [19]. Deze query maakt een gebruiker 'admimpots' aan met het wachtwoord 'mdpimpots' en verleent hem alle rechten (grant all privileges) op alle objecten in de database bdimpots (on bdimpots.*). Hierdoor kunnen we in de database [bdimpots] werken met de gebruiker [admimpots] in plaats van met de beheerder [root].

9.8.2. De Visual Studio-oplossing

We volgen de aanpak die is besproken voor versie 5 van de voorbeeldtoepassing (zie paragraaf 6.4). We gaan stap voor stap de volgende Visual Studio-oplossing opbouwen:

  • in [1]: de oplossing ImpotsV7 bestaat uit drie projecten, één voor elk van de drie lagen van de applicatie
  • in [2]: het project [dao] van de laag [dao], dat voortaan gebruik zal maken van een database
  • in [3]: het project [metier] van de laag [metier]. Hier nemen we de laag [metier] uit versie 5 over, zoals beschreven in paragraaf 6.4.4.
  • in [4]: het project [ui] van de laag [ui]. We nemen hier de laag [ui] uit versie 6 over, zoals beschreven in paragraaf 7.6.

We maken gebruik van de bestaande code om twee reeds geschreven lagen op te halen, namelijk de lagen [ui] en [metier]. Dit wordt mogelijk gemaakt door de gekozen gelaagde architectuur. We hebben echter wel de broncodes nodig van de lagen [ui] en [metier]. Het is namelijk niet mogelijk om alleen gebruik te maken van de DLL van de lagen. Toen in versie 5 de DLL van de laag [metier] werd aangemaakt, was deze afhankelijk van de DLL van de laag [dao]. Deze afhankelijkheid is hard gecodeerd in de DLL van de laag [metier] (naam van de DLL van de laag [dao], versie, identiteitstoken, ...). De DLL van versie 5 [ImpotsV5-metier.dll] werkt dus alleen samen met de DLL en [ImpotsV5-dao.dll] waarmee deze is gecompileerd. Als de DLL van de laag [dao] wordt gewijzigd, moet de laag [metier] opnieuw worden gecompileerd om een nieuwe DLL te genereren. Hetzelfde geldt voor de laag [ui]. De lagen [ui] en [metier] worden dus niet gewijzigd, maar wel opnieuw gecompileerd om te kunnen samenwerken met de DLL van de nieuwe laag [dao].

9.8.3. De laag [dao]

De projectreferenties (zie [1] in het project)

  • nunit.framework: voor de test NUnit
  • System.Configuration: om het configuratiebestand [App.config] te gebruiken
  • System.Data: omdat er een database wordt gebruikt.

De entiteiten (zie [2] in het project)

De klassen [TrancheImpot] en [ImpotException] zijn die van de vorige versies.

De laag [dao] (zie [3] in het project)

De interface [IImpotDao] is niet gewijzigd:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // de belastingschijven
        TrancheImpot[] TranchesImpot{get;}
    }
}

De implementatieklasse [DataBaseImpot] van deze interface is als volgt:


using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;

namespace Dao {
    public class DataBaseImpot : IImpotDao {
        // belastingschijven
        private TrancheImpot[] tranchesImpot;
        public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }

        // fabrikant
        public DataBaseImpot(string factory, string connectionString, string requête) {
            // fabriek: de fabriek van het doelobject SGBD
            // connectionString: de verbindingsstring naar de database met belastingschijven
            // eventuele uitzonderingen worden afgehandeld
            try {
                // er wordt een generieke connector opgehaald voor de SGBD
                DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // verbinding configureren
                    connexion.ConnectionString = connectionString;
                    // verbinding openen
                    connexion.Open();
                    // configuratie van het commando 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // verzoek uitvoeren
                    List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
                    using (DbDataReader reader = sqlCommand.ExecuteReader()) {
                        while (reader.Read()) {
                            // een nieuwe belastingschijf aanmaken
                            listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
                        }
                    }
                    // de belastingschijven worden in de instantie geplaatst
                    tranchesImpot = listTrancheImpot.ToArray();
                }
            } catch (Exception ex) {
                // de uitzondering wordt ingekapseld in een type ImpotException
                throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
            }

        }
    }
}
  • regel 7: de klasse [DataBaseImpot] implementeert de interface [IImpotDao].
  • regel 10: de implementatie van de methode [TranchesImpot] van de interface. Deze methode geeft alleen een verwijzing naar de tabel met belastingschijven uit regel 9. Deze tabel wordt aangemaakt door de constructor van de klasse.
  • regel 13: de constructor. Deze maakt gebruik van een generieke connector (zie paragraaf 9.4.5) om de database met belastingschijven te raadplegen. De constructor ontvangt drie parameters:
    • de naam van de "factory" waar hij de klassen zal opvragen om verbinding te maken met de database, SQL-opdrachten te versturen en het resultaat van een Select-query te verwerken.
    • de verbindingsstring die hij moet gebruiken om verbinding te maken met de database
    • de SQL Select-opdracht die hij moet uitvoeren om de belastingschijven op te halen.
  • regel 19: vraagt een connector aan bij de "factory"
  • regel 20: maakt een verbinding met deze connector. De verbinding is aangemaakt, maar nog niet operationeel
  • regel 22: de verbindingsstring van de verbinding wordt geïnitialiseerd. Er kan nu verbinding worden gemaakt.
  • regel 24: er wordt verbinding gemaakt
  • regel 26: vraagt de connector om een object [DbCommand] om een opdracht SQL uit te voeren
  • regel 27: stelt de uit te voeren opdracht SQL vast
  • regel 28: de verbinding wordt vastgelegd waarop de opdracht moet worden uitgevoerd
  • regel 30: er wordt een lege lijst [listTrancheImpot] met objecten van het type [TrancheImpot] aangemaakt.
  • regel 31: de opdracht SQL Select wordt uitgevoerd
  • regels 32-35: het object [DbDataReader], het resultaat van de Select-opdracht, wordt verwerkt. Elke regel van de resultatentabel van de Select-opdracht wordt gebruikt om een object van het type [TrancheImpot] te instantiëren, dat wordt toegevoegd aan de lijst [listTrancheImpot].
  • regel 38: de lijst met objecten van het type [TrancheImpot] wordt overgebracht naar de array in regel 9.
  • regels 40-43: een eventuele uitzondering wordt ingekapseld in een type [ImpotException] en krijgt de foutcode 101 (willekeurig) toegewezen.

De test [Test1] (zie [4] in het project)

De klasse [Test1] geeft alleen de belastingschijven op het scherm weer. Dit is dezelfde klasse die al in versie 5 werd gebruikt (paragraaf 6.4.3), met uitzondering van de instructie die de laag [dao] instantiëert (regel 14).


using System;
using Dao;
using Entites;
using System.Configuration;

namespace Tests {
    class Test1 {
        static void Main() {

            // de laag wordt aangemaakt [dao]
            IImpotDao dao = null;
            try {
                // aanmaken van laag [dao]
                dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
            } catch (ImpotException e) {
                // foutmelding
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // programma beëindigen
                Environment.Exit(1);
            }
            // de belastingschijven worden weergegeven
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}

Regel 14 maakt gebruik van het volgende configuratiebestand [App.config]:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
    </connectionStrings>
    <appSettings>
        <add key="requete" value="select limite, coeffr, coeffn from tranches"/>
        <add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
    </appSettings>
</configuration>
  • regel 4: de verbindingsstring naar de database MySQL5. Merk op dat de gebruiker [admimpots] de verbinding tot stand brengt.
  • regel 8: de „factory” om te werken met de SGBD MySQL5
  • regel 7: de Select-query SQL om de belastingschijven op te halen.

Het project is geconfigureerd om [Test1.cs] uit te voeren:

Image

De testoplegging levert de volgende resultaten op:

1
2
3
4
5
6
7
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

De test NUnit [NUnit1] (zie [4] in het project)

De unit-test [NUnit1] is dezelfde als die al in versie 5 werd gebruikt (paragraaf 6.4.3), met uitzondering van de instructie die de laag [dao] instantiëert (regel 16).


using System;
using System.Configuration;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // laag [dao] te testen
        private IImpotDao dao;

        // ontwikkelaar
        public NUnit1() {
            // initialisatie laag [dao]
            dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
        }

        // test
        [Test]
        public void ShowTranchesImpot(){
            // de belastingschijven worden weergegeven
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // enkele tests
            Expect(tranchesImpot.Length,EqualTo(7));
            Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
            Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
            Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
        }
    }
}

Om deze unit-test uit te voeren, moet het project van het type [Class Library] zijn:

  • in [1]: de aard van het project is gewijzigd
  • naar [2]: het gegenereerde DLL krijgt de naam [ImpotsV7-dao.dll]
  • in [3]: na het genereren (F6) van het project bevat de map [dao/bin/Release] de bestanden DLL en [ImpotsV7-dao.dll]. Het bevat ook het configuratiebestand [App.config], dat is hernoemd naar [nom DLL].config. Dit is standaard in Visual Studio.

Vervolgens wordt DLL [ImpotsV7-dao.dll] geladen in het framework NUnit en uitgevoerd:

  • in [1]: de tests zijn geslaagd. We beschouwen de laag [dao] nu als operationeel. De bijbehorende DLL bevat alle klassen van het project, inclusief de testklassen. Deze zijn overbodig. We bouwen de DLL opnieuw op om de testklassen ervan uit te sluiten.
  • naar [2]: de map [tests] wordt uit het project verwijderd
  • in [3]: het nieuwe project. Dit wordt opnieuw gegenereerd door F6 om een nieuwe DLL te genereren. Het is dit DLL-bestand dat zal worden gebruikt door de lagen [metier] en [ui] van de applicatie.

9.8.4. De laag [metier]

  • in [1] is het project [metier] het actieve project van de oplossing geworden
  • in [2]: de projectreferenties. Let op de verwijzing naar DLL in de eerder aangemaakte laag [dao]. Deze procedure voor het toevoegen van een verwijzing is beschreven in versie 5, in paragraaf 6.4.4.
  • in [3]: de laag [metier]. Dit is de laag uit versie 5, beschreven in paragraaf 6.4.4.

Het project [metier] is geconfigureerd om een DLL te genereren:

  • [1]: het project is van het type „klassenbibliotheek“
  • [2]: het genereren van het project levert de bestanden DLL, [ImpotsV7-metier.dll] en [3] op.

Het project is gegenereerd (F6).

9.8.5. De laag [ui]

  • in [1] is het project [ui] het actieve project van de oplossing geworden
  • in [2]: de projectreferenties. Let op de verwijzingen in DLL naar de lagen [dao] en [metier].
  • in [3]: de laag [ui]. Dit is de laag van versie 6 die in paragraaf 7.6 wordt beschreven.
  • in [4] is het configuratiebestand [App.config] vergelijkbaar met dat van versie 6. Het verschilt er alleen in de manier waarop de laag [dao] door Spring wordt geïnstantieerd:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • regels 11-25: de Spring-configuratie
  • regels 15-24: de door Spring geïnstantieerde objecten
  • regels 16-20: instantiëring van de laag [dao]
  • regel 16: de laag [dao] wordt geïnstantieerd door de klasse [Dao.DataBaseImpot], die zich bevindt in de DLL [ImpotsV7-Dao]
  • regels 17-19: de drie parameters (de gebruikte factory van SGBD, de verbindingsstring, de aanvraag SQL) die aan de constructor van de klasse [Dao.DataBaseImpot] moeten worden doorgegeven
  • regels 21-23: instantiëren van de laag [metier]. Dit is dezelfde configuratie als in versie 6.

Tests

Het project [ui] is als volgt geconfigureerd:

  • [1]: het project is van het type „Windows Application“
  • [2]: bij het genereren van het project wordt het uitvoerbare bestand [ImpotsV7-ui.exe] geproduceerd

Een voorbeeld van de uitvoering wordt gegeven in [3].

9.8.6. De database wijzigen

De bovenstaande laag [dao] is geschreven met een generieke connector en een basis MySQL5. We willen hier overschakelen naar een basis SQL Server Compact om aan te tonen dat alleen de configuratie zal veranderen.

De SQL Server Compact- -database ziet er als volgt uit:

  • [1]: de database [dbimpots.sdf] in de weergave [DataBase Explorer] van Visual Studio [2]. Deze is zonder wachtwoord aangemaakt.
  • [3]: de tabel [data] die de gegevens bevat. Er is bewust gekozen voor andere namen voor de tabel en de kolommen dan die welke worden gebruikt bij de database MySQL5, om nogmaals te benadrukken dat het zinvol is om dit soort details in het configuratiebestand op te nemen in plaats van in de code.
  • [4]: de kolom [id] is de primaire sleutel en heeft het attribuut Identity: het is de kolom SGBD die hieraan de waarden toekent.
  • [5]: de inhoud van de tabel [data].
  • [6]: de database [dbimpots.sdf] is in de projectmap [ui] geplaatst en in dit project geïntegreerd.
  • [7]: de database [dbimpots.sdf] wordt gekopieerd naar de uitvoermap van het project.

Het configuratiebestand [App.config] voor de nieuwe database is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <!--
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            -->
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • regels 23-27: de configuratie van de laag [dao] om de database [dbimpots.sdf] te gebruiken.

De resultaten van de uitvoering zijn identiek aan de vorige. Het is interessant om een generieke connector te gebruiken, zodat de laag [dao] ongevoelig is voor de wijziging van SGBD. We hebben echter gezien dat deze connector niet in alle situaties geschikt is, met name wanneer er geparametriseerde query’s worden gebruikt. Er zijn dan ook andere oplossingen, zoals de eerder genoemde: frameworks van derden voor gegevenstoegang (Spring, iBatis, NHibernate, LINQ, ...).

9.9. Voor meer informatie ...

  • LINQ wordt in talrijke publicaties behandeld, met name in het boek: C# 3.0 in a Nutshell, Joseph en Ben Albahari, uitgeverij O’Reilly, dat al in de inleiding van dit document werd genoemd.
  • iBatis wordt behandeld in het boek: iBatis in Action, door Clinton Begin, uitgegeven door Manning
  • 'Nhibernate in Action', uitgegeven door Manning, staat gepland voor juli 2008

Voor Spring, iBatis en NHibernate zijn referentiehandleidingen beschikbaar op de websites van deze verschillende frameworks.