当有大量的、复杂的数据需要进行持久化的时候,文件、键值对就不适用了,就需要 SQLite 登场了,Flutter 同样也支持 SQLite。
添加依赖
使用 SQLite 需要导入 sqflite
和 path
这两个 package。
sqflite
提供了丰富的类和方法,以便你能便捷实用 SQLite 数据库。path
提供了大量方法,以便你能正确的定义数据库在磁盘上的存储位置。
dependencies:
flutter:
sdk: flutter
sqflite: ^1.3.0+1
path: ^1.6.4
并且将包引入到要使用的文件中:
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
path
这是一个跨平台的路径处理库,因为在不同的平台,路径分隔符会不相同,在 windows 中的值为 \
,而在 linux 中的值为 /
。在 Java 中可以使用 File.separator
来保证在不同的系统中路径相同,在 Dart 语言中,使用这个库。
使用 join
方法来进行操作:
p.join('directory', 'file.txt');
它会根据不同的平台对路径进行链接。如果想要指定平台:
var context = Context(style: Style.windows);
context.join('directory', 'file.txt');
join
方法会返回拼接后的完整路径。
该包还可以处理浏览器路径,这里不赘述。
sqflite
这个包是 Flutter 中的 SQLite 插件,支持 iOS,Android 和 MacOS。
通过 getDatabasesPath()
方法来获取 SQLite 文件的存储路径,再搭配 path 库,就可以获取到数据库文件的路径:
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'xxxx.db');
获取到数据库文件,就可以打开文件执行操作了,通过 openDatabase
来打开数据库:
Future<Database> openDatabase(String path,
{int version,
OnDatabaseConfigureFn onConfigure,
OnDatabaseCreateFn onCreate,
OnDatabaseVersionChangeFn onUpgrade,
OnDatabaseVersionChangeFn onDowngrade,
OnDatabaseOpenFn onOpen,
bool readOnly = false,
bool singleInstance = true})
openDatabase
会在给定的 path
下根据 version
决定是否调用 onCreate
,onUpgrade
和 onDowngrade
,执行顺序如下:
onConfigure
onCreate
或onUpgrade
或onDowngrade
,这三个方法是互斥的,只会调用其中一个。onOpen
onConfigure
是打开数据库时调用的第一个回调。 它允许您执行数据库初始化,例如启用外键或预写日志记录。
如果在调用 openDatabase
之前数据库不存在,则调用 onCreate
。 可以借此机会根据您的模式在数据库中创建所需的表。
如果满足以下任一条件,则调用 onUpgrade
:
onCreate
为空- 数据库已经存在,并且
version
高于已存数据库版本。
在没有指定 onCreate
的第一种情况下,将调用 onUpgrade
并将其 oldVersion
参数设为0。在第二种情况下,可以执行必要的迁移过程来处理不同的架构。
仅当 version
低于上一个数据库版本时才调用 onDowngrade
。 这是一种罕见的情况,只有在较新版本的代码创建了一个数据库之后又与较旧版本的代码进行交互的情况下,才会出现这种情况。 应该尝试避免这种情况。
onOpen
是最后一个要调用的可选回调。 在设置数据库版本之后且 openDatabase
返回之前调用它。
当 readOnly
(默认为 false)为 true
时,将忽略所有其他参数,并按原样打开数据库。
当 singleInstance
为 true
(默认值)时,将为给定路径返回一个数据库实例。 随后使用相同路径对openDatabase
的调用将返回相同的实例,并将丢弃所有其他参数,例如该调用的回调。
打开数据库
在进行数据库操作之前,需要先打开数据库,步骤如下:
- 使用
sqflite
package 里的getDatabasesPath
方法并配合path
package里的path
方法定义数据库的路径。 - 使用
sqflite
package 里的openDatabase
方法打开数据库。
Future<Database> openDatabaseAndCreateTable() async {
final Future<Database> database = openDatabase(
join(await getDatabasesPath(), 'LifeKeeper.db'),
onCreate: (db, version) {
return db.execute(
"CREATE TABLE IF NOT EXISTS Student(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
);
},
version: 1,
);
return database;
}
添加数据
Database
对象提供了两个方法来帮助我们插入数据:
insert
Future<int> add(Student student) async { return database.insert("Student", student.toMap()); }
第一个参数是要插入数据的表名,第二个参数是需要添加的数据
rawInsert
Future<int> add(Student student) async { return database.rawInsert('INSERT INTO Student(name, age) VALUES(?,?)',["tom",20]); }
第一个参数是 SQL 语句,可以使用
?
作为占位符,通过第二个参数填充数据。
删除数据
同样有两个方法进行删除操作:
delete
database.delete("Student", where: "id = ?", whereArgs: [1]);
第一个参数是要删除数据的表名,后面两个参数是删除的条件,如果后面两个参数为空,为删除整个表内容。
rawDelete
database.rawDelete("delete from Student where id = ?", ["1"]);
参数含义同
rawInsert
查找数据
同样是两个方法:
query
Future<List<Map<String, dynamic>>> query(String table, {bool distinct, List<String> columns, String where, List<dynamic> whereArgs, String groupBy, String having, String orderBy, int limit, int offset})
参数写过 android 的都知道是啥意思,不赘述了就,如果可选参数不填写的话,就返回整个表内容。
rawQuery
和之前添加、删除一样,不赘述
修改数据
同样也有两个方法:
Future<int> update(String table, Map<String, dynamic> values,
{String where,
List<dynamic> whereArgs,
ConflictAlgorithm conflictAlgorithm})
和
Future<int> rawUpdate(String sql, [List<dynamic> arguments]);
使用方法也就不赘述了。
冲突处理
在插入和更新操作方法中,有一个 ConflictAlgorithm
参数,这个参数定义了当操作数据发生冲突时该怎么办,它是一个枚举类,有以下值可选:
CONFLICT_ROLLBACK
,当发生约束冲突时,会立即回滚,从而结束当前事务,并且命令将终止,返回代码为SQLITE_CONSTRAINT
。如果没有事务处于活动状态(每个命令上创建的隐式事务除外),则此算法的工作原理与ABORT
相同。CONFLICT_ABORT
,当发生约束冲突时,不会执行回滚,因此将保留同一事务中先前命令的更改。这是默认行为。CONFLICT_FAIL
,当发生约束冲突时,该命令将使用返回代码SQLITE_CONSTRAINT
中止。但是,命令在遇到违反约束之前对数据库所做的任何更改都将保留,不会被取消。CONFLICT_IGNORE
,当发生约束违反时,不插入或更改包含约束违反的一行。但是命令仍然正常执行。包含违反约束的行之前和之后的其他行将继续正常插入或更新。没有返回错误。CONFLICT_REPLACE
,当发生唯一约束违反时,在插入或更新当前行之前,将删除导致违反约束的现有行。因此,插入或更新总是发生。命令继续正常执行。没有返回错误。如果发生NOT NULL
约束冲突,则该列的默认值将替换 NULL 值。如果列没有默认值,则使用ABORT
算法。如果发生检查约束冲突,则使用忽略算法。当此冲突解决策略删除行以满足约束时,它不会对这些行调用delete
触发器。这种行为在将来的版本中可能会改变。CONFLICT_NONE
,当没有指定冲突操作时,请使用以下命令。
事务
有数据库操作就离不开事务,它可以保证多条原子操作放在一起执行,保证操作要么全部执行完成,要么都不执行。
Future<String> update() async {
await database.transaction((txn) async => {
// txn.update...;
// txn.insert...;
// txn.delete...;
// txn.query...;
});
}
需要注意的是,不能在 transaction 对象里面使用 database 对象,这会发生死锁,可以直接调用 transaction 对象的增删查改方法进行数据操作。
批量执行
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();
这些操作返回结果,都会有一些开销;如果你不考虑结果,可以使用:
await batch.commit(noResult: true);
事务期间,直到事务被提交,批量操作才能提交
await database.transaction((txn) async {
var batch = txn.batch();
//...
// 实际提交将在事务启动时发生
await batch.commit();
});
关闭数据库
当然关闭数据库不能忘,养成用完就关的好习惯,节省资源:
database.close()