JDBC

Il y a fort longtemps dans une galaxie lointaine ... lorsque l'on souhaitait accéder à une base de données relationnelle, on utilisait un driver spécifique pour LA base de données visée et on écrivait du code SQL spécifique (car le SQL n'était pas encore très standardisé). On utilisait également des opérations spécifiques du driver.

Puis est arrivé JDBC ... une abstraction Java permettant de standardiser un peu les méthodes qui manipulent ces bases. JDBC est une spécification : des interfaces et des objets disponibles dans le package java.sql.

Il reste nécessaire d'avoir le driver de la base de données compatible JDBC, mais vous devriez être capable de changer de driver (et donc de base de données) sans avoir à modifier trop de choses dans votre code.

La vidéo suivante est une présentation générale de JDBC.

Transparents présentés dans la vidéo

Ci dessous une explications pour chaque élément de JDBC présenté dans la vidéo.

DriverManager

Les implémentations de JDBC se présentent sous la forme d'un fichier jar à mettre dans le classpath.
Ces librairies ne sont normalement pas nécessaires lors de la compilation, l'idée étant de pouvoir en changer facilement sans recompiler les sources.

Les drivers s'enregistrent auprès d'un objet DriverManager. Cela se fait normalement au chargement de la classe du driver.
Dans des sources Java, vous pourriez trouver quelque chose du type :

  Class.forName("org.mysql.Driver");
Il n'y a donc pas de dépendance directe vers la classe du driver, c'est une chaîne de caractères vue par le compilateur. Le chargement de la classe se fait au moment de l'exécution du code (au runtime).

Le DriverManager va associer un nom de driver à cette librairie, ce qui permettra de faire le lien lors de l'ouverture de connexion.

Connexion

La connexion à la base de données se fait avec une URL dont le format sera : jdbc:driver:xxx.
Selon les spécifications, l'URL commence nécessairement par jdbc.
Le nom du driver sera par exemple : mysql, sqlite, postgresql ...
La dernière partie de l'URL est dépendante du driver mais sera souvent du type : //serveur:port/dbName?options. Les connexions aux bases de données se font généralement en TCP sur un port défini dans l'URL, ou le port par défaut du driver. Le serveur est désigné par son IP ou son adresse connue du DNS.
dbName est le nom de la base de données à utiliser.
De nombreux drivers proposent également la possibilité de mettre des options à la suite sous la même forme que dans une URL web : ?cle=valeur&cle2=valeur2.
Une gestion des utilisateurs est également souvent proposée dans les systèmes de base de données.
Au moment de l'ouverture de connexion, vous pouvez donc préciser un utilisateur et un mot de passe.

L'ouverture de connexion peut se faire sur l'objet DriverManager :

  Class.forName("org.mysql.Driver");
  Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/baseTest", "user", "password");

ou en utilisant un objet DataSource.

  SQLiteDataSource ds = new SQLiteDataSource();
  ds.setUrl("jdbc:sqlite:data.db");
  Connection conn = ds.getConnection();

Dans ce dernier cas le driver est nécessaire à la compilation puisque l'objet SQLiteDataSource qui implémente DataSource est présent dans les sources.

SQLException

La plupart des opérations sur la base de données peuvent lancer une SQLException. Vous devez donc placer ces opérations dans un try et réaliser un traitement approprié dans le catch, ou transformer cette exception technique en une exception ayant un sens plus "métier".
Par exemple, lors de l'ajout d'un utilisateur, si une contrainte de clé primaire n'est pas respectée, cela lève une erreur SQL qui peut être traduite en erreur "métier" : "cet utilisateur existe déjà" ; que ce soit par un code d'erreur ou une autre exception.

Il n'est pas facile de déterminer quelle erreur technique est survenue. Cela passe souvent par un code d'erreur spécifique à la base de donnée utilisée ...
On préfère donc faire une vérification en base via une autre requête avant d'essayer d'insérer ou mettre à jour une données qui ne serait pas valide.
Dans le cas de notre ajout d'utilisateur il faudrait donc vérifier que la clé primaire n'est pas déjà utilisée (son login, ou son mail ...).
Les exceptions SQLException ne devraient pas remontées au delà de vos DAO.

Requêtes

Il y a deux grands types de requêtes :

Dans les deux cas il faut d'abord obtenir un objet "Statement" depuis la connexion, sur lequel on fera une opération executeQuery ou executeUpdate.

Les requêtes de sélection renvoient un "ResultSet", c'est un curseur permettant de récupérer les données de la base, ligne par ligne.
Il faut faire avancer ce curseur avec l'opération "next" jusqu'à ce qu'il renvoie "false".
Cela ce manipule donc de la même manière qu'un Itérateur, souvent dans une boucle while( rs.next() ).
C'est le ResultSet qui permet d'accéder à chaque valeur de tuple avec des opérations de type rs.getString(X).
X est soit un entier entre 1 et le nombre de colonnes du résultat, soit une chaîne de caractères désignant le nom de la colonne.
Il existe différents getters (getString, getInt, getDate, getDouble) permettant de récupérer un type compatible avec la variable Java à affecter.

Un exemple de sélection pourra ressembler à cela :

ResultSet rs = connection
	.createStatement()
	.executeQuery(
		"select firstname, lastname, avg(note) as moyenne "
		+ "from students inner join scores using student_id group by scores.student_id"
	); 

while( rs.next() ) {
	String prenom = rs.getString("firstname");
	String nom = rs.getString("firstname");
	double moyenne = rs.getDouble(3);
	
	if ( moyenne < 10.0 ) System.out.println( prenom + " " + nom + ", moyenne : " + moyenne);
}

PreparedStatement

La vidéo suivante présente les requêtes préparées.

Transparents présentés dans la vidéo

Illustrons l'usage d'une requête préparée par une mise en situation mettant en avant l'importance de filtrée les paramètres de l'utilisateur.
Rappelez-vous que les requêtes se construisent à partir d'une chaîne de caractères qui est ensuite interprétée par la base de données.

  String paramUniv = ...; // un paramètre saisi par l'utilisateur
  ResultSet rs = connection
  	.createStatement()
  	.executeQuery(
  		"select date_examen, note from diplomes where universite = '" + paramUniv + "' "
  		+ "and diplome = 'licence-info' and student_id = " + studentId
  	);

Supposons qu'il s'agisse d'une application permettant à un étudiant de s'inscrire en master en fonction de ses notes en licence. Si le paramètre "paramMatiere" est saisi par l'utilisateur, rien ne l'empêche de saisir LILLE1' and student_id = 123456 OR '1' = '0. On obtient alors une requête valide qui retourne les notes d'un autre étudiant ...
Il est donc indispensable de filtrer les paramètres saisis par un utilisateur que l'on souhaite ajouter à notre requête SQL.

JDBC fournit la possibilité de faire des requêtes préparées et d'y substituer un ensemble de valeurs qui seront correctement échappées en fonction de leur type. Ce sont les PreparedStatement qui s'obtiennent sur l'objet connexion également.
La requête doit contenir des "?" qui sont remplacés par une valeur avec les opérations setString, setDate, setDouble ...
Une fois les paramètres affectés, il suffit de réaliser le "executeUpdate" ou "executeQuery" sur le preparedStatement sans paramètre.
exemple :

	String email = "";
	
	PreparedStatement ps = connection.prepareStatement("select * from user where email = ?");
	ps.setString(1, email);
	ResultSet rs = ps.executeQuery();
	// ...

ResultSetMetaData

Il peut être intéressant de connaitre le nombre de colonnes et les types retournés par la base de données sur l'exécution d'une requête ; par exemple à la suite d'un "select *".
Cela peut se faire à l'aide de l'objet ResultSetMetaData qui s'obtient sur un ResultSet. Cette opération peut se faire avant même d'avoir fait le premier "rs.next()".
De cette manière il peut être possible de générer des tableaux html dynamiques en fonction du contenu des tables par exemple.

	PreparedStatement ps = connection.prepareStatement("select * from user");
	ResultSet rs = ps.executeQuery();
	ResultSetMetaData meta = rs.getMetaData();
	for ( int i = 1; i <= meta.getColumnCount(); i++ ) {
		System.out.println("Colonne : " + meta.getColumnName(i));
		System.out.println("  Type  : " + meta.getColumnTypeName(i));
	}

DAO

La vidéo suivante présente le patron de conception "Data Access Object" (DAO).

Transparents présentés dans la vidéo

Le screencast suivant implémente un DAO en JDBC et montre comment l'utiliser avec une JSP.