image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Remarque préliminaire : nous décrivons ici synthétiquement l'usage de l'API JDBC permettant l'accès à des bases de données relationnelles depuis des programmes Java. On suppose que le lecteur possède des bases préalables d'utilisation de bases relationnelles avec une connaissance du langage SQL.

JDBC ?

Pilotes JDBC

Oracle distingue 4 types de pilotes JDBC permettant de d'interfacer avec une base :

  1. Le type 1 qui repose sur une pilote JDBC faisant office de passerelle vers un pilote Open DataBase Connectivity (ODBC)
    • ODBC est une API d'abstraction initiée par Microsoft en 1992 avant la création de Java et offrant des fonctionnalités similaires à JDBC pour des langages tels que le C, C++...
    • La passerelle JDBC-ODBC ne devrait être utilisée que s'il n'existe pas de pilote JDBC pour la base accédée
    • Le support de la passerelle JDBC-ODBC n'est plus assuré depuis Java 1.8
  2. Le type 2 qui utilise une bibliothèque d'accès native à la base avec un pilote Java d'interfacage allégé réalisant des appels JNI
  3. Le type 3 qui est un pilote JDBC écrit nativement en Java et ne reposant sur aucune bibliothèque externe
  4. Le type 4 qui est un pilote JDBC exploitant des connexions réseau pour contacter la base

Comment créer un pilote JDBC ?

Pilotes disponibles

Les bases de données relationnelles SQL les plus populaires disposent de pilotes JDBC :

SQL

Connexion à la base de données

Pour se connecter à la base de données, il faut récupérer un objet Connection en indiquant l'URL de connexion (dont le format varie selon le pilote de BDD utilisé, par exemple jdbc:postgresql://localhost:5432/MyDatabase):

Connection conn = DriverManager.getConnection(url, user, passwd);

Avant d'initier la connexion, il faut charger la classe correspondant au pilote JDBC, par exemple pour le pilote PostgreSQL :

Class.forName("org.postgresql.Driver");

Cette instruction peut lever ClassCastException si le pilote n'est pas trouvable notamment si on a oublié de rajouter le JAR contenant le pilote dans le classpath utilisé par le chargeur de classe (option -cp de java ou variable d'environnement CLASSPATH).

Idéalement, les données nécessaires à la connexion à la BDD (URL, utilisateur, mot de passe...) doivent être conservées dans un fichier de configuration avec droits d'accès limités (ne surtout pas stocker statiquement ces données dans le code source !).

La plupart des instructions impliquant l'API JDBC sont susceptibles de lever l'exception vérifiée SQLException qu'il faut donc capturer ou propager dans la pile d'appel. Plus d'informations sur les différents types d'exception sont disponibles ici.

Optimisation avec un pool de connexions

PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource();
pds.setConnectionFactoryClassName("org.postgresql.ds.PGPoolingDataSource");
pds.setURL("jdbc:postgresql://localhost:5432/MyDatabase");
pds.setUser(user);
pds.setPassword(password);
//Setting pool properties
pds.setInitialPoolSize(5);
pds.setMinPoolSize(5);
pds.setMaxPoolSize(10);
//Borrowing a connection from the pool
Connection conn = pds.getConnection();

Émission de requêtes SQL brutes

Pour exécuter une requête SQL :

  1. On créé tout d'abord un Statement depuis la connection avec la BDD : Statement s = conn.getStatement()
  2. On exécute la requête sur le Statement : s.execute("SELECT * from mytable WHERE name = 'foo'")
  3. Des commandes peuvent être exécutées en lot en les ajoutant avec s.addBatch("...") puis en demandant l'exécution du lot de commandes avec int[] s.executeBatch() (qui retourne un tableau avec le nombre d'enregistrements impactés par chaque requête)
  4. On appelle des getters pour travailler sur le résultat de la requête :
    • ResultSet getResultSet() pour obtenir les enregistrements retournés par la requête, ce qui n'est pertinent que pour les requêtes de type SELECT (dans les autres cas, null est retourné)
    • ìnt getUpdateCount() pour connaître le nombre d'enregistrements impacté par une requête de type UPDATE
    • Il peut y avoir potentiellement plusieurs résultats (ResultSet ou indications de nombre d'update) ; la méthode getMoreResults() permet de se déplacer au résultat suivant
  5. La méthode SQLWarning getWarnings() permet de connaître d'éventuels avertissements survenus lors de l'exécution
  6. ResultSet getGeneratedKeys() est une méthode utile pour des commandes INSERT afin de connaître les clés primaires auto-générées pour les enregistrements insérés
  7. A l'issue du travail réalisé sur le Statement, on peut appeler sa méthode close() pour libérer les ressurces qui lui sont associées ; on peut initialiser un Statement dans un bloc try-with-resource

Récupération des resultats d'un SELECT avec un ResultSet

Utilisation de PreparedStatement

Exemple pour l'insertion d'un enregistrement

public class StudentManager implements AutoCloseable
{
	private Connection connection;
	
	...
	
	private PreparedStatement studentInsertion;
	
	public synchronized boolean insertStudent(String name, int birthYear, double grade) throws SQLException
	{
		if (studentInsertion == null)
		{
			studentInsertion = connection.prepareStatement("INSERT INTO student (name, birthYear, grade) VALUES (?,?,?)");
			// the statement is prepared, it will be reused for the future calls to insertStudent
		}
		studentInsertion.setString(0, name);
		studentInsertion.setInt(1, birthYear);
		studentInsertion.setDouble(2, grade);
		studentInsertion.execute();
		return studentInsertion.getUpdateCount() == 1;
	}
	
	public static void main(String[] args) throws IOException, SQLException
	{
		Scanner s = new Scanner(System.in);
		try (StudentManager m = new StudentManager())
		{
			m.init(); // we initialize the connection to the database
			while (scanner.hasNextLine())
			{
				// we ask the data of the student to add
				System.out.println("Enter the name of the student to add");
				String name = scanner.nextLine();
				System.out.println("Enter her/his birth year");
				int birthYear = scanner.nextInt();
				System.out.println("Enter her/his grade");
				double grade = scanner.nextDouble();
				System.out.println("Thanks, we insert the student in the database");
				m.insertStudent(name, birthYear, grade);
			}
		}
	}
}

Si l'on avait utilisé un Statement plutôt qu'un PreparedStatement, il aurait été nécessaire de bien vérifier que les apostrophes (caractère ') soient bien déspécialisés en les doublant car leur rôle spécial est de délimiter les valeurs. Cette bande dessinée résume bien le problème.

Utilisation d'une transaction

Une transaction permet de grouper l'exécution de plusieurs requêtes SQL de façon atomique. Cela signifie que soit toutes les requêtes sont exécutées, soit aucune (il ne peut y avoir d'exécution partielle). Dans certaines situations, les transactions sont indispensables pour garantir l'intégrité des données en base.

Par défaut le mode autocommit est activé : cela signifie que chaque requête SQL donne lieu à une transaction. Si l'on souhaite regrouper plusieurs requêtes dans une transaction, on désactive d'abord ce mode :

connection.setAutoCommit(false);

On peut ensuite exécuter les requêtes. On peut clore la transaction de deux façons :

  1. soit en appelant connection.commit() pour la valider et impacter les modifications sur la base
  2. soit en utilisant connection.rollback() pour annuler la transaction : aucune modification n'est alors répercutée sur la base ; il est hautement conseillé d'inclure les instructions SQL réalisée dans un bloc try et d'appeler connection.rollback() en cas de capture d'exception