用 SQLite 做資料持久化

如果你正在編寫一個需要持久化且查詢大量本地裝置資料的 app,可考慮採用資料庫,而不是本地資料夾或關鍵值函式庫。總的來說,相比於其他本地持久化方案來說,資料庫能夠提供更為迅速的插入、更新、查詢功能。

Flutter應用程式中可以透過 sqflite package 來使用 SQLite 資料庫。本文將透過使用 sqflite 來示範插入,讀取,更新,刪除各種狗狗的資料。

如果你對於 SQLite 和 SQL 的各種語句還不熟悉,請檢視 SQLite 官方的教程 SQLite 教程,在檢視本文之前需要掌握基本的SQL語句。

總共有以下的步驟:

  1. 新增依賴;

  2. 定義 Dog (狗) 資料模型;

  3. 開啟資料庫;

  4. 建立 dogs 資料表;

  5. 將一條 Dog 資料插入資料庫;

  6. 查詢所有狗狗的資料;

  7. 更新(修改)一條 Dog 的資料;

  8. 刪除一條 Dog 的資料。

1. 新增依賴

為了使用 SQLite 資料庫,首先需要匯入 sqflitepath package。

  • sqflite 提供了豐富的類別和方法,以便你能便捷實用 SQLite 資料庫。

  • path 提供了大量方法,以便你能正確的定義資料庫在磁碟上的儲存位置。

執行 flutter pub add 將其新增為依賴:

$ flutter pub add sqflite path

確保你已將 packages 匯入要使用的檔案中。

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

2. 定義狗狗的資料模型

在你準備在新建的表裡儲存狗狗們的訊息的的時候,你需要先定義這些資料。例如,定義一個狗類時,每一條狗狗的資料將包含三個欄位:一個唯一的 id ;名字 name ;年齡 age

class Dog {
  final int id;
  final String name;
  final int age;

  const Dog({
    required this.id,
    required this.name,
    required this.age,
  });
}

3. 開啟資料庫

在你準備讀寫資料庫的資料之前,你要先開啟這個資料庫。開啟一個資料庫有以下兩個步驟:

  1. 使用 sqflite package 裡的 getDatabasesPath 方法並配合 path package裡的 join 方法定義資料庫的路徑。

  2. Open the database with the openDatabase() function from sqflite.

使用 sqflite package 裡的 openDatabase 方法開啟資料庫。
// Avoid errors caused by flutter upgrade.
// Importing 'package:flutter/widgets.dart' is required.
WidgetsFlutterBinding.ensureInitialized();
// Open the database and store the reference.
final database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
);

4. 建立 dogs

接下來,你需要建立一個表用以儲存各種狗狗的訊息。在這個範例中,建立一個名為 dogs 資料庫表,它定義了可以被儲存的資料。這樣,每條 Dog 資料就包含了一個 idnameage。因此,在 dogs 資料庫表中將有三列,分別是 idnameage

  1. id 是 Dart 的 int 型別,在資料表中是 SQLite 的 INTEGER 資料型別。最佳實踐是將 id 作為資料庫表的主鍵,用以改善查詢和修改的時間。

  2. name 是Dart的 String型別,在資料表中是SQLite的 TEXT 資料型別。

  3. age 也是Dart的 int 型別,在資料表中是SQLite的 INTEGER 資料型別。

關於 SQLite 資料庫能夠儲存的更多的資料型別訊息請查閱官方的 SQLite Datatypes 文件

final database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
  // When the database is first created, create a table to store dogs.
  onCreate: (db, version) {
    // Run the CREATE TABLE statement on the database.
    return db.execute(
      'CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
    );
  },
  // Set the version. This executes the onCreate function and provides a
  // path to perform database upgrades and downgrades.
  version: 1,
);

5. 插入一條狗狗的資料

現在你已經準備好了一個資料庫用於儲存各種狗狗的訊息資料,現在開始讀寫資料咯。

首先,在 dogs 資料表中插入一條 Dog 資料。分以下兩步:

  1. Dog 轉換成一個 Map 資料型別;

  2. 使用 insert() 方法把 Map 儲存到 dogs 資料表中。

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({
    required this.id,
    required this.name,
    required this.age,
  });

  // Convert a Dog into a Map. The keys must correspond to the names of the
  // columns in the database.
  Map<String, Object?> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // Implement toString to make it easier to see information about
  // each dog when using the print statement.
  @override
  String toString() {
    return 'Dog{id: $id, name: $name, age: $age}';
  }
}
// Define a function that inserts dogs into the database
Future<void> insertDog(Dog dog) async {
  // Get a reference to the database.
  final db = await database;

  // Insert the Dog into the correct table. You might also specify the
  // `conflictAlgorithm` to use in case the same dog is inserted twice.
  //
  // In this case, replace any previous data.
  await db.insert(
    'dogs',
    dog.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}
// Create a Dog and add it to the dogs table
var fido = Dog(
  id: 0,
  name: 'Fido',
  age: 35,
);

await insertDog(fido);

6. 查詢狗狗清單

現在已經有了一條 Dog 儲存在資料庫裡。你可以透過查詢資料庫,檢索到一隻狗狗的資料或者所有狗狗的資料。分為以下兩步:

  1. 呼叫 dogs 表物件的 query 方法。這將回傳一個List <Map>

  2. List<Map> 轉換成 List<Dog> 資料型別。

// A method that retrieves all the dogs from the dogs table.
Future<List<Dog>> dogs() async {
  // Get a reference to the database.
  final db = await database;

  // Query the table for all the dogs.
  final List<Map<String, Object?>> dogMaps = await db.query('dogs');

  // Convert the list of each dog's fields into a list of `Dog` objects.
  return [
    for (final {
          'id': id as int,
          'name': name as String,
          'age': age as int,
        } in dogMaps)
      Dog(id: id, name: name, age: age),
  ];
}
// Now, use the method above to retrieve all the dogs.
print(await dogs()); // Prints a list that include Fido.

7. 修改一條 Dog 資料

使用 sqflite package 中的 update()方法,可以對已經插入到資料庫中的資料進行修改(更新)。

修改資料操作包含以下兩步:

  1. 將一條狗狗的資料轉換成 Map 資料型別;

  2. 使用 where 語句定位到具體將要被修改的資料。

Future<void> updateDog(Dog dog) async {
  // Get a reference to the database.
  final db = await database;

  // Update the given Dog.
  await db.update(
    'dogs',
    dog.toMap(),
    // Ensure that the Dog has a matching id.
    where: 'id = ?',
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [dog.id],
  );
}
// Update Fido's age and save it to the database.
fido = Dog(
  id: fido.id,
  name: fido.name,
  age: fido.age + 7,
);
await updateDog(fido);

// Print the updated results.
print(await dogs()); // Prints Fido with age 42.

8. 刪除一條 Dog 的資料

除了插入和修改狗狗們的資料,你還可以從資料庫中刪除狗狗的資料。刪除資料用到了 sqflite package 中的 delete() 方法。

在這一小節,新建一個方法用來接收一個 id 並且刪除資料庫中與這個 id 對應的那一條資料。為了達到這個目的,你必須使用 where 語句限定哪一條才是被刪除的資料。

Future<void> deleteDog(int id) async {
  // Get a reference to the database.
  final db = await database;

  // Remove the Dog from the database.
  await db.delete(
    'dogs',
    // Use a `where` clause to delete a specific dog.
    where: 'id = ?',
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [id],
  );
}

範例

執行範例需要以下幾步:

  1. 建立一個新的 Flutter 工程;

  2. sqflitepath 套件新增到 pubspec.yaml 檔案裡;

  3. 將以下程式碼貼上在 lib/db_test.dart 檔案裡(若無則新建,若有則覆蓋);

  4. 執行 flutter run lib/db_test.dart

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

void main() async {
  // Avoid errors caused by flutter upgrade.
  // Importing 'package:flutter/widgets.dart' is required.
  WidgetsFlutterBinding.ensureInitialized();
  // Open the database and store the reference.
  final database = openDatabase(
    // Set the path to the database. Note: Using the `join` function from the
    // `path` package is best practice to ensure the path is correctly
    // constructed for each platform.
    join(await getDatabasesPath(), 'doggie_database.db'),
    // When the database is first created, create a table to store dogs.
    onCreate: (db, version) {
      // Run the CREATE TABLE statement on the database.
      return db.execute(
        'CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
      );
    },
    // Set the version. This executes the onCreate function and provides a
    // path to perform database upgrades and downgrades.
    version: 1,
  );

  // Define a function that inserts dogs into the database
  Future<void> insertDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Insert the Dog into the correct table. You might also specify the
    // `conflictAlgorithm` to use in case the same dog is inserted twice.
    //
    // In this case, replace any previous data.
    await db.insert(
      'dogs',
      dog.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  // A method that retrieves all the dogs from the dogs table.
  Future<List<Dog>> dogs() async {
    // Get a reference to the database.
    final db = await database;

    // Query the table for all the dogs.
    final List<Map<String, Object?>> dogMaps = await db.query('dogs');

    // Convert the list of each dog's fields into a list of `Dog` objects.
    return [
      for (final {
            'id': id as int,
            'name': name as String,
            'age': age as int,
          } in dogMaps)
        Dog(id: id, name: name, age: age),
    ];
  }

  Future<void> updateDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Update the given Dog.
    await db.update(
      'dogs',
      dog.toMap(),
      // Ensure that the Dog has a matching id.
      where: 'id = ?',
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [dog.id],
    );
  }

  Future<void> deleteDog(int id) async {
    // Get a reference to the database.
    final db = await database;

    // Remove the Dog from the database.
    await db.delete(
      'dogs',
      // Use a `where` clause to delete a specific dog.
      where: 'id = ?',
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [id],
    );
  }

  // Create a Dog and add it to the dogs table
  var fido = Dog(
    id: 0,
    name: 'Fido',
    age: 35,
  );

  await insertDog(fido);

  // Now, use the method above to retrieve all the dogs.
  print(await dogs()); // Prints a list that include Fido.

  // Update Fido's age and save it to the database.
  fido = Dog(
    id: fido.id,
    name: fido.name,
    age: fido.age + 7,
  );
  await updateDog(fido);

  // Print the updated results.
  print(await dogs()); // Prints Fido with age 42.

  // Delete Fido from the database.
  await deleteDog(fido.id);

  // Print the list of dogs (empty).
  print(await dogs());
}

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({
    required this.id,
    required this.name,
    required this.age,
  });

  // Convert a Dog into a Map. The keys must correspond to the names of the
  // columns in the database.
  Map<String, Object?> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // Implement toString to make it easier to see information about
  // each dog when using the print statement.
  @override
  String toString() {
    return 'Dog{id: $id, name: $name, age: $age}';
  }
}