Drift (precedentemente Moor) è un ORM reattivo per Flutter e Dart, costruito su SQLite con un proprio DSL per le query. A differenza degli ORM tradizionali, Drift compila le query Dart in SQL in fase di build, eliminando gli errori runtime. Secondo Drift Docs, 2024, Drift genera fino al 40% in più di codice rispetto alle query SQL manuali, ma elimina completamente la scrittura manuale di SQL, sostituendola con sintassi Dart type-safe.
Punti chiave
Drift è un ORM per Dart e Flutter, precedentemente noto come Moor. È stato sviluppato da Simon Binder nel 2019 e da allora ha attraversato diverse versioni principali. Drift compila le query Dart in SQL in fase di build utilizzando drift_dev e build_runner, fornendo completa sicurezza dei tipi ed eliminando gli errori di sintassi SQL in fase di esecuzione.
A differenza di Floor, Drift utilizza un proprio DSL (Domain-Specific Language) per costruire le query — lo sviluppatore scrive in Dart e il generatore lo traduce in SQL. Ciò consente all’IDE di verificare la sintassi, autocompletare i campi delle tabelle e rifattorizzare il modello dati senza timore di rompere le query.
Secondo Drift (2024), la libreria è utilizzata in oltre 8000 progetti Flutter. Supporta tutte le piattaforme popolari: Android tramite sqflite, iOS tramite sqflite, web tramite sqlite3 WASM, desktop tramite driver nativo sqlite3.
Moor è stato rinominato in Drift nella versione 2.0 (2022). Il motivo era un conflitto di nomi con altri progetti e il desiderio di prendere le distanze dal vecchio codice. L’API è rimasta compatibile: per migrare, è sufficiente sostituire l’import da moor a drift e aggiornare le dipendenze.
Drift fornisce: query Stream integrate con aggiornamento automatico al variare dei dati, supporto alle transazioni con rollback, query SQL personalizzate tramite rawQuery, pattern DAO per l’incapsulamento della logica, migrazioni multipiattaforma e integrazione con Riverpod e BLoC tramite i pacchetti drift_riverpod e drift_bloc.
Drift utilizza la generazione di codice in fase di compilazione. Lo sviluppatore descrive le tabelle tramite annotazioni @DataClass o classi Dart che estendono Table. Il generatore crea classi helper: Companion (per campi nullable durante inserimento/aggiornamento), DriftDatabase (punto di ingresso) e implementazioni DAO.
Drift non esegue query SQLite direttamente. Invece, lo sviluppatore scrive in Dart: select(tasks).where(tasks.priority.greaterThan(3)).build(). Il generatore traduce ciò in SQL, e in fase di esecuzione Drift invia semplicemente la query SQL pronta a SQLite. Ciò unisce la comodità della sintassi Dart con le prestazioni del SQL nativo.
Drift supporta due modalità di query: DSL (consigliato) e SQL puro. Le query DSL sono più sicure da scrivere — il compilatore verifica nomi dei campi, tipi e compatibilità. L’SQL puro è necessario per query complesse non coperte dal DSL: funzioni finestra, CTE ricorsive, estensioni SQLite specifiche.
Drift offre due modi per scrivere query: Dart DSL (nativo) e SQL puro (per casi complessi). Il DSL è preferibile per il 90% degli scenari: è più sicuro, più leggibile e supporta il refactoring. L’SQL puro viene utilizzato solo quando il DSL non copre la costruzione necessaria.
| Aspetto | Drift DSL | SQL puro in Drift |
|---|---|---|
| Sicurezza dei tipi | Completa (compilazione) | No (runtime) |
| Autocompletamento | Sì (IDE) | Solo nei file sql |
| Refactoring | Automatico | Ricerca manuale nelle stringhe |
| JOIN complessi | Supportati | Piena libertà |
| Funzioni finestra | Limitato | Supporto completo |
| Reattività | Integrata (Stream) | Tramite .watch() |
Drift DSL è la modalità di lavoro principale. Copre SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN e raggruppamento. Per tutte le query CRUD tipiche, usa DSL: è più breve, più sicuro e aggiorna automaticamente lo Stream alle modifiche.
SQL puro in Drift è necessario per: funzioni SQLite personalizzate (FTS5, JSON1), sottoquery complesse con EXISTS, INSERT OR REPLACE, UPDATE massivo con CASE, nonché query dove le prestazioni sono critiche e il DSL non genera il piano di esecuzione ottimale. L’SQL puro può essere scritto in file .sql con supporto alla tipizzazione tramite drift_dev.
Drift utilizza classi che estendono Table o l’annotazione @DataClass. Di seguito è riportato un esempio completo del modello Task con query tramite DSL, SQL puro e aggiornamento reattivo. Dopo aver eseguito build_runner, tutte le classi generate sono pronte.
La classe Tasks estende Table e definisce le colonne. Ogni colonna è un’espressione di tipo Column<T>. Parametri: withDefault() imposta un valore predefinito, autoIncrement() imposta l’auto-incremento. Il database è una classe astratta che estende $DriftDatabase.
class Tasks extends Table {
IntColumn get id => integer().autoIncrement();
TextColumn get title => text().withDefault(const Constant(''))();
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
IntColumn get priority => integer().withDefault(const Constant(0))();
}
@DriftDatabase(tables: [Tasks])
class AppDatabase extends $AppDatabase {
AppDatabase(QueryExecutor e) : super(e);
}
Drift genera i metodi into(tasks).insert(), select(tasks), update(tasks) e delete(tasks) per le tabelle. Tutte le operazioni restituiscono Future — lavorare con SQLite è asincrono. Per tracciare le modifiche, usa .watch() invece di .get().
// Inserisci
await into(tasks).insert(TasksCompanion.insert(
title: Value('Comprare generi alimentari'),
priority: Value(3),
));
// Lettura con filtro
final highPriority = await (select(tasks)
..where((t) => t.priority.greaterThan(2))
..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)]))
.get();
// Osservazione reattiva
select(tasks).watch().listen((tasksList) {
// tasksList — List, si aggiorna a ogni modifica della tabella
updateUi(tasksList);
});
Per query complesse, Drift consente di scrivere SQL puro mantenendo la tipizzazione. Il metodo customSelect accetta una stringa di query e restituisce un risultato tipizzato tramite il generatore di codice. Questo approccio combina la flessibilità di SQL con la sicurezza dei tipi di Drift.
final result = await customSelect(
'SELECT title, COUNT(*) as cnt FROM tasks GROUP BY title',
readsFrom: { tasks },
).get();
for (final row in result) {
print('${row.readString("title")}: ${row.readInt("cnt")}');
}
Drift supporta sia migrazioni automatiche (per modifiche semplici) che manuali (per trasformazioni complesse). La versione del database viene impostata nel costruttore di AppDatabase. Se la versione non corrisponde, Drift applica tutte le migrazioni in sospeso in sequenza.
Per aggiungere una colonna con un valore predefinito, Drift può generare automaticamente una migrazione tramite MigrationStrategy. Se la modifica non danneggia i dati esistenti (aggiunta di un campo nullable), puoi usare beforeOpen con controllo della versione ed esecuzione di ALTER TABLE.
Per modifiche complesse (rinominare una tabella, unire dati, cambiare il tipo di una colonna), Drift richiede una migrazione SQL manuale. Le migrazioni vengono specificate tramite il parametro migrations nella classe del database. Ogni migrazione è un oggetto con numeri from/to e query SQL.
Drift supporta l’esecuzione in modalità test tramite NativeDatabase.memory(). Il database in memoria viene creato da zero prima di ogni test e distrutto dopo. Per il mocking, usa il pacchetto mocktail con un QueryExecutor simulato. Drift fornisce anche DatabaseTestHelper per test di integrazione con verifica di migrazioni e query.
Drift supporta DAO (Data Access Object) tramite classi astratte con l’annotazione @DriftAccessor. DAO incapsula le query a una o più tabelle e può essere testato separatamente dal database. A differenza delle query dirette tramite Database, DAO consente di riutilizzare la logica delle query tra diverse parti dell’applicazione e semplifica i test unitari.
@DriftDatabase(tables: [Tasks])
class AppDatabase extends $AppDatabase {
AppDatabase(QueryExecutor e) : super(e) {
migrations.add(Migration(1, 2, (m) async {
await m.addColumn(tasks, tasks.dueDate);
await m.createIndex(tasks.idxPriority);
}));
}
}
Domande frequenti
Drift utilizza un proprio DSL invece di stringhe SQL, fornendo completa sicurezza dei tipi e autocompletamento nell’IDE. Floor utilizza stringhe SQL nell’annotazione @Query. Drift supporta anche più piattaforme (incluso il web) e ha reattività integrata tramite Stream, mentre in Floor lo Stream deve essere dichiarato manualmente.
Sì, Drift supporta migrazioni con conservazione dei dati. Per aggiungere colonne, usa addColumn in Migration. Per trasformazioni complesse (rinominare, unire), scrivi SQL puro all’interno della migrazione. Se non viene specificata alcuna migrazione, Drift ricrea il database con perdita di dati quando lo schema non corrisponde.
Drift richiede la generazione di codice tramite build_runner e drift_dev. Senza generazione, è impossibile creare query tipizzate. Tuttavia, per progetti piccoli, Drift supporta sqlparser — scrittura manuale di file SQL con tipizzazione automatica, ma richiede comunque un passaggio di generazione.
Per l’integrazione con Riverpod, usa il pacchetto drift_riverpod. Fornisce provider per Database, DAO e query Stream. Esempio: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — l’UI si ricostruisce automaticamente quando i dati cambiano.
Drift non ha crittografia integrata, ma supporta la connessione di librerie sqlite3 personalizzate con SEE (SQLite Encryption Extension). Per piattaforme mobili, usa sqflite_sqlcipher come QueryExecutor — Drift funziona con qualsiasi implementazione SQLite tramite l’QueryExecutor astratto.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche