I. Introduction▲
I-A. Prérequis▲
Le lecteur devra avoir une maitrise correcte de PHP, quelques notions d'objets, une connaissance d'un SGBD. Cet article est d'un niveau intermédiaire.
I-B. Pourquoi cet article ?▲
Je me suis rendu compte au travers du forum PHP de développez.com que les questions concernant des problèmes de compatibilité entre SGBD étaient récurrents ainsi que les problèmes de portage de site. Cet article ne traitera pas des étapes d'installation et configuration de chaque SGBD, mais uniquement de leur utilisation par PHP.
I-C. Qu'est-ce que PEAR ?▲
Afin de définir PEAR, je citerai un article de ce même forum :
PEAR (pour PHP Extension and Application Repository) est une bibliothèque de scripts PHP. Tous les scripts déposés dans PEAR respectent un certain nombre de règles qui les rendent portables (indépendants de la plate-forme) et réutilisables.
Cet article vous permettra d'ailleurs d'installer de façon manuelle les packages qui vous intéressent.
I-D. Problématique abordée▲
Lors d'un développement de site PHP/mysql, il est courant d'utiliser les API standards : mysql_connect, mysql_query, mysql_fetch_array, etc. Cependant, lors d'un portage vers un autre SGBD et parfois même lors d'une montée de version du même SGBD, l'utilisation des API pose problème puisqu'une réécriture s'impose afin de rendre le code compatible. C'est ici qu'interviennent les couches d'abstraction de données.
MDB2 fait partie de ces bibliothèques vous permettant de vous affranchir de ces problèmes de compatibilité. Attention toutefois, l'utilisation de cette couche d'abstraction aux données vous permettra d'éviter les problèmes d'API, mais pas ceux de compatibilité des requêtes. Si par exemple vous utilisez la fonction NOW() dans vos requêtes SQL mysql, celle-ci ne passera pas sous ORACLE.
Dans le cadre de cet article nous parlerons donc d'une couche d'abstraction de données fournie par PEAR : MDB2, issu de PEAR::DB et Metabase.
II. Caractéristiques▲
II-A. Drivers▲
MDB2 s'utilise avec des drivers. Un driver n'est autre qu'une interface (objet dans le cas de MDB2) permettant l'accès à différentes bases de données. Les bases de données actuellement disponibles pour MDB2 sont :
- MySQL ;
- PostgreSQL ;
- Oracle ;
- Frontbase ;
- Querysim ;
- Interbase/Firebird ;
- MSSQL ;
- SQLite.
II-B. Fonctionnalités▲
MDB2 est une bibliothèque orientée objet. Sa grande force repose sur la portabilité. Voici quelques-unes des caractéristiques principales listées dans la doc officielle :
- abstraction des types de données et conversion sur demande ;
- multiples options de fetch (par nom, par indice, les deux, etc.) ;
- codes d'erreurs portables ;
- émulation du mode prepare execute pour les SGBD qui ne le supportent pas ;
- émulation des séquences ;
- émulation des sous-requêtes ;
- support des transactions ;
- support des Large Objects ;
- support des clés uniques, des index, des clés primaires ;
- émulation de l'auto-incrément ;
- émulation de LIMIT ;
- fonctions de lecture des informations schémas ;
- fonctions de gestion du SGBD (création, altération, etc.) ;
- fonctions d'ingénierie inversée à partir d'une base de données existante.
III. Cas d'usage▲
III-A. Connexion▲
Pour se connecter par MDB2, il faut utiliser un DSN. La documentation la plus complète sur les DSN est celle de la documentation de PEAR::DB : http://pear.php.net/manual/fr/package.database.db.intro-dsn.php.
Pour résumer le DSN est une chaine de caractères comportant les paramètres de connexion à votre base de données. Pour un utilisateur de mysql avec un accès classique, la chaine sera donc :
mysql
:
//username:password@hostspec/database
Cependant ce formalisme vous permettra aussi d'utiliser des connexions par socket, des connexions sur port non standard, des options de lancement, une connexion ssl, etc.
À noter que cette représentation est utilisée par les autres projets PEAR (Auth, LiveUser, LiveAdmin etc.).
La connexion se réalise ensuite avec la fonction statique MDB2::Connect. Celle-ci accepte une chaine pour le $dsn ou un array. Les deux syntaxes suivantes sont donc équivalentes :
$dsn
=
'
mysql://someuser:apasswd@localhost/thedb
'
;
$options
=
array(
'
debug
'
=>
2
,
'
portability
'
=>
MDB2_PORTABILITY_ALL,
);
$db
=
&
MDB2::
connect
($dsn
,
$options
);
$dsn
=
array(
'
phptype
'
=>
'
mysql
'
,
'
username
'
=>
'
someuser
'
,
'
password
'
=>
'
apasswd
'
,
'
hostspec
'
=>
'
localhost
'
,
'
database
'
=>
'
thedb
'
,
);
$options
=
array(
'
debug
'
=>
2
,
'
portability
'
=>
MDB2_PORTABILITY_ALL,
);
$db
=
&
MDB2::
connect
($dsn
,
$options
);
Le & signifie que vous récupérez une référence sur l'objet et non une copie. Il est important de le conserver. $options est facultatif, c'est un tableau associatif : paramètre => valeur.
L'option la plus intéressante concerne la portabilité. Ici nous avons demandé d'activer toutes les options de portabilité (MDB2_PORTABILITY_ALL).
Voici quelques options possibles :
- MDB2_PORTABILITY_DELETE_COUNT : force les requêtes de delete à renvoyer le nombre de lignes supprimées ;
- MDB2_PORTABILITY_EMPTY_TO_NULL : convertit les valeurs vides en valeur null ;
- MDB2_PORTABILITY_ERRORS : rend compatible les messages d'erreurs ;
- MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES : supprime les informations de tables et bases de données des index associatifs ;
- MDB2_PORTABILITY_FIX_CASE : convertit les noms des tables et des champs dans la casse définie par l'option « field_case » ;
- MDB2_PORTABILITY_NONE : désactive les options de compatibilité ;
- MDB2_PORTABILITY_RTRIM : supprime les blancs à droite des résultats.
Remarque : il est aussi possible de travailler avec un singleton :
$db
=&
MDB2::
singleton($dsn
);
ou avec une factory :
$db
=&
MDB2::
factory($dsn
);
La déconnexion se fait avec la fonction disconnect :
$db
->
disconnect();
III-B. Requêtes▲
MDB2 vous permet d'interroger la base de données grâce à différentes fonctions. On peut citer les classiques :
query (utilisé pour des select, update, insert, delete) ;
queryAll (utilisé pour des select, renvoie un tableau associatif avec les valeurs demandées) ;
queryCol (renvoie un tableau de valeurs correspondant à la colonne demandée) ;
queryOne (renvoie un seul résultat pour la première colonne, première ligne) ;
queryRow (renvoie la première ligne) ;
exec (utilisé pour des update, insert, delete ). Cette fonction vous renvoie le nombre de lignes affectées.
Pour toutes ces requêtes, il est très important d'avoir spécifié avant le mode de FETCH. Par exemple pour obtenir un tableau associatif :
$db
->
setFetchMode(MDB2_FETCHMODE_ASSOC);
Les modes suivants sont disponibles : MDB2_FETCHMODE_ORDERED, MDB2_FETCHMODE_ASSOC, MDB2_FETCHMODE_OBJECT , MDB2_FETCHMODE_DEFAULT , MDB2_FETCHMODE_FLIPPED.
Ainsi pour effectuer un update, nous ferons :
$result
=
$db
->
exec ( "
UPDATE machin set truc = 'bidule'
"
);
// $result contient le nombre de lignes mise à jour.
Pour une sélection :
IV. Cas d'utilisation plus poussé▲
Nous pourrions nous arrêter là et pour la plupart des utilisateurs qui ne désirent qu’utiliser les fonctionnalités de base, la lecture du second chapitre suffit. Cependant, pour aller plus loin, l'utilisation de MDB2 permet, quel que soit le système, d'utiliser des fonctionnalités intéressantes. Nous allons en détailler quelques-unes.
IV-A. La gestion des erreurs▲
La gestion des erreurs respecte le formalisme de PEAR. Si vous avez déjà utilisé des paquets PEAR, vous ne serez pas dépaysé. La fonction statique isError permet de déterminer si une valeur de retour est une erreur MDB2. Cet objet erreur contient des informations intéressantes, notamment un message d'erreur.
Exemple :
$db
=
MDB2::
connect
($this
->
dsn,
$options
);
// Vérification des erreurs
if (MDB2::
isError($db
))
{
echo $db
->
getDebugInfo().
'
<BR/>
'
;
echo $db
->
getMessage();
}
$result
=
$db
->
query
($sql
);
// Vérification des erreurs
if (MDB2::
isError($result
))
{
echo $result
->
getDebugInfo().
'
<BR/>
'
;
echo $result
->
getMessage();
}
L'objet MDB2_Error dérive lui-même de PEAR_Error, c'est-à-dire qu'il possède les mêmes méthodes et les mêmes attributs. La gestion des erreurs est différente suivant un paramètre qu'on peut positionner en début de programme.
PEAR::
setErrorHandling(PEAR_ERROR_PRINT);
Le paramètre passé à cette fonction va déterminer le comportement attendu en cas d'erreur :
- PEAR_ERROR_CALLBACK ;
- PEAR_ERROR_DIE ;
- PEAR_ERROR_TRIGGER ;
- PEAR_ERROR_RETURN ;
- PEAR_ERROR_PRINT.
Ces différentes options permettront en cas d'erreur d'appeler une fonction de callback, de stopper le script par un die, de renvoyer un code d'erreur ou d'afficher une erreur. La fonction de callback demande que l'on précise une fonction de callback en second paramètre.
IV-B. Les transactions et les insertions par tableaux▲
Une transaction est un regroupement de requêtes qu'on désire exécuter si et seulement si aucune n'échoue. Au contraire d'une requête atomique exécute quel que soit le résultat des autres requêtes. Ces transactions sont émulées par MDB2, il faut cependant que votre SGBD les supporte.
Une transaction débute par beginTransaction et se termine par la propagation des résultats (commit). Un retour en arrière peut se faire avec la fonction rollback.
Les transactions sont intéressantes à utiliser avec les insertions par tableau de valeurs (host array). Ici il s'agit de préparer une requête générique et de donner un ensemble de valeurs utilisées pour cette requête. Si votre SGBD est optimisé pour ce type d'opérations, celles-ci sont bien plus rapides. Et le mode transactionnel vous assure de la réussite totale ou non de votre demande empêchant ainsi de vous retrouver dans un état intermédiaire.
Exemple d'utilisation :
$alldata
=
array(
array(1
,
'
one
'
,
'
un
'
),
array(2
,
'
two
'
,
'
deux
'
),
array(3
,
'
three
'
,
'
trois
'
),
array(4
,
'
four
'
,
'
quatre
'
)
);
$stmt
=
$mdb2
->
prepare
('
INSERT INTO numbers VALUES(?,?,?)
'
,
array('
integer
'
,
'
text
'
,
'
text
'
),
false);
foreach ($alldata
as $row
)
{
echo('
running execute<br>
'
);
$stmt
->
bindParamArray($row
);
$stmt
->
execute
();
}
IV-C. Les sous-requêtes▲
Les sous-requêtes ne sont pas gérées par tous les SGBD, mais MDB2 permet de les émuler :
$sub_select
=
$mdb2
->
subSelect('
SELECT test_name from test WHERE test_name =
'
.
$mdb2
->
quote('
gummihuhn
'
,
'
text
'
),
'
text
'
);
$query_with_subselect
=
'
SELECT * FROM test WHERE test_name IN (
'
.
$sub_select
.
'
)
'
;
$result
=
$mdb2
->
query
($query_with_subselect
);
$array
=
$result
->
fetchAll();
$result
->
free
();
IV-D. La clause LIMIT▲
La clause LIMIT connue en mysql n'est pas disponible sur tous les SGBD. Elle est donc émulée par MDB2 de la façon suivante :
$db
->
setLimit(3
,
1
);
$result
=
$db
->
query
('
SELECT * FROM users
'
);
$numrows
=
$result
->
numRows();
// numrows doit être égal à 3 dans notre exemple
IV-E. Les modules▲
IV-E-1. Gestion de base de données▲
Le paquet MDB2 peut utiliser des modules avec des fonctionnalités intéressantes. L'un d'entre eux permet la gestion de base de données :
Ce manager est une référence vers le gestionnaire interne de MDB2. Nous pourrons donc y faire référence de façon directe $manager-> ou bien par $db->manager->
Son API est décrite ici.
Ces fonctions vont nous permettre de créer une table à partir d'un tableau, de créer une base, de créer des séquences (émulé pour certains SGBD), de créer des contraintes, etc. Ce gestionnaire est donc un parfait couteau suisse pour la gestion du schéma à proprement parler.
Voici par exemple une création de tables :
$this
->
fields =
array(
'
id
'
=>
array(
'
type
'
=>
'
integer
'
,
'
unsigned
'
=>
true,
'
notnull
'
=>
true,
'
default
'
=>
0
,
),
'
textfield
'
=>
array(
'
type
'
=>
'
text
'
,
'
length
'
=>
12
,
),
'
booleanfield
'
=>
array(
'
type
'
=>
'
boolean
'
,
),
'
decimalfield
'
=>
array(
'
type
'
=>
'
decimal
'
,
),
'
floatfield
'
=>
array(
'
type
'
=>
'
float
'
,
),
'
datefield
'
=>
array(
'
type
'
=>
'
date
'
,
),
'
timefield
'
=>
array(
'
type
'
=>
'
time
'
,
),
'
timestampfield
'
=>
array(
'
type
'
=>
'
timestamp
'
,
),
);
$db
->
manager->
createTable('
exemple
'
,
$this
->
fields);
Nous remarquons dans cet exemple l'utilisation des types MDB2 pour la définition des champs, garantissant ainsi une portabilité sur l'ensemble des SGBD supportés.
Un autre exemple intéressant est l'utilisation de ces types MDB2 combinée avec une représentation sous forme de tableau pour exécuter des modifications de tables (ALTER):
$changes
=
array(
'
add
'
=>
array(
'
quota
'
=>
array(
'
type
'
=>
'
integer
'
,
'
unsigned
'
=>
1
,
),
'
note
'
=>
array(
'
type
'
=>
'
text
'
,
'
length
'
=>
'
20
'
,
),
),
'
rename
'
=>
array(
'
sex
'
=>
array(
'
name
'
=>
'
gender
'
,
'
definition
'
=>
array(
'
type
'
=>
'
text
'
,
'
length
'
=>
1
,
'
default
'
=>
'
M
'
,
),
),
),
'
change
'
=>
array(
'
id
'
=>
array(
'
unsigned
'
=>
false,
'
definition
'
=>
array(
'
type
'
=>
'
integer
'
,
'
notnull
'
=>
false,
'
default
'
=>
0
,
),
),
'
somename
'
=>
array(
'
length
'
=>
'
20
'
,
'
definition
'
=>
array(
'
type
'
=>
'
text
'
,
'
length
'
=>
20
,
),
)
),
'
remove
'
=>
array(
'
somedescription
'
=>
array(),
),
'
name
'
=>
$newer
,
);
$result
=
$db
->
manager->
alterTable('
exemple2
'
,
$changes
,
true);
Pour les personnes familières avec les séquences, MDB2 en propose aussi une émulation :
$result
=
$db
->
manager->
createSequence($seq_name
);
$array
=
$db
->
manager->
listSequences();
$id
=
$db
->
currID($seq_name
);
// récupération de la valeur courante de la séquence
$id
=
$db
->
nextID ($seq_name
);
// incrémentation de la séquence
$result
=
$db
->
manager->
dropSequence($seq_name
);
IV-E-2. Le module fonction▲
Inutile de s'attarder longuement sur ce module, il s'agit d'un ensemble de fonctions pratiques réécrites pour être portables sur chaque SGBD. Il s'agit de :
- concat ;
- executeStoredProc ;
- functionTable ;
- now ;
- substring.
IV-E-3. Le module reverse▲
Ce module permet de récupérer des informations sur le schéma. Il est souvent couplé avec le module Manager.
$table_info
=
$db
->
reverse->
tableInfo('
users
'
);
IV-E-4. Les Large Objects▲
L'un des types de données sur lequel nous pouvons nous attarder est certainement celui des Large Objects. Ce type de champ permet de stocker des fichiers en base. Deux types de LOB sont utilisés :
- les BLOBS (fichiers binaires) ;
- les CLOBS (fichiers texte normaux).
Avec MDB2, ces objets peuvent être insérés en utilisant les requêtes préparées (voir le point 2).
$query
=
$db
->
prepareQuery('
INSERT INTO files (id, b_data, c_data) VALUES (1, ?, ?)
'
,
array('
blob
'
,
'
clob
'
),
MDB2_PREPARE_MANIP,
array('
b_data
'
,
'
c_data
'
));
$db
->
bindParam(0
,
$binary_lob
);
// binary_lob contiendra le contenu d'un fichier (ouvert par fopen par exemple)
$db
->
bindParam(1
,
$character_lob
);
// character_lob contiendra une longue chaine de caractère
$result
=
$db
->
executeQuery($query
);
Pour la sélection d'un LOB, nous utiliserons la même méthode que pour une requête de sélection classique.
$result
=
$db
->
query
('
SELECT b_data, c_data FROM files WHERE id = 1
'
,
array(blob,
clob));
$row
=
$result
->
fetchRow();
$blob
=
$row
[
0
];
$clob
=
$row
[
1
];
IV-E-5. La maintenance de schéma indépendante du SGBD▲
Remarque : le paquet MDB2_Schema est, à l'heure actuelle, toujours en version bêta. Son utilisation n'est donc pas sans risque.
Grâce à MDB2, il est désormais possible de représenter son schéma à partir d'un fichier descriptif en format XML. Le paquet Schema directement dépendant de MDB2 permet ainsi de maintenir sa base de données à partir d'un formalisme non lié au SGBD utilisé.
L'une de ces utilisations permet de faire de l'ingénierie inversée à partir d'une base existante :
$schema
=&
MDB2_Schema::
factory($dsn
);
Vous remarquerez ici que nous utilisons MDB2_Schema qui est un paquet utilisant MDB2.
$dump_config
=
array(
'
output_mode
'
=>
'
file
'
,
'
output
'
=>
'
monfichier.xml
'
);
$operation
=
$schema
->
dumpDatabase($dump_config
,
MDB2_SCHEMA_DUMP_STRUCTURE);
Nous pouvons noter ici qu'il est possible de récupérer la structure du schéma, les données, ou les deux grâce au second argument :
- MDB2_SCHEMA_DUMP_STRUCTURE ;
- MDB2_SCHEMA_DUMP_CONTENT ;
- MDB2_SCHEMA_DUMP_ALL.
Cette représentation sous forme XML utilisera les types de données MDB2. Ceux-ci seront convertis dans leurs équivalents sur le SGBD utilisé lors d'un éventuel chargement avec updateDatabase.
Ce chargement se fera avec la syntaxe suivante :
$operation
=
$schema
->
updateDatabase('
monfichier.xml
'
,
'
monfichier.xml.old
'
,
array(),
false );
Cette opération créera le fichier 'monfichier.xml.old', copie conforme du fichier initial.
Attention, cette même fonction est utilisée pour la mise à jour du schéma. Si le fichier 'monfichier.xml.old' existe, updateDatabase établira les différences entre les deux fichiers et fera une mise à jour de la base (ALTER).
Liens utiles▲
Merci à Lukas Smith pour ses remarques qui ont permis de corriger certaines parties de cet article.