5.3Base de données locale avec SQLite
5.3.1 – Introduction à SQLite dans Flutter
🎯 Qu'est-ce que SQLite ?
SQLite est un système de gestion de base de données relationnelle (SGBDR) léger et embarqué. Contrairement aux bases de données serveur comme MySQL ou PostgreSQL, SQLite fonctionne directement dans votre application sans nécessiter de serveur séparé.
SQLite stocke toutes les données dans un seul fichier sur l'appareil. Ce fichier contient toutes les tables, les relations, et les données. C'est simple, efficace, et parfait pour les applications mobiles.
📊 Pourquoi utiliser SQLite dans Flutter ?
SQLite est idéal lorsque vous avez besoin de :
- Données structurées : Stocker des informations organisées en tables (utilisateurs, produits, commandes, etc.)
- Relations entre données : Établir des liens entre différentes entités (un utilisateur a plusieurs commandes, une commande contient plusieurs produits)
- Requêtes complexes : Rechercher, filtrer, trier, et agréger des données avec le langage SQL
- Grandes quantités de données : Gérer des milliers ou millions d'enregistrements efficacement
- Performance : Accès rapide aux données avec indexation et optimisations
đź“‹ Cas d'utilisation typiques
🔍 SQLite vs autres solutions de stockage
âś… Avantages de SQLite
- Puissant : Support complet du langage SQL (SELECT, INSERT, UPDATE, DELETE, JOIN, etc.)
- Rapide : Performances excellentes pour les opérations locales
- Fiable : Base de données mature et largement utilisée
- Léger : Pas de serveur, tout est dans un fichier
- Cross-platform : Fonctionne sur Android, iOS, Web, Desktop
- ACID : Garantit l'intégrité des données (Atomicité, Cohérence, Isolation, Durabilité)
❌ Limites de SQLite
- Pas de réseau : Base de données locale uniquement, pas de partage entre appareils
- Pas de concurrence élevée : Optimisé pour un seul utilisateur/application
- Courbe d'apprentissage : Nécessite de connaître SQL
- Configuration initiale : Plus complexe que SharedPreferences
Utilisez SQLite si vous avez besoin de relations entre données, de requêtes complexes, ou de gérer de grandes quantités de données structurées. Pour des préférences simples, préférez SharedPreferences. Pour des données sensibles, utilisez Secure Storage en complément.
5.3.2 – Installation du package sqflite
📦 Qu'est-ce que sqflite ?
sqflite est le package Flutter officiel pour utiliser SQLite. Il fournit une API Dart simple et intuitive pour interagir avec une base de données SQLite dans votre application.
Le package sqflite est un wrapper autour de SQLite qui permet d'utiliser SQLite de manière native dans Flutter. Il gère automatiquement les différences entre Android et iOS, vous offrant une API unifiée.
📝 Installation
Pour installer sqflite, ajoutez-le Ă votre fichier pubspec.yaml :
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0
path: ^1.8.3
Le package
path est nécessaire pour gérer les chemins de fichiers de manière cross-platform. Il permet de créer le chemin vers le fichier de base de données de manière compatible avec Android, iOS, et les autres plateformes.
🔧 Installation des dépendances
Après avoir ajouté les dépendances, exécutez :
flutter pub get
📚 Import dans votre code
Pour utiliser sqflite dans votre code Dart, importez les packages nécessaires :
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Sur certaines plateformes (notamment Web), sqflite peut nécessiter des configurations supplémentaires. Pour Web, vous devrez peut-être utiliser
sqflite_common_ffi ou une alternative comme drift. Pour Android et iOS, sqflite fonctionne nativement sans configuration supplémentaire.
✅ Vérification de l'installation
Pour vérifier que sqflite est correctement installé, vous pouvez créer un simple test :
import 'package:sqflite/sqflite.dart';
void main() async {
// Vérifier la version de SQLite
print('Version SQLite: ${await getDatabasesPath()}');
}
Si cette commande s'exécute sans erreur, sqflite est correctement installé ! ✅
5.3.3 – Créer et initialiser une base de données
📝 Créer une base de données
Pour créer une base de données SQLite dans Flutter, vous devez :
- Obtenir le chemin où stocker la base de données
- Ouvrir ou créer la base de données
- Initialiser les tables
🧪 Exemple : Créer et initialiser une base de données
Voici un exemple complet de création et d'initialisation d'une base de données :
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
Database? _database;
String _status = 'Initialisation...';
@override
void initState() {
super.initState();
_initialiserBaseDeDonnees();
}
// Initialiser la base de données
Future<void> _initialiserBaseDeDonnees() async {
try {
// 1. Obtenir le chemin de la base de données
String databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'ma_base_de_donnees.db');
// 2. Ouvrir ou créer la base de données
_database = await openDatabase(
path,
version: 1,
onCreate: (db, version) {
// 3. Créer les tables lors de la première création
db.execute('''
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)
''');
},
);
setState(() {
_status = 'Base de données créée avec succès !';
});
} catch (e) {
setState(() {
_status = 'Erreur: $e';
});
}
}
@override
void dispose() {
_database?.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Initialisation SQLite'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_status.contains('succès') ? Icons.check_circle : Icons.info,
size: 64,
color: _status.contains('succès') ? Colors.green : Colors.blue,
),
const SizedBox(height: 24),
Text(
_status,
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
🔍 Explication du code
Analysons les éléments clés de cet exemple :
getDatabasesPath(): Retourne le chemin par défaut pour les bases de données sur la plateforme actuellejoin(databasesPath, 'ma_base_de_donnees.db'): Crée le chemin complet vers le fichier de base de donnéesopenDatabase(): Ouvre une base de données existante ou en crée une nouvelle si elle n'existe pasversion: 1: Définit la version de la base de données (important pour les migrations)onCreate: Callback exécuté uniquement lors de la première création de la base de donnéesdb.execute(): Exécute une commande SQL pour créer les tables
Sur Android, la base de données est stockée dans
/data/data/com.votre.app/databases/. Sur iOS, elle est dans le répertoire Documents de l'application. Le package sqflite gère automatiquement ces différences.
đź“‹ Structure d'une table SQL
Dans l'exemple ci-dessus, nous avons créé une table utilisateurs avec la structure suivante :
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)
Explication des types et contraintes :
INTEGER PRIMARY KEY AUTOINCREMENT: Identifiant unique qui s'incrémente automatiquementTEXT: Chaîne de caractèresNOT NULL: Le champ ne peut pas être videUNIQUE: La valeur doit être unique dans la tableINTEGER: Nombre entier
đź”§ Gestion des erreurs
Il est important de gérer les erreurs lors de la création de la base de données :
try {
_database = await openDatabase(
path,
version: 1,
onCreate: (db, version) {
// Création des tables
},
);
} catch (e) {
print('Erreur lors de la création de la base de données: $e');
// Gérer l'erreur (afficher un message, logger, etc.)
}
- Toujours fermer la base de données dans
dispose()pour libérer les ressources - Gérer les erreurs avec try-catch
- Utiliser des transactions pour les opérations multiples
- Ne pas ouvrir plusieurs instances de la même base de données
5.3.4 – Définir des tables et des modèles
📊 Modèle de données
Avant de créer vos tables, il est important de définir un modèle de données en Dart. Cela permet de structurer votre code et de faciliter la conversion entre les objets Dart et les enregistrements de la base de données.
🧪 Exemple : Modèle Utilisateur
Voici un exemple complet avec un modèle Utilisateur :
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
// Modèle de données
class Utilisateur {
final int? id;
final String nom;
final String email;
final int? age;
Utilisateur({
this.id,
required this.nom,
required this.email,
this.age,
});
// Convertir un Map (de la base de données) en Utilisateur
factory Utilisateur.fromMap(Map<String, dynamic> map) {
return Utilisateur(
id: map['id'] as int?,
nom: map['nom'] as String,
email: map['email'] as String,
age: map['age'] as int?,
);
}
// Convertir un Utilisateur en Map (pour la base de données)
Map<String, dynamic> toMap() {
return {
'id': id,
'nom': nom,
'email': email,
'age': age,
};
}
@override
String toString() {
return 'Utilisateur{id: $id, nom: $nom, email: $email, age: $age}';
}
}
// Classe helper pour gérer la base de données
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('ma_base.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)
''');
}
Future<void> close() async {
final db = await database;
db.close();
}
}
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
@override
void dispose() {
_dbHelper.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Modèles et Tables'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Base de données initialisée avec modèle Utilisateur',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// Exemple d'utilisation du modèle
final utilisateur = Utilisateur(
nom: 'Jean Dupont',
email: 'jean@example.com',
age: 30,
);
print(utilisateur.toMap());
},
child: const Text('Tester le modèle'),
),
],
),
),
);
}
}
🔍 Explication du modèle
Le modèle Utilisateur contient :
- Propriétés : Les champs de données (id, nom, email, age)
fromMap(): Constructeur factory qui convertit un Map (résultat d'une requête SQL) en objet UtilisateurtoMap(): Méthode qui convertit un Utilisateur en Map pour l'insérer dans la base de données
📋 Types de données SQLite
SQLite supporte les types suivants :
đź”— Relations entre tables
Pour créer des relations entre tables, utilisez des clés étrangères (FOREIGN KEY) :
-- Table utilisateurs
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
-- Table commandes (avec relation vers utilisateurs)
CREATE TABLE commandes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
utilisateur_id INTEGER NOT NULL,
montant REAL NOT NULL,
date TEXT NOT NULL,
FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
);
Une clé étrangère établit une relation entre deux tables. Dans l'exemple ci-dessus, chaque commande est liée à un utilisateur via
utilisateur_id. Cela garantit l'intégrité référentielle : on ne peut pas créer une commande pour un utilisateur qui n'existe pas.
âś… Bonnes pratiques
- Utiliser des modèles Dart : Facilite la manipulation des données
- Créer une classe helper : Centralise la gestion de la base de données
- Utiliser des noms cohérents : Nommez vos tables et colonnes de manière claire
- Définir des contraintes : Utilisez NOT NULL, UNIQUE, PRIMARY KEY pour garantir l'intégrité
- Documenter votre schéma : Commentez vos tables et relations
5.3.5 – Opérations CRUD (Create, Read, Update, Delete)
📝 Qu'est-ce que CRUD ?
CRUD est un acronyme pour les quatre opérations de base sur les données :
- Create (Créer) : Insérer de nouvelles données
- Read (Lire) : Récupérer des données
- Update (Mettre à jour) : Modifier des données existantes
- Delete (Supprimer) : Supprimer des données
➕ CREATE - Insérer des données
Pour insérer des données dans une table, utilisez la méthode insert() :
// Méthode dans DatabaseHelper
Future<int> insertUtilisateur(Utilisateur utilisateur) async {
final db = await database;
return await db.insert(
'utilisateurs',
utilisateur.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
🧪 Exemple : Application CRUD complète
Voici un exemple complet avec toutes les opérations CRUD :
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
// Modèle Utilisateur (identique à la section précédente)
class Utilisateur {
final int? id;
final String nom;
final String email;
final int? age;
Utilisateur({
this.id,
required this.nom,
required this.email,
this.age,
});
factory Utilisateur.fromMap(Map<String, dynamic> map) {
return Utilisateur(
id: map['id'] as int?,
nom: map['nom'] as String,
email: map['email'] as String,
age: map['age'] as int?,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'nom': nom,
'email': email,
'age': age,
};
}
}
// DatabaseHelper avec toutes les opérations CRUD
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('crud_example.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)
''');
}
// CREATE - Insérer un utilisateur
Future<int> insertUtilisateur(Utilisateur utilisateur) async {
final db = await database;
return await db.insert(
'utilisateurs',
utilisateur.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// READ - Lire tous les utilisateurs
Future<List<Utilisateur>> getAllUtilisateurs() async {
final db = await database;
final result = await db.query('utilisateurs', orderBy: 'nom');
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
// READ - Lire un utilisateur par ID
Future<Utilisateur?> getUtilisateurById(int id) async {
final db = await database;
final result = await db.query(
'utilisateurs',
where: 'id = ?',
whereArgs: [id],
);
if (result.isNotEmpty) {
return Utilisateur.fromMap(result.first);
}
return null;
}
// UPDATE - Mettre Ă jour un utilisateur
Future<int> updateUtilisateur(Utilisateur utilisateur) async {
final db = await database;
return await db.update(
'utilisateurs',
utilisateur.toMap(),
where: 'id = ?',
whereArgs: [utilisateur.id],
);
}
// DELETE - Supprimer un utilisateur
Future<int> deleteUtilisateur(int id) async {
final db = await database;
return await db.delete(
'utilisateurs',
where: 'id = ?',
whereArgs: [id],
);
}
Future<void> close() async {
final db = await database;
db.close();
}
}
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
final _nomController = TextEditingController();
final _emailController = TextEditingController();
final _ageController = TextEditingController();
List<Utilisateur> _utilisateurs = [];
Utilisateur? _utilisateurSelectionne;
@override
void initState() {
super.initState();
_chargerUtilisateurs();
}
@override
void dispose() {
_nomController.dispose();
_emailController.dispose();
_ageController.dispose();
_dbHelper.close();
super.dispose();
}
// Charger tous les utilisateurs
Future<void> _chargerUtilisateurs() async {
final utilisateurs = await _dbHelper.getAllUtilisateurs();
setState(() {
_utilisateurs = utilisateurs;
});
}
// CREATE - Ajouter un utilisateur
Future<void> _ajouterUtilisateur() async {
if (_nomController.text.isEmpty || _emailController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez remplir tous les champs')),
);
return;
}
final utilisateur = Utilisateur(
nom: _nomController.text,
email: _emailController.text,
age: _ageController.text.isNotEmpty ? int.tryParse(_ageController.text) : null,
);
await _dbHelper.insertUtilisateur(utilisateur);
_chargerUtilisateurs();
_viderChamps();
}
// UPDATE - Modifier un utilisateur
Future<void> _modifierUtilisateur() async {
if (_utilisateurSelectionne == null) return;
final utilisateur = Utilisateur(
id: _utilisateurSelectionne!.id,
nom: _nomController.text,
email: _emailController.text,
age: _ageController.text.isNotEmpty ? int.tryParse(_ageController.text) : null,
);
await _dbHelper.updateUtilisateur(utilisateur);
_chargerUtilisateurs();
_viderChamps();
setState(() {
_utilisateurSelectionne = null;
});
}
// DELETE - Supprimer un utilisateur
Future<void> _supprimerUtilisateur(int id) async {
await _dbHelper.deleteUtilisateur(id);
_chargerUtilisateurs();
}
void _selectionnerUtilisateur(Utilisateur utilisateur) {
setState(() {
_utilisateurSelectionne = utilisateur;
_nomController.text = utilisateur.nom;
_emailController.text = utilisateur.email;
_ageController.text = utilisateur.age?.toString() ?? '';
});
}
void _viderChamps() {
_nomController.clear();
_emailController.clear();
_ageController.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CRUD SQLite'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Formulaire
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _ageController,
decoration: const InputDecoration(
labelText: 'Âge',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _utilisateurSelectionne == null
? _ajouterUtilisateur
: _modifierUtilisateur,
child: Text(_utilisateurSelectionne == null
? 'Ajouter'
: 'Modifier'),
),
),
if (_utilisateurSelectionne != null) ...[
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: () {
_viderChamps();
setState(() {
_utilisateurSelectionne = null;
});
},
child: const Text('Annuler'),
),
),
],
],
),
],
),
),
),
const SizedBox(height: 16),
// Liste des utilisateurs
Expanded(
child: ListView.builder(
itemCount: _utilisateurs.length,
itemBuilder: (context, index) {
final utilisateur = _utilisateurs[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(utilisateur.nom),
subtitle: Text('${utilisateur.email} - ${utilisateur.age ?? "N/A"} ans'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _selectionnerUtilisateur(utilisateur),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _supprimerUtilisateur(utilisateur.id!),
),
],
),
),
);
},
),
),
],
),
),
);
}
}
🔍 Explication des opérations CRUD
1. CREATE - Insert
await db.insert(
'utilisateurs',
utilisateur.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
insert(): Insère une nouvelle ligne dans la tableconflictAlgorithm: Définit le comportement en cas de conflit (doublon, etc.)- Retourne l'ID de la ligne insérée
2. READ - Query
await db.query(
'utilisateurs',
where: 'id = ?',
whereArgs: [id],
orderBy: 'nom',
);
query(): Récupère des données avec des conditionswhere: Condition SQL (utilisez?pour éviter les injections SQL)whereArgs: Valeurs à substituer aux?orderBy: Tri des résultats
3. UPDATE - Update
await db.update(
'utilisateurs',
utilisateur.toMap(),
where: 'id = ?',
whereArgs: [utilisateur.id],
);
update(): Met à jour des lignes existantes- Retourne le nombre de lignes modifiées
4. DELETE - Delete
await db.delete(
'utilisateurs',
where: 'id = ?',
whereArgs: [id],
);
delete(): Supprime des lignes- Retourne le nombre de lignes supprimées
Toujours utiliser
? dans les requêtes WHERE et passer les valeurs via whereArgs. Ne jamais concaténer directement des valeurs dans les requêtes SQL !
5.3.6 – Requêtes et filtres
🔍 Requêtes simples
La méthode query() permet de faire des requêtes avec filtres, tri, et limites :
// Récupérer tous les utilisateurs
final tous = await db.query('utilisateurs');
// Récupérer avec une condition
final majeurs = await db.query(
'utilisateurs',
where: 'age >= ?',
whereArgs: [18],
);
// Récupérer avec tri
final tries = await db.query(
'utilisateurs',
orderBy: 'nom ASC',
);
// Récupérer avec limite
final premiers = await db.query(
'utilisateurs',
limit: 10,
);
🧪 Exemple : Recherche et filtres avancés
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
// Modèle Utilisateur (identique aux sections précédentes)
class Utilisateur {
final int? id;
final String nom;
final String email;
final int? age;
Utilisateur({
this.id,
required this.nom,
required this.email,
this.age,
});
factory Utilisateur.fromMap(Map<String, dynamic> map) {
return Utilisateur(
id: map['id'] as int?,
nom: map['nom'] as String,
email: map['email'] as String,
age: map['age'] as int?,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'nom': nom,
'email': email,
'age': age,
};
}
}
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('requetes.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)
''');
// Données de test
await db.insert('utilisateurs', {'nom': 'Alice', 'email': 'alice@example.com', 'age': 25});
await db.insert('utilisateurs', {'nom': 'Bob', 'email': 'bob@example.com', 'age': 30});
await db.insert('utilisateurs', {'nom': 'Charlie', 'email': 'charlie@example.com', 'age': 35});
await db.insert('utilisateurs', {'nom': 'David', 'email': 'david@example.com', 'age': 20});
},
);
}
// Recherche par nom (LIKE)
Future<List<Utilisateur>> rechercherParNom(String recherche) async {
final db = await database;
final result = await db.query(
'utilisateurs',
where: 'nom LIKE ?',
whereArgs: ['%$recherche%'],
);
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
// Filtrer par âge minimum
Future<List<Utilisateur>> filtrerParAge(int ageMin) async {
final db = await database;
final result = await db.query(
'utilisateurs',
where: 'age >= ?',
whereArgs: [ageMin],
orderBy: 'age ASC',
);
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
// Trier par nom
Future<List<Utilisateur>> trierParNom({bool asc = true}) async {
final db = await database;
final result = await db.query(
'utilisateurs',
orderBy: 'nom ${asc ? "ASC" : "DESC"}',
);
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
// RequĂŞte avec plusieurs conditions
Future<List<Utilisateur>> rechercherAvancee({
String? nom,
int? ageMin,
int? ageMax,
}) async {
final db = await database;
List<String> conditions = [];
List<dynamic> args = [];
if (nom != null && nom.isNotEmpty) {
conditions.add('nom LIKE ?');
args.add('%$nom%');
}
if (ageMin != null) {
conditions.add('age >= ?');
args.add(ageMin);
}
if (ageMax != null) {
conditions.add('age <= ?');
args.add(ageMax);
}
String whereClause = conditions.isNotEmpty
? conditions.join(' AND ')
: '1=1';
final result = await db.query(
'utilisateurs',
where: whereClause,
whereArgs: args,
orderBy: 'nom ASC',
);
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
// Compter les enregistrements
Future<int> compterUtilisateurs() async {
final db = await database;
final result = await db.rawQuery('SELECT COUNT(*) as count FROM utilisateurs');
return Sqflite.firstIntValue(result) ?? 0;
}
// RequĂŞte SQL brute
Future<List<Utilisateur>> requeteBrute(String sql, [List<dynamic>? args]) async {
final db = await database;
final result = await db.rawQuery(sql, args);
return result.map((map) => Utilisateur.fromMap(map)).toList();
}
Future<void> close() async {
final db = await database;
db.close();
}
}
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
final _rechercheController = TextEditingController();
List<Utilisateur> _utilisateurs = [];
int _nombreTotal = 0;
@override
void initState() {
super.initState();
_chargerDonnees();
}
@override
void dispose() {
_rechercheController.dispose();
_dbHelper.close();
super.dispose();
}
Future<void> _chargerDonnees() async {
final utilisateurs = await _dbHelper.trierParNom();
final nombre = await _dbHelper.compterUtilisateurs();
setState(() {
_utilisateurs = utilisateurs;
_nombreTotal = nombre;
});
}
Future<void> _rechercher() async {
if (_rechercheController.text.isEmpty) {
_chargerDonnees();
return;
}
final resultats = await _dbHelper.rechercherParNom(_rechercheController.text);
setState(() {
_utilisateurs = resultats;
});
}
Future<void> _filtrerParAge(int ageMin) async {
final resultats = await _dbHelper.filtrerParAge(ageMin);
setState(() {
_utilisateurs = resultats;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RequĂŞtes et Filtres'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Barre de recherche
TextField(
controller: _rechercheController,
decoration: InputDecoration(
labelText: 'Rechercher par nom',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
onPressed: _rechercher,
),
),
onSubmitted: (_) => _rechercher(),
),
const SizedBox(height: 16),
// Filtres rapides
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _filtrerParAge(25),
child: const Text('Âge >= 25'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () => _filtrerParAge(30),
child: const Text('Âge >= 30'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _chargerDonnees,
child: const Text('Tous'),
),
),
],
),
const SizedBox(height: 16),
// Compteur
Text(
'Total: $_nombreTotal utilisateurs',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Liste des résultats
Expanded(
child: ListView.builder(
itemCount: _utilisateurs.length,
itemBuilder: (context, index) {
final utilisateur = _utilisateurs[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(utilisateur.nom),
subtitle: Text('${utilisateur.email} - ${utilisateur.age ?? "N/A"} ans'),
),
);
},
),
),
],
),
),
);
}
}
🔍 Opérateurs SQL courants
where: 'age = ?'where: 'age != ?'where: 'age > ?'where: 'age >= ?'where: 'age < ?'where: 'age <= ?'where: 'nom LIKE ?' avec '%texte%'where: 'id IN (?,?)'where: 'age BETWEEN ? AND ?'📊 Requêtes SQL brutes
Pour des requĂŞtes complexes, vous pouvez utiliser rawQuery() :
// RequĂŞte avec JOIN
final result = await db.rawQuery('''
SELECT u.*, COUNT(c.id) as nb_commandes
FROM utilisateurs u
LEFT JOIN commandes c ON u.id = c.utilisateur_id
GROUP BY u.id
''');
// Requête avec agrégation
final stats = await db.rawQuery('''
SELECT
AVG(age) as age_moyen,
MIN(age) as age_min,
MAX(age) as age_max,
COUNT(*) as total
FROM utilisateurs
''');
MĂŞme avec
rawQuery(), utilisez toujours des paramètres ? et arguments pour éviter les injections SQL. Ne concaténez jamais directement des valeurs utilisateur dans les requêtes SQL !
5.3.7 – Gestion des versions et migrations
🔄 Pourquoi gérer les versions ?
Lorsque vous publiez une mise Ă jour de votre application, vous pouvez avoir besoin de :
- Ajouter de nouvelles tables : Nouvelle fonctionnalité nécessitant de nouvelles données
- Modifier des tables existantes : Ajouter des colonnes, modifier des types
- Supprimer des tables : Nettoyer des données obsolètes
- Migrer des données : Transformer des données existantes vers un nouveau format
Si vous modifiez le schéma de votre base de données sans gérer les versions, les utilisateurs qui ont déjà installé votre application verront des erreurs car leur base de données ne correspond plus au nouveau schéma.
📝 Comment fonctionnent les migrations ?
SQLite utilise un système de versions pour gérer les migrations :
- Chaque base de données a un numéro de version
- Lors de l'ouverture, sqflite compare la version actuelle avec la version demandée
- Si la version demandée est supérieure,
onUpgradeest appelé - Si la version demandée est inférieure,
onDowngradeest appelé (rare)
đź§Ş Exemple : Gestion des versions et migrations
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
// Version actuelle de la base de données
static const int _version = 3;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('migrations.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: _version,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
onDowngrade: _onDowngrade,
);
}
// Création initiale (version 1)
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
''');
}
// Migration entre versions
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
print('Migration de la version $oldVersion vers $newVersion');
// Migration de la version 1 vers 2 : ajouter la colonne age
if (oldVersion < 2) {
await db.execute('ALTER TABLE utilisateurs ADD COLUMN age INTEGER');
print('✓ Colonne age ajoutée');
}
// Migration de la version 2 vers 3 : ajouter la colonne telephone
if (oldVersion < 3) {
await db.execute('ALTER TABLE utilisateurs ADD COLUMN telephone TEXT');
print('✓ Colonne telephone ajoutée');
}
// Vous pouvez aussi créer de nouvelles tables
if (oldVersion < 3) {
await db.execute('''
CREATE TABLE IF NOT EXISTS commandes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
utilisateur_id INTEGER NOT NULL,
montant REAL NOT NULL,
date TEXT NOT NULL,
FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
)
''');
print('✓ Table commandes créée');
}
}
// Downgrade (rare, mais possible)
Future<void> _onDowngrade(Database db, int oldVersion, int newVersion) async {
print('Downgrade de la version $oldVersion vers $newVersion');
// Gérer le downgrade si nécessaire
// Attention : SQLite ne supporte pas DROP COLUMN directement
// Il faut recréer la table sans la colonne
}
Future<void> close() async {
final db = await database;
db.close();
}
}
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
String _status = 'Initialisation...';
@override
void initState() {
super.initState();
_initialiser();
}
@override
void dispose() {
_dbHelper.close();
super.dispose();
}
Future<void> _initialiser() async {
try {
final db = await _dbHelper.database;
setState(() {
_status = 'Base de données initialisée avec succès !\nVersion: ${DatabaseHelper._version}';
});
} catch (e) {
setState(() {
_status = 'Erreur: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Migrations SQLite'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.storage,
size: 64,
color: Colors.blue,
),
const SizedBox(height: 24),
Text(
_status,
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
🔍 Explication des migrations
1. Version de la base de données
static const int _version = 3;
Définissez la version actuelle de votre schéma de base de données. Incrémentez ce numéro à chaque modification du schéma.
2. Callback onCreate
onCreate: _onCreate,
Exécuté uniquement lors de la première création de la base de données. Créez ici le schéma initial (version 1).
3. Callback onUpgrade
onUpgrade: _onUpgrade,
Exécuté lorsque la version demandée est supérieure à la version actuelle. Utilisez des conditions if (oldVersion < X) pour appliquer les migrations progressivement.
đź“‹ Bonnes pratiques pour les migrations
- Incémenter la version progressivement : Ne sautez pas de versions (1 → 2 → 3, pas 1 → 5)
- Tester les migrations : Testez toujours vos migrations avec des données existantes
- Sauvegarder les données : Avant de supprimer des colonnes/tables, sauvegardez les données importantes
- Documenter les changements : Commentez chaque migration pour comprendre l'historique
- Gérer les erreurs : Entourez les migrations de try-catch pour gérer les erreurs
⚠️ Limitations de SQLite
- DROP COLUMN : SQLite ne supporte pas la suppression directe de colonnes
- RENAME COLUMN : Nécessite une recréation de table (anciennes versions)
- MODIFY COLUMN TYPE : Nécessite une recréation de table
Solution : Pour ces opérations, vous devez :
- Créer une nouvelle table avec le nouveau schéma
- Copier les données de l'ancienne table vers la nouvelle
- Supprimer l'ancienne table
- Renommer la nouvelle table
âś… Exemple de migration complexe
// Migration complexe : renommer une colonne
if (oldVersion < 4) {
// 1. Créer une nouvelle table avec le bon schéma
await db.execute('''
CREATE TABLE utilisateurs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom_complet TEXT NOT NULL, -- anciennement "nom"
email TEXT NOT NULL UNIQUE,
age INTEGER,
telephone TEXT
)
''');
// 2. Copier les données
await db.execute('''
INSERT INTO utilisateurs_new (id, nom_complet, email, age, telephone)
SELECT id, nom, email, age, telephone FROM utilisateurs
''');
// 3. Supprimer l'ancienne table
await db.execute('DROP TABLE utilisateurs');
// 4. Renommer la nouvelle table
await db.execute('ALTER TABLE utilisateurs_new RENAME TO utilisateurs');
}
Pour les migrations complexes, utilisez des transactions pour garantir que toutes les opérations réussissent ou échouent ensemble :
await db.transaction((txn) async {
// Toutes les opérations de migration ici
});