Flutter 資料庫sqflite使用知識點

藍色微笑ing發表於2019-12-21

前言

sqflite是Flutter的SQLite外掛,在App端能夠高效的儲存和處理資料庫資料,官方地址:pub.flutter-io.cn/packages/sq…

SQLite

關於SQLite的學習,推薦 菜 鳥 教 程 的 SQLite 教 程

SQLite的特點:

  • 不需要單獨的伺服器或作業系統
  • SQLite不需要配置,即不需要手動安裝和管理
  • 儲存在一個單一的跨平臺的磁碟檔案
  • 不需要外部依賴,完全自給自足
  • 輕量級

sqflite

sqflite全稱,我的理解是Structured Query Flutter Language Lite,即用於Flutter的輕量級結構化查詢語言。如果理解有誤,請及時更正,萬分謝謝!

一起看一下sqflite官方介紹:

用於Flutter的SQLite外掛,支援iOS、Android、MacOS.

  • 支援事務和批量操作
  • 程式開啟期間,自動化版本管理
  • 增刪改查的幫助程式
  • 在iOS和Android後臺執行緒中執行DB操作

當前sqflite版本:1.2.0

關於持久化儲存

關於App端的持久化儲存,用的比較多有shared_preferences、資料庫儲存、檔案儲存。

  • shared_preferences以key-value的方式儲存資料,是一種輕量級的資料持久化儲存方案。
  • 資料庫儲存資料量較大的場合,能夠高效的儲存、組織和處理資料。
  • 像長篇文章、圖片、視訊等Size比較大的,利用File儲存。

sqflite支援的資料型別

如果你對英文比較自信,可以嘗試讀這篇文章 Datatypes In SQLite Version 3.

SQLite並沒有對值進行型別檢查,即儲存INTEGER型別的列是可以儲存TEXT型別的,但是當我們解析查詢的結果進行對映時,會報型別異常的。所以還是要避免儲存型別不一致的資料。

sqflite支援5種資料型別:NULL, INTEGER, REAL, TEXT, BLOB.

  • NULL

    某一列不儲存資料的時候,預設值是NULL.

  • INTEGER

    dart中的int型別,值的範圍是-2^63 到 2^63 - 1

  • REAL

    dart中的num型別,即int和double型別

  • TEXT

    dart中的String型別

  • BLOB

    dart中的Uint8List型別,雖然能夠儲存List< int >,但官方並不建議,因為轉化比較慢。

如果我們需要儲存其他型別,比如bool,DateTime,List< String >等資料,需要我們自行處理,每個人或許都有自己獨特的方法,希望您能夠提出一些建議。我們可以通過封裝實體類和解析類,在外部程式碼看來,就是實現了儲存bool,DateTime,List< String >等這些型別。

  • bool

    儲存INTEGER型別,0為false,1為true.

  • DateTime

    儲存INTEGER型別,一列資料的建立時間和更新時間,一般是比較重要的。當然還有其他的,比如一個訂單的付款時間、發貨時間、取消時間等很多的時間資訊。如果儲存TEXT型別,程式如果支援多種語言的話,還是不方便的。

  • List

    儲存TEXT型別,我們可以根據特殊的分隔符,把資料組合成String儲存到資料庫。然後根據String的split解析成List< String >。還是有很多需要注意的,比如List的元素中一定不能包含定義的分隔符。對List的某一個Item修改比較麻煩,只能整體覆蓋List。

  • Map、json、實體類

    儲存TEXT型別,一般我使用實體類的toMap方法把實體類轉換成Map, 通過jsonEncode把實體類轉換成String,反過來,利用jsonDecode把String轉換成Map,通過實體類的fromMap轉換成實體類。

資料庫操作

資料庫的建立

根據資料庫的名稱和版本號,Open資料庫。

import 'package:sqflite/sqflite.dart';

String databasesPath = await getDatabasesPath();
// Database Path: /data/user/0/com.package.name/databases
String path = join(databasesPath, 'db_name.db');
// Path: /data/user/0/com.package.name/databases/db_name.db
Database database = await openDatabase(
  path,
  version: VERSION,
  onCreate: (Database db, int version) async {
    // 表格建立等初始化操作
  },
  onUpgrade: (Database db, int oldVersion, int newVersion) async {
    // 資料庫升級
  },
);
複製程式碼

資料庫刪除

await deleteDatabase(path);
複製程式碼

資料庫關閉

await database.close();
複製程式碼

資料庫的升級

官方的使用文件中,表格是按需建立的,但我在使用過程中遇到一個麻煩,關於資料庫的升級問題,因為有些Table,使用者可能並未觸及到,在更新表格結構的時候,需要首先判斷Table是否存在等操作。所以,我在建立資料庫的時候,選擇建立所有好表格(一般情況下,App端並沒有太多表格,我寫的專案中最多的一個有11個表格。)。

只需要把openDatabase的引數version加1,程式在開啟的時候,會自動呼叫openDatabase的onUpgrade方法。所以我們需要在onUpgrade方法中,執行資料庫的升級操作。最霸道的做法是,Drop所有的表格,然後Create最新的表格(會不會被領導打,我就不清楚了)。

使用sqflite,就必須要對SQL語句比較熟悉。對資料庫的ORM封裝,可以使用外掛庫 sqfentity

新增表格

database.execute('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
複製程式碼

刪除表格

database.execute('DROP table test');
複製程式碼

清空表格

database.execute('DELETE FROM test');
複製程式碼

重新命名錶格

database.execute('ALTER TABLE test RENAME TO test_1');
複製程式碼

新增欄位

database.execute('ALTER TABLE test ADD age integer');
複製程式碼

刪除欄位

database.execute('ALTER TABLE test DROP COLUMN age');
複製程式碼

修改欄位型別

database.execute('ALTER TABLE test ALTER COLUMN value TEXT');
複製程式碼

資料的增刪改查

Insert

返回值需要注意下:Returns the last inserted record id(返回最後插入的記錄ID).

int id = await database.insert(‘test’, {'name': 'Civet', 'value': '18', 'num': '456.7'});
複製程式碼
int id = await database.rawInsert('INSERT INTO test(name, value, num) VALUES("Civet", 18, 456.7)');
複製程式碼

Delete

返回值需要注意下:Returns the number of changes made(返回受影響的的數量,即刪除的條目數量)。另外,如果沒where語句,會清空表格,要特別小心。

int count = await database.delete('test', where: 'name = ?', whereArgs: ['Civet']);
複製程式碼
int count = await database.rawDelete('DELETE FROM test WHERE name = ?', ['Civet']);
複製程式碼

Update

返回值需要注意下:Returns the number of changes made(返回受影響的的數量).

int count = await database.update(
  'test', 
  {'name': 'Home', 'value': '20'}
  where: 'name = ?', 
  whereArgs: ['Civet']
);
複製程式碼
int count = await database.rawUpdate('UPDATE test SET name = ?, value = ? WHERE name = ?', ['Home', 20, 'Civet']);
複製程式碼

Query

Query的返回值:List<Map<String, dynamic>>,List是行資料,Map是列資料。

Query是SQL語句中最複雜的一個,關鍵詞包含distinct, where, group by, having, count, order by asc/desc, limit, offset, in, join, as, nuion等等。

List<Map<String, dynamic>> result = await database.query(
  ‘test’,
  distinct: true, // 是否是獨特的,即是否不讓重複
  columns: ['name','value'], // 需要查詢的列
  where: 'age > ?', // 查詢條件
  whereArgs: [16], // 查詢條件引數
  groupBy: 'name', // 按列分組
  having: 'count(name) < 2', // 給分組設定條件
  orderBy: 'name asc', // 按列排序 asc/desc
  limit: 5, // 限制查詢結果數量
  offset: 2, // 跳過幾條資料
);
複製程式碼
List<Map<String, dynamic>> result = await database.rawQuery(
  'SELECT distinct name, value FROM test WHERE age > ? group by name having count(name) < 2 order by name asc limit 5 offset 2',
  [16],
);
複製程式碼

對於 in 語句的組合:

/// SQL query where in
/// If colunm is INTEGER, use this method.
/// Reslut: (2, 3, 4)
static String whereInIntToString(List<int> data) {
  String result;
  for (int sub in data) {
    if (result == null) {
      result = '($sub';
    } else {
      result = '$result, $sub';
    }
  }
  result = '$result)';
  return result;
}

/// SQL query where in
/// If colunm is TEXT, use this method.
/// Reslut: ('2', '3', '4')
static String whereInStringToString(List<String> data) {
  String result;
  for (String sub in data) {
    if (result == null) {
      result = '(\'$sub\'';
    } else {
      result = '$result, \'$sub\'';
    }
  }
  result = '$result)';
  return result;
}
複製程式碼

關於 join 的使用:

連線兩個表格的資料,使用on, using, natural限定連線條件,join分為cross join(x * y的一種實現), inner join(預設join方式), outer join(只支援left outer join)。

用的比較多的是內連線inner join, 它把兩個表的資料,以限定條件on Table1.column = Table2.column組合起來。

/// SQL Statement
SELECT user_id, user_name, address, postal_code FROM at_user INNER JOIN at_address ON at_user.id = at_address.user_id;
複製程式碼

Batch 批量操作

需要注意的是,如果並不關心batch.commit()的返回值,傳入noResult為true,這時候返回的List< dynamic >即為null.

Batch batch = database.batch();
batch.insert('test', {'name': 'item'});
batch.update('test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('test', where: 'name = ?', whereArgs: ['item']);
List<dynamic> results = await batch.commit(
    noResult: true, // 是否關心返回值
    continueOnError: true, // 出現錯誤是否繼續
);
複製程式碼

Transaction 事務的支援

事務就像一個封閉的執行環境,只有當事務提交時,一系列操作對外才是可見的。

需要特別注意的是,事務執行期間,不能使用外部的DataBase物件,避免造成死鎖。

你能看出下面操作的返回值嗎?

/// 測試事務
Future<void> testTransaction({bool noResult, bool continueOnError}) async {
  print('--------------- Transaction(noResult: $noResult, continueOnError: $continueOnError) --------------- ');
  var db = await getDataBase();
  // 清空表
  await db.delete('test');
  // 啟動事務
  await db.transaction((Transaction txn) async {
    // Ok
    var batch = txn.batch();
    // 操作(1-3):插入資料
    batch.insert('test', {'name': 'item-0'});
    batch.insert('test', {'name': 'item-1'});
    batch.insert('test', {'name': 'item-2'});
    // 操作4:更新一條資料
    batch.update(
      'test',
      {'name': 'item-3'},
      where: 'name = ?',
      whereArgs: ['item-0'],
    );
    // 操作5:模擬異常: no such column no_column
    batch.insert(
      'test',
      {'no_column': 'item-0'},
    );
    // 操作6:刪除一條資料
    batch.delete(
      'test',
      where: 'name = ?',
      whereArgs: ['item-3'],
    );
    // 執行上述一系列操作
    List<dynamic> results = await batch.commit(
      noResult: noResult,
      continueOnError: continueOnError,
    );
    // 操作返回值
    print('Reslut<dynamic>: $results');
  });
  // 事務執行後:查詢表中多少資料
  List<Map<String, dynamic>> result = await db.rawQuery(
    'SELECT * FROM Test',
  );
  print('List<Map> Reslut: $result');
  print('------------------------------ ');
  print('');
}
複製程式碼
  1. 需要返回值,錯誤不繼續執行
await testTransaction(noResult: false, continueOnError: false);
複製程式碼

結果:

  • 傳送異常 table Test has no column named no_column
  • 分析:操作5發生錯誤,不繼續執行,直接丟擲異常。
  • 表中並未插入任何資料,操作全部Rollback
  1. 需要返回值,錯誤繼續執行
await testTransaction(noResult: false, continueOnError: true);
複製程式碼

結果:

  • Reslut: [1, 2, 3, 1, DatabaseException(table Test has no column named no_column (Sqlite code 1): , while compiling: INSERT INTO Test (no_column) VALUES (?), (OS error - 2:No such file or directory)) sql 'INSERT INTO Test (no_column) VALUES (?)' args [item-0]}, 1]
  • List Reslut: [{id: 2, name: item-1, value: null, num: null}, {id: 3, name: item-2, value: null, num: null}]
  • 分析:操作5發生錯誤,繼續執行,操作5返回值是一個異常,其他操作正常
  1. 不需要返回值,錯誤不繼續執行
  2. 不需要返回值,錯誤繼續執行

既然你已經讀到這裡了,返回值已經在你腦海中了。

視覺化介面

有時候為了除錯,或許為了分析資料,我們需要檢視資料庫中具體有哪些資料,通過視覺化介面展示資料就比較方便了。

1. Database Navigator

Android Studio的Plugin,直接線上(Marketplace)安裝即可。最上邊的狀態列有個DB Navigator,然後點選Database browser, 設定資料庫檔案路徑。

Flutter 資料庫sqflite使用知識點

2. SQLiteStudio

Flutter 資料庫sqflite使用知識點

相關文章