Drift (پیشتر Moor) — ORM واکنشگرا برای Flutter و Dart ساخته شده بر روی SQLite با DSL مخصوص خود برای کوئریها. برخلاف ORMهای سنتی، Drift کوئریهای Dart را در مرحله build به SQL کامپایل میکند و خطاهای runtime را حذف میکند. به گفته Drift Docs, 2024، Drift تا 40% کد بیشتر نسبت به کوئریهای دستی SQL تولید میکند، اما نوشتن دستی SQL را کاملاً حذف کرده و آن را با نحو type-safe Dart جایگزین میکند.
نکات اصلی
Drift — ORM برای Dart و Flutter که پیشتر با نام Moor شناخته میشد. توسط Simon Binder در سال 2019 توسعه یافت و از آن زمان چندین نسخه اصلی را پشت سر گذاشته است. Drift کوئریهای Dart را با استفاده از drift_dev و build_runner در مرحله build به SQL کامپایل میکند که امنیت نوع کامل را فراهم کرده و خطاهای نحوی SQL را در runtime حذف میکند.
برخلاف Floor، Drift از DSL (Domain-Specific Language) مخصوص خود برای ساخت کوئریها استفاده میکند — توسعهدهنده به Dart مینویسد و مولد آن را به SQL ترجمه میکند. این به IDE امکان بررسی نحو، تکمیل خودکار فیلدهای جدول و بازسازی مدل داده را بدون ترس از شکستن کوئری میدهد.
طبق Drift (2024)، این کتابخانه در بیش از 8000 پروژه Flutter استفاده میشود. از تمامی پلتفرمهای محبوب پشتیبانی میکند: Android از طریق sqflite، iOS از طریق sqflite، وب از طریق sqlite3 WASM، دسکتاپ از طریق درایور بومی sqlite3.
Moor در نسخه 2.0 (2022) به Drift تغییر نام داد. دلیل — تداخل نام با پروژههای دیگر و تمایل به فاصله گرفتن از کد قدیمی. API سازگار باقی ماند: برای مهاجرت کافیست import را از moor به drift تغییر داده و وابستگیها را بهروز کنید.
Drift فراهم میکند: کوئریهای Stream داخلی با بهروزرسانی خودکار هنگام تغییر داده، پشتیبانی از تراکنشها با بازگشت، کوئریهای سفارشی SQL از طریق rawQuery، الگوی DAO برای کپسولهسازی منطق، مهاجرتهای چندسکویی و یکپارچهسازی با Riverpod و BLoC از طریق بستههای drift_riverpod و drift_bloc.
Drift از تولید کد در مرحله کامپایل استفاده میکند. توسعهدهنده جداول را از طریق حاشیهنویسی @DataClass یا کلاسهای Dart که Table را گسترش میدهند توصیف میکند. مولد کلاسهای کمکی ایجاد میکند: Companion (برای فیلدهای nullable در درج/بهروزرسانی)، DriftDatabase (نقطه ورود) و پیادهسازیهای DAO.
کوئریهای SQLite را Drift مستقیماً اجرا نمیکند. در عوض توسعهدهنده به Dart مینویسد: select(tasks).where(tasks.priority.greaterThan(3)).build(). مولد این را به SQL ترجمه میکند و در زمان اجرا Drift به سادگی کوئری آماده SQL را به SQLite ارسال میکند. این راحتی نحو Dart را با کارایی SQL بومی ترکیب میکند.
Drift از دو حالت کوئری پشتیبانی میکند: DSL (توصیه شده) و raw SQL. کوئریهای DSL ایمنتر هستند — کامپایلر نام فیلدها، نوعها و سازگاری را بررسی میکند. Raw SQL برای کوئریهای پیچیدهای که توسط DSL پوشش داده نمیشوند مورد نیاز است: توابع پنجرهای، CTE بازگشتی، افزونههای خاص SQLite.
Drift دو روش برای نوشتن کوئری ارائه میدهد: Dart DSL (بومی) و raw SQL (برای موارد پیچیده). DSL برای 90% سناریوها ترجیح داده میشود: ایمنتر، خواناتر و از بازسازی پشتیبانی میکند. Raw SQL فقط زمانی استفاده میشود که DSL ساختار مورد نیاز را پوشش ندهد.
| جنبه | Drift DSL | Raw SQL در Drift |
|---|---|---|
| امنیت نوع | کامل (کامپایل) | ندارد (runtime) |
| تکمیل خودکار | بله (IDE) | فقط در فایلهای sql |
| بازسازی | خودکار | جستجوی دستی در رشتهها |
| JOIN پیچیده | پشتیبانی میشود | آزادی کامل |
| توابع پنجرهای | محدود | پشتیبانی کامل |
| واکنشگرایی | داخلی (Stream) | از طریق .watch() |
Drift DSL — روش اصلی کار. SELECT، INSERT، UPDATE، DELETE، WHERE، ORDER BY، LIMIT، JOIN و گروهبندی را پوشش میدهد. برای تمام کوئریهای معمول CRUD از DSL استفاده کنید: کوتاهتر، ایمنتر و Stream را هنگام تغییرات به طور خودکار بهروز میکند.
Raw SQL در Drift برای موارد زیر لازم است: توابع سفارشی SQLite (FTS5, JSON1)، زیرکوئریهای پیچیده با EXISTS، INSERT OR REPLACE، بهروزرسانی انبوه با CASE، همچنین کوئریهایی که در آنها کارایی بحرانی است و DSL برنامه اجرای بهینه تولید نمیکند. Raw SQL را میتوان در فایلهای .sql با پشتیبانی از تایپسازی از طریق drift_dev نوشت.
Drift از کلاسهای گسترشدهنده Table یا حاشیهنویسی @DataClass استفاده میکند. در زیر — مثال کامل مدل Task با کوئری از طریق DSL، raw SQL و بهروزرسانی واکنشگرا. پس از اجرای build_runner، تمام کلاسهای تولید شده آماده هستند.
کلاس Tasks Table را گسترش میدهد و ستونها را تعریف میکند. هر ستون عبارتی از نوع Column<T> است. پارامترها: withDefault() مقدار پیشفرض را تعیین میکند، autoIncrement() افزایش خودکار را فراهم میکند. پایگاه داده کلاس انتزاعی است که $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 متدهای into(tasks).insert()، select(tasks)، update(tasks) و delete(tasks) را برای جداول تولید میکند. همه عملیات Future برمیگردانند — کار با SQLite ناهمزمان است. برای ردیابی تغییرات از .watch() به جای .get() استفاده کنید.
// نوشتن
await into(tasks).insert(TasksCompanion.insert(
title: Value('خرید محصولات'),
priority: Value(3),
));
// خواندن با فیلتر
final highPriority = await (select(tasks)
..where((t) => t.priority.greaterThan(2))
..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)]))
.get();
// مشاهده واکنشگرا
select(tasks).watch().listen((tasksList) {
// tasksList — List، با هر تغییر در جدول بهروزرسانی میشود
updateUi(tasksList);
});
برای کوئریهای پیچیده Drift اجازه نوشتن raw SQL را میدهد و همزمان تایپسازی را حفظ میکند. متد customSelect رشته کوئری را دریافت کرده و نتیجه تایپشده را از طریق مولد کد برمیگرداند. این رویکرد انعطافپذیری SQL را با امنیت نوع 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 هم از مهاجرتهای خودکار (برای تغییرات ساده) و هم دستی (برای تغییرات پیچیده) پشتیبانی میکند. نسخه پایگاه داده در سازنده AppDatabase تنظیم میشود. در صورت عدم تطابق نسخه، Drift تمام مهاجرتهای بسته نشده را به ترتیب اعمال میکند.
برای افزودن ستون با مقدار پیشفرض، Drift میتواند به طور خودکار از طریق MigrationStrategy مهاجرت ایجاد کند. اگر تغییر دادههای موجود را نقض نکند (افزودن فیلد nullable)، میتوان از beforeOpen با بررسی نسخه و اجرای ALTER TABLE استفاده کرد.
برای تغییرات پیچیده (تغییر نام جدول، ادغام دادهها، تغییر نوع ستون) Drift نیاز به مهاجرت دستی SQL دارد. مهاجرتها از طریق پارامتر migrations در کلاس پایگاه داده تعیین میشوند. هر مهاجرت شیئی با شمارههای from/to و کوئریهای SQL است.
Drift از اجرا در حالت آزمایشی از طریق NativeDatabase.memory() پشتیبانی میکند. پایگاه داده در حافظه قبل از هر تست از نو ایجاد شده و پس از آن نابود میشود. برای mock کردن از بسته mocktail با QueryExecutor تقلیدی استفاده کنید. Drift همچنین DatabaseTestHelper را برای تستهای یکپارچهسازی با بررسی مهاجرتها و کوئریها فراهم میکند.
Drift از DAO (Data Access Object) از طریق کلاسهای انتزاعی با حاشیهنویسی @DriftAccessor پشتیبانی میکند. DAO کوئریهای یک یا چند جدول را کپسوله میکند و میتواند جدا از پایگاه داده آزمایش شود. برخلاف کوئریهای مستقیم از طریق Database، DAO امکان استفاده مجدد از منطق کوئری بین بخشهای مختلف برنامه را فراهم کرده و تستهای ماژولار را ساده میکند.
@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);
}));
}
}
سؤالات متداول
Drift به جای رشتههای SQL از DSL مخصوص خود استفاده میکند که امنیت نوع کامل و تکمیل خودکار در IDE را فراهم میکند. Floor از رشتههای SQL در حاشیهنویسی @Query استفاده میکند. Drift همچنین از پلتفرمهای بیشتری (شامل وب) پشتیبانی کرده و واکنشگرایی داخلی از طریق Stream دارد، در حالی که در Floor Stream باید به صورت دستی اعلام شود.
بله، Drift از مهاجرت با حفظ دادهها پشتیبانی میکند. برای افزودن ستون از addColumn در Migration استفاده کنید. برای تغییرات پیچیده (تغییر نام، ادغام) raw SQL در داخل مهاجرت بنویسید. اگر مهاجرت مشخص نشده باشد، Drift در صورت عدم تطابق طرح، پایگاه داده را با از دست دادن داده بازسازی میکند.
Drift نیاز به تولید کد از طریق build_runner و drift_dev دارد. بدون تولید نمیتوان کوئریهای تایپشده ایجاد کرد. با این حال، برای پروژههای کوچک Drift از sqlparser پشتیبانی میکند — نوشتن دستی فایلهای SQL با تایپسازی خودکار، اما این هنوز نیاز به مرحله تولید دارد.
برای یکپارچهسازی با Riverpod از بسته drift_riverpod استفاده کنید. این بسته providerهایی برای Database، DAO و کوئریهای Stream فراهم میکند. مثال: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — UI به طور خودکار هنگام تغییر داده بازسازی میشود.
Drift رمزگذاری داخلی ندارد، اما از اتصال کتابخانههای سفارشی sqlite3 با SEE (SQLite Encryption Extension) پشتیبانی میکند. برای پلتفرمهای موبایل از sqflite_sqlcipher به عنوان QueryExecutor استفاده کنید — Drift با هر پیادهسازی SQLite از طریق QueryExecutor انتزاعی کار میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید