IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > [General]Généralités
        Qu'est ce qu'un fournisseur managé ?
        Qu'est ce que l'approche générique ?
        Le paramétrage de la propriété FireInfoMessageEventOnUserErrors à True provoque un plantage aléatoire ?
        Quelles sont les méthodes pour connaître le nombre de lignes d'une table ?
        Quel est l'intérêt de la méthode GetOrdinal et/ou de la propriété Ordinal ?
        Que signifie l'avertissement ReviewSqlQueriesForSecurityVulnerabilities ?
        J'obtiens parfois l'erreur 'Échec de la conversion de la valeur de paramètre d'un String en un DateTime' ?
        Comment compacter une base Access ?
        Comment récupérer une image stockée dans SQL Server ?
        Quelles différences entre NChar, NVarChar et VarChar ?
        Access ne stocke pas mes dates correctement, pourquoi ?
        Je reçois l'erreur 'Une exception non gérée du type 'System.Data.OleDb.OleDbException' s'est produite dans system.data.dll' lors de l'appel de la méthode Update ?
        Comment fonctionne l'exception ConcurrencyException ?
        Comment récupérer l'identifiant d'un champ auto-incrémenté après son insertion dans le SGBD ?
        Qu'est ce que le traitement asynchrone ?



Qu'est ce qu'un fournisseur managé ?
auteur : Jean-Marc Rabilloud
Dans ADO.NET, un fournisseur managé est un ensemble de classe permettant l'interaction avec un SGBD spécifique. Microsoft donne comme schéma pour le fournisseur :

Fournisseur managé DotNet
Les fournisseurs managés contiennent généralement d'autres classes comme CommandBuilder ou Transaction. A l'installation, Visual Studio contient par défaut quatre fournisseurs managés :

  • SQL-SERVER
  • OLE DB
  • ODBC
  • Oracle
Il en existe d'autres que l'on trouve généralement chez l'éditeur du SGBD comme Connector/Net 5.0 pour MySQL ou Borland Data Provider pour InterBase.


Qu'est ce que l'approche générique ?
auteur : Jean-Marc Rabilloud
Cela revient à écrire un code permettant l'interaction avec plusieurs SGBD différents de la manière la plus concise possible.

En toute rigueur, si tous les SGBD avaient les mêmes fonctionnalités, il devrait être possible de faire un code fonctionnant pour tous les fournisseurs managés à la chaîne de connexion prêt. De fait, on en est assez loin. En pratique, tous les fournisseurs managés héritent des objets génériques de l'espace de nom System.Data.Common. Tant que le code reste sur les membres hérités, la généricité va être correcte, en dehors de cela il faudra implémenter les fonctionnalités à concurrence du modèle désiré ce qui peut être rapidement assez lourd.

Vous trouverez un exemple de base dans cet article de Didier Danse

lien : fr Principes de réalisation d'un DAC indépendant du SGBD

Le paramétrage de la propriété FireInfoMessageEventOnUserErrors à True provoque un plantage aléatoire ?
auteur : Jean-Marc Rabilloud
La propriété FireInfoMessageEventOnUserErrors permet de traiter les exceptions comme des avertissements jusqu'à un certain niveau de gravité. Un avertissement (InfoMessage) est géré par un gestionnaire d'évènement spécifique mais ne bloque pas le code déclencheur comme dans le cas d'une exception.

Ceci implique que des modifications peuvent avoir lieu alors qu'une ou plusieurs ont échouées. Hors contexte transactionnel, cela ne pose pas de problème si la cohérence de ce fonctionnement a bien été envisagée. Dans le cadre d'une transaction, il est possible cependant qu'il y ait levée d'une exception InvalidOperationException lorsqu'il y a appel d'un RollBack sur une connexion en erreur. Pour contourner le problème, vous devez gérer un bloc Catch spécifique pour l'exception InvalidOperationException.


Quelles sont les méthodes pour connaître le nombre de lignes d'une table ?
auteur : Jean-Marc Rabilloud
Utiliser une requête d'agrégation Count

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT Count(*) FROM Publishers")
    MaCommande.Connection = MaConn
    Dim NbLigne As Integer = CInt(MaCommande.ExecuteScalar)
    MessageBox.Show(NbLigne.ToString)
    MaConn.Close()
End Using
Si vous utilisez un DataReader

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT pub_name FROM Publishers")
    MaCommande.Connection = MaConn
    Dim NbLigne As Integer
    Dim dr As SqlClient.SqlDataReader = MaCommande.ExecuteReader
    Do While dr.Read
Me.TableComboBox.Items.Add(dr.GetString(0))
NbLigne += 1
    Loop
    dr.Close()
    MessageBox.Show(NbLigne.ToString)
    MaConn.Close()
End Using
Si vous utilisez un stockage dans un DataTable

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT * FROM Publishers")
    MaCommande.Connection = MaConn
    Dim NbLigne As Integer
    Dim MaTable As New DataTable("Editeurs")
    MaTable.Load(MaCommande.ExecuteReader)
    NbLigne = MaTable.Rows.Count
    MessageBox.Show(NbLigne.ToString)
    MaConn.Close()
End Using

Quel est l'intérêt de la méthode GetOrdinal et/ou de la propriété Ordinal ?
auteur : Jean-Marc Rabilloud
Il s'agit d'un problème de performance / Maintenance. Prenons le cas de la propriété Ordinal pour des DataColumn, la problématique étant identique pour GetOrdinal. Imaginons un exemple classique de formulaire Maitre/Détails. Cet exemple ne pose évidemment pas de problème de performance mais la logique est la même.

    Private tblEditeur As DataTable

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT * FROM Publishers", MaConn)
    tblEditeur = New DataTable("Editeurs")
    tblEditeur.Load(MaCommande.ExecuteReader)
    tblEditeur.Constraints.Add("PKediteur", tblEditeur.Columns("pub_id"), True)
    With Me.EditeurComboBox
.DisplayMember = "pub_name"
.ValueMember = "pub_id"
.DataSource = tblEditeur.DefaultView
    End With
    MaConn.Close()
End Using
    End Sub

    Private Sub EditeurComboBox_SelectedIndexChanged(ByVal sender As System.Object, & _
ByVal e As System.EventArgs) Handles EditeurComboBox.SelectedIndexChanged
If Me.EditeurComboBox.SelectedIndex > -1 Then
    Dim LigneSelect As DataRow = tblEditeur.Rows.Find(Me.EditeurComboBox.SelectedValue)
    With LigneSelect
Me.Pub_nameTextBox.Text = .Item("pub_name").ToString
Me.CityTextBox.Text = .Item("city").ToString
Me.StateTextBox.Text = .Item("state").ToString
Me.CountryTextBox.Text = .Item("country").ToString
    End With
End If
    End Sub
Si ce code présente l'avantage d'être très lisible et donc facile à maintenir, il va avoir de mauvaise performance puisque chaque nom de champ devra être converti dans sa valeur numérique à chaque appel.

On pourrait évidemment écrire :

    Private Sub EditeurComboBox_SelectedIndexChanged(ByVal sender As System.Object, & _
ByVal e As System.EventArgs) Handles EditeurComboBox.SelectedIndexChanged
If Me.EditeurComboBox.SelectedIndex > -1 Then
    Dim LigneSelect As DataRow = tblEditeur.Rows.Find(Me.EditeurComboBox.SelectedValue)
    With LigneSelect
Me.Pub_nameTextBox.Text = .Item(1).ToString
Me.CityTextBox.Text = .Item(2).ToString
Me.StateTextBox.Text = .Item(3).ToString
Me.CountryTextBox.Text = .Item(4).ToString
    End With
End If
    End Sub
Ce qui conduirait à un code ayant de très bonnes performances. Cependant, outre la perte évidente de lisibilité, ce code est alors dépendant de l'ordre des champs dans la requête. Si le problème n'est pas très grave lorsque la requête est écrite dans le code (encore que?) cela peut être plus gênant lorsqu'on utilise une requête stockée dans le SGBD, surtout si on en n'est pas le rédacteur.

On trouve alors parfois un code qui tente de marier le meilleur des deux mondes en utilisant Ordinal tel que :

    Private tblEditeur As DataTable
    Private intEditeur, intVille, intEtat, intPays As Integer

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT * FROM Publishers", MaConn)
    tblEditeur = New DataTable("Editeurs")
    tblEditeur.Load(MaCommande.ExecuteReader)
    tblEditeur.Constraints.Add("PKediteur", tblEditeur.Columns("pub_id"), True)
    intEditeur = tblEditeur.Columns("pub_name").Ordinal
    intVille = tblEditeur.Columns("city").Ordinal
    intEtat = tblEditeur.Columns("state").Ordinal
    intPays = tblEditeur.Columns("country").Ordinal
    With Me.EditeurComboBox
.DisplayMember = "pub_name"
.ValueMember = "pub_id"
.DataSource = tblEditeur.DefaultView
    End With
    MaConn.Close()
End Using
    End Sub

    Private Sub EditeurComboBox_SelectedIndexChanged(ByVal sender As System.Object,& _
 ByVal e As System.EventArgs) Handles EditeurComboBox.SelectedIndexChanged
If Me.EditeurComboBox.SelectedIndex > -1 Then
    Dim LigneSelect As DataRow = tblEditeur.Rows.Find(Me.EditeurComboBox.SelectedValue)
    With LigneSelect
Me.Pub_nameTextBox.Text = .Item(intEditeur).ToString
Me.CityTextBox.Text = .Item(intVille).ToString
Me.StateTextBox.Text = .Item(intEtat).ToString
Me.CountryTextBox.Text = .Item(intPays).ToString
    End With
End If
    End Sub
On conserve ainsi une bonne lisibilité tout en améliorant nettement les performances. Evidemment, lorsqu'il y a beaucoup de champs, l'affectation peut être un peu fastidieuse. Notez qu'il existe une autre technique également utilisée reposant sur les énumérations telle que :

Private Enum Editeur
pub_id = 0
pub_name = 1
city = 2
state = 3
country = 4
End Enum

    Private tblEditeur As DataTable

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Using MaConn As New SqlClient.SqlConnection("Data Source=FIXE;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand(" SELECT * FROM Publishers", MaConn)
    tblEditeur = New DataTable("Editeurs")
    tblEditeur.Load(MaCommande.ExecuteReader)
    tblEditeur.Constraints.Add("PKediteur", tblEditeur.Columns("pub_id"), True)
    With Me.EditeurComboBox
.DisplayMember = "pub_name"
.ValueMember = "pub_id"
.DataSource = tblEditeur.DefaultView
    End With
    MaConn.Close()
End Using
    End Sub

    Private Sub EditeurComboBox_SelectedIndexChanged(ByVal sender As System.Object, & _
 ByVal e As System.EventArgs) Handles EditeurComboBox.SelectedIndexChanged
If Me.EditeurComboBox.SelectedIndex > -1 Then
    Dim LigneSelect As DataRow = tblEditeur.Rows.Find(Me.EditeurComboBox.SelectedValue)
    With LigneSelect
Me.Pub_nameTextBox.Text = .Item(Editeur.pub_name).ToString
Me.CityTextBox.Text = .Item(Editeur.city).ToString
Me.StateTextBox.Text = .Item(Editeur.state).ToString
Me.CountryTextBox.Text = .Item(Editeur.country).ToString
    End With
End If
    End Sub

Que signifie l'avertissement ReviewSqlQueriesForSecurityVulnerabilities ?
auteur : Jean-Marc Rabilloud
Lorsque le code doit contenir des requêtes SQL, il ne faut pas que celles-ci prennent en compte les saisies de l'utilisateur par concaténation.

Imaginons un formulaire contenant une grille ainsi qu'une zone de texte. En entrant le code de l'auteur on obtient la liste de ses livres ainsi que ses royalties.

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand("SELECT  authors.au_lname, authors.au_fname," & _
" titles.title, titles.price, titles.royalty, titles.advance, authors.au_id" & _
" FROM authors INNER JOIN titleauthor ON authors.au_id = titleauthor.au_id " & _
"INNER JOIN titles ON titleauthor.title_id = titles.title_id " & _
"WHERE authors.au_id = '" + Me.IdentTextBox.Text + "'", MaConn)
    Dim MaTable As New DataTable
    MaTable.Load(MaCommande.ExecuteReader)
    Me.DataGridView1.DataSource = MaTable
    MaConn.Close()
End Using
Si on ne souhaite pas que quelqu'un puisse obtenir l'affichage de ces informations sans connaître le code de l'auteur, ce code n'est pas correctement sécurisé puisqu'il est sensible à une injection SQL. En l'état, si l'utilisateur entre dans la zone de texte la chaîne "' OR 1=1 -" il obtiendra l'affichage des informations pour tous les auteurs. C'est cette faille de sécurité qui déclenche l'avertissement ReviewSqlQueriesForSecurityVulnerabilities.

Pour ne pas avoir ce problème, on utilise plutôt une requête paramétrée telle que :

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand("SELECT  authors.au_lname, authors.au_fname, " & _
"titles.title, titles.price, titles.royalty, titles.advance, authors.au_id " & _
"FROM authors INNER JOIN titleauthor ON authors.au_id = titleauthor.au_id " & _
"INNER JOIN titles ON titleauthor.title_id = titles.title_id " & _
"WHERE authors.au_id = @Identification", MaConn)
    MaCommande.Parameters.Add("@Identification", SqlDbType.NChar).Value = Me.IdentTextBox.Text
    Dim MaTable As New DataTable
    MaTable.Load(MaCommande.ExecuteReader)
    Me.DataGridView1.DataSource = MaTable
    MaConn.Close()
End Using

J'obtiens parfois l'erreur "Échec de la conversion de la valeur de paramètre d'un String en un DateTime" ?
auteur : Jean-Marc Rabilloud
Il s'agit souvent d'un problème de conversion lorsque la valeur doit être NULL. En utilisant un contrôle de time DateTimePicker, on ne peut pas facilement entrer une valeur NULL dans le SGBD. On tend alors à utiliser un contrôle TextBox qu'on laisse vide pour gérer le NULL.

Prenons l'exemple suivant :

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim MaCommande As New SqlClient.SqlCommand("INSERT INTO [sales] ([stor_id], [ord_num], " & _
"[ord_date], [qty], [payterms], [title_id]) VALUES (@stor_id, @ord_num, @ord_date, @qty, @payterms, @title_id)", MaConn)
    MaCommande.Parameters.Add("@stor_id", System.Data.SqlDbType.[Char], 0).Value = Me.Stor_idTextBox.Text
    MaCommande.Parameters.Add("@ord_num", System.Data.SqlDbType.VarChar, 0).Value = Me.Ord_numTextBox.Text
    MaCommande.Parameters.Add("@ord_date", System.Data.SqlDbType.DateTime, 0).Value = Me.Ord_dateTextBox.Text
    MaCommande.Parameters.Add("@qty", System.Data.SqlDbType.SmallInt, 0).Value = Me.QtyTextBox.Text
    MaCommande.Parameters.Add("@payterms", System.Data.SqlDbType.VarChar, 0).Value = Me.PaytermsTextBox.Text
    MaCommande.Parameters.Add("@title_id", System.Data.SqlDbType.VarChar, 0).Value = Me.Title_idTextBox.Text
    MaCommande.ExecuteNonQuery()
    MaConn.Close()
End Using
Si la zone de texte Ord_dateTextBox est vide, j'obtiendrais l'erreur de conversion. Il convient donc d'écrire :

MaCommande.Parameters.Add("@ord_date", System.Data.SqlDbType.DateTime, 0)
    If Not IsDate(Me.Ord_dateTextBox.Text) Then
MaCommande.Parameters("@ord_date").Value = SqlTypes.SqlDateTime.Null
    Else
MaCommande.Parameters("@ord_date").Value = Me.Ord_dateTextBox.Text
    End If

Comment compacter une base Access ?
auteur : Jean-Marc Rabilloud
Il n'existe pas de méthodes pour compacter une base Access sans utiliser un composant COM. Bien qu'il soit possible de le faire en faisant une référence à DAO, il est plutôt recommandé d'utiliser la bibliothèque Microsoft Jet and Replication Objects 2.x Library (JRO).

Ensuite de quoi il suffit d'écrire :

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim MyJRO As New JRO.JetEngine
MyJRO.CompactDatabase("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\user\JMARC\biblio2.mdb", " & _
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\user\JMARC\biblio3.mdb;Jet OLEDB:Engine Type=5")
MyJRO = Nothing
MsgBox("Done")
End Sub

Comment récupérer une image stockée dans SQL Server ?
auteur : Jean-Marc Rabilloud
Il y a plusieurs méthodes de récupération selon la finalité envisagée, mais la plupart demande l'utilisation d'un flux intermédiaire. Tous les objets de données, Commande, DataReader ou DataRow permettent cette récupération. Dans l'exemple suivant, on affiche dans un PictureBox une image extraite à l'aide de la méthode ExecuteScalar de l'objet Command :

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim command As SqlClient.SqlCommand = New SqlClient.SqlCommand("SELECT logo FROM pub_info WHERE pub_id = @pubid", MaConn)
    command.Parameters.Add("@pubid", SqlDbType.Char, 4).Value = "0877"
    Dim MS As New IO.MemoryStream(CType(command.ExecuteScalar(), Byte()))
    Dim MonLogo As New Drawing.Bitmap(MS)
    Me.LogoPictureBox.Image = MonLogo
    MaConn.Close()
End Using
L'exemple suivant extrait toutes les images de la table sous forme de fichier à l'aide d'un DataReader :

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    Dim command As SqlClient.SqlCommand = New SqlClient.SqlCommand("SELECT pub_id,logo FROM pub_info", MaConn)
    Dim MonReader As SqlClient.SqlDataReader = command.ExecuteReader()
    Do While MonReader.Read
Dim bytBLOBData(CInt(MonReader.GetBytes(1, 0, Nothing, 0, Integer.MaxValue) - 1)) As Byte
MonReader.GetBytes(1, 0, bytBLOBData, 0, bytBLOBData.Length)
Dim MS As New IO.MemoryStream(bytBLOBData)
Dim MonLogo As New Drawing.Bitmap(MS)
MonLogo.Save("publogo" + MonReader.GetString(0) + ".bmp")
    Loop
    MaConn.Close()
End Using

Quelles différences entre NChar, NVarChar et VarChar ?
auteur : Jean-Marc Rabilloud
Les définitions sont :

char[(n)] Chaine de longueur n de caractères non unicode (1 < n < 8000). La taille sera de n octets

nchar[(n)] Chaine de longueur n de caractères unicode (1 < n < 4000). La taille sera de n*2 octets

varchar[(n)] Chaine de longueur variable de caractères non unicode. La valeur n maximale est de 8000. La taille réelle sera de 1 octet par caractère effectivement stockée. La chaîne stockée peut être de longueur nulle.

nvarchar[(n)] Chaine de longueur variable de caractères unicode. La valeur n maximale est de 4000. La taille réelle sera de 2 octet par caractère effectivement stockée. La chaîne stockée peut être de longueur nulle.


Access ne stocke pas mes dates correctement, pourquoi ?
auteur : Jean-Marc Rabilloud
Le moteur Jet d'Access tente toujours de convertir les dates entrées au format américain, c'est-à-dire MM/DD/YYYY. Lorsque vous entrez une date au format français, celle-ci sera converti au format américain si cela est possible (c.a.d. si le jour est inférieur à 13) ou correctement interprété si la conversion n'est pas possible. Vous devez donc employer des fonction de conversion ou de création de Date et non des littéraux. Par exemple :

Using MaConn As New OleDb.OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=D:\User\tutos\NorthWind.mdb")
    Dim Command As New OleDb.OleDbCommand("SELECT * FROM Employees", MaConn)
    Dim MonDta As New OleDb.OleDbDataAdapter
    MonDta.SelectCommand = Command
    Dim CommBuild As OleDb.OleDbCommandBuilder = New OleDb.OleDbCommandBuilder(MonDta)
    Dim tblEmploye As New DataTable
    MaConn.Open()
    MonDta.Fill(tblEmploye)
    Dim svgBirthDate As Date = CDate(tblEmploye.Rows(0).Item("BirthDate"))
    'Cette date ne peut pas être interprété
    tblEmploye.Rows(0).Item("BirthDate") = CDate("30/11/1967")
    MonDta.Update(tblEmploye)
    MonDta.Fill(tblEmploye)
    MessageBox.Show("la date stockée est le : " + tblEmploye.Rows(0).Item("BirthDate").ToString)
    'cette date peut être interprétée
    tblEmploye.Rows(0).Item("BirthDate") = #4/11/1967#
    MonDta.Update(tblEmploye)
    MonDta.Fill(tblEmploye)
    MessageBox.Show("la date stockée est le : " + tblEmploye.Rows(0).Item("BirthDate").ToString)
    'il faut donc la stocker sous forme
    tblEmploye.Rows(0).Item("BirthDate") = CDate("04/11/1967")
    MonDta.Update(tblEmploye)
    MonDta.Fill(tblEmploye)
    MessageBox.Show("la date stockée est le : " + tblEmploye.Rows(0).Item("BirthDate").ToString)
    'ou sous la forme
    tblEmploye.Rows(0).Item("BirthDate") = New Date(1967, 11, 4)
    MonDta.Update(tblEmploye)
    MonDta.Fill(tblEmploye)
    MessageBox.Show("la date stockée est le : " + tblEmploye.Rows(0).Item("BirthDate").ToString)
    'restauration
    tblEmploye.Rows(0).Item("BirthDate") = svgBirthDate
    MonDta.Update(tblEmploye)
    MaConn.Close()
End Using

Je reçois l'erreur "Une exception non gérée du type 'System.Data.OleDb.OleDbException' s'est produite dans system.data.dll" lors de l'appel de la méthode Update ?
auteur : Jean-Marc Rabilloud
C'est un peu le message d'erreur fourre tout. Il existe de nombreuses erreurs qui peuvent déclencher ce message, celui-ci étant principalement le signe d'une ambiguïté dans une commande ou du fonctionnement mal paramétré d'un objet de données.

On obtient par exemple ce message lorsqu'on tente d'insérer une valeur dans un champ auto incrémenté, lorsque la requête utilise un mot réservé, lorsque des éléments de la requête contiennent des espaces ou des accents, et ainsi de suite.

Dans la plupart des cas, l'erreur se situe au niveau de la requête et un examen de celle-ci suffit.


Comment fonctionne l'exception ConcurrencyException ?
auteur : Jean-Marc Rabilloud
Nous sommes donc dans un mode déconnecté. C'est-à-dire qu'il n'y a pas de notion de position courante, dans l'absolu on peut même travailler sans connexion une fois les données chargées.

Pour identifier un enregistrement afin de faire des modifications, l'objet d'action utilisé va donc se servir de critère situé dans la requête d'action puisque c'est in fine de toute façon une commande qui va agir. Globalement il y a trois cas, pour ces commandes :

  • Utilisation de requêtes/procédures stockées dans le SGBD
  • Utilisation de requêtes générées par un objet CommandBuilder
  • Utilisation de requête explicitement définie dans le code.
Nous allons travailler sur ce troisième cas puisque dans le cas de la gestion concurrentielle les problèmes sont toujours de même ordre quel que soit le cas choisis.

Prenons un exemple simple pour commencer en travaillant sur la table 'publishers' que l'on peut représenter comme :

Commençons par une construction sans objet intégré, c'est-à-dire en ne travaillant qu'avec des commandes. Nous allons avoir un code de définition et de remplissage :

Private tblEditeur As DataTable
Private CommandSelect, CommandDelete, CommandInsert, CommandUpdate As SqlClient.SqlCommand
Private MaConn As SqlClient.SqlConnection

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    MaConn = New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    'définition de la commande de remplissage //pas de paramètre
    CommandSelect = New SqlClient.SqlCommand("SELECT pub_id, pub_name, city, state, " & _
"country FROM publishers", MaConn)
    'définition de la commande de suppression  // un paramètre pour la clé primaire
    CommandDelete = New SqlClient.SqlCommand("DELETE FROM publishers " & _
"WHERE pub_id = @Original_pub_id", MaConn)
     'définition de la commande d'ajout  // un paramètre pour chaque valeur
    CommandInsert = New SqlClient.SqlCommand("INSERT INTO publishers (pub_id, pub_name, " & _
"city, state, country) VALUES (@pub_id, @pub_name, @city, @state, @country)", MaConn)
    'définition de la commande de mise à jour  
' un paramètre pour chaque valeur + un paramètre pour la clé primaire
    CommandUpdate = New SqlClient.SqlCommand("UPDATE publishers SET pub_id = @pub_id, " & _
"pub_name = @pub_name, city = @city, state = @state, country = @country " & _
"WHERE pub_id = @Original_pub_id", MaConn)
    tblEditeur = New DataTable("Editeur")
    tblEditeur.Load(CommandSelect.ExecuteReader)
    tblEditeur.Constraints.Add("PKEditeur", tblEditeur.Columns("pub_id"), True)
    Me.DataGridView1.DataSource = tblEditeur.DefaultView
    MaConn.Close()

End Sub
Un code créant des modifications :

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    'ajout d'un enregistrement
    Dim LigneEditeur As DataRow = tblEditeur.NewRow
    With LigneEditeur
.Item("pub_id") = "9911"
.Item("pub_name") = "Developpez.com"
.Item("city") = "Paris"
.Item("state") = DBNull.Value
.Item("country") = "France"
    End With
    tblEditeur.Rows.Add(LigneEditeur)
    LigneEditeur = tblEditeur.Rows.Find("0877")
    LigneEditeur.Delete()
    LigneEditeur = tblEditeur.Rows.Find("9999")
    LigneEditeur.Item("city") = "Lyon"
    'Appel de la fonction personnalisée de mise à jour
    Dim NbMaJ As Integer = MaFctMaJ()
    MsgBox("Nombre de lignes affectées : " + NbMaJ.ToString)
End Sub
Et un code appliquant ces modifications.

Private Function MaFctMaJ() As Integer
    Dim NbLigne As Integer = 0
    Dim LigneEditeur As DataRow
    Dim tmpTable As DataTable
    'Traitement des ajouts
    MaConn.Open()
    tmpTable = tblEditeur.GetChanges(DataRowState.Added)
    If tmpTable.Rows.Count > 0 Then
With CommandInsert.Parameters
    .Add("@pub_id", System.Data.SqlDbType.[Char], 4)
    .Add("@pub_name", System.Data.SqlDbType.VarChar, 40)
    .Add("@city", System.Data.SqlDbType.VarChar, 20)
    .Add("@state", System.Data.SqlDbType.[Char], 2)
    .Add("@country", System.Data.SqlDbType.VarChar, 30)
End With
For Each LigneEditeur In tmpTable.Rows
    With CommandInsert.Parameters
.Item("@pub_id").Value = LigneEditeur.Item("pub_id", DataRowVersion.Current)
.Item("@pub_name").Value = LigneEditeur.Item("pub_name", DataRowVersion.Current)
.Item("@city").Value = LigneEditeur.Item("city", DataRowVersion.Current)
.Item("@state").Value = LigneEditeur.Item("state", DataRowVersion.Current)
.Item("@country").Value = LigneEditeur.Item("country", DataRowVersion.Current)
    End With
    NbLigne += CommandInsert.ExecuteNonQuery
    LigneEditeur.AcceptChanges()
Next
    End If
    'Traitement des suppression
    tmpTable = tblEditeur.GetChanges(DataRowState.Deleted)
    If tmpTable.Rows.Count > 0 Then
CommandDelete.Parameters.Add("@Original_pub_id", System.Data.SqlDbType.[Char], 4)
For Each LigneEditeur In tmpTable.Rows
    CommandDelete.Parameters.Item("@Original_pub_id").Value = LigneEditeur.Item("pub_id", DataRowVersion.Original)
    NbLigne += CommandDelete.ExecuteNonQuery
    LigneEditeur.AcceptChanges()
Next
    End If
    'traitement des modifications
    tmpTable = tblEditeur.GetChanges(DataRowState.Modified)
    If tmpTable.Rows.Count > 0 Then
With CommandUpdate.Parameters
    .Add("@pub_id", System.Data.SqlDbType.[Char], 4)
    .Add("@pub_name", System.Data.SqlDbType.VarChar, 40)
    .Add("@city", System.Data.SqlDbType.VarChar, 20)
    .Add("@state", System.Data.SqlDbType.[Char], 2)
    .Add("@country", System.Data.SqlDbType.VarChar, 30)
    .Add("@Original_pub_id", System.Data.SqlDbType.[Char], 4)
End With
For Each LigneEditeur In tmpTable.Rows
    With CommandUpdate.Parameters
.Item("@pub_id").Value = LigneEditeur.Item("pub_id", DataRowVersion.Current)
.Item("@pub_name").Value = LigneEditeur.Item("pub_name", DataRowVersion.Current)
.Item("@city").Value = LigneEditeur.Item("city", DataRowVersion.Current)
.Item("@state").Value = LigneEditeur.Item("state", DataRowVersion.Current)
.Item("@country").Value = LigneEditeur.Item("country", DataRowVersion.Current)
.Item("@Original_pub_id").Value = LigneEditeur.Item("pub_id", DataRowVersion.Original)
    End With
    NbLigne += CommandUpdate.ExecuteNonQuery
    LigneEditeur.AcceptChanges()
Next
    End If
    MaConn.Close()
    Return NbLigne

End Function
Détaillons un peu cela avant d'aller plus loin. Nous avons donc défini des requêtes paramétrées pour chaque type d'action, ajout, modification et suppression. Puis nous avons rempli la table des éditeurs. Dans le code de modification, nous avons ajouté un enregistrement, supprimé un enregistrement et modifié un.

Si la fonction de mise à jour peut paraître complexe, elle reprend le cheminement des objets "automatiques" intégrés dans le Framework comme le DataAdapter.

C'est-à-dire qu'elle extrait les lignes modifiées regroupées par type de modification, applique les paramètres en récupérant les valeurs dans la table et envoie les modifications vers le SGBD.

On pourrait donc écrire un code identique utilisant un DataAdapter tel que :

Private tblEditeur As DataTable
Private dtaEditeur As SqlClient.SqlDataAdapter

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
MaConn.Open()
dtaEditeur = New SqlClient.SqlDataAdapter
With dtaEditeur
    'définition des commandes
    .SelectCommand = New SqlClient.SqlCommand("SELECT pub_id, pub_name, city, state," & _
" country FROM publishers", MaConn)
    .DeleteCommand = New SqlClient.SqlCommand("DELETE FROM publishers " & _
"WHERE pub_id = @Original_pub_id", MaConn)
    .InsertCommand = New SqlClient.SqlCommand("INSERT INTO publishers (pub_id, pub_name," & _
" city, state, country) VALUES (@pub_id, @pub_name, @city, @state, @country)", MaConn)
    .UpdateCommand = New SqlClient.SqlCommand("UPDATE publishers SET pub_id = @pub_id," & _
" pub_name = @pub_name, city = @city, state = @state, country = @country " & _
"WHERE pub_id = @Original_pub_id", MaConn)
    'paramétrage du dataadapter
    .AcceptChangesDuringFill = True
    .AcceptChangesDuringUpdate = True
    .ContinueUpdateOnError = True
    'mappage des paramètre de la commande Delete
    With .DeleteCommand.Parameters
.Add(New System.Data.SqlClient.SqlParameter("@Original_pub_id", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_id",  & _
System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    End With
    'mappage des paramètre de la commande Insert
    With .InsertCommand.Parameters
.Add(New System.Data.SqlClient.SqlParameter("@pub_id", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_id",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@pub_name", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_name",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@city", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "city",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@state", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "state",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@country", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "country",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    End With
    'mappage des paramètre de la commande update
    With .UpdateCommand.Parameters
.Add(New System.Data.SqlClient.SqlParameter("@pub_id", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_id",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@pub_name", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_name",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@city", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "city",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@state", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "state",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@country", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "country",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
.Add(New System.Data.SqlClient.SqlParameter("@Original_pub_id", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_id",  & _
System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    End With
End With
tblEditeur = New DataTable("Editeur")
dtaEditeur.Fill(tblEditeur)
tblEditeur.Constraints.Add("PKEditeur", tblEditeur.Columns("pub_id"), True)
Me.DataGridView1.DataSource = tblEditeur.DefaultView
MaConn.Close()
    End Using

End Sub

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    'ajout d'un enregistrement
    Dim LigneEditeur As DataRow = tblEditeur.NewRow
    With LigneEditeur
.Item("pub_id") = "9911"
.Item("pub_name") = "Developpez.com"
.Item("city") = "Paris"
.Item("state") = DBNull.Value
.Item("country") = "France"
    End With
    tblEditeur.Rows.Add(LigneEditeur)
    LigneEditeur = tblEditeur.Rows.Find("0877")
    LigneEditeur.Delete()
    LigneEditeur = tblEditeur.Rows.Find("9999")
    LigneEditeur.Item("city") = "Lyon"
    'Appel de la méthode update du dataadapter
    Dim NbMaJ As Integer = dtaEditeur.Update(tblEditeur)
    MsgBox("Nombre de lignes affectées : " + NbMaJ.ToString)
    Me.DataGridView1.DataSource = tblEditeur.DefaultView
End Sub
N.B : De fait, l'enregistrement qui doit être supprimé ne le sera que si les clés étrangères dans la base source liée aux éditeurs sont réglées sur cascade.

Mais revenons à notre sujet principal, la gestion de la concurrence. Les actions sont exécutées en respectant les requêtes. Dans notre exemple, l'insertion aura donc toujours lieu sauf si un enregistrement avec la même clé existe déjà, la suppression aura toujours lieu si l'enregistrement avec une clé identique existe et la modification aura toujours lieu si un enregistrement avec la clé existe. On appelle ce mode mise à jour sur clé, puisque seul le champ de la clé primaire est utilisé pour identifier l'enregistrement qui doit subir l'action.

Évidemment dans ce cas, nous n'aurons un problème de concurrence que si l'enregistrement ciblé a été supprimé entre le moment où nous avons rempli le Dataset et le moment de l'appel Update. Si l'enregistrement a été modifié par un autre utilisateur, nos modifications vont aller écraser les siennes sans qu'il y ait une levée de l'exception ConcurrencyException.

Pour éviter cela, on génère parfois un autre type de requête qui contient dans la clause WHERE la totalité des champs. Cela donnerait alors un code tel que :

Using MaConn As New SqlClient.SqlConnection("Data Source=ISPCF261025\SQLEXPRESS;" & _
"Initial Catalog=pubs;Integrated Security=True")
    MaConn.Open()
    dtaEditeur = New SqlClient.SqlDataAdapter
    With dtaEditeur
'définition des commandes
.SelectCommand = New SqlClient.SqlCommand("SELECT pub_id, pub_name, city, state, country " & _
"FROM publishers", MaConn)
.DeleteCommand = New SqlClient.SqlCommand("DELETE FROM publishers WHERE " & _
"((pub_id = @Original_pub_id) AND ((@IsNull_pub_name = 1 AND pub_name IS NULL) " & _
"OR (pub_name = @Original_pub_name)) AND ((@IsNull_city = 1 AND city IS NULL) " & _
"OR (city = @Original_city)) AND ((@IsNull_state = 1 AND state IS NULL) " & _
"OR (state = @Original_state)) AND ((@IsNull_country = 1 AND country IS NULL) " & _
"OR (country = @Original_country)))", MaConn)
.InsertCommand = New SqlClient.SqlCommand("INSERT INTO publishers (pub_id, pub_name, " & _
"city, state, country) VALUES (@pub_id, @pub_name, @city, @state, @country)", MaConn)
.UpdateCommand = New SqlClient.SqlCommand("UPDATE publishers SET pub_id = @pub_id," & _
" pub_name = @pub_name, city = @city, state = @state, country = @country " & _
"WHERE ((pub_id = @Original_pub_id) AND ((@IsNull_pub_name = 1 AND pub_name IS NULL) " & _
"OR (pub_name = @Original_pub_name)) AND ((@IsNull_city = 1 AND city IS NULL) " & _
"OR (city = @Original_city)) AND ((@IsNull_state = 1 AND state IS NULL) " & _
"OR (state = @Original_state)) AND ((@IsNull_country = 1 AND country IS NULL) " & _
"OR (country = @Original_country)))", MaConn)
'paramétrage du dataadapter
.AcceptChangesDuringFill = True
.AcceptChangesDuringUpdate = True
.ContinueUpdateOnError = True
'mappage des paramètre de la commande Delete
With .DeleteCommand.Parameters
    .Add(New System.Data.SqlClient.SqlParameter("@Original_pub_id",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_id", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_pub_name",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_name", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_pub_name",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_name", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_city",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "city", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_city",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "city", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_state",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "state", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_state",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "state", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_country",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "country", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_country",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "country", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
End With
'mappage des paramètre de la commande Insert
With .InsertCommand.Parameters
    .Add(New System.Data.SqlClient.SqlParameter("@pub_id", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_id",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@pub_name", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "pub_name",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@city", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "city",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@state", System.Data.SqlDbType.[Char], & _
 0, System.Data.ParameterDirection.Input, 0, 0, "state",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@country", System.Data.SqlDbType.VarChar, & _
 0, System.Data.ParameterDirection.Input, 0, 0, "country",  & _
System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
End With
'mappage des paramètre de la commande update
With .UpdateCommand.Parameters
    .Add(New System.Data.SqlClient.SqlParameter("@pub_id",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_id", System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@pub_name",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_name", System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@city",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "city", System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@state",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "state", System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@country",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "country", System.Data.DataRowVersion.Current, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_pub_id",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_id", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_pub_name",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_name", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_pub_name",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "pub_name", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_city",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "city", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_city",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "city", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_state",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input,  & _
0, 0, "state", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_state",  & _
System.Data.SqlDbType.[Char], 0, System.Data.ParameterDirection.Input,  & _
0, 0, "state", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@IsNull_country",  & _
System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.Input, & _
 0, 0, "country", System.Data.DataRowVersion.Original, True, Nothing, "", "", ""))
    .Add(New System.Data.SqlClient.SqlParameter("@Original_country",  & _
System.Data.SqlDbType.VarChar, 0, System.Data.ParameterDirection.Input, & _
 0, 0, "country", System.Data.DataRowVersion.Original, False, Nothing, "", "", ""))
End With
    End With
    tblEditeur = New DataTable("Editeur")
    dtaEditeur.Fill(tblEditeur)
    tblEditeur.Constraints.Add("PKEditeur", tblEditeur.Columns("pub_id"), True)
    Me.DataGridView1.DataSource = tblEditeur.DefaultView
    MaConn.Close()
End Using
Comme nous le voyons, il s'agit déjà de requête plus complexe nécessitant vite un grand nombre de paramètre. Vous noterez aussi qu'il faut gérer explicitement le cas des champs pouvant être Null. La chaîne de la clause WHERE contient tous les champs que l'on souhaite utiliser pour l'identification associés par des AND tel que :

Pour les champs ne pouvant pas être Null

NomChamp = @Original_param

Pour les champs pouvant être Null

((@Param_Null = 1 AND NomChamp IS NULL) OR (NomChamp = @Original_param))

Ce mode de contrôle sur tous les champs est parfois appelé mode "Optimiste pessimiste", puisqu'il ressemble à un verrouillage pessimiste dans le sens où tout enregistrement ayant été modifié par ailleurs ne pourra plus l'être lors de la mise à jour, il y aura levée de l'exception ConcurrencyException.

Il existe un troisième mode qui consiste à ne tester que les champs ayant été modifiés. Ce troisième mode est relativement complexe à mettre en place car il demande une génération automatique de la requête pour chaque ligne modifiée.


Comment récupérer l'identifiant d'un champ auto-incrémenté après son insertion dans le SGBD ?
auteur : Jean-Marc Rabilloud
C'est la question à 3 $ des forums de développement, avec la réponse associée qui est du même tonneau, 'ça dépend' puisque cela dépend généralement du SGBD ciblé.

La problématique est généralement la suivante, dans un environnement de table en relation, on crée des enregistrements parents et des enregistrements fils et la clé primaire de la table parent est de type "auto-incrémentée". Cela veut dire que la valeur de la clé dans le Dataset à peu de chance d'être celle qui sera effectivement attribuée par le SGBD. Or cette valeur de clé doit être connue puisqu'elle sera la valeur dans la colonne de clé étrangère de la table fille et que la relation ne peut être correcte qu'à cette condition. Le problème se décompose donc en deux aspect, comment récupérer une valeur auto incrémentée attribuée par le SGBD et comment la propager dans les champs adéquats.

Pour réaliser ces actions, on pratique généralement selon le scénario suivant :

  • Gérer un identifiant local
  • Récupérer le numéro généré
  • Propagation dans les tables filles
  • Remise à jour
Prenons d'abord un exemple classique sur la base Northwind qui présente des tables en relation ayant des clés auto incrémentées que nous pourrions représenter telles que :

Commençons par manipuler une base Access, ce qui est un peu plus complexe puisqu'Access n'accepte ni les procédures stockée ni les lots de requêtes.

Je vais remplir le Dataset et utiliser des objets DataAdapter définis à minima pour pouvoir faire des insertions, le code de base pourrait être :

MaConn = New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\user\tutos\Northwind.mdb")
'définition des DataAdapters
dtaCategorie = New OleDbDataAdapter("SELECT * FROM Categories", MaConn)
dtaProduit = New OleDbDataAdapter("SELECT * FROM Products", MaConn)
dtaFournisseur = New OleDbDataAdapter("SELECT * FROM Suppliers", MaConn)
dtaProduit.InsertCommand = New OleDbCommand("INSERT INTO `Products` (`ProductName`," & _
" `SupplierID`, `CategoryID`, `QuantityPerUnit`, `UnitPrice`, `UnitsInStock`, "  & _
"`UnitsOnOrder`, `ReorderLevel`, `Discontinued`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", MaConn)
With dtaProduit.InsertCommand.Parameters
    .Add(New OleDbParameter("@ProductName", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "ProductName", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@SupplierID", OleDbType.[Integer], 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "SupplierID", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@CategoryID", OleDbType.[Integer], 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "CategoryID", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@QuantityPerUnit", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "QuantityPerUnit", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@UnitPrice", OleDbType.Currency, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "UnitPrice", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@UnitsInStock", OleDbType.SmallInt, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "UnitsInStock", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@UnitsOnOrder", OleDbType.SmallInt, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "UnitsOnOrder", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@ReorderLevel", OleDbType.SmallInt, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "ReorderLevel", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Discontinued", OleDbType.[Boolean], 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Discontinued", DataRowVersion.Current, False, Nothing))
End With
dtaFournisseur.InsertCommand = New OleDbCommand("INSERT INTO `Suppliers` (`CompanyName`," & _
" `ContactName`, `ContactTitle`, `Address`, `City`, `Region`, `PostalCode`, `Country`, " & _
"`Phone`, `Fax`, `HomePage`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", MaConn)
With dtaFournisseur.InsertCommand.Parameters
    .Add(New OleDbParameter("@CompanyName", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "CompanyName", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@ContactName", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "ContactName", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@ContactTitle", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "ContactTitle", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Address", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Address", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@City", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "City", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Region", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Region", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@PostalCode", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "PostalCode", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Country", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Country", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Phone", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Phone", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@Fax", OleDbType.VarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "Fax", DataRowVersion.Current, False, Nothing))
    .Add(New OleDbParameter("@HomePage", OleDbType.LongVarWChar, 0, ParameterDirection.Input, & _
 CType(0, Byte), CType(0, Byte), "HomePage", DataRowVersion.Current, False, Nothing))
End With
'remplissage du dataset
dsNWind = New DataSet
'Identifiant local, généralement entier négatif
With dsNWind.Tables.Add("Categories")
    dtaCategorie.FillSchema(dsNWind, SchemaType.Source, "Categories")
    .Columns(0).AutoIncrementSeed = 0
    .Columns(0).AutoIncrementStep = -1
End With
With dsNWind.Tables.Add("Fournisseurs")
    dtaFournisseur.FillSchema(dsNWind, SchemaType.Source, "Fournisseurs")
    .Columns(0).AutoIncrementSeed = 0
    .Columns(0).AutoIncrementStep = -1
End With
With dsNWind.Tables.Add("Produits")
    dtaProduit.FillSchema(dsNWind, SchemaType.Source, "Produits")
    .Columns(0).AutoIncrementSeed = 0
    .Columns(0).AutoIncrementStep = -1
End With
dtaCategorie.Fill(dsNWind, "Categories")
dtaFournisseur.Fill(dsNWind, "Fournisseurs")
dtaProduit.Fill(dsNWind, "Produits")
Me.DataGridView1.DataSource = dsNWind.Tables("Fournisseurs")
Le début du code est l'affectation classique de commande d'insertion dans les DataAdapter. On établit ensuite un identifiant local pour chaque table avant une clé primaire auto générée. Il existe plusieurs méthodes pour faire cela, mais la technique la plus couramment utilisée consiste à modifier les valeurs d'initialisation de l'auto incrément afin que les enregistrements ajoutés ne puissent avoir la même clé que les enregistrements existants. Il suffit pour cela de générer l'attribution d'un entier négatif ou nul, c'est ce que l'on fait avec le code :

    .Columns(0).AutoIncrementSeed = 0
    .Columns(0).AutoIncrementStep = -1
N.B : Ce code doit être placé avant le remplissage de la table

Nous allons ensuite ajouter un fournisseur et un produit, celui-ci étant lié au nouveau fournisseur.

Dim LigneCategorie As DataRow = dsNWind.Tables("Categories").Rows(0)
Dim LigneFournisseur As DataRow = dsNWind.Tables("Fournisseurs").NewRow
Dim LigneProduit As DataRow = dsNWind.Tables("Produits").NewRow
With LigneFournisseur
    .Item("CompanyName") = "Developpez.com"
    .Item("City") = "Paris"
    .Item("Country") = "France"
End With
dsNWind.Tables("Fournisseurs").Rows.Add(LigneFournisseur)
With LigneProduit
    .Item("ProductName") = "Mouffles"
    .Item("QuantityPerUnit") = 2
    .Item("UnitPrice") = 10
    .Item("UnitsInStock") = 10
    .Item("UnitsOnOrder") = 0
    .Item("CategoryID") = LigneCategorie.Item("CategoryID")
    .Item("SupplierID") = LigneFournisseur.Item("SupplierID")
End With
dsNWind.Tables("Produits").Rows.Add(LigneProduit)
AddHandler dtaFournisseur.RowUpdated, New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
Dim tmpTable As DataTable = dsNWind.Tables("Fournisseurs").GetChanges(DataRowState.Added)
dtaFournisseur.Update(tmpTable)
dsNWind.Tables("Fournisseurs").Merge(tmpTable)
dsNWind.Tables("Fournisseurs").AcceptChanges()
AddHandler dtaProduit.RowUpdated, New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
tmpTable = dsNWind.Tables("Produits").GetChanges(DataRowState.Added)
dtaProduit.Update(tmpTable)
dsNWind.Tables("Produits").Merge(tmpTable)
dsNWind.Tables("Produits").AcceptChanges() 
Expliquons la deuxième partie de ce code, puisque la première n'est que la valorisation et l'insertion des lignes dans les tables.

Les enregistrements ajoutés ont dans le Dataset une clé primaire dont la valeur a été attribué par le Dataset, c'est-à-dire négative ou nulle avec les règles que nous avons définis. Nous allons déclencher les mises à jour des tables. Cette mise à jour ne peut être faite que dans l'ordre parents -> filles sinon la mise à jour ne sera généralement pas possible (puisque le SGBD va corner si on tente de lui faire ajouter un enregistrement dont le père n'est pas défini. Nous allons abonner le DataAdapter à l'évènement RowUpdated afin de pouvoir récupérer et propager la valeur de clé générée par le SGBD.

Nous allons envoyer une copie filtrée de chaque table, celle-ci ne contenant que les lignes à insérer. Pour chaque ligne traiter, il y aura exécution d'un évènement RowUpdated que nous aurons codé sous la forme :

Private Sub OnRowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs)

    If e.StatementType = StatementType.Insert Then
        'récupération de l'identifiant
        Dim cmdNewID As New OleDbCommand("SELECT @@IDENTITY", MaConn)
        e.Row(0) = CInt(cmdNewID.ExecuteScalar)
        e.Status = UpdateStatus.SkipCurrentRow
        'propagation
        If String.Compare("Fournisseurs", e.Row.Table.TableName, True) = 0 Then
            Dim Lignes() As DataRow = dsNWind.Tables("Produits").Select("SupplierID="  & _
+ e.Row(0, DataRowVersion.Original).ToString)
            For Each LigProduit As DataRow In Lignes
                LigProduit.Item("SupplierID") = e.Row(0)
            Next
        End If
    End If

End Sub
Que signifie ce traitement. Pour chaque ligne traitée, il va vérifier que nous sommes bien dans le cas d'un ajout d'enregistrement. Cet évènement se produit après l'envoi de la requête.

Dans Access, la requête SELECT @@IDENTITY renvoie la valeur du dernier numéro généré par le SGBD pour la connexion en cours. On récupère donc par l'exécution de la commande cmdNewID l'identifiant de l'enregistrement qui vient d'être inséré. A ce stade, il y a normalement appel de la méthode AcceptChanges dans une opération classique de mise à jour. Cet appel va entre autre modifier le statut de la ligne en "Unchanged" et envoyer les valeurs courantes vers les valeurs originales. Il faut à ce stade bloquer cet appel, ce que l'on fait à l'aide de la valeur de status 'SkipCurrentRow'.

Pourquoi ce blocage est-il indispensable ?

D'abord parce que pour réintégrer correctement la valeur généré par le SGBD, il va falloir fusionner la table d'origine et la table temporaire modifiée et que pour que cette fusion se passe correctement il faut que l'état de la ligne et la valeur originale dans la table modifiée soit identique à celle de la table d'origine. Ensuite parce qu'à ce moment, les enregistrements enfants possèdent encore comme clé étrangère la valeur initialement attribuée comme clé primaire par le Dataset. Nous devons donc propager la valeur d'identité récupérée vers les tables enfants. Pour cela, nous allons récupérer l'ensemble des lignes ayant comme clé étrangère l'ancienne valeur de clé pour affecter la nouvelle valeur de clé.

Ensuite de quoi nous pourrons fusionner les tables puis appeler 'AcceptChanges' pour finir de valider les modifications. On enverra ensuite la table enfant faire sa mise à jour selon le même principe.

Une approche un peu plus simple est possible avec des SGBD permettant d'envoyer des paramètres sortant et/ou des jeux de résultats multiples. Prenons un exemple sur la base Northwind pour SQL Server maintenant.

Le code de remplissage va être de la forme :

    Private MaConn As SqlConnection
    Private dsNWind As DataSet
    Private dtaCategorie, dtaProduit, dtaFournisseur As SqlDataAdapter

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        MaConn = New SqlConnection("Data Source=ISPCF261025\sqlexpress;" & _
"Initial Catalog=Northwind;Integrated Security=True")
        'définition des DataAdapters
        dtaCategorie = New SqlDataAdapter("SELECT * FROM Categories", MaConn)
        dtaProduit = New SqlDataAdapter("SELECT * FROM Products", MaConn)
        dtaFournisseur = New SqlDataAdapter("SELECT * FROM Suppliers", MaConn)
        dtaProduit.InsertCommand = New SqlCommand("INSERT INTO Products (ProductName, SupplierID, CategoryID," & _
" QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued)" & _
" VALUES (@ProductName, @SupplierID, @CategoryID, @QuantityPerUnit, @UnitPrice," & _
 @UnitsInStock, @UnitsOnOrder, @ReorderLevel, @Discontinued);"  & _
+ ChrW(10) + ChrW(13) +  & _
"SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice,"  & _
" UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued FROM Products "  & _
"WHERE (ProductID = SCOPE_IDENTITY())", MaConn)
        With dtaProduit.InsertCommand.Parameters
            .Add(New SqlParameter("@ProductName", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "ProductName", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@SupplierID", SqlDbType.Int, 0, ParameterDirection.Input, & _
 0, 0, "SupplierID", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@CategoryID", SqlDbType.Int, 0, ParameterDirection.Input, & _
 0, 0, "CategoryID", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@QuantityPerUnit", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "QuantityPerUnit", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@UnitPrice", SqlDbType.Money, 0, ParameterDirection.Input, & _
 0, 0, "UnitPrice", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@UnitsInStock", SqlDbType.SmallInt, 0, ParameterDirection.Input, & _
 0, 0, "UnitsInStock", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@UnitsOnOrder", SqlDbType.SmallInt, 0, ParameterDirection.Input, & _
 0, 0, "UnitsOnOrder", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@ReorderLevel", SqlDbType.SmallInt, 0, ParameterDirection.Input, & _
 0, 0, "ReorderLevel", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Discontinued", SqlDbType.Bit, 0, ParameterDirection.Input, & _
 0, 0, "Discontinued", DataRowVersion.Current, False, Nothing, "", "", ""))
        End With
        dtaFournisseur.InsertCommand = New SqlCommand("INSERT INTO Suppliers (CompanyName, ContactName,"  & _
" ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax, HomePage)"  & _
" VALUES (@CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region," & _
" @PostalCode, @Country, @Phone, @Fax, @HomePage);" & _
 + ChrW(10) + ChrW(13) +  & _
"SELECT SupplierID, CompanyName, ContactName, ContactTitle, Address, City, Region,"  & _
" PostalCode, Country, Phone, Fax, HomePage FROM Suppliers "  & _
"WHERE (SupplierID = SCOPE_IDENTITY())", MaConn)
        With dtaFournisseur.InsertCommand.Parameters
            .Add(New SqlParameter("@CompanyName", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "CompanyName", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@ContactName", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "ContactName", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@ContactTitle", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "ContactTitle", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Address", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "Address", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@City", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "City", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Region", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "Region", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@PostalCode", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "PostalCode", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Country", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "Country", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Phone", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "Phone", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@Fax", SqlDbType.NVarChar, 0, ParameterDirection.Input, & _
 0, 0, "Fax", DataRowVersion.Current, False, Nothing, "", "", ""))
            .Add(New SqlParameter("@HomePage", SqlDbType.NText, 0, ParameterDirection.Input, & _
 0, 0, "HomePage", DataRowVersion.Current, False, Nothing, "", "", ""))
        End With
        'remplissage du dataset
        dsNWind = New DataSet
        'Identifiant local, généralement entier négatif
        With dsNWind.Tables.Add("Categories")
            dtaCategorie.FillSchema(dsNWind, SchemaType.Source, "Categories")
            .Columns(0).AutoIncrementSeed = 0
            .Columns(0).AutoIncrementStep = -1
        End With
        With dsNWind.Tables.Add("Fournisseurs")
            dtaFournisseur.FillSchema(dsNWind, SchemaType.Source, "Fournisseurs")
            .Columns(0).AutoIncrementSeed = 0
            .Columns(0).AutoIncrementStep = -1
        End With
        With dsNWind.Tables.Add("Produits")
            dtaProduit.FillSchema(dsNWind, SchemaType.Source, "Produits")
            .Columns(0).AutoIncrementSeed = 0
            .Columns(0).AutoIncrementStep = -1
        End With
        dtaFournisseur.InsertCommand.UpdatedRowSource = UpdateRowSource.Both
        dtaFournisseur.MissingSchemaAction = MissingSchemaAction.AddWithKey
        dtaProduit.InsertCommand.UpdatedRowSource = UpdateRowSource.Both
        dtaProduit.MissingSchemaAction = MissingSchemaAction.AddWithKey
        dtaCategorie.Fill(dsNWind, "Categories")
        dtaFournisseur.Fill(dsNWind, "Fournisseurs")
        dtaProduit.Fill(dsNWind, "Produits")
        Me.DataGridView1.DataSource = dsNWind.Tables("Fournisseurs")
    End Sub
Ce qui est sensiblement identique sauf pour le paramétrage des DataAdapter ou nous allons ajouter :

dtaFournisseur.InsertCommand.UpdatedRowSource = UpdateRowSource.Both
dtaFournisseur.MissingSchemaAction = MissingSchemaAction.AddWithKey
La propriété UpdatedRowSource permet de définir comment les résultats d'une requête sont mappés sur la ligne de donnée. Dans ce cas nous préciserons qu'il s'agit de remplir la ligne avec les résultats renvoyés par la requête Select intégrée dans la mise à jour.

La propriété MissingSchemaAction permet de définir le comportement à adopter si des données récupérées ne cadre pas avec le schéma sortant.

Pour le reste c'est équivalent si ce n'est que la récupération de l'identité est gérée par le lot de requête et qu'il n'est plus besoin d'émettre une requête identité dans le gestionnaire d'évènement OnRowUpdated

    Private Sub OnRowUpdated(ByVal sender As Object, ByVal e As SqlRowUpdatedEventArgs)

        If e.StatementType = StatementType.Insert Then
            e.Status = UpdateStatus.SkipCurrentRow
            'propagation
            If String.Compare("Fournisseurs", e.Row.Table.TableName, True) = 0 Then
                Dim Lignes() As DataRow = dsNWind.Tables("Produits").Select("SupplierID="  & _
+ e.Row(0, DataRowVersion.Original).ToString)
                For Each LigProduit As DataRow In Lignes
                    LigProduit.Item("SupplierID") = e.Row(0)
                Next
            End If
        End If

    End Sub 

Qu'est ce que le traitement asynchrone ?
auteur : Jean-Marc Rabilloud
Le chargement asynchrone consiste à récupérer les données sans interrompre l'exécution du programme principal. On s'en sert souvent lorsque le volume de données est important ou lorsque l'interface d'affichage fait que l'on peut raisonnablement penser que la récupération asynchrone ne sera pas préjudiciable. Il ne faut pas le confondre avec le chargement différé qui repose sur un autre principe, bien qu'il soit possible de faire les deux.

Le traitement asynchrone n'est pas particulièrement complexe à mettre en place. Il existe un traitement spécifique aux objets commandes avec le fournisseur managé pour SQL Server.

Commençons par celui-ci, c'est-à-dire l'exécution des méthodes BeginExecuteReader et EndExecuteReader.

Imports System
Imports System.Data
Imports System.Data.SqlClient

Public Class Form1

    Private Delegate Sub AffichageDelegate(ByVal reader As SqlDataReader)
    Private flgExecEnCours As Boolean

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Me.StatusStrip1.Items(0).Text = "Prêt"
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        If flgExecEnCours Then
            Me.StatusStrip1.Items(0).Text = "Execution déja en cours"
        Else
            Dim MaConn As New SqlConnection("Data Source=FIXE;Initial Catalog=pubs;"  & _
"Integrated Security=True; Asynchronous Processing=true")
            Dim MaCommande As SqlCommand
            Try
                MaCommande = New SqlCommand("sp_Simulation", MaConn)
                MaCommande.CommandType = CommandType.StoredProcedure
                MaConn.Open()
                Me.StatusStrip1.Items(0).Text = "En cours..."
                flgExecEnCours = True
                Dim callback As New AsyncCallback(AddressOf FonctionRappel)
                MaCommande.BeginExecuteReader(callback, MaCommande, CommandBehavior.CloseConnection)
                Me.Close()
            Catch ex As Exception
                MessageBox.Show("Error: " + ex.Message)
                If MaConn IsNot Nothing Then
                    MaConn.Close()
                End If
            End Try
        End If
    End Sub

    Private Sub Affichage(ByVal reader As SqlDataReader)
        Try
            Dim MaTable As New DataTable
            MaTable.Load(reader)
            Me.DataGridView1.DataSource = MaTable.DefaultView
            Me.StatusStrip1.Items(0).Text = "Prêt"
        Catch ex As Exception
            Me.StatusStrip1.Text = ex.Message
        Finally
            If reader IsNot Nothing Then
                reader.Close()
            End If
        End Try
    End Sub

    Private Sub FonctionRappel(ByVal result As IAsyncResult)
        Try
            Dim MaCommand As SqlCommand = CType(result.AsyncState, SqlCommand)
            Dim MonDtr As SqlDataReader = MaCommand.EndExecuteReader(result)
            Dim del As New AffichageDelegate(AddressOf Affichage)
            Me.Invoke(del, MonDtr)
        Catch ex As Exception
            My.Application.Log.WriteException(ex)
        Finally
            flgExecEnCours = False
        End Try
    End Sub

    Private Sub Form1_FormClosing(ByVal sender As Object,  & _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        If flgExecEnCours Then
            MessageBox.Show(Me, "Impossible de fermer tant qu'il y a des traitement en cours" + vbCrLf + "Attendre l'état prêt dans la barre d'état")
            e.Cancel = True
        End If
    End Sub

End Class
Notons tout d'abord qu'il faut utiliser un délégué parce que nous utilisons dans cet exemple une surcharge utilisant une fonction de rappel. Il existe une autre surcharge qui ne passe pas par cette fonction. Vous devez utilisez au moins un délégué dans ce cas car seul le thread créateur des éléments de l'interface peut interagir avec celle-ci. Ceci implique que la fonction de rappel ne doit jamais directement agir sur l'interface d'où la nécessité du délégué.

Notez aussi que la chaine de connexion pour SQL Server doit contenir la paire clé/valeur ' Asynchronous Processing=true'

Notons enfin qu'il n'est pas possible d'intercepter une exception dans la fonction de rappel en utilisant l'UI sans délégué, d'où l'utilisation du journal d'erreur dans cet exemple.

Envisageons maintenant un cas plus général, avec une commande Access par exemple qui ne possède pas de méthode asynchrone BeginExecuteReader.

Dans ce cas, nous pouvons utiliser le composant BackgroundWorker qui permet de gérer assez simplement les traitements asynchrones. On obtiendrait alors un code tel que :

Imports System
Imports System.Data
Imports System.Data.OleDb

Public Class Form1

    Private BgrWork As ComponentModel.BackgroundWorker
    Private MaTable As DataTable

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        BgrWork = New ComponentModel.BackgroundWorker
        AddHandler BgrWork.DoWork, AddressOf BackgroundWorker_DoWork
        AddHandler BgrWork.ProgressChanged, AddressOf BackgroundWorker_ProgressChanged
        AddHandler BgrWork.RunWorkerCompleted,  & _
AddressOf BackgroundWorker_RunWorkerCompleted
        Me.StatusStrip1.Items(0).Text = "Prêt"
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) & _
 Handles Button1.Click

        If Me.BgrWork.IsBusy Then
            Me.StatusStrip1.Items(0).Text = "Execution déja en cours"
        Else
            BgrWork.WorkerReportsProgress = True
            BgrWork.RunWorkerAsync()
            Me.StatusStrip1.Items(0).Text = "En cours..."
            Me.Close()
        End If
    End Sub

    Private Sub Affichage(ByVal UneTable As DataTable)
        Try
            Dim MaTable As New DataTable
            MaTable.Merge(UneTable)
            Me.DataGridView1.DataSource = MaTable.DefaultView
            Me.StatusStrip1.Items(0).Text = "Prêt"
        Catch ex As Exception
            Me.StatusStrip1.Text = ex.Message
        End Try
    End Sub

    Private Sub Form1_FormClosing(ByVal sender As Object,  & _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        If Me.BgrWork.IsBusy Then
            MessageBox.Show(Me, "Impossible de fermer tant qu'il y a des traitement en cours" + vbCrLf + "Attendre l'état prêt dans la barre d'état")
            e.Cancel = True
        End If
    End Sub

    Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As System.Object, & _
 ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs)
        Affichage(CType(e.Result, DataTable))
    End Sub

    Private Sub BackgroundWorker_DoWork(ByVal sender As System.Object, & _
 ByVal e As System.ComponentModel.DoWorkEventArgs)
        Dim MaConn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;"  & _
"Data Source=D:\user\JMARC\biblio1.mdb")
        Dim MaCommande As OleDbCommand
        Try
            MaCommande = New OleDbCommand("All Titles", MaConn)
            MaCommande.CommandType = CommandType.TableDirect
            MaConn.Open()
            Dim LocTable As New DataTable
            LocTable.Load(MaCommande.ExecuteReader)
            e.Result = LocTable
        Catch ex As Exception
            My.Application.Log.WriteException(ex)
            If MaConn IsNot Nothing Then
                MaConn.Close()
            End If
        End Try
    End Sub

    Private Sub BackgroundWorker_ProgressChanged(ByVal sender As System.Object, & _
 ByVal e As System.ComponentModel.ProgressChangedEventArgs)
        Me.StatusStrip1.Items(0).Text = "En cours..." + e.ProgressPercentage.ToString
    End Sub
End Class
Notez que si le gestionnaire DoWork est soumis à la règle de non utilisation des éléments de l'interface, le gestionnaire ProgressChanged n'y est pas soumis.



Consultez les autres F.A.Q's


Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.