[Dịch] Chọn một CSDL phù hợp cho ứng dụng Flutter của bạn
Bài đăng này đã không được cập nhật trong 4 năm
Bài viết này được dịch từ bài Choosing the right database for your Flutter app bởi Lewis Cianci
Bất kể bạn là ai hoặc bạn muốn làm gì, dù sớm dù muộn bạn cũng sẽ cần lưu trữ dữ liệu trong app của bạn và đọc nó sau này. Vậy thì bạn nên dùng gì để làm việc đó? Lewis Cianci sẽ trả lời câu hỏi này cho bạn.
Hiện nay có rất nhiều sự lựa chọn khi nhắc đến việc thêm CSDL (database) vào ứng dụng của bạn. Thường thì sẽ có 3 loại:
-
Relational (CSDL quan hệ) - Đó là những CSDL cơ bản. Nó không chỉ lưu trữ dữ liệu mà còn lưu trữ cả quan hệ (relationship) giữa chúng. SQLite chính là một ví dụ điển hình của CSDL kiểu quan hệ.
-
NoSQL - CSDL kiểu này sẽ lưu trữ dữ liệu dưới dạng document. Nó sẽ không cứng nhắc theo một form giống như CSDL quan hệ phía trên mà thay vào đó rất linh động. Tốc độ của nó cũng rất nhanh và xử lí những dữ liệu có khối lượng lớn, không theo một cấu trúc nhất định, rất là tốt. MongoDB là một ví dụ của NoSQL.
-
Lưu trữ dữ liệu đặc thù - Thực chất lựa chọn này về mặt technical thì không được gọi là một CSDL, bạn không cần phải sử dụng các giải pháp bên trên. Thay vào đó, bạn chỉ cần lưu JSON vào file và xử lí việc encode - decode thủ công. Cách này sẽ rất nhanh nhưng sẽ có thể đem lại những rủi ro mà nếu bạn không phải là một dev thành thạo, bạn sẽ khó lòng mà xử lí được.
Khi đọc hết bài viết này, bạn sẽ có thể:
- Nắm được cơ bản cách mà các loại CSDL hoạt động
- Cách tích hợp chúng vào dự án của bạn
- Điểm mạnh của từng loại
CSDL quan hệ (Relational)
CSDL quan hệ đã ra đời từ rất lâu (kể từ 1970, theo như những kết quả tôi đã tìm kiếm nhanh từ Google). Hãy cùng xem những lựa chọn nào bạn có thể chọn với Flutter ở thời điểm hiện tại.
SQflite
SQflite là một trong những lib sử dụng SQLite trên Flutter. Nó cung cấp toàn quyền cho bạn để có thể hoàn toàn kiểm soát CSDL, các câu truy vấn (queries), các quan hệ (relationships), và mọi thứ mà bạn có thể đòi hỏi nó.
Tương tác với SQLite trên Flutter sẽ giống như này (trích từ docs)
// Get a location using getDatabasesPath
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'demo.db');
// Delete the database
await deleteDatabase(path);
// open the database
final database = await openDatabase(path, version: 1, onCreate: (Database db, int version) async {
// When creating the db, create the table
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
// Inserting data follows the same age-old SQLite tenants
// Insert some records in a transaction
await database.transaction((txn) async {
final id1 = await txn.rawInsert('INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
print('inserted1: $id1');
final id2 = await txn.rawInsert('INSERT INTO Test(name, value, num) VALUES(?, ?, ?)', ['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});
Và query sẽ dạng như thế này:
// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
Ưu điểm
- Kiểm soát hoàn toàn CSDL
- Là một cách áp dụng SQLite rất hiệu quả cho ứng dụng của bạn (cần phải nắm rõ kiến thức về SQL/SQLite)
- Dễ dàng đọc bởi các ứng dụng bên thứ ba mà có thể đọc được CSDL của SQLite (vì vậy nên bạn có thể mở CSDL trên máy của bạn và xem data đang như thế nào)
Nhược điểm
- Việc viết tất cả các câu truy vấn (queries) bằng tay có thể sẽ tốn rất nhiều thời gian
- Dữ liệu trả về không được chặt chẽ về kiểu nếu bạn không parse nó cẩn thận
- Rất khó để migrate giữa các database version khác nhau.
- Không support cho web
Nên dùng khi nào
SQFlite rất tốt trong trường hợp bạn cần lưu các dữ liệu có quan hệ và bạn có đủ kiến thức và khả năng để viết những câu truy vấn khó nhằn. Nếu bạn quen với việc tự viết các câu truy vấn và không ngại viết rất nhiều các câu truy vấn sau này cũng như viết code để convert nó sang dữ liệu để có thể sử dụng trong app (array, object, class) thì nó sẽ là một lựa chọn tốt cho bạn.
SQLite abstractions
Việc có thể tương tác trực tiếp với DB có thể sẽ rất hữu dụng nhưng rất khó để sử dụng. Vì vậy nên đã có rất nhiều giải pháp để trừu tượng hoá một vài tính năng từ SQLite để có thể dễ dàng sử dụng. Những đối tượng trừu tượng này có thể kiến SQLite dễ dàng sử dụng mà vấn giữ lại được rất nhiều các lợi ích mà SQLite mang lại. Floor và Moor là những ví dụ đã được rất nhiều người biết đến và sử dụng.
Moor
Để sử dụng Moor, chúng ta cần import moor package từ flutter pub, nhưng chúng ta cũng cần import thêm một thư viện nữa là moor_generator
, được dùng cùng với build_runner
để tự động generate code, giúp chúng ta tiết kiệm thời gian hơn rất nhiều.
Vì sao chúng ta lại dùng
build_runner
?build_runner
có tác dụng chính dùng để generate code cho các project Flutter. Trước khi tôi biết đến Flutter, tôi rất hiếm khi cần phải dùng đến các công cụ tự động gen code. Lí do chính để dùng nó là vì đa số các ngôn ngữ tôi từng dùng cho tới thời điểm này đều hỗ trợ reflectionReflection hiểu một cách đơn giản, nó cung cấp cho framework có khả năng truy cập vào và đọc một phần của code trong quá trình chạy. Nó khá hữu dụng và mạnh mẽ, nhưng thường thì sẽ hơi chậm. Nó cũng ảnh hưởng đến khả năng linking của ứng dụng hoàn thiện, bởi với reflection, về lí thuyết mà nói thì mọi phần của ứng dụng đều có thể truy cập hoặc sử dụng.
Với những package sử dụng các tiện ích của reflection trong Flutter, chúng thường dùng kèm với
build_runner
để gen những đoạn code cần thiết trước khi build chứ không phải trong lúc chạy (bạn có thể tìm hiểu về AOT và JIT để nắm rõ hơn). Bằng việc gen code AOT (Ahead of time), nó giúp cho quá trình sử dụng sản phẩm hoàn thiện trở nên nhanh hơn (vì code được gen trước khi build chứ không phải trong lúc app chạy), không phải lo lắng về những đoạn code dư thừa và giảm được code size khi deploy (vì bạn có thể ignore các file đã gen với gitignore và gen lại phía CI/CD).
Một ví dụ từ trang Getting started của Moor giúp chúng ta hiểu rõ hơn cách thức mà DB được tạo ra với codegen
import 'package:moor/moor.dart';
// giả sử như file hiện tại có tên là filename.dart. Dòng này sẽ báo lỗi lần đầu tiên và sẽ mất khi code đã được gen
// nhưng dòng này là điều cần thiết để Moor hiểu là cần gen ra file gì
part 'filename.g.dart';
// Class này sau khi chạy code gen, Moor sẽ tạo ra một table có tên là "todos" ở trong CSDL. Và type của các item mỗi cột
// sẽ có tên là "Todo".
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 6, max: 32)();
TextColumn get content => text().named('body')();
IntColumn get category => integer().nullable()();
}
// Annotation này sẽ báo Moor biết tên class mình muốn đặt là "Category" đại diện cho 1 row trong CSDL.
// Mặc định, "Categorie" sẽ được sử dụng bởi vì về cơ bản, code gen chỉ có thể tự động xoá chữ "s" ở cuối, nhưng trong trường hợp này tên class sẽ không đúng Tiếng Anh, nó phải là "Category" chứ không phải là "Categorie"
// in the table name.
@DataClassName("Category")
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get description => text()();
}
// Annotation này dùng để khai báo với Moor các table mà mình sẽ sử dụng, bạn có thể tìm hiểu kĩ hơn tại trang document của Moor
@UseMoor(tables: [Todos, Categories])
class MyDatabase {
}
Moor tự động tạo cấu trúc DB (schema) cho các table
của bạn tuỳ thuộc vào cách bạn quy định nó. Ở đoạn đầu tiên của ví dụ phía trên, chúng ta có thể thấy từ khoá part
. Khi chúng ta chạy build_runner
, Moor sẽ gen ra các schema theo những gì bạn đã quy định trong file này. Bạn có thể quay lại sử dụng câu lệnh SQL thông thường bất cứ lúc nào nếu như bạn cần một câu query phức tạp hoặc nếu bạn muốn kiểm soát chặt hơn.
Ưu điểm
- Type chặt chẽ
- Dựa trên SQLite
- Bạn không cần viết tất cả các query bằng tay
- Rất nhiều những đoạn code đáng ra phải viết bằng tay, giờ đã được gen bằng một câu lệnh
- Database của SQLite có thể mở bằng rất nhiều công cụ hiện nay để kiểm tra dữ liệu trong DB trong quá trình phát triển
Nhược điểm
- Nó sẽ có thể cồng kềnh khi bạn cần update lại schema của database cũ
- Có hỗ trợ web nhưng tính tới thời điểm bài viết này thì vẫn còn đang trong quá trình preview
- Source code của nó không hoàn toàn được viết bằng dart mà có một phần code được viết phía native platform
Nên dùng khi nào?
Giống như SQflite, khi bạn vẫn cần CSDL kiểu quan hệ nhưng lại lười viết query/muốn viết ít nhất có thể
NoSQL
Cũng sẽ có một vài sự lựa chọn khi bạn quyết định chọn NoSQL làm CSDL khi dùng với Flutter. Chúng ta đã có một cái tên rất nổi và đã ra đời từ rất lâu, đó là Firebase, cũng như một sự lựa chọn gần đây đó là Hive. Có rất nhiều sự khác biệt giữa Hive và Firebase, nhưng sự khác biệt lớn nhất đó là Firebase có thể sync online data lên CSDL của firebase, trong khi đó Hive thì phù hợp hơn với việc lưu trữ ở local.
Firebase - CSDL NoSQL online
Firebase là một phương pháp lưu trữ dữ liệu dưới dạng document truyền thống. Bạn lưu data dưới dạng collection, khá giống với table bên SQLite. Những collection này lưu các document. Document lưu trữ các data type, ví dụ như string
, int
,... Nó còn được dùng để lưu trữ link đến các document khác. Vì vậy mặc dù Firebase không quy định chặt chẽ về kiểu, nhưng bạn vẫn có thể tạo relationship giữa các data với nhau.
Cách setup Firebase cũng khá lằng nhằng và rắc rối khi so sánh với các CSDL local như Moor hay Hive, nhưng bù lại bạn sẽ có thể sync data của mình giữa client và server. Điều đó có nghĩa rằng nếu bạn có nhiều client cùng sử dụng một nguồn data, thì data đó sẽ được đồng bộ giữa các client với nhau. Hơn nữa, quá trình setup cũng được hướng dẫn khá tỉ mỉ tại Google Codelab. Điểm trừ duy nhất đó là dữ liệu ra của Firebase cũng không chặt chẽ về type như khi bạn dùng Moor hay Hive, mà bạn phải xử lí nó bằng tay.
Ưu điểm
- Đồng bộ dữ liệu giữa client và server gần như là real-time
- Có nhiều công cụ hỗ trợ
- Dễ dàng theo dõi hay inspect data qua Firebase Console
Nhược điểm
- Việc setup Firebase có thể sẽ rất phức tạp nếu như bạn chưa có kinh nghiệm
- Vì dữ liệu được sync giữa các client, bạn sẽ phải cẩn thận và tính toán nhiều hơn, set các rule và permission chặt chẽ hơn để tránh mất mát dữ liệu
- Firebase đến thời điểm bài viết vẫn chưa stable để có thể dùng trong production
Nên dùng khi nào?
Nếu dữ liệu của bạn cần đồng bộ giữa các client và không muốn mất quá nhiều thời gian cho việc đó
Hive - CSDL NoSQL offline
Hive là một lựa chọn CSDL sử dụng NoSQL với tốc độ cực kì nhanh. Điều đáng giá nhất của nó chính là nó được viết hoàn toàn bằng Dart. Nó có nghĩa rằng: bất kì thứ gì sử dụng dart thì đều có thể sử dụng Hive, và nó không phụ thuộc vào bất kì device nào cả.
Hive cho chúng ta lưu trữ dữ liệu dưới dạng một HiveObject
, điều đó có nghĩa rằng chúng ta có thể sử dụng relation giữa các object với nhau. Dữ liệu được lưu trong một box
, và bạn có thể viết một adapter để convert những data phức tạp như class, object để có thể lưu vào DB.
Tạo một box rất đơn giản:
var box = await Hive.openBox('testBox');
Đọc và ghi dữ liệu thì cũng chỉ đơn giản thế này thôi:
import 'package:hive/hive.dart';
void main() async {
var box = await Hive.openBox('testBox');
box.put('name', 'David');
print('Name: ${box.get('name')}');
}
Điều mà Hive trở nên nổi bật đó chính là việc tạo các adapter cũng có thể được thực hiện bằng code generation như đầu bài viết. Điều này sẽ giúp dữ liệu của bạn sẽ chặt chẽ hơn về Type, và giúp bạn lưu các loại data phức tạp.
Document của Hive cunngx hướng dẫn rõ cách tạo ra một box
theo class mà bạn tự định nghĩa, ví dụ như:
import 'package:hive/hive.dart';
part 'person.g.dart';
@HiveType()
class Person {
@HiveField(0) String name;
@HiveField(1) int age;
@HiveField(2) List<Person> friends;
}
Như các bạn có thể thấy, class này chứa một List các Person
, vì vậy Hive có thể tạo liên kết đến các object đó
Nên dùng khi nào?
Nếu bạn cần một CSDL cơ bản để lưu dữ liệu, và không cần đồng bộ dữ liệu online, thì Hive là sự lựa chọn rất tốt cho bạn. Tôi luôn dùng nó trong các ứng dụng của mình.
Tự tạo một DB riêng cho mình
Nếu bạn không hứng thú với các lựa chọn phía trên, bạn có thể tự viết riêng cho mình một CSDL phù hợp. Một trong những cách làm đó là sử dụng json_serializable
để parse và encode json .
Theo cá nhân tôi, nó khá là rủi ro và mất công khi viết một hệ thống từ đầu. Tất nhiên là bạn có thể làm điều đó, và nhiều người cũng đã làm điều đó, nhưng tôi nghĩ không có lí do gì phải làm vậy cả. Tạo ra một CSDL tức là tạo ra một thư viện mới với các bug tiềm tàng và rất nhiều rủi ro và vấn đề có thể đến. Nếu bạn chỉ đơn thuần muốn làm một app, thì liệu việc tạo mới một CSDL có cần thiết và nằm trong kế hoạch đó không?
Những giải pháp đã tồn tại từ trước đã được sử dụng bởi rất nhiều người, và mọi người đều tìm bug và raise nó lên Github. Đó mới chính là thứ đánh giá chất lượng của một library. Liệu sẽ có ai tìm bug cho một cái lib mới toanh và không có khả năng cạnh tranh không nhỉ? Bởi vì chỉ có bạn sử dụng nó, nên chỉ có mình bạn tìm mà thôi.
Nên dùng khi nào?
Nếu bạn không tin tưởng bất kì code của ai khác ngoài bạn, hoặc bạn có một trường hợp hi hữu hay rất đặc biệt, bạn có thể xem xét tạo ra nó. Với tôi thì tôi sẽ không làm với tất cả các app mà tôi sẽ làm rồi...
Có quá nhiều lựa chọn. Tôi nên chọn gì đây?
Thật không hề dễ để trả lời câu hỏi DB nào là tốt nhất. Nó phụ thuộc vào trường hợp của app bạn. Nhưng hãy để tôi tóm tắt lại nhé.
- Nếu dữ liệu của bạn là kiểu quan hệ và bạn muốn xem DB dễ dàng trên máy tính trong quá trình phát triển, và bạn không có ý định support cho web, bạn nên dùng Moor
- Nếu bạn cần dữ liệu phải được sync giữa các device và bạn không ngại việc setup, hãy dùng Firebase
- Nếu bạn muốn quá trình dev cũng như sử dụng với tốc độ nhanh, và muốn hỗ trợ web cũng như mọi thứ chạy bằng dart, hãy dùng Hive
- Nếu bạn quan ngại đến bảo mật và không ai có thể thuyết phục được bạn, và bạn có rất nhiều thời gian cho nó, bạn có thể xem xét viết một CSDL riêng cho mình dưới dạng JSON, nhưng tôi thì không
Nếu cho tôi phải lựa chọn, tôi sẽ chọn Hive (Trans: Cá nhân mình đã dùng thử và cũng lựa chọn vậy ^^)
Cám ơn bạn đã đọc bài viết này. Hi vọng bạn sẽ chọn được một CSDL phù hợp với mình.
All rights reserved