Enregistrer des données avec SQLite

Enregistrer des données dans une base de données est idéal pour les données répétitives ou structurées telles que les coordonnées. Dans cette section, nous partons du principe que vous connaissez les bases de données SQL. Vous y trouverez les informations nécessaires pour commencer à utiliser les bases de données SQLite sur Android. Les API dont vous aurez besoin pour utiliser une base de données sur Android sont disponibles dans le package android.database.sqlite.

Attention : Bien que ces API soient performantes, elles sont relativement basiques, et leur utilisation nécessite beaucoup de temps et d'efforts.

  • Les requêtes SQL brutes ne sont pas vérifiées au moment de la compilation. À mesure que votre graphique de données change, vous devez mettre à jour manuellement les requêtes SQL concernées. Ce processus peut prendre beaucoup de temps et représenter une source d'erreurs.
  • Vous devez utiliser une grande quantité de code récurrent pour effectuer une conversion entre les requêtes SQL et les objets de données.

C'est pourquoi il est vivement conseillé d'utiliser la bibliothèque de persistance Room comme couche d'abstraction pour accéder aux informations contenues dans les bases de données SQLite de votre application.

Définir un schéma et un contrat

Le schéma est l'un des grands principes des bases de données SQL. Il s'agit d'une déclaration formelle de l'organisation de la base de données. Le schéma est reflété dans les instructions SQL que vous utilisez pour créer votre base de données. Il peut être intéressant de créer une classe associée, appelée classe de contrat, qui spécifie explicitement l'organisation de votre schéma de manière systématique et auto-documentée.

Une classe de contrat est un conteneur de constantes qui définissent les noms des URI, des tables et des colonnes. Cette classe vous permet d'utiliser les mêmes constantes dans toutes les autres classes du même package. Cela vous permet de modifier le nom d'une colonne à un endroit et de le propager à l'ensemble du code.

Un bon moyen d'organiser une classe de contrat consiste à placer, au niveau racine de la classe, les définitions communes à l'ensemble de votre base de données. Créez ensuite une classe interne pour chaque table. Chaque classe interne énumère les colonnes de la table correspondante.

Remarque : En implémentant l'interface BaseColumns, votre classe interne peut hériter d'un champ de clé primaire, appelé _ID, auquel s'attendent certaines classes Android telles que CursorAdapter. Cela n'est pas obligatoire, mais cela peut permettre à votre base de données de fonctionner en harmonie avec le framework Android.

Par exemple, le contrat suivant définit le nom d'une table et les noms des colonnes d'une seule table représentant un flux RSS :

Kotlin

object FeedReaderContract {
    // Table contents are grouped together in an anonymous object.
    object FeedEntry : BaseColumns {
        const val TABLE_NAME = "entry"
        const val COLUMN_NAME_TITLE = "title"
        const val COLUMN_NAME_SUBTITLE = "subtitle"
    }
}

Java

public final class FeedReaderContract {
    // To prevent someone from accidentally instantiating the contract class,
    // make the constructor private.
    private FeedReaderContract() {}

    /* Inner class that defines the table contents */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

Créer une base de données à l'aide d'un outil d'aide SQL

Une fois que vous avez défini l'apparence de votre base de données, vous devez implémenter des méthodes permettant de créer et de gérer la base de données ainsi que les tables. Voici quelques instructions standards permettant de créer et de supprimer une table :

Kotlin

private const val SQL_CREATE_ENTRIES =
        "CREATE TABLE ${FeedEntry.TABLE_NAME} (" +
                "${BaseColumns._ID} INTEGER PRIMARY KEY," +
                "${FeedEntry.COLUMN_NAME_TITLE} TEXT," +
                "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)"

private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"

Java

private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
    FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

À l'instar des fichiers que vous enregistrez dans la mémoire de stockage interne de l'appareil, Android stocke votre base de données dans le dossier privé de l'application. Vos données sont sécurisées car, par défaut, cette zone n'est pas accessible aux autres applications ni à l'utilisateur.

La classe SQLiteOpenHelper contient un ensemble d'API utiles permettant de gérer votre base de données. Lorsque vous utilisez cette classe pour obtenir des références à votre base de données, le système crée la base de données et la met à jour uniquement lorsque c'est nécessaire et non au démarrage de l'application, car ces opérations peuvent prendre beaucoup de temps. Il vous suffit simplement d'appeler getWritableDatabase() ou getReadableDatabase().

Remarque : Étant donné que l'exécution de getWritableDatabase() et de getReadableDatabase() peut prendre beaucoup de temps, veillez à les appeler dans un thread d'arrière-plan. Pour en savoir plus, consultez la page Exécution de threads sur Android.

Pour utiliser SQLiteOpenHelper, créez une sous-classe qui remplace les méthodes de rappel onCreate() et onUpgrade(). Vous pouvez également implémenter les méthodes onDowngrade() ou onOpen(), mais elles ne sont pas obligatoires.

Voici un exemple d'implémentation de SQLiteOpenHelper qui utilise certaines des commandes présentées ci-dessus :

Kotlin

class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(SQL_CREATE_ENTRIES)
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db)
    }
    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        onUpgrade(db, oldVersion, newVersion)
    }
    companion object {
        // If you change the database schema, you must increment the database version.
        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "FeedReader.db"
    }
}

Java

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

Pour accéder à votre base de données, instanciez votre sous-classe de SQLiteOpenHelper :

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Placer des informations dans une base de données

Pour insérer des données dans la base de données, transmettez un objet ContentValues à la méthode insert() :

Kotlin

// Gets the data repository in write mode
val db = dbHelper.writableDatabase

// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
}

// Insert the new row, returning the primary key value of the new row
val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)

Java

// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

Le premier argument de la méthode insert() est simplement le nom de la table.

Le deuxième argument indique au framework ce qu'il doit faire si ContentValues est vide (c'est-à-dire, si vous n'avez inséré aucune valeur avec put). Si vous spécifiez le nom d'une colonne, le framework insère une ligne et définit la valeur de cette colonne sur "null". Si vous spécifiez null, comme dans cet exemple de code, le framework n'insère aucune ligne en l'absence de valeurs.

La méthode insert() renvoie l'ID de la nouvelle ligne ou -1 si une erreur est survenue lors de l'insertion des données. Cela peut se produire en cas de conflit avec des données préexistantes dans la base de données.

Lire les informations d'une base de données

Pour lire les informations d'une base de données, utilisez la méthode query() en lui transmettant vos critères de sélection et les colonnes souhaitées. Cette méthode combine des éléments des méthodes insert() et update(), la différence étant que la liste de colonnes définit les données que vous souhaitez extraire (la "projection"), plutôt que les données à insérer. Les résultats de la requête vous sont renvoyés dans un objet Cursor.

Kotlin

val db = dbHelper.readableDatabase

// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")

// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

val cursor = db.query(
        FeedEntry.TABLE_NAME,   // The table to query
        projection,             // The array of columns to return (pass null to get all)
        selection,              // The columns for the WHERE clause
        selectionArgs,          // The values for the WHERE clause
        null,                   // don't group the rows
        null,                   // don't filter by row groups
        sortOrder               // The sort order
)

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
    };

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Les troisième et quatrième arguments (selection et selectionArgs) sont combinés pour créer une clause WHERE. Étant donné que les arguments sont fournis séparément de la requête de sélection, ils sont échappés avant d'être combinés. De ce fait, vos instructions de sélection sont protégées contre l'injection SQL. Pour en savoir plus sur tous les arguments, consultez la documentation de référence sur query().

Pour examiner une ligne dans le curseur, utilisez l'une des méthodes de déplacement Cursor, que vous devez toujours appeler avant de commencer à lire des valeurs. Étant donné que le curseur commence à la position -1, l'appel de la méthode moveToNext() place la "position de lecture" sur la première entrée dans les résultats et indique si le curseur a déjà dépassé la dernière entrée dans l'ensemble de résultats. Pour chaque ligne, vous pouvez lire la valeur d'une colonne en appelant l'une des méthodes get Cursor, telles que getString() ou getLong(). Pour chacune des méthodes get, vous devez transmettre la position d'index de la colonne souhaitée. Vous pouvez l'obtenir en appelant getColumnIndex() ou getColumnIndexOrThrow(). Une fois l'itération des résultats terminée, appelez close() sur le curseur pour libérer ses ressources. L'exemple ci-dessous montre comment récupérer tous les ID d'élément stockés dans un curseur et les ajouter à une liste :

Kotlin

val itemIds = mutableListOf<Long>()
with(cursor) {
    while (moveToNext()) {
        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
        itemIds.add(itemId)
    }
}
cursor.close()

Java

List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
  long itemId = cursor.getLong(
      cursor.getColumnIndexOrThrow(FeedEntry._ID));
  itemIds.add(itemId);
}
cursor.close();

Supprimer des informations d'une base de données

Pour supprimer des lignes d'une table, vous devez fournir des critères de sélection qui identifient les lignes auprès de la méthode delete(). Le mécanisme fonctionne de la même manière que les arguments de sélection de la méthode query(). La spécification de sélection est divisée en une clause de sélection et en arguments de sélection. La clause définit les colonnes à examiner et vous permet également de combiner des tests de colonne. Les arguments sont des valeurs à tester qui sont liées à la clause. Comme le résultat n'est pas traité de la même manière qu'une instruction SQL standard, il est protégé contre l'injection SQL.

Kotlin

// Define 'where' part of query.
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
// Specify arguments in placeholder order.
val selectionArgs = arrayOf("MyTitle")
// Issue SQL statement.
val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)

Java

// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);

La valeur renvoyée pour la méthode delete() indique le nombre de lignes qui ont été supprimées de la base de données.

Mettre à jour une base de données

Lorsque vous devez modifier un sous-ensemble des valeurs de votre base de données, utilisez la méthode update().

La mise à jour de la table combine la syntaxe ContentValues de la méthode insert() à la syntaxe WHERE de la méthode delete().

Kotlin

val db = dbHelper.writableDatabase

// New value for one column
val title = "MyNewTitle"
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
}

// Which row to update, based on the title
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
val selectionArgs = arrayOf("MyOldTitle")
val count = db.update(
        FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs)

Java

SQLiteDatabase db = dbHelper.getWritableDatabase();

// New value for one column
String title = "MyNewTitle";
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);

// Which row to update, based on the title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyOldTitle" };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);

La valeur renvoyée par la méthode update() correspond au nombre de lignes affectées dans la base de données.

Utiliser une connexion persistante à la base de données

L'appel des méthodes getWritableDatabase() et getReadableDatabase() est coûteux en ressources lorsque la base de données est fermée. Par conséquent, vous devez laisser votre connexion à la base de données ouverte aussi longtemps que vous en avez besoin. En règle générale, il est préférable de fermer la base de données dans la méthode onDestroy() de l'activité d'appel.

Kotlin

override fun onDestroy() {
    dbHelper.close()
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    dbHelper.close();
    super.onDestroy();
}

Déboguer votre base de données

Le SDK Android inclut un outil shell sqlite3 qui vous permet de parcourir le contenu d'une table, d'exécuter des commandes SQL et d'exécuter d'autres fonctions utiles sur des bases de données SQLite. Pour en savoir plus, découvrez comment émettre des commandes shell.