I. Introduction▲
Tous ceux qui ont déjà piloté une base Access sous ASP se sont heurtés au problème de la récupération de l'index d'un enregistrement que l'on vient d'insérer. Cet article propose une méthode fiable pour traiter le problème. Nous verrons comment programmer cette méthode, en ASP et sous Delphi 6 Entreprise.
II. Présentation du problème▲
II-A. Notre base Access▲
Soit une base Access, avec une table nommée PERSONNE.
La déclaration SQL de la table est la suivante :
2.
3.
4.
5.
6.
7.
CREATE
TABLE
PERSONNE (
ID_PERSONNE COUNTER CONSTRAINT
MyFieldConstraint PRIMARY
KEY
,
NOM VARCHAR
(
100
)
,
PRENOM VARCHAR
(
50
)
,
EMAIL VARCHAR
(
200
)
,
AGE INT
)
;
La déclaration varie d'un SGBD à l'autre. Cette syntaxe est adaptée à celle du SQL utilisé par Access.
ID_PERSONNE est un index auto-incrémenté : chaque fois que vous ajoutez un enregistrement à la table, sa valeur est générée automatiquement par la base.
La commande SQL suivante :
2.
3.
INSERT
INTO
PERSONNE(
NOM,PRENOM,EMAIL,AGE)
VALUES
(
'
DUPONT
'
, '
Bernard
'
, '
b.dupont@developpez.com
'
, 35
)
;
ajoute l'enregistrement dans la table :
ID_PERSONNE |
NOM |
PRENOM |
|
AGE |
---|---|---|---|---|
1454 |
'JACKSON' |
'Samuel Leroy' |
'slj@hotmail.com' |
53 |
455 |
'THURMAN' |
'Uma' |
'ut@freemail.com' |
31 |
1456 |
'DUPONT' |
'Bernard' |
'b.dupont@developpez.com' |
35 |
II-B. Récupérer l'index sous différents SGBD▲
II-B-1. MySQL & PHP▲
Une fonction PHP permet de récupérer l'index du dernier élément inséré :
(int)
mysql_insert_id()
II-B-2. Interbase▲
Sylvain James et Kloo ont proposé deux méthodes :
- Génération de la valeur de l'index par un trigger et un générateur, puis récupération par une procédure stockée ;
- Utilisation d'un composant, IBGen.
Référence : article sur www.developpez.com
II-B-3. SQL Server 2000▲
La fonction IDENT_CURRENT(
'
Nom
de
la
table
'
)
retourne l'index.
II-B-4. SQL Server (avant 2000)▲
La valeur du dernier index est contenue dans une colonne appelée @@IDENTITY. Il est préférable de procéder à l'INSERT dans une procédure stockée et d'y récupérer la valeur de @@IDENTITY.
II-B-5. Access▲
Il existe plusieurs façons de procéder. Nous allons en détailler quelques-unes dans le chapitre suivant.
III. Récupération de l'index de l'enregistrement nouvellement créé ▲
Il existe plusieurs méthodes pour récupérer l'index. Nous allons en détailler trois :
- la première est incomplète, car elle ne gère pas les accès concurrents ;
- la deuxième semble correcte, elle se base sur l'objet recordset de ADO, mais est-elle fiable lors d'une montée en charge du serveur ?
- la troisième est la bonne, car elle est 100 % SQL et ne dépend pas du SGBD.
III-A. La mauvaise méthode▲
C'est la méthode bricolage que trouvera rapidement le débutant. L'enregistrement inséré se retrouve forcément à la fin de la table. Pour le sélectionner, il suffit alors de trier les enregistrements par ordre d'index décroissant :
2.
3.
SELECT
*
FROM
PERSONNE
ORDER
BY
ID_PERSONNE DESC
;
Cette méthode est barbare, car elle suppose qu'il n'y a qu'un seul utilisateur connecté simultanément à la base. Elle ne gère pas les accès concurrents.
Prenons l'exemple d'un site marchand, deux utilisateurs créent en même temps un caddie :
instant |
utilisateur 1 |
utilisateur 2 |
---|---|---|
T |
Création du caddie |
--- |
T+1 |
Récupération de l'index |
Création du caddie |
T+2 |
--- |
Récupération de l'index |
instant T + 1 : l'utilisateur 1 récupère le caddie du 2e utilisateur.
instant T + 2 : l'utilisateur 2 récupère son propre caddie.
conséquences : l'utilisateur 2 verra son caddie s'enrichir spontanément en fonction des choix de l'utilisateur 1 et réciproquement !
Cette méthode barbare est à proscrire. Le chapitre qui suit expose une méthode qui tient compte des accès concurrents.
III-B. Une méthode infaillible ?▲
Cette méthode est standard. Elle s'appuie sur l'utilisation de l'objet ADO recordset. Elle fonctionne convenablement, mais je pense qu'elle est susceptible de générer des surprises lors d'une montée en charge du serveur et d'écraser des index. Je la présente pour information.
-
Ouverture de la connexion ADO :
Sélectionnez1.
2.
3.set
objCon=
Server
.
CreateObject
(
"
ADODB.Connection
"
) objCon.
ConnectionString
=
"
base_tutorial
"
objCon.
Open
-
Création d'un objet recordset :
Sélectionnez1.
2.
3.set
rs1=
Server
.
CreateObject
(
"
ADODB.Recordset
"
) rs1.
Open
"
SELECT
*
FROM
PERSONNE
"
,objCon, adOpenKetset,adLockOptimistic
rs1.
addNew
- Affectation et mise à jour :
2.
3.
rs1
(
"
NOM
"
)=
'
Dupont'
rs1.
update
rs1.
Close
III-C. La bonne méthode : 100 % SQL▲
III-C-1. Explication▲
Nous n'avons pas de contrôle sur la façon dont est généré l'enregistrement. Qu'importe, nous forcerons le destin :) .
Deux requêtes d'insertion ne sont pas distinguables lorsque l'on insère deux enregistrements identiques. Nous allons ajouter le grain de sable qui permettra d'identifier notre enregistrement, sans possibilité d'erreur.
La ligne d'insertion SQL va subir une modification. Nous ne renseignerons pas tous les champs. Nous nous en réservons un pour permettre l'identification.
Nous choisissons le champ AGE.
Nous verrons juste après (en B.3.2) une variante plus rigoureuse. L'exemple est ici volontairement trivial.
Ébauche de la méthode :
Si nous affectons de façon temporaire un AGE de 5000 ans à une personne, nous sommes certains (quasi) que cet âge ne figure pas dans la table. Cette condition est nécessaire, mais pas suffisante, car il réside un risque de doublon, notamment dans le cas d'accès concurrents en création (cf. paragraphe précédent).
Le bon choix :
Le principe est bon, nous allons le raffermir :
Sélectionnons la valeur maximale de AGE dans la table.
Nomenclature : les variables du langage (ASP, Delphi) qui serviront à construire les requêtes SQL sont notées en rouge.
2.
SELECT
MAX
(
AGE)
AS
max
FROM
PERSONNE
Nous plaçons max dans une variable : M
Créons un nombre aléatoire supérieur à M :
R = Nombre_aleatoire (compris entre 1 et 100 000) + M
Insérons l'enregistrement, avec R à la place de AGE :
2.
3.
INSERT
INTO
PERSONNE(
NOM,PRENOM,EMAIL,AGE)
VALUES
(
'
DUPONT
'
, '
Bernard
'
, '
b.dupont@developpez.com
'
, R)
;
Notez que la probabilité de trouver un enregistrement identique issu d'un accès concurrent en écriture au même instant est pratiquement nulle.
Nous récupérons notre enregistrement :
2.
3.
SELECT
ID_PERSONNE
FROM
PERSONNE
WHERE
AGE=
M;
Plaçons la valeur de l'index dans une variable ID, et mettons à jour la valeur de AGE :
2.
3.
UPDATE
PERSONNE
SET
AGE=
35
WHERE
ID_PERSONNE=
ID;
C'est tout !
III-C-2. Deux variantes encore plus fiables▲
L'exemple précédent s'appuyait sur une table minimaliste. Voici deux variantes qui réduiront encore la probabilité d'erreur d'indexation lors d'accès concurrents simultanés en écriture.
III-C-2-a. Variante basée sur un champ numérique▲
Considérons une deuxième table, SOCIETE :
2.
3.
4.
5.
CREATE
TABLE
SOCIETE (
ID_SOCIETE COUNTER CONSTRAINT
MyFieldConstraint PRIMARY
KEY
,
. . .
. . .
)
;
Chaque individu est dans une seule société. Nous ajoutons un nouveau champ dans notre table PERSONNE : SOCIETE_ID (INTEGER). C'est une référence à l'index de la table SOCIETE.
Attention : il ne s'agit pas d'une clef étrangère, mais d'une simple référence d'index via un champ entier.
Puisque ce n'est pas une clef étrangère, la base n'effectue pas de contrôle d'intégrité des références. Nous pouvons affecter temporairement n'importe quelle valeur à SOCIETE_ID.
Voici un système plus fiable que pour AGE :
2.
SELECT
MAX
(
ID_SOCIETE)
AS
max
FROM
SOCIETE
Nous plaçons max dans une variable : M, et créons un nombre aléatoire supérieur à M :
R = Nombre_aleatoire (compris entre 1 et 100 000) + M + 100
Il ne reste plus qu'à appliquer la méthode précédente.
III-C-2-b. Variante basée sur un champ alphanumérique ▲
Remplacez, au moment de la première insertion, NOM par la chaîne suivante :
N = [ADRESSE IP DU CLIENT ou ID de session] + [HEURE SYSTEME] + [CHAINE ALPHANUMERIQUE ALEATOIRE d'une dizaine de caractères]
Vous êtes certain de l'unicité de la valeur de NOM dans la table au moment où vous effectuerez le SELECT qui suit l'INSERT :
2.
3.
SELECT
ID_PERSONNE
FROM
PERSONNE
WHERE
NOM=
N;
III-C-3. Optimisation en vitesse▲
Sur une grande table, vous pouvez placer une clef secondaire sur le champ AGE pour accélérer la recherche.
III-D. Débat sur la fiabilité des méthodes ▲
III-D-1. La maîtrise des contraintes de son environnement▲
Il y aura toujours des détracteurs pour critiquer le choix de telle ou telle méthode. Il faut analyser le problème à la source. Le choix du SGBD est-il en adéquation avec les performances escomptées de la base ? Combien y aura-t-il d'accès concurrents simultanés ? Si vous avez décidé de travailler avec une base Access, c'est parce que les exigences de votre environnement ne sont pas très élevées, sinon, optez pour SQL Server, Interbase, MySQL, Oracle, etc.
III-D-2. Solution ISAPI avec Delphi 6▲
Il n'existe pas d'équivalent sous ASP.
Il est possible de protéger les accès concurrents les uns des autres dans un environnement ISAPI Delphi 6 en utilisant une section critique, à placer avant implémentation :
2.
var
FLock: TCriticalSection;
À la fin du code, insérer avant le end. :
2.
3.
4.
5.
initialization
if
not
Assigned(fLock) then
fLock := TCriticalSection.Create;
finalization
fLock.Free;
Pour exécuter le code dans la section critique :
2.
3.
4.
5.
6.
7.
8.
try
FLock.Enter;
{
INSERTION
DE
L'ENREGISTREMENT
}
finally
FLock.Release;
end
;
IV. Exemples▲
La création de la base Access est détaillée en annexe Annexe.
IV-A. Exemple en ASP▲
Les variables suivantes contiennent les informations du nouvel enregistrement :
nom = "DUPONT";
prenom = "Bernard";
email = "b.dupont@developpez.com";
age = 35;
Lors du premier INSERT, on appelle la fonction filtre :
2.
3.
Function
filtre
(
s)
filtre =
replace
(
s,"
'
"
,"
''
"
)
end
Function
Son rôle est de doubler les caractères quote ' dans les lignes de commandes SQL.
-
Ouverture de la connexion ADO :
Sélectionnez1.
2.
3.set
objCon=
Server
.
CreateObject
(
"
ADODB.Connection
"
) objCon.
ConnectionString
=
"
base_tutorial
"
objCon.
Open
-
Création d'un objet recordset :
set
rs1=
Server
.
CreateObject
(
"
ADODB.Recordset
"
) -
Nous récupérons la valeur maximale de AGE. Nous ne récupérons pas directement la valeur de MAX(AGE), car il n'y a pas de valeur retournée dans le cas d'une table vide. Nous prenons la précaution de vérifier si la table contient des enregistrements.
Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.rs1
.
Open
"
SELECT
COUNT(*)
AS
recordcount
FROM
PERSONNE
"
, objConif
rs1
(
"
recordcount
"
)=
0
then
m=
0
else
rs1.
Close
rs1.
Open
"
SELECT
MAX(AGE)
AS
max
FROM
PERSONNE
"
, objCon m=
rs1
(
"
max
"
)end
if
rs1.
Close
La variable m contient la valeur maximale de AGE.
-
Nous générons la valeur aléatoire r :
r = Int(Rnd * 100000) + m
-
Nous insérons l'enregistrement, avec r à la place de AGE :
Sélectionnez1.objCon
.
Execute
(
"
INSERT
INTO
PERSONNE(NOM,PRENOM,EMAIL,AGE)
VALUES('
"
&
filtre
(
nom)&
"
',
'
"
&
filtre
(
prenom)&
"
',
'
"
&
filtre
(
email)&
"
',
"
&
m&
"
);
"
) -
Nous récupérons l'index ID_PERSONNE :
Sélectionnez1.
2.
3.rs1
.
Open
"
SELECT
ID_PERSONNE
FROM
PERSONNE
WHERE
AGE=
"
&
r, objCon ID=
rs1
(
"
ID_PERSONNE
"
) rs1.
Close
ID contient la valeur de l'index inséré.
-
Nous mettons à jour l'enregistrement avec la bonne valeur affectée à AGE :
Sélectionnez1.objCon
.
Execute
(
"
UPDATE
PERSONNE
SET
AGE=
"
&
age&
"
WHERE
ID_PERSONNE=
"
&
ID) - Fermeture de la connexion ADO.
2.
objCon.
Close
Set
objCon =
nothing
IV-B. Exemple en Delphi 6 Entreprise ▲
Le programme suivant a été réduit au maximum afin de ne présenter que du code utile à notre démonstration.
Télécharger la source. Pour créer la base Access et la liaison DSN, reportez-vous à l'annexe Annexe.
Il contient une fiche :
Et un DataModule :
Propriétés des composants du DataModule :
- DataSource1 : TDataSource;
DataSet := ADOQuery1; - ADOConnection1 : TADOConnection;
ConnectionString := 'base_tutorial';
LoginPrompt := false;
puis ADOConnection1.Connected := true;
Fonctionnement :
Le clic sur le bouton Insérer provoque l'ajout de l'enregistrement dans la base.
Section critique :
N'oubliez pas que vous pouvez compléter le code avec une section critique cf. III-D-2Solution ISAPI avec Delphi 6
Source :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
unit
UMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TFMain = class
(TForm)
Button1: TButton;
EdNom: TEdit;
EdPrenom: TEdit;
EdEmail: TEdit;
EdAge: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
GroupBox1: TGroupBox;
GroupBox2: TGroupBox;
LabID: TLabel;
procedure
Button1Click(Sender: TObject);
private
{
Déclarations
privées
}
function
filtre(s:string
):string
;
public
{
Déclarations
publiques
}
end
;
var
FMain: TFMain;
implementation
uses
UDataModule1;
{$
R
*.dfm
}
function
TFMain.filtre(s:string
):string
;
begin
stringreplace(s,'
'
'
'
,'
'
'
'
'
'
,[rfReplaceAll]);
end
;
procedure
TFMain.Button1Click(Sender: TObject);
var
M : integer
;
SqlQuery : string
;
R,ID : cardinal
;
nom,prenom,email : string
;
age : integer
;
begin
nom := EdNom.text;
prenom := EdPrenom.text;
email := EdEmail.text;
age := StrToInt(EdAge.text);
with
DataModule1.ADOQuery1 do
begin
Close;
SQL.Clear;
try
SqlQuery := '
SELECT
COUNT(*)
AS
recordcount
FROM
PERSONNE
'
;
SQL.Add(SqlQuery);
Open;
if
FieldValues['
recordcount
'
] = 0
then
M := 0
else
begin
Close;
SQL.Clear;
SqlQuery := '
SELECT
MAX(AGE)
AS
max
FROM
PERSONNE
'
;
SQL.Add(SqlQuery);
Open;
M := FieldValues['
max
'
];
end
;
randomize;
R := Random(100000
) + 1
+ M;
Close;
SQL.Clear;
SqlQuery := '
INSERT
INTO
PERSONNE(NOM,PRENOM,EMAIL,AGE)
'
+ '
VALUES(
'
'
'
+ filtre(nom) + '
'
'
,
'
'
'
+ filtre(prenom) + '
'
'
,
'
'
'
+ filtre(email) + '
'
'
,
'
+ IntToStr(R) + '
);
'
;
SQL.Add(SqlQuery);
ExecSQL;
Close;
SQL.Clear;
SqlQuery := '
SELECT
ID_PERSONNE
FROM
PERSONNE
WHERE
AGE=
'
+ IntToStr(R);
SQL.Add(SqlQuery);
Open;
ID := FieldValues['
ID_PERSONNE
'
];
LabID.Caption := IntToStr(ID);
Close;
SQL.Clear;
SqlQuery := '
UPDATE
PERSONNE
SET
AGE=
'
+ IntToStr(age)
+ '
WHERE
ID_PERSONNE=
'
+ IntToStr(ID);
SQL.Add(SqlQuery);
ExecSQL;
except
on
E:Exception do
ShowMessage(SqlQuery + #
13
+ E.Message
);
end
;
end
;
end
;
end
.
V. Annexe▲
V-A. Créer la base Access ▲
Rappel : soit une base Access, avec une table nommée PERSONNE.
La déclaration SQL de la table est la suivante :
2.
3.
4.
5.
6.
7.
CREATE
TABLE
PERSONNE (
ID_PERSONNE COUNTER CONSTRAINT
MyFieldConstraint PRIMARY
KEY
,
NOM VARCHAR
(
100
)
,
PRENOM VARCHAR
(
50
)
,
EMAIL VARCHAR
(
200
)
,
AGE INT
)
;
Pour créer une nouvelle base, ouvrir Access, choisir la création d'une nouvelle base de données :
Il y a deux façons de créer une table. À la main, ou par une requête.
-
Créer une table à la main
Cliquez sur le bouton Nouveau :
Remplissez les champs de la façon suivante :
ID_PERSONNE : NuméroAuto, Entier long, Incrément, indexé sans doublons
NOM : Texte, taille 100, non indexé
PRENOM : Texte, taille 50, non indexé
EMAIL : Texte, taille 200, non indexé
AGE : Entier long, non indexé - Créer une table avec une requête SQL
Onglet Requête :
- Cliquez sur le bouton Nouveau. Deux fenêtres apparaissent.
- Fermer la fenêtre « Ajouter une table ».
- Dans la fenêtre :« Requête : Requête Sélection », bouton droit, menu contextuel, premier choix : « Mode SQL ».
- Dans la fenêtre texte, copier / coller la requête (cf. D.1).
- Fermer la fenêtre, confirmer son enregistrement, et la nommer.
- Double-cliquer sur cette nouvelle requête pour exécuter le code SQL.
V-B. Créer une liaison DSN▲
Notre base Access s'appelle base_test.mdb, elle est située dans le répertoire : C:\base_tutorial. Il faut référencer cette base dans les liaisons DSN du système, pour pouvoir y accéder via ASP ou ADO.
V-B-1. Ouvrir le gestionnaire ODBC ▲
OS |
chemin |
---|---|
Windows 95/98/NT |
Panneau de configuration / ODBC |
Windows 2000 |
Panneau de configuration / Outils d'administration / Sources de données (ODBC) |
V-B-2. Créer une nouvelle source ODBC ▲
Onglet sources de données système, bouton ajouter :
Choisissez le driver Access :
Donnez un nom à votre source ODBC « base_tutorial » et renseignez son chemin :
La liaison DSN sur votre base est créée !
V-C. Crédits ▲
Article écrit par Olivier Népomiachty, © janvier 2002
e-mail :
Site Internet Delphi : en construction…
V-D. L'auteur▲
Olivier Nepomiachty est le responsable technique historique de developpez.com.
- 2001 à 2005 : responsabilité de l'architecture ainsi que l'exploitation des serveurs GNU/Linux.
- Actuellement évangéliste CRM et speaker à SugarCRM.
- Précédemment 11/2008-2/2012 Technical Architect à Salesforce.com.
LinkedIn : http://www.linkedin.com/in/oliviernepomiachty
Blog #1 : Tortuga CRM (in English)
Blog #2 : Training for Spartathlon (in English)
Twitter : TortugaCrm
Webcasts series |
Agenda
|
Conférences passées
Conférence : Heroku
Jeudi 18 novembre 2004
Une étude comparative expose les avantages et inconvénients de ces différentes solutions. |
Les articles sur DVP |
Ultra runner
Je suis ultra-marathonien, deux fois finisher du Grand Raid du Morbihan (08 & 09), course de 177 km à allure libre en temps limité. |
VI. Remerciements ▲
et Alphomega pour avoir relu cet article,
JJM pour la méthode utilisant la section critique sous Delphi.
VII. Remerciements Developpez.com▲
Nous tenons à remercier f-leb pour la relecture orthographique, et Malick SECK pour la mise au gabarit.