Drift (เดิมเป็น Moor) เป็น ORM แบบใช้งานได้แบบเร็วสำหรับ Flutter และ Dart ที่สร้างบน SQLite โดยมี DSL ของตัวเองสำหรับคำเรียกข้อมูล ต่างจาก ORM แบบดั้งเดิม Drift จะคอมไพล์คำเรียกข้อมูล Dart เป็น SQL ในข้นตอนสร้าง ทำให้ไม่มีข้อผิดพลาดขณะรันไทม์ ตามแหล่งข้อมูล Drift Docs, 2024, Drift สร้างโค้ดมากกว่าคำเรียกข้อมูล SQL ที่ทำด้วยมือถึง 40% แต่ก็ทำให้ไม่ต้องเขียน SQL ด้วยมือโดยสิ้นเชิง แทนที่ด้วยไวยากรณ์ Dart ที่ปลอดภัยจากประเภท
ประเด็นสำคัญ
Drift เป็น ORM สำหรับ Dart และ Flutter ที่เดิมเป็นที่รู้จักในชื่อ Moor พัฒนาโดย Simon Binder ในปี 2019 และตั้งแต่นั้นมาก็ผ่านหลายรุ่นหลัก Drift คอมไพล์คำเรียกข้อมูล Dart เป็น SQL ในข้นตอนสร้างโดยใช้ drift_dev และ build_runner ซึ่งให้ความปลอดภัยของประเภทอย่างสมบูรณ์และกำจัดข้อผิดพลาดไวยากรณ์ SQL ในขณะรันไทม์
ต่างจาก Floor Drift ใช้ DSL (ภาษาเฉพาะเป้าหมาย) ของตัวเองในการสร้างคำเรียกข้อมูล — นักพัฒนาเขียนใน Dart และตัวสร้างจะแปลงมันเป็น SQL ซึ่งทำให้ IDE สามารถตรวจสอบไวยากรณ์ เติมข้อมูลฟิลด์ของตารางแบบอัตโนมัติ และปรับโครงสร้างโมเดลข้อมูลโดยไม่ต้องกลัวว่าจะทำลายคำเรียกข้อมูล
ตามข้อมูลจาก Drift (2024) ไลบรารีนี้ถูกใช้ในโครงการ Flutter มากกว่า 8000 โครงการ รองรับทุกแพลตฟอร์มยอดนิยม: Android ผ่าน sqflite, iOS ผ่าน sqflite, web ผ่าน sqlite3 WASM, เดสก์ทอป ผ่านไดรเวอร์เดิม sqlite3
Moor ถูกเปลี่ยนชื่อเป็น Drift ในรุ่น 2.0 (2022) สาเหตุคือการซ้ำชื่อกับโครงการอื่นและความต้องการที่จะห่างไกลจากโค้ดเก่า API ยังคงเข้ากันได้: ในการย้ายข้อมูล เพียงแทนที่ import จาก moor เป็น drift และอัพเดตไลบรารีที่ขึ้นอยู่
Drift ให้: คำเรียกข้อมูล Stream ในตัวพร้อมการอัพเดตอัตโนมัติเมื่อข้อมูลเปลี่ยนแปลง, รองรับธุรกรรมแบบย้อนกลับได้, คำเรียกข้อมูล SQL ที่กำหนดเองผ่าน rawQuery, รูปแบบ DAO สำหรับการหุ่มหุ้อโลจิก, การย้ายข้อมูลข้ามแพลตฟอร์ม, และการผนวกานกับ Riverpod และ BLoC ผ่านแพ็คเกจ drift_riverpod และ drift_bloc
Drift ใช้การสร้างโค้ดในขณะคอมไพล์ นักพัฒนาอธิบายตารางผ่านคำอธิบาย @DataClass หรือคลาส Dart ที่เพิ่มจาก Table ตัวสร้างจะสร้างคลาสช่วย: Companion (สำหรับฟิลด์ที่สามารถเป็น null ได้ในระหว่างการแทรก/อัพเดต), DriftDatabase (จุดเข้า) และการนำไปใช้ DAO
Drift ไม่ดำเนินการคำเรียกข้อมูล SQLite โดยตรง แทนที่ นักพัฒนาเขียนใน Dart: select(tasks).where(tasks.priority.greaterThan(3)).build() ตัวสร้างจะแปลงสิ่งนี้เป็น SQL และในขณะรันไทม์ Drift จะส่งคำเรียกข้อมูล SQL ที่พร้อมแล้วไปยัง SQLite ซึ่งผสมความสะดวกของไวยากรณ์ Dart กับประสิทธิภาพ SQL เดิม
Drift รองรับสองโหมดคำเรียกข้อมูล: DSL (แนะนำ) และ SQL ดิบ คำเรียกข้อมูล DSL ปลอดภัยกว่าในการเขียน — คอมไพล์ร์จะตรวจสอบชื่อฟิลด์ ประเภท และความเข้ากันได้ SQL ดิบจำเป็นสำหรับคำเรียกข้อมูลที่ซับซ้อนไม่ได้รับการคุ้มครองโดย DSL: ฟังก์ชันหน้าต่าง, CTE แบบเรียกซ้ำ, ส่วนขยาย SQLite เฉพาะเจาะจง
Drift มีสองวิธีในการเขียนคำเรียกข้อมูล: Dart DSL (เดิม) และ SQL ดิบ (สำหรับกรณีซับซ้อน) DSL เหมาะสมกว่าสำหรับ 90% ของสถานการณ์: ปลอดภัยกว่า อ่านง่ายกว่า และรองรับการปรับโครงสร้าง SQL ดิบจะใช้เมื่อ DSL ไม่คุมครองโครงสร้างที่จำเป็น
| แง่มมุม | Drift DSL | SQL ดิบใน Drift |
|---|---|---|
| ความปลอดภัยของประเภท | สมบูรณ์ (เวลาคอมไพล์) | ไม่มี (รันไทม์) |
| การเติมอัตโนมัติ | มี (IDE) | เฉพาะในไฟล์ sql |
| การปรับโครงสร้าง | อัตโนมัติ | ค้นหาสตริงด้วยมือ |
| JOIN ที่ซับซ้อน | รองรับ | อิสระโอะที่เต็มที่ |
| ฟังก์ชันหน้าต่าง | จำกัด | รองรับเต็มที่ |
| ความสามารถในการตอบสนอง | ในตัว (Stream) | ผ่าน .watch() |
Drift DSL เป็นวิธีการทำงานหลัก ครอบคลุม SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN และการจับกลุ่ม สำหรับคำเรียกข้อมูล CRUD ทั่วไป ใช้ DSL: สั้นกว่า ปลอดภัยกว่า และอัพเดต Stream โดยอัตโนมัติเมื่อมีการเปลี่ยนแปลง
SQL ดิบ ใน Drift จำเป็นสำหรับ: ฟังก์ชัน SQLite ที่กำหนดเอง (FTS5, JSON1), คำเรียกข้อมูลย่อยที่ซับซ้อนด้วย EXISTS, INSERT OR REPLACE, UPDATE จำนวนมากด้วย CASE รวมถึงคำเรียกข้อมูลที่ประสิทธิภาพสำคัญและ DSL ไม่สร้างแผนการดำเนินงานที่เหมาะสม SQL ดิบสามารถเขียนในไฟล์ .sql โดยมีการรองรับการกำหนดประเภทผ่าน drift_dev
Drift ใช้คลาสที่เพิ่มจาก Table หรือคำอธิบาย @DataClass ด้านล่างเป็นตัวอย่างที่สมบูรณ์ของโมเดล Task พร้อมคำเรียกข้อมูลผ่าน DSL, 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 อนุญาตให้เขียน 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 หากการเปลี่ยนแปลงไม่ทำลายข้อมูลที่มีอยู่ (การเพิ่มฟิลด์ที่สามารถเป็น null ได้) คุณสามารถใช้ beforeOpen กับการตรวจสอบเวอร์ชันและดำเนินการ ALTER TABLE
สำหรับการเปลี่ยนแปลงที่ซับซ้อน (การเปลี่ยนชื่อตาราง, การรวมข้อมูล, การเปลี่ยนประเภทของคอลัมน์) Drift ต้องการการย้ายข้อมูล SQL ด้วยมือ การย้ายข้อมูลจะถูกระบุผ่านพารามิเตอร์ migrations ในคลาสฐานข้อมูล แต่ละการย้ายข้อมูลเป็นอ็อบเจกต์ที่มีหมายเลข from/to และคำเรียกข้อมูล SQL
Drift รองรับการทำงานในโหมดทดสอผ่าน NativeDatabase.memory() ฐานข้อมูลในหน่วยความจำจะถูกสร้างใหม่ก่อนแต่ละการทดสอบและถูกทำลาะลังจาก สำหรับการจำลองใช้แพ็คเกจ mocktail กับ QueryExecutor ที่ถูกจำลอง Drift ยังให้ DatabaseTestHelper สำหรับการทดสอบแบบผนวกานโดยมีการตรวจสอบการย้ายข้อมูลและคำเรียกข้อมูล
Drift รองรับ DAO (วัตถุการเข้าถึงข้อมูล) ผ่านคลาสนามธรรมที่มีคำอธิบาย @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 ใช้ DSL ของตัวเองแทนสตริง SQL ซึ่งให้ความปลอดภัยของประเภทอย่างสมบูรณ์และการเติมอัตโนมัติใน IDE Floor ใช้สตริง SQL ในคำอธิบาย @Query Drift ยังรองรับแพลตฟอร์มมากกว่า (รวมถึง web) และมีความสามารถในการตอบสนองในตัวผ่าน Stream ในขณะที่ Floor ต้องประกาศ Stream ด้วยตนเอง
ใช่ Drift รองรับการย้ายข้อมูลโดยการคงรักษาข้อมูล ในการเพิ่มคอลัมน์ ใช้ addColumn ใน Migration สำหรับการแปลงรูปแบบที่ซับซ้อน (การเปลี่ยนชื่อ, การรวม) เขียน SQL ดิบภายในการย้ายข้อมูล หากไม่มีการย้ายข้อมูลใดๆ Drift จะสร้างฐานข้อมูลใหม่โดยสูญเสียข้อมูลเมื่อโครงสร้างไม่ตรงกัน
Drift ต้องการการสร้างโค้ดผ่าน build_runner และ drift_dev หากไม่มีการสร้างโค้ด จะไม่สามารถสร้างคำเรียกข้อมูลที่กำหนดประเภทได้ อย่างไรก็ตาม สำหรับโครงการขนาดเล็ก Drift รองรับ sqlparser — การเขียนไฟล์ SQL ด้วยมือโดยมีการกำหนดประเภทโดยอัตโนมัติ แต่ยังต้องมีขั้นตอนการสร้างโค้ด
สำหรับการผนวกานกับ Riverpod ใช้แพ็คเกจ drift_riverpod ซึ่งให้ประเภทสำหรับ Database, DAO และคำเรียกข้อมูล Stream ตัวอย่าง: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — UI จะสร้างใหม่โดยอัตโนมัติเมื่อข้อมูลเปลี่ยนแปลง
Drift ไม่มีการเข้ารหัสในตัว แต่รองรับการเชื่อมต่อไลบรารี sqlite3 ที่กำหนดเองด้วย SEE (ส่วนขยายการเข้ารหัส SQLite) สำหรับแพลตฟอร์มมือถือ ใช้ sqflite_sqlcipher เป็น QueryExecutor — Drift ทำงานกับการนำไปใช้ SQLite ใดๆ ผ่าน QueryExecutor แบบนามธรรม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม