前言
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('');
}
複製程式碼
- 需要返回值,錯誤不繼續執行
await testTransaction(noResult: false, continueOnError: false);
複製程式碼
結果:
- 傳送異常 table Test has no column named no_column
- 分析:操作5發生錯誤,不繼續執行,直接丟擲異常。
- 表中並未插入任何資料,操作全部Rollback
- 需要返回值,錯誤繼續執行
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
- 分析:操作5發生錯誤,繼續執行,操作5返回值是一個異常,其他操作正常。
- 不需要返回值,錯誤不繼續執行
- 不需要返回值,錯誤繼續執行
既然你已經讀到這裡了,返回值已經在你腦海中了。
視覺化介面
有時候為了除錯,或許為了分析資料,我們需要檢視資料庫中具體有哪些資料,通過視覺化介面展示資料就比較方便了。
1. Database Navigator
Android Studio的Plugin,直接線上(Marketplace)安裝即可。最上邊的狀態列有個DB Navigator,然後點選Database browser, 設定資料庫檔案路徑。