Drift (trước đây là Moor) là một ORM phản ứng cho Flutter và Dart, được xây dựng trên SQLite với DSL riêng cho các truy vấn. Không giống các ORM truyền thống, Drift biên dịch các truy vấn Dart thành SQL tại thời điểm xây dựng, loại bỏ lỗi runtime. Theo Drift Docs, 2024, Drift tạo ra nhiều hơn đến 40% mã so với các truy vấn SQL thủ công, nhưng loại bỏ hoàn toàn việc viết SQL thủ công, thay thế nó bằng cú pháp Dart an toàn kiểu.
Những điểm chính
Drift là một ORM cho Dart và Flutter, trước đây được gọi là Moor. Nó được phát triển bởi Simon Binder vào năm 2019 và từ đó đã trải qua nhiều phiên bản lớn. Drift biên dịch các truy vấn Dart thành SQL tại thời điểm xây dựng bằng cách sử dụng drift_dev và build_runner, cung cấp an toàn kiểu đầy đủ và loại bỏ lỗi cú pháp SQL tại runtime.
Không giống Floor, Drift sử dụng DSL riêng (Ngôn ngữ dành riêng cho miền) để xây dựng các truy vấn — nhà phát triển viết bằng Dart và trình tạo mã dịch nó sang SQL. Điều này cho phép IDE kiểm tra cú pháp, tự động hoàn thành các trường bảng và tái cấu trúc mô hình dữ liệu mà không sợ làm hỏng truy vấn.
Theo Drift (2024), thư viện này được sử dụng trong hơn 8000 dự án Flutter. Nó hỗ trợ tất cả các nền tảng phổ biến: Android qua sqflite, iOS qua sqflite, web qua sqlite3 WASM, máy tính qua trình điều khiển sqlite3 gốc.
Moor được đổi tên thành Drift trong phiên bản 2.0 (2022). Nguyên nhân là xung đột tên với các dự án khác và mong muốn tách biệt khỏi mã cũ. API vẫn tương thích: để di trú, chỉ cần thay đổi import từ moor sang drift và cập nhật các phụ thuộc.
Drift cung cấp: truy vấn Stream tích hợp với cập nhật tự động khi dữ liệu thay đổi, hỗ trợ giao dịch với rollback, truy vấn SQL tùy chỉnh qua rawQuery, mô hình DAO để đóng gói logic, di trú đa nền tảng và tích hợp với Riverpod và BLoC thông qua các gói drift_riverpod và drift_bloc.
Drift sử dụng tạo mã tại thời điểm biên dịch. Nhà phát triển mô tả các bảng thông qua các chú thích @DataClass hoặc các lớp Dart mở rộng Table. Trình tạo mã tạo các lớp trợ giúp: Companion (cho các trường nullable khi chèn/cập nhật), DriftDatabase (điểm vào) và các triển khai DAO.
Drift không thực thi trực tiếp các truy vấn SQLite. Thay vào đó, nhà phát triển viết bằng Dart: select(tasks).where(tasks.priority.greaterThan(3)).build(). Trình tạo mã dịch này thành SQL, và tại runtime Drift chỉ đơn giản gửi truy vấn SQL đã sẵn sàng đến SQLite. Điều này kết hợp sự tiện lợi của cú pháp Dart với hiệu suất SQL gốc.
Drift hỗ trợ hai chế độ truy vấn: DSL (được khuyến nghị) và SQL thô. Truy vấn DSL an toàn hơn khi viết — trình biên dịch kiểm tra tên trường, kiểu và tương thích. SQL thô cần thiết cho các truy vấn phức tạp không được DSL bao phủ: hàm cửa sổ, CTE đệ quy, các phần mở rộng SQLite cụ thể.
Drift cung cấp hai cách viết truy vấn: Dart DSL (gốc) và SQL thô (cho các trường hợp phức tạp). DSL được ưu tiên cho 90% kịch bản: nó an toàn hơn, dễ đọc hơn và hỗ trợ tái cấu trúc. SQL thô chỉ được sử dụng khi DSL không bao phủ được cấu trúc cần thiết.
| Khía cạnh | Drift DSL | SQL thô trong Drift |
|---|---|---|
| An toàn kiểu | Đầy đủ (biên dịch) | Không (runtime) |
| Tự động hoàn thành | Có (IDE) | Chỉ trong tệp sql |
| Tái cấu trúc | Tự động | Tìm kiếm thủ công chuỗi |
| JOIN phức tạp | Được hỗ trợ | Tự do hoàn toàn |
| Hàm cửa sổ | Giới hạn | Hỗ trợ đầy đủ |
| Tính phản ứng | Tích hợp (Stream) | Qua .watch() |
Drift DSL là cách làm việc chính. Nó bao gồm SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN và nhóm. Đối với tất cả các truy vấn CRUD điển hình, hãy sử dụng DSL: ngắn hơn, an toàn hơn và tự động cập nhật Stream khi có thay đổi.
SQL thô trong Drift cần thiết cho: các hàm SQLite tùy chỉnh (FTS5, JSON1), truy vấn con phức tạp với EXISTS, INSERT OR REPLACE, UPDATE hàng loạt với CASE, cũng như các truy vấn mà hiệu suất là quan trọng và DSL không tạo ra kế hoạch thực thi tối ưu. SQL thô có thể được viết trong các tệp .sql với hỗ trợ phân loại thông qua drift_dev.
Drift sử dụng các lớp mở rộng Table hoặc chú thích @DataClass. Dưới đây là một ví dụ đầy đủ về mô hình Task với các truy vấn qua DSL, SQL thô và cập nhật phản ứng. Sau khi chạy build_runner, tất cả các lớp được tạo đã sẵn sàng.
Lớp Tasks mở rộng Table và định nghĩa các cột. Mỗi cột là một biểu thức kiểu Column<T>. Tham số: withDefault() đặt giá trị mặc định, autoIncrement() đặt tự động tăng. Cơ sở dữ liệu là một lớp trừu tượng mở rộng $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 tạo các phương thức into(tasks).insert(), select(tasks), update(tasks) và delete(tasks) cho các bảng. Tất cả các thao tác trả về Future — làm việc với SQLite là bất đồng bộ. Để theo dõi các thay đổi, hãy sử dụng .watch() thay vì .get().
// Chèn
await into(tasks).insert(TasksCompanion.insert(
title: Value('Mua hàng tạp hóa'),
priority: Value(3),
));
// Đọc với bộ lọc
final highPriority = await (select(tasks)
..where((t) => t.priority.greaterThan(2))
..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)]))
.get();
// Theo dõi phản ứng
select(tasks).watch().listen((tasksList) {
// tasksList — List, cập nhật mỗi khi bảng thay đổi
updateUi(tasksList);
});
Đối với các truy vấn phức tạp, Drift cho phép viết SQL thô trong khi vẫn giữ phân loại. Phương thức customSelect nhận một chuỗi truy vấn và trả về kết quả được phân loại thông qua trình tạo mã. Cách tiếp cận này kết hợp tính linh hoạt của SQL với an toàn kiểu của 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 hỗ trợ cả di trú tự động (cho các thay đổi đơn giản) và thủ công (cho các chuyển đổi phức tạp). Phiên bản cơ sở dữ liệu được đặt trong hàm tạo AppDatabase. Nếu phiên bản không khớp, Drift sẽ áp dụng tất cả các di trú đang chờ xử lý tuần tự.
Để thêm một cột với giá trị mặc định, Drift có thể tự động tạo di trú thông qua MigrationStrategy. Nếu thay đổi không làm hỏng dữ liệu hiện có (thêm trường nullable), bạn có thể sử dụng beforeOpen với kiểm tra phiên bản và thực thi ALTER TABLE.
Đối với các thay đổi phức tạp (đổi tên bảng, hợp nhất dữ liệu, thay đổi kiểu cột), Drift yêu cầu di trú SQL thủ công. Các di trú được chỉ định thông qua tham số migrations trong lớp cơ sở dữ liệu. Mỗi di trú là một đối tượng với các số from/to và các truy vấn SQL.
Drift hỗ trợ chạy ở chế độ kiểm thử thông qua NativeDatabase.memory(). Cơ sở dữ liệu trong bộ nhớ được tạo từ đầu trước mỗi bài kiểm thử và bị hủy sau đó. Để giả lập, hãy sử dụng gói mocktail với QueryExecutor được giả lập. Drift cũng cung cấp DatabaseTestHelper cho các kiểm thử tích hợp với xác minh di trú và truy vấn.
Drift hỗ trợ DAO (Đối tượng truy cập dữ liệu) thông qua các lớp trừu tượng với chú thích @DriftAccessor. DAO đóng gói các truy vấn đến một hoặc nhiều bảng và có thể được kiểm thử riêng biệt với cơ sở dữ liệu. Không giống các truy vấn trực tiếp qua Database, DAO cho phép tái sử dụng logic truy vấn giữa các phần khác nhau của ứng dụng và đơn giản hóa kiểm thử đơn vị.
@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);
}));
}
}
Câu hỏi thường gặp
Drift sử dụng DSL riêng thay vì chuỗi SQL, cung cấp an toàn kiểu đầy đủ và tự động hoàn thành trong IDE. Floor sử dụng chuỗi SQL trong chú thích @Query. Drift cũng hỗ trợ nhiều nền tảng hơn (bao gồm web) và có tính phản ứng tích hợp qua Stream, trong khi ở Floor, Stream phải được khai báo thủ công.
Có, Drift hỗ trợ di trú với bảo toàn dữ liệu. Để thêm cột, hãy sử dụng addColumn trong Migration. Đối với các chuyển đổi phức tạp (đổi tên, hợp nhất), hãy viết SQL thô bên trong di trú. Nếu không có di trú nào được chỉ định, Drift sẽ tạo lại cơ sở dữ liệu với mất dữ liệu khi lược đồ không khớp.
Drift yêu cầu tạo mã thông qua build_runner và drift_dev. Nếu không có tạo mã, không thể tạo các truy vấn được phân loại. Tuy nhiên, đối với các dự án nhỏ, Drift hỗ trợ sqlparser — viết tệp SQL thủ công với phân loại tự động, nhưng điều này vẫn yêu cầu một bước tạo mã.
Để tích hợp với Riverpod, hãy sử dụng gói drift_riverpod. Nó cung cấp các provider cho Database, DAO và truy vấn Stream. Ví dụ: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — UI tự động xây dựng lại khi dữ liệu thay đổi.
Drift không có mã hóa tích hợp, nhưng nó hỗ trợ kết nối các thư viện sqlite3 tùy chỉnh với SEE (Phần mở rộng mã hóa SQLite). Đối với các nền tảng di động, hãy sử dụng sqflite_sqlcipher làm QueryExecutor — Drift hoạt động với bất kỳ triển khai SQLite nào thông qua QueryExecutor trừu tượng.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm