Drift (anteriormente Moor) es un ORM reactivo para Flutter y Dart, construido sobre SQLite con su propio DSL para consultas. A diferencia de los ORM tradicionales, Drift compila consultas Dart a SQL en tiempo de compilación, eliminando errores en tiempo de ejecución. Según Drift Docs, 2024, Drift genera hasta un 40% más de código que las consultas SQL manuales, pero elimina por completo la escritura manual de SQL, reemplazándola con sintaxis Dart con seguridad de tipos.
Puntos clave
Drift es un ORM para Dart y Flutter, anteriormente conocido como Moor. Fue desarrollado por Simon Binder en 2019 y desde entonces ha pasado por varias versiones importantes. Drift compila consultas Dart a SQL en tiempo de compilación usando drift_dev y build_runner, lo que proporciona seguridad total de tipos y elimina errores de sintaxis SQL en tiempo de ejecución.
A diferencia de Floor, Drift utiliza su propio DSL (Domain-Specific Language) para construir consultas — el desarrollador escribe en Dart y el generador lo traduce a SQL. Esto permite que el IDE verifique la sintaxis, autocomplete los campos de la tabla y refactorice el modelo de datos sin miedo a romper consultas.
Según Drift (2024), la biblioteca se utiliza en más de 8000 proyectos Flutter. Soporta todas las plataformas populares: Android mediante sqflite, iOS mediante sqflite, web mediante sqlite3 WASM, escritorio mediante el controlador nativo sqlite3.
Moor fue renombrado a Drift en la versión 2.0 (2022). El motivo fue un conflicto de nombres con otros proyectos y el deseo de distanciarse del código antiguo. La API se mantuvo compatible: para migrar, simplemente reemplaza el import de moor a drift y actualiza las dependencias.
Drift proporciona: consultas Stream integradas con actualización automática al cambiar los datos, soporte de transacciones con reversión, consultas SQL personalizadas mediante rawQuery, patrón DAO para encapsulación de lógica, migraciones multiplataforma e integración con Riverpod y BLoC a través de los paquetes drift_riverpod y drift_bloc.
Drift utiliza generación de código en tiempo de compilación. El desarrollador describe las tablas mediante anotaciones @DataClass o clases Dart que extienden Table. El generador crea clases auxiliares: Companion (para campos anulables durante inserción/actualización), DriftDatabase (punto de entrada) e implementaciones DAO.
Drift no ejecuta consultas SQLite directamente. En su lugar, el desarrollador escribe en Dart: select(tasks).where(tasks.priority.greaterThan(3)).build(). El generador traduce esto a SQL, y en tiempo de ejecución Drift simplemente envía la consulta SQL lista a SQLite. Esto combina la comodidad de la sintaxis Dart con el rendimiento de SQL nativo.
Drift soporta dos modos de consulta: DSL (recomendado) y SQL puro. Las consultas DSL son más seguras de escribir — el compilador verifica nombres de campos, tipos y compatibilidad. El SQL puro es necesario para consultas complejas no cubiertas por DSL: funciones de ventana, CTE recursivas, extensiones SQLite específicas.
Drift ofrece dos formas de escribir consultas: Dart DSL (nativo) y SQL puro (para casos complejos). DSL es preferible para el 90% de los escenarios: es más seguro, más legible y soporta refactorización. El SQL puro se usa solo cuando DSL no cubre la construcción requerida.
| Aspecto | Drift DSL | SQL puro en Drift |
|---|---|---|
| Seguridad de tipos | Completa (compilación) | No (ejecución) |
| Autocompletado | Sí (IDE) | Solo en archivos sql |
| Refactorización | Automática | Búsqueda manual en cadenas |
| JOIN complejos | Soportados | Libertad total |
| Funciones de ventana | Limitado | Soporte completo |
| Reactividad | Integrada (Stream) | Mediante .watch() |
Drift DSL es la forma principal de trabajo. Cubre SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN y agrupación. Para todas las consultas CRUD típicas, usa DSL: es más corto, más seguro y actualiza automáticamente Stream en los cambios.
SQL puro en Drift es necesario para: funciones SQLite personalizadas (FTS5, JSON1), subconsultas complejas con EXISTS, INSERT OR REPLACE, UPDATE masivo con CASE, así como consultas donde el rendimiento es crítico y DSL no genera el plan de ejecución óptimo. El SQL puro se puede escribir en archivos .sql con soporte de tipificación mediante drift_dev.
Drift utiliza clases que extienden Table o la anotación @DataClass. A continuación se muestra un ejemplo completo del modelo Task con consultas mediante DSL, SQL puro y actualización reactiva. Después de ejecutar build_runner, todas las clases generadas están listas.
La clase Tasks extiende Table y define columnas. Cada columna es una expresión de tipo Column<T>. Parámetros: withDefault() establece un valor predeterminado, autoIncrement() establece auto-incremento. La base de datos es una clase abstracta que extiende $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 los métodos into(tasks).insert(), select(tasks), update(tasks) y delete(tasks) para las tablas. Todas las operaciones devuelven Future — trabajar con SQLite es asíncrono. Para rastrear cambios, usa .watch() en lugar de .get().
// Insertar
await into(tasks).insert(TasksCompanion.insert(
title: Value('Comprar alimentos'),
priority: Value(3),
));
// Lectura con filtro
final highPriority = await (select(tasks)
..where((t) => t.priority.greaterThan(2))
..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)]))
.get();
// Observación reactiva
select(tasks).watch().listen((tasksList) {
// tasksList — List, se actualiza con cada cambio de la tabla
updateUi(tasksList);
});
Para consultas complejas, Drift permite escribir SQL puro manteniendo la tipificación. El método customSelect toma una cadena de consulta y devuelve un resultado tipificado mediante el generador de código. Este enfoque combina la flexibilidad de SQL con la seguridad de tipos de 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 soporta tanto migraciones automáticas (para cambios simples) como manuales (para transformaciones complejas). La versión de la base de datos se establece en el constructor de AppDatabase. Si la versión no coincide, Drift aplica todas las migraciones pendientes secuencialmente.
Para agregar una columna con un valor predeterminado, Drift puede generar una migración automáticamente mediante MigrationStrategy. Si el cambio no afecta los datos existentes (agregar un campo anulable), puedes usar beforeOpen con verificación de versión y ejecución de ALTER TABLE.
Para cambios complejos (renombrar una tabla, fusionar datos, cambiar el tipo de una columna), Drift requiere migración SQL manual. Las migraciones se especifican mediante el parámetro migrations en la clase de base de datos. Cada migración es un objeto con números from/to y consultas SQL.
Drift soporta la ejecución en modo de prueba mediante NativeDatabase.memory(). La base de datos en memoria se crea desde cero antes de cada prueba y se destruye después. Para simular, usa el paquete mocktail con un QueryExecutor simulado. Drift también proporciona DatabaseTestHelper para pruebas de integración con verificación de migraciones y consultas.
Drift soporta DAO (Data Access Object) mediante clases abstractas con la anotación @DriftAccessor. DAO encapsula consultas a una o más tablas y se puede probar por separado de la base de datos. A diferencia de las consultas directas a través de Database, DAO permite reutilizar la lógica de consultas entre diferentes partes de la aplicación y simplifica las pruebas unitarias.
@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);
}));
}
}
Preguntas frecuentes
Drift utiliza su propio DSL en lugar de cadenas SQL, lo que proporciona seguridad total de tipos y autocompletado en el IDE. Floor usa cadenas SQL en la anotación @Query. Drift también soporta más plataformas (incluyendo web) y tiene reactividad integrada mediante Stream, mientras que en Floor Stream debe declararse manualmente.
Sí, Drift soporta migraciones con conservación de datos. Para agregar columnas, usa addColumn en Migration. Para transformaciones complejas (renombrar, fusionar), escribe SQL puro dentro de la migración. Si no se especifica una migración, Drift recrea la base de datos con pérdida de datos cuando el esquema no coincide.
Drift requiere generación de código mediante build_runner y drift_dev. Sin generación, es imposible crear consultas tipificadas. Sin embargo, para proyectos pequeños, Drift soporta sqlparser — escritura manual de archivos SQL con tipificación automática, pero esto aún requiere un paso de generación.
Para la integración con Riverpod, usa el paquete drift_riverpod. Proporciona proveedores para Database, DAO y consultas Stream. Ejemplo: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — la UI se reconstruye automáticamente cuando los datos cambian.
Drift no tiene cifrado integrado, pero soporta la conexión de bibliotecas sqlite3 personalizadas con SEE (SQLite Encryption Extension). Para plataformas móviles, usa sqflite_sqlcipher como QueryExecutor — Drift funciona con cualquier implementación de SQLite a través del QueryExecutor abstracto.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también