Flutter中使用sqflite插件简单介绍

sqflite是github上tekartik提供的可供Android和iOS使用的SQLite插件

  • 支持数据保存及批量增删查改等操作
  • 支持数据库在使用中进行版本管理等
  • 支持插入/查询/更新/删除数据等操作
  • 支持在iOS和Android上的后台线程中执行数据库操作

使用介绍

在你的flutter项目中添加依赖项

1
2
3
dependencies:
...
sqflite: ^1.1.6

导入 sqflite.dart

1
import 'package:sqflite/sqflite.dart';

创建数据库

SQLite数据库是由路径标识的文件系统中的文件。

如果是相对的,则此路径相对于getDatabasesPath()获取的路径,getDatabasesPath()是Android上的默认数据库目录和iOS上的文档目录。

数据库操作

创建
  • 简单示例

    1
    var db = await openDatabase('my_db.db');
  • 查找数据库的位置路径

    1
    2
    3
    4
    5
    6
    7
    var databasesPath = await getDatabasesPath();
    var path = join(databasesPath, dbName);

    // Make sure the directory exists
    try {
    await Directory(databasesPath).create(recursive: true);
    } catch (_) {}
  • 初始化执行数据库操作

    1
    2
    3
    4
    5
    _onConfigure(Database db) async {
    // Add support for cascade delete
    await db.execute("PRAGMA foreign_keys = ON");
    }
    var db = await openDatabase(path, onConfigure: _onConfigure);
  • 预加载数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    _onCreate(Database db, int version) async {
    // 创建数据库,创建表
    await db.execute(
    "CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
    }
    //填充数据
    await db.insert(...);
    }

    // 打开数据库,指定版本和onCreate回调
    var db = await openDatabase(path,
    version: 1,
    onCreate: _onCreate);
  • 打开数据库回调

在设置数据库版本之后和openDatabase返回之前调用onOpen

1
2
3
4
5
6
7
8
9
_onOpen(Database db) async {
// 数据库已打开,打印其版本
print('db version ${await db.getVersion()}');
}

var db = await openDatabase(
path,
onOpen: _onOpen,
);
  • 创建只读数据库
    1
    var db = await openReadOnlyDatabase(path);
升级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_onCreate(Database db, int version) async {
// 创建数据库,创建表
await db.execute(
"CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
}

_onUpgrade(Database db, int oldVersion, int newVersion) async {
// 更新数据库版本,更改表
await db.execute("ALTER TABLE Test ADD name TEXT");
}

// 这里用于onDowngrade的特殊回调来重新创建数据库
var db = await openDatabase(path,
version: 1,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
onDowngrade: onDatabaseDowngradeDelete);
检查数据库是否损坏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 检查数据库文件是否是有效的数据库文件、空文件是有效的空sqlite文件
Future<bool> isDatabase(String path) async {
Database db;
bool isDatabase = false;
try {
db = await openReadOnlyDatabase(path);
int version = await db.getVersion();
if (version != null) {
isDatabase = true;
}
} catch (_) {} finally {
await db?.close();
}
return isDatabase;
}
关闭
  • 关闭数据库
    1
    await db.close();
解决数据库被锁住的问题

异常问题:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
建议解决方案:强烈建议只打开一次数据库。默认情况下,数据库作为单个实例打开(singleInstance:true)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:synchronized/synchronized.dart';

class Helper {
final String path;
Helper(this.path);
Database _db;
final _lock = new Lock();
Future<Database> getDb() async {
if (_db == null) {
//防止异步创建多次
await _lock.synchronized(() async {
// 进入同步块后再次检查
if (_db == null) {
_db = await openDatabase(path);
}
});
}
return _db;
}
}

如果在打开数据库时出现异常:

  • 查看故障排除部分
  • 确保存在创建数据库的目录
  • 确保数据库路径指向现有数据库(或不指向任何内容),而不是指向不是sqlite数据库的文件
  • 处理打开的回调中的任何预期异常(onCreate / onUpgrade / onConfigure / onOpen)

数据库简单操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 使用getDatabasePath获取db路径
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');

//删除数据库
await deleteDatabase(path);

//打开数据库
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
// 创建数据库时,创建表
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});

//在事务中插入一些记录
await database.transaction((txn) async {
int id1 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
print('inserted1: $id1');
int id2 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});

//修改更新一些记录
int count = await database.rawUpdate(
'UPDATE Test SET name = ?, VALUE = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');

//获取查找一些表的数据
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [//预期查出来的数据作比较
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));

// 获取表的总条数
count = Sqflite
.firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);

// 删除指定条件的表数据
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// 关闭数据库
await database.close();

实战程序的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';

class Todo {
int id;
String title;
bool done;

Map<String, dynamic> toMap() {
var map = <String, dynamic>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}

Todo();

Todo.fromMap(Map<String, dynamic> map) {
id = map[columnId];
title = map[columnTitle];
done = map[columnDone] == 1;
}
}

class TodoProvider {
Database db;

Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}

Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}

Future<Todo> getTodo(int id) async {
List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Todo.fromMap(maps.first);
}
return null;
}

Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}

Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id]);
}

Future close() async => db.close();
}

扩展及注意事项

读取数据注意事项

1
2
3
4
5
6
7
8
9
10
11
12
//假设读取的数据是它
List<Map<String, dynamic>> records = await db.query('my_table');
//以上数据是只读的
Map<String, dynamic> mapRead = records.first;
//在下面操作修改数据会出现异常
mapRead['my_column'] = 1;

// Crash... `mapRead` is read-only

//如果要在内存中修改它,可如下操作
Map<String, dynamic> map = Map<String, dynamic>.from(mapRead);
map['my_column'] = 1;

使用Transaction对象注意事项

如果回调没有引发错误,则提交事务。如果抛出错误,则取消该事务。因此,以一种方式回滚事务就是抛出异常

1
2
3
4
5
6
7
await database.transaction((txn) async {
// 没有问题
await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');

//不要在事务中使用数据库对象//这会死锁!
await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});

批量操作支持

1
2
3
4
5
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();

获取每个操作的结果都有成本(插入ID和更新和删除的更改次数),尤其是在执行额外SQL请求的Android上。

如果您不关心结果并担心大批量的性能,您可以使用

1
await batch.commit(noResult: true);
  • 警告:在事务期间,在提交事务之前不会提交批处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    await database.transaction((txn) async {
    var batch = txn.batch();

    // ...

    // 提交但实际提交将在提交事务时发生,但是此事务中的数据可用
    await batch.commit();

    // ...
    });
    默认情况下,批处理在遇到错误时会立即停止(通常会还原未提交的更改)。
    您可以忽略错误,以便即使一个操作失败也会运行并提交每个成功的操作:
    1
    await batch.commit(continueOnError: true);

表和列名称

通常,最好避免将SQLite关键字用于实体名称。
如果使用以下任何名称:

1
"add","all","alter","and","as","autoincrement","between","case","check","collate","commit","constraint","create","default","deferrable","delete","distinct","drop","else","escape","except","exists","foreign","from","group","having","if","in","index","insert","intersect","into","is","isnull","join","limit","not","notnull","null","on","or","order","primary","references","select","set","table","then","to","transaction","union","unique","update","using","values","when","where"

但是在任何其他原始语句(包括orderBy,where,groupBy)中,请确保使用双引号正确地转义名称。
例如,请参阅下面的列名称组未在columns参数中进行转义,但在where参数中进行转义。

1
db.query('table', columns: ['group'], where: '"group" = ?', whereArgs: ['my_group']);

数据库中的数据类型

INTEGER

Dart type: int
Supported values: from -2^63 to 2^63 - 1

REAL

Dart type: num

TEXT

Dart type: String

BLOB

Dart type: Uint8List
Dart type List is supported but not recommended (slow conversion)

当前存在的 issues
  • 由于事务在SQLite(线程)中的工作方式,不支持并发读写事务。所有调用当前都已同步,事务块是独占的。
    【支持并发访问的基本方法是多次打开数据库,但它只能在iOS上运行,因为Android会重用相同的数据库对象。】
    【线程可能是一个潜在的未来解决方案,但在Android访问另一个线程中的数据库时,在事务中被阻止…】
  • 目前INTEGER仅限于-2 ^ 63 到 2 ^ 63 - 1(尽管Android支持更大的)
  • DateTime不是受支持的SQLite类型。可将它们存储为int(millisSinceEpoch)或字符串(iso8601)
  • bool不是受支持的SQLite类型。使用INTEGER【0和1】值