SQLite sous Android

 

Introduction

SQLite propose un moteur de base de données relationnelle utilisant le langage SQL. Il sera directement intégré dans vos programmes (contrairement au client-serveur traditionnel) et ne consomme pas beaucoup de place (c'est bien l'intérêt).

SQLite et ses sources sont dans le domaine public. Pour cette raison, vous pouvez retrouver une utilisation de SQLite un peu partout: Firefox, Apple (iPhone...), Android, ...

SQLite ne devrait être utiliser que pour des volumes de données non importants (D'ailleurs, n'oubliez pas qu'il s'agit d'un portable que vous avez entre les mains), avec peu de risque d'accès concurrents (car la base est intégralement lockée lors d'une écriture par exemple). SQLite pourra être intéressant pour remplacer les fichiers de configuration de votre application.

Cf. Wikipedia pour plus d'informations ou le site officiel de SqlLite.

Image non trouvée !Je ne vais pas faire un cours de SQL, car il s'agit ici d'expliquer android et non le SQL. Je considère donc que vous avez au moins les bases : )) ...

 

SQLite sous Android

Comme chaque application embarque sa propre base de données (sous forme d'un fichier comprenant toutes les informations sur la base, mais aussi les données), ce n'est pas à Android de gérer votre base mais bien à vous.

SQLiteOpenHelper

Il existe un objet utilitaire qui va permettre gérer la création/modification de votre base, et récupérer un identifiant sur votre base de données pour pouvoir accéder aux enregistrements. Ce qui va bien vous simplifier la vie. Voir ici pour plus de détails.

 

SQLiteDataBase

La classe permettant de gérer une SQLite est SQLiteDataBase.

Les méthodes qui vous intéresseront le plus seront:

execSQL

La méthode execSQL() qui reçoit en paramètre l’instruction du LDD (langage de définition des données) à exécuter. La méthode retourne null si erreur.

Image non trouvée !Cette méthode n'exécute que les commandes ne retournant pas de résultat (exit donc select, ...)

Cette méthode sera d'autant plus intéressante pour la création/modification des tables de la base (create table/drop table/create index/drop index). Mais il sera possible de lancer les commandes insert/update/delete.

insert()

Cette méthode permet d'insérer des enregistrements. Equivalent à execSQL (insert ...). Mais en passant les paramètres de la commande SQL insert directement dans la méthode insert.

public long insert (String table, String nullColumnHack, ContentValues values)

En entrée:

table: Nom de la table dans laquelle sera insérée le nouvel enregistrement.

nullColumnHack: bidouille permettant d'insérer des enregistrements vides...Pourra être positionné à null si non nécessaire. Sinon, il faudra indiquer le nom d'un champ qui pourra recevoir null pour permettre d'insérer un enregistrement vide. SQL n'autorisant pas l'utilisation de la commande insert sans indiquer au moins un champ.

values Il s'agit d'une map qui va contenir le nom des champs et la valeur de ces champs.

En sortie

Le row ID de la nouvelle ligne insérée, ou -1 si une erreur vient à se produire.

update()

Mettre à jour un ou plusieurs enregistrements d'une table.

public int update (String table, ContentValues values, String whereClause, String[] whereArgs)

En entrée:

table le nom de la table à mettre à jour.
values Map des champs de l'enregitrement à mettre à jour, suivi des nouvelles valeurs (valeurs à null étant autorisées et automatiquement remplacées par NULL au niveau SQL).
whereClause Clause where de l'instruction update qui pourra être positionnée à null (dans ce cas, se sera tous les enregistrements de la table qui seront mis à jour).

En sortie:

Le nombre d'enregistrements mis à jour.

delete()

Permet d'effacer des enregistrements d'une table

En entrée:
La table qui possède les enregistrements à effacer.
whereClause Clause where pour indiquer les enregistrements à effacer. null effacera la totalité des enregistrements de la table (n'efface pas la structure de la table !)

En sortie:

Retourne le nombre d'enregistrements effacés si whereClause possède une clause where. 0 sinon.

Image non trouvée !Si vous voulez effacer tous les enregistrements d'une table et connaître en sortie le nombre d'enregistrements effacés, il faut indiquer une clause where, donc passez 1 (pour vrai, ce qui indique "pour tous les enregistrements dont vrai", or vrai est toujours vrai).

getPath()

La méthode getPath() permet de récupérer le chemin complet avec le nom du fichier de la base.

isReadOnly()

Cette méthode indique que la base est ouverte en lecture seule (true), en lecture/écriture sinon (false).

isOpen()

Retourne vrai si la base est actuellement ouverte.

getVersion()

Retourne la version de la base de données. Si vous utilisez la classe SQLiteOpenHelper(), cette méthode n'aura pas forcement beaucoup d'intérêt.

deleteDatabase()

Permet d'effacer une base de données et tout ce qui va avec.

public static boolean deleteDatabase (File file) (à partir de l'API 16)

En entrée:

Le chemin et le nom de la base sous forme d'instance de la classe File. Utilisez la méthode getPath() pour connaître le chemin complet et le fichier de votre base:

File file = new File (idSQLiteDBBis.getPath());

Boolean result=idSQLiteDB.deleteDatabase (file);

En sortie:

True si la base est supprimée.

 

Gestion des transactions

void beginTransaction() permet de démarrer une transaction en mode EXCLUSIVE

void beginTransactionNonExclusive() permet de démarrer une transaction en mode IMMEDIAT

public void setTransactionSuccessful () Indique que la transaction courante est valide et pourra donc être commitée.
En sortie Throws IllegalStateException dans le cas où le thread n'aurait pas de transaction ou que celle-ci serait déjà marquée comme valide !

Image non trouvée ! Evidemment, il ne faudra plus faire de mise à jour de la base après l'exécution de cette commande !


public void endTransaction() pour indiquer la fin d'une transaction.

 

Exemple de gestion d'une transaction

db.beginTransaction();
try {

...votre code ici...
db.setTransactionSuccessful();

} finally {

db.endTransaction();

}

Commande select

public Cursor rawQuery (String sql, String[] selectionArgs)

La méthode rawQuery permettra de lancer une commande select:

En entrée
sql Une requète SQL comme select (ne pas terminer par ; ) avec éventuellement une clause where
selectionArgs La clause where déclarée éventuellement dans la requète SQL pourra contenir des ?. Ces ? seront remplacés automatiquement avec les différentes valeurs de selectionArgs.

En sortie

Un objet curseur positionné avant le premier élément répondant à la requète.

Le curseur sera utilisable de la manière suivante:

getCount() retourne le nombre d'enregistrements

moveToFirst() pour se positionner sur le premier enregistrement

moveToNext() pour se positionner sur l'enregistrement suivant

getString (index du champ), getInt, ...pour récupérer la valeur d'un champ (suivant son index ou sa position dans l'enregistrement).

cursor.getColumnIndex (String nom) pour récupérer l'index (la position) d'un champ suivant son nom.

close() pour terminer l'utilisation du curseur.

 

Une autre solution consistant à utiliser la méthode query(). il en existe plusieurs variantes:

Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

En entrée:

table le nom de la table

columns tableau de type String contenant les champs à retourner

selection clause where sans le where

selectionArgs Tableau des valeurs qui peuvent être passées à la clause where. tous ? de la clause where étant remplacés par ces valeurs.

puis ordre, limite, ...

En sortie:

Un objet de type curseur dont le fonctionnement a été vu précédemment...

Exemple de code

Exemple de code montrant comment utiliser SQLite. A ne surtout pas reproduire, car risque d'ANR ! Et puis, ce n'est pas forcement très objet !

Création d'une table personnes avec 3 enregistrements, puis traitement des ces informations.

 

SQLiteQueryBuilder

Il s'agit d'une classe qui va permettre de construire une query (Basée sur le pattern builder, soit la construction en plusieurs étapes d'un objet complexe - Ne pas confondre avec le pattern Factory qui lui construit l'objet une fois pour toute).

On se construit une instance de sqlBuilder

SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();

Puis on ajoute la/les tables (pour les jointures) avec sqlBuilder.setTables(String tables); avec par exemple tables = "nomTable1, nomTable2"

ou setTables("nomTable1 LEFT OUTER JOIN nomTable2 ON (nomTable1.id = nomTable2.id)"), ...

La clause where : public void appendWhere (CharSequence inWhere)

...

Il sera possible de récupérer ensuite un curseur sur la query ainsi construite:

public Cursor query (SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder));

 

Problème base relationnelle et l'objet

On s'écarte un peu du sujet...Il s'agit d'un problème qui va bien au delà d'Android.

Et oui, SQLite permet de gérer des bases de données relationnelles, or nous développons en objet ! Donc deux mondes absolument incompatibles !

Vous risquez d'être rapidement confronté au problème du mapping Objet/Relationnel (attributs d'un objet/attributs d'une table) qui va vous prendre du temps de développement ainsi que du temps à l'exécution !

Image non trouvée !Vous pourrez trouver du code utilisant une classe dérivée de SQLiteOpenHelper avec tous ces traitements "DAO" dedans...Bon, cela va marcher, mais il ne me semble pas que cela soit vraiment une classe prévue à cet effet: SQLite Open Helper donc Aide à l'ouverture d'une ressource SQLite, pas aide à la lecture/maj des tables. De plus, si demain il y a un changement de la source de données, le travail de maintenance sera plus conséquent !

Image non trouvée !Pire, du code SQL éparpillé dans plusieurs classes (voire dupliqué). Ce n'est évidemment pas à faire !

 

Pour essayer de s'en sortir, une solution classique à base de DAO:

    - Utilisation du design pattern DAO (Data Access Object). Ce design pattern permet de faire le lien entre la couche métier (ici nos classes JAVA) et la couche persistante (ici SQLite, mais cela aurait pu être un fichier XML, ...). c'est dans cette classe que nous retrouverons toutes les commandes SQL (et uniquement là !) pour accéder à une table.

    Avantage: maintenance simplifiée, possibilité de changer/migrer rapidement la source de donnée vers une autre, ...

    Le codage:

    Création d'un classe abstraite DAO (contenant les méthodes de base comme la création, l'insertion ou la suppression d'enregistrements), puis création des DAO à partir de cette classe.

     

    - Utilisation de beans Java (donc uniquement des méthodes getter et setter ou accessor/mutator) sur tous les champs des entités. Un bean par entité (table). Ce bean sera alimenté par le DAO correspondant. C'est aussi lui qui sera réutilisé pour mettre à jour (toujours via DAO) les champs d'un enregistrement.

    - Création d'une factory (pattern Factory), qui est une fabrique permettant d'obtenir une instance typique d’un objet donné. C'est à partir de cette classe que nous pourrons obtenir des instances de DAO (moyennant quelques paramètres passés eventuellement).

    Avantage: Si un DAO devait changer, là aussi la maintenance devrait se limiter à la factory.

    Pour rappel (ou non): Pas de constructeur, et des méthodes publiques et statiques permettant la création des différentes instances DAO.

    Le codage par exemple avec une table "personnes":

    public class DAOFactory {

    public static DAO<Personnes> getPersonnesDAO(){
    return new PersonnesDAO();
    }

    Qui pourrait elle aussi étendre une classe abstraite pour pouvoir gérer les différentes sources de données.

    Image non trouvée !Je ne parle pas du design pattern DTO (Data transfer Object), qui permet essentiellement de désolidariser la structure physique de la base de données de l'application. Ici, les données et les interfaces sont toutes proches avec SQLite. Si la source de données devait être distante ou d'origines diverses (fichier texte, base de données), ..., alors il faudrait peut-être étudier l'usage de ce pattern.

     

    Pour tous ces problèmes, vous trouverez un exemple de mise en oeuvre dans l'application Anniversaire développée comme exemple de développement sous Android (Pas d'utilisation de DTO).

 

Rendre accessible la base de donnée hors application

Le ContentProvider est intéressant si vos données peuvent être accédées depuis une autre application que la votre.

 

Gestion de la ressource d'une base de données

Où se connecter à une base de données et se déconnecter ?

Il s'agit d'une ressource comme une autre. Donc il vaut mieux libérer la ressource le plus vite possible. Mais si la portée est grande (plusieurs lectures/écritures dans la base durant le cycle de vie d'une activité), alors la gestion devra se faire de la manière suivante:

Pour une connexion: Dans le onResume() puisque l'on y passe systématiquement (en création comme en reprise d'activité).

Pour une déconnexion: Dans le onPause(). Le système verra ensuite s'il a besoin de resumer l'activité (et donc passage dans le onResume() ou de la détruire.

 

Annexes

De la doc.

http://developer.android.com/reference/android/database/SQLite/SQLiteDatabase.html pour avoir toutes les méthodes disponibles de la classe SQLiteDatabase.

 

Supprimer la base de données hors programme

Durant la période de développement, il est fort probable que vous ayez à supprimer complètement votre base de données SQLite.

Pour ce faire (root nécessaire si vous testez sur un vrai appareil Android et non un émulateur):

adb shell ou terminal emultor
cd /data/data/le.package.de.votre.application/Databases
rm nom_base_donnees

Image non trouvée ! Evitez les jokers surtout * lorsque vous êtes root, une suppression hazardeuse pourrait rendre votre portable complètement inutile - imaginez si vous effaciez tout le système par erreur !!!

 

Ou via DDMS, choisissez votre mobile de test et onglet file explorer, positionnez vous sous /data/data/le.package.de.votre.application/Databases puis supprimez les fichiers de la base de données correspondant au nom de votre base.

Ou encore via es Explorer (ou équivalent) sous /data/data/le.package.de.votre.application/Databases puis supprimez les fichiers de la base de données correspondant au nom de votre base.