Database Migrations Trong Flutter Với Sqflite

5 min read

Trong quá trình phát triển ứng dụng Flutter, bạn có thể cần cập nhật cấu trúc cơ sở dữ liệu của mình theo thời gian, chẳng hạn như thêm bảng mới, thay đổi bảng hiện có hoặc loại bỏ các bảng không cần thiết. Điều này đòi hỏi phải sử dụng một hệ thống database migrations để thực hiện các thay đổi một cách an toàn và có kiểm soát.

Trong bài viết này tôi sẽ sử dụng thư viện Sqflite trong flutter:

1. Tại Sao Cần Sử Dụng Database Migrations?

Khi ứng dụng của bạn phát triển, nhu cầu thay đổi cấu trúc cơ sở dữ liệu là điều tất yếu. Ví dụ:

  • Thêm bảng mới để lưu trữ dữ liệu mới.
  • Thay đổi bảng hiện có để thêm cột mới hoặc thay đổi kiểu dữ liệu.
  • Loại bỏ hoặc hợp nhất các bảng không còn cần thiết.

Thực hiện những thay đổi này trực tiếp trên cơ sở dữ liệu đang hoạt động có thể gây rủi ro, chẳng hạn như làm hỏng dữ liệu hoặc gây lỗi cho các chức năng hiện tại. Database migrations cho phép bạn thực hiện những thay đổi này một cách tuần tự và an toàn.

2. MigrationBase:

abstract class MigrationBase {
  Future up(Database db);
  Future down(Database db);
}

Lớp trừu tượng này định nghĩa cho tất cả các migrations:

  • up: Phương thức này chịu trách nhiệm áp dụng các thay đổi cho cơ sở dữ liệu, chẳng hạn như thêm bảng mới, cột mới hoặc chỉ mục.
  • down: Phương thức này đảo ngược các thay đổi được thực hiện bởi phương thức up. Nó hữu ích khi bạn cần khôi phục cơ sở dữ liệu về phiên bản trước.

Mỗi lớp migration mà bạn tạo sẽ kế thừa từ lớp cơ bản này và triển khai hai phương thức trên.

3. Tạo migration để khởi tạo Database:

Hãy cùng xem xét kỹ hơn lớp InitDatabase:

class InitDatabase implements MigrationBase {
  List<String> scripts = [
    """CREATE TABLE IF NOT EXISTS Student (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        age INTEGER,
        name TEXT,
        address TEXT,
      )
    """,
    // Các script tạo bảng khác...
  ];

  @override
  Future up(Database db) async {
    await Future.forEach(scripts, (sql) async {
      await db.execute(sql);
    });
  }

  @override
  Future down(Database db) async {
    // Triển khai logic để xóa bảng hoặc hoàn tác các thay đổi nếu cần
  }
}

scripts: Danh sách này chứa các lệnh SQL cần thiết để tạo bảng ban đầu. Các lệnh này đảm bảo rằng nếu bảng chưa tồn tại, chúng sẽ được tạo ra.

Phương thức up: Lặp qua danh sách scripts và thực thi từng lệnh SQL, áp dụng các thay đổi cho cơ sở dữ liệu.

Phương thức down: Mặc dù chưa được triển khai trong ví dụ này, nó sẽ bao gồm các lệnh để xóa bảng hoặc hoàn tác các thay đổi khác, điều này rất quan trọng trong quá trình phát triển hoặc kiểm thử.

4. Quản Lý Nhiều Migrations

Khi ứng dụng của bạn phát triển, bạn sẽ tạo thêm các lớp migration mới. Để quản lý những thay đổi này, bạn tổ chức chúng trong một map:

Map<int, List<MigrationBase>> migrationScripts = {
  1: [InitDatabase()], // Migration được tạo ở trên
  2: [AlterStudentTableMigration()],
  3: [AddClassroomTableMigration()],
  // Thêm các migrations khác...
};
  • Khóa (Keys): Đại diện cho số phiên bản của migration. Ví dụ, 1 tương ứng với việc khởi tạo database, 2 là cho các thay đổi tiếp theo, và cứ thế.
  • Giá trị (Values): Danh sách các lớp migration cần được áp dụng tại phiên bản đó.

Bằng cách tăng số phiên bản, bạn đảm bảo rằng chỉ những migrations cần thiết mới được áp dụng khi databases phát triển.

5. Áp Dụng Migrations Trong DatabaseHelper

Lớp DatabaseHelper chịu trách nhiệm mở cơ sở dữ liệu, áp dụng các migrations, và quản lý phiên bản cơ sở dữ liệu:

Future<Database> _openDb() async {
  final dbPath = await getDatabasesPath();
  _db = await openDatabase(
    dbPath + '/' + dbName,
    version: dbVersion,
    onCreate: (Database db, int version) async {
      for (int i = 1; i <= dbVersion; i++) {
        if (migrationScripts[i] != null) {
          await Future.forEach(migrationScripts[i]!, (migrationScript) async {
            await migrationScript.up(db);
          });
        }
      }
    },
    onUpgrade: (db, oldVersion, newVersion) async {
      for (int i = oldVersion + 1; i <= newVersion; i++) {
        if (migrationScripts[i] != null) {
          await Future.forEach(migrationScripts[i]!, (migrationScript) async {
            await migrationScript.up(db);
          });
        }
      }
    },
    onDowngrade: (db, oldVersion, newVersion) async {
      for (int i = oldVersion; i > newVersion; i--) {
        if (migrationScripts[i] != null) {
          await Future.forEach(migrationScripts[i]!, (migrationScript) async {
            await migrationScript.down(db);
          });
        }
      }
    },
  );
  return _db!;
}

onCreate: Khi cơ sở dữ liệu được tạo lần đầu, phương thức này sẽ áp dụng tất cả các migrations cho đến phiên bản hiện tại.

onUpgrade: Khi nâng cấp từ một phiên bản cũ hơn, phương thức này đảm bảo rằng chỉ những migrations cần thiết giữa phiên bản cũ và mới được áp dụng. Điều này ngăn ngừa việc áp dụng migration lặp lại.

onDowngrade: Phương thức này rất quan trọng nếu bạn cần khôi phục về một phiên bản cũ của cơ sở dữ liệu. Nó đảm bảo rằng các migrations thích hợp sẽ được hoàn tác, lùi lại các thay đổi theo thứ tự ngược lại.

6. Phiên Bản Cơ Sở Dữ Liệu

Mỗi khi bạn chỉnh sửa map migrationScripts bằng cách thêm các migrations mới, hằng số dbVersion cần được cập nhật để phản ánh phiên bản mới nhất:

static final int dbVersion = migrationScripts.keys.length;

Điều này đảm bảo rằng phương thức onUpgrade trong DatabaseHelper sẽ kích hoạt các migrations thích hợp cho những người dùng đã có các phiên bản cũ hơn của cơ sở dữ liệu.

7. Sử dụng vào trong project:

Chỉ cần openDb để thực hiện các thao tác với dữ liệu thì migration sẽ được áp dụng:

Future<void> _loadStudents() async {
    final db = await DatabaseHelper().openDb();
    List<Map<String, dynamic>> studentMaps = await db.query('students');
    
    setState(() {
      _students = studentMaps.map((studentMap) => Student.fromMap(studentMap)).toList();
    });
  }

  Future<void> _addStudent(String name, int age) async {
    Student newStudent = Student(name: name, age: age);
    await DatabaseHelper().openDb().addStudent(newStudent);
    _loadStudents();
  }

8. Đóng Kết Nối Cơ Sở Dữ Liệu

Để quản lý tài nguyên tốt, bạn nên đóng kết nối cơ sở dữ liệu khi không còn cần thiết:

Future closeDb() async {
  final db = await this.db;
  db.close();
}

Phương thức này đảm bảo rằng cơ sở dữ liệu được đóng đúng cách, giải phóng tài nguyên hệ thống.

Kết luận

Bằng cách sử dụng database migrations, bạn có thể tự tin phát triển cấu trúc cơ sở dữ liệu của mình mà không gặp rủi ro làm hỏng dữ liệu hoặc yêu cầu người dùng cài đặt lại ứng dụng.

Hệ thống migrations này không chỉ làm cho ứng dụng của bạn dễ bảo trì hơn mà còn đảm bảo rằng dữ liệu của người dùng vẫn an toàn và nhất quán qua các bản cập nhật.

Nguồn:

https://medium.com/@buddi/haddle-flutter-sqlite-database-migration-without-losing-data-087f91902af2

https://stackoverflow.com/questions/61749660/sqlite-migration-in-flutter

https://docs.flutter.dev/cookbook/persistence/sqlite

Avatar photo

4 Replies to “Database Migrations Trong Flutter Với Sqflite”

Leave a Reply

Your email address will not be published. Required fields are marked *