Flutter 数据库指南
最近一段时间,在学 Flutter,打算把自己写过的项目 HOO 用 Flutter 实现。
里面用到了一些数据库的知识,正好这周我在组内也是分享的SQLite,所以我们就来聊一聊 SQLite 的基础,以及怎么在 Flutter 中去使用数据库。
![](https://img.haomeiwen.com/i9271486/406259e29a05bf13.jpg)
目录
![](https://img.haomeiwen.com/i9271486/82b0579a71b29d61.jpg)
一、SQLite 基础
1. SQLite 定义
SQLite 是一个进程内的库,实现了自给自足、无服务器的、零配置的、事务性的SQL数据库引擎。
2. SQLite 特点
从上面的定义中,我们已经看到 SQLite 的一些特点,我在这里做一下详细的解释:
- 嵌入式数据库:与MySQL、Oracle 这类数据库服务器相比,SQLite 无需单独的服务器进程或者操作系统,并且可以和应用的进程处在同一个进程。
- 无需配置:不需要进行一大堆的配置。
- 存储在磁盘文件:可以跨平台的存储在一个磁盘文件中。
- 轻量级:完全配置时小于400kb,省略可选功能时低于250kb。
- 无需外部依赖。
- 跨平台:可以在 Unix 和 Windows 中运行。
所以,Android 和 ios 设备一般都会采用 SQLite 作为应用的本地数据库。
二、SQL 语法
SQL 语法是比较重要的,因为无论是什么基于 SQLite 的第三方库,它们在进行数据库操作的时候,最后都会转化为 SQL 语句,这个时候即使遇到 bug,我们也能够迎刃而解。
推荐看一下基础教程:SQLite教程
数据库的语言学习可以分为三个部分:
![](https://img.haomeiwen.com/i9271486/ca9451788c22cafb.jpg)
1. 创建数据库
在进行学习基础语言之前,我们需要了解一下如何创建一个数据库,它通常对应着一个 .db
文件。
如果是在项目中,我们通常会在代码中进行配置,在进入 App 中就会生成对应的 .db
文件。
如果是在电脑中,我们以 mac 为例,需要在系统中安装 SQLite,成功以后,输入命令:
sqlite3 数据库名
就可以成功创建一个数据库。
2. 数据库定义语言
先说一下我们的目标,要建一个用户收藏商品的关系,思考一下,我们要建立几张表?
没错需要建立三张表,分别是 User
(用户表)、Goods
(商品表) 和 Fav_Goods
(收藏表)。User
和 Goods
是独立的表,Fav_Goods
则要存放 其他两张表的主键。
2.1 CREATE
数据库在之前已经建立完,下一步就是建立表,对应建立表的语法就是 CREATE TABLE
,有几个点需要注意。
基础的存储类型只有五种:
存储类型 | 解释 |
---|---|
NULL |
值是一个 NULL 值 |
INTEGER |
值是一个带符号的整数,值支持 1,2,3,4,6,8个字节 |
REAL |
浮点数 |
TEXT |
文本字符串 |
BLOB |
二进制大对象,用来存储图片,视频等 |
一些常用的关键字:
关键字 | 解释 |
---|---|
PRIMARY KEY |
主键 |
AUROINCREMENT |
主键自增长,一般需要将主键设置为 INTEGER |
FOREIGN KEY |
外键 |
详细的 USER
表、GOODS
表和 FAV_GOODS
表创建语句:
# main 对应数据库名称
CREATE TABLE main.USER(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
account TEXT,
pwd TEXT,
age INT
);
CREATE TABLE main.GOODS(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
price REAL,
category TEXT,
brand TEXT
);
CREATE TABLE main.FAV_GOODS(
id INTEGER PRIMARY KEY AUTOINCREMENT,
date STRING,
user_id INTEGER,
good_id INTEGER,
FOREIGN KEY(user_id) REFERENCES STUDENT(id),
FOREIGN KEY(good_id) REFERENCES GOODS(id)
);
2.2 ALTER 和 DROP
DROP 代表删除表的操作,具体的命令是:
DROP TABLE 数据库名.表名;
ALTER 代表修改表的操作,支持修改表名和在已有的表中增加新的列。具体的命令:
# 修改表名
ALTER TABLE database_name.table_name RENAME TO new_table_name;
# 增加新列
ALTER TABLE database_name.table_name ADD COLUMN 列名 存储类型;
3. 数据库操作语言
3.1 INSERT
INSERT
对应着增加数据记录,需要注意的是插入的字段和列名要一一对应:
# 新增用户
# name 对应 "lisi", account 对应 "13222",pwd 对应 "123456",age 对应 28
INSERT INTO USER (name,account,pwd,age) VALUES ("lisi", "13222", "123456", 28);
另外一个需要注意的地方是,如果表设置主键自增长,那么这张表插入数据就可以忽略主键。
3.2 DELETE 和 UPDATE
UPDATE
对应这更新表中的记录,对应的命令是:
UPDATE table_name SET column1 = value1, column2 = value2...., columnN = valueN WHERE [condition];
UPDATE USER SET name = "小王" WHERE id = 1;
DELETE
对应删除表中的记录,对应的命令是:
DELETE FROM table_name WHERE [condition];
DELETE FROM USER WHERE name = "九心";
4. 数据库查询语言
最复杂的就是数据库的查询,里面使用了很多关键字,我用了一张脑图来表达:
![](https://img.haomeiwen.com/i9271486/71d5e46b1b94f34b.jpg)
对应的命令是:
SELECT * 或者 具体的字段
FROM 表名
[ WHERE <条件> ]
[ GROUP BY 列名 ]
[ HAVING <条件> ]
[ ORDER BY 列名 <DESC> | <ASC> ]
[ LIMIT 行数 OFFSET 偏移量]
# 查询 姓张并且年龄大于20 的用户
SELECT * FROM USER WHERE name like "张%" and age > 20;
# 统计每个名字的用户数,按降序排列
SELECT name,count(name) FROM USER GROUP BY name ORDER BY count(name) DESC;
除了上述,还有复合查询,也就是交叉连接、内连接和外连接,不过 SQLite 不支持右外连接,感兴趣的同学可以自行了解。
三、Flutter 实战
有了一定的基础,我们就可以在 Flutter 中运用了。如果我们想要在 Flutter 中使用数据,就需要引入 sqflite。
1. 添加依赖
# 当前最新版本是 1.3.0
dependencies:
...
sqflite: ^1.3.0
2. 建表
创建表格是一个数据库操作,数据库操作应该是一个异步操作。
上面讲解SQL语法的时候,使用的用户收藏商品,这里我改成了用户收藏鞋子,三张表依次是 user
、shoe
和 fav_shoe
表,建表使用的原生 SQL 语句:
// 创建表
Future _onCreate(Database db, int version) async {
db.execute(
"CREATE TABLE IF NOT EXISTS user("
"id INTEGER PRIMARY KEY autoincrement, "
"account TEXT, "
"pwd TEXT, "
"name TEXT, "
"headImage Text);");
db.execute(
"CREATE TABLE IF NOT EXISTS shoe("
"id INTEGER PRIMARY KEY autoincrement, "
"name TEXT, "
"description TEXT, "
"price REAL, "
"brand Text, "
"imageUrl Text);");
db.execute(
"CREATE TABLE IF NOT EXISTS fav_shoe("
"id INTEGER PRIMARY KEY autoincrement, "
"show_id INTEGER, "
"user_id INTEGER, "
"date INTEGER , "
"FOREIGN KEY(user_id) REFERENCES user(id), "
"FOREIGN KEY(show_id) REFERENCES shoe(id));");
return;
}
创建表的操作放在哪里呢?sqflite
是放在打开数据库的时候,打开数据库的时候不仅要添加hook数据库创建的方法 onCreate
,还需要添加 数据库存放的路径 和 数据库的版本:
Future<Database> _initDB() async {
// 路径获取是通过 path_provider 库,需要自行添加依赖
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, 'dbHoo');
return await openDatabase(path, onCreate: _onCreate, version: 1);
}
3. 创建实体类
user
表 和 shoe
表是类似的,我们放一个 shoe
表即可,对于 fav_shoe
这种有外键的表,我们也要考虑一下如何去创建。
3.1 User 表
用户表构建很简单,除了必须的基础属性和构造方法外,最好再写一下 User 对象和 Map 互转的方法,方便后面的数据库操作。
class User {
int id;
String account;
String pwd;
String name;
String headImage;
User(this.account, this.pwd, this.name, this.headImage, {this.id});
User.fromJson(Map<String,dynamic> json){
id = json['id'];
account = json['account'];
pwd = json['pwd'];
name = json['name'];
headImage = json['headImage'];
}
Map<String, dynamic> toMap() {
return {
'id': id,
'account': account,
'pwd': pwd,
'name': name,
'headImage': headImage,
};
}
}
3.2 FavShoe 表
对于有外键约束的表,我猜想查询结果的时候,会不会提供给我们外键相关的对象?比如说 FavShoe
表,外键有 useId
,会不会提供给我们 User
对象呢?
从前面 User
实体中的方法来看,显然是不会的,如果我们想要在得到结果的时候也要获取到外键相关的对象,我们就要做更多的事情,我们在设计实体的时候,就可以考虑把这些东西加进来了。
除了创建基础的属性以外,我还创建了跟外键相关的对象属性 user
和 shoe
:
class FavShoe {
final int id;
final int userId;
User user;
final int shoeId;
Shoe shoe;
final int date;
FavShoe(this.id, this.userId, this.shoeId, this.date);
factory FavShoe.fromJson(Map<String,dynamic> map){
return FavShoe(map['id'], map['userId'], map['shoeId'], map['date']);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'userId': userId,
'shoeId': shoeId,
'date': date
};
}
}
4. 插入数据
插入数据有两种方法,rawInsert
和 insert
方法,rawInsert
更接近原生 SQL 语句,insert
方法更简单。
4.1 rawInsert 方法
这个 db
对象类型 Future<Database>
,我这里做了一层封装,简单起见,就不展示了:
// 新增用户
Future<void> insertUserRaw(User user) async {
var _db = await db;
await _db.rawInsert("INSERT INTO USER (account, pwd, name, headImage) VALUES (?, ?, ?, ?);",[user.account,user.pwd,user.name,user.headImage]);
}
如果使用rawInsert
方法,相当于直接操作的原生语句,手写 SQL 语句的时候,很容易发生小问题,更推荐直接使用第二种方法。
后面 rawUpdate
、rawDelete
和 rawQuery
就不讲解了。
4.2 insert 方法
Future<void> insertUser(User user) async {
var _db = await db;
await _db.insert("user", user.toMap(),
conflictAlgorithm: ConflictAlgorithm.fail);
}
主要会用到三个参数:第一个指定表名,第二个是我们 User
对象转化成的 Map
,第三个是插入发生冲突使用的规则。
这里有两点注意,分别是二和三。
先谈二,当对象转化成 Map
的时候, 对于普通表来说,没什么难的,对于有外键的表呢,只处理外键就行了,我在实体中生成的外键对象是不用管的。
class FavShoe {
final int id;
final int userId;
User user;
final int shoeId;
Shoe shoe;
final int date;
// ...
Map<String, dynamic> toMap() {
return {
'id': id,
'userId': userId,
'shoeId': shoeId,
'date': date
};
}
}
上面的 user
和 shoe
对象就不用管了。
再谈三,当相同主键的记录再一次插入的时候,我们处理冲突规则如下:
规则 | 解释 |
---|---|
rollback |
回滚,事务支持回滚 |
abort |
终止,冲突发生的事务前的操作都会被保存 |
fail |
失败,冲突前SQL操作结果都会被保存 |
ignore |
忽略,发生冲突时 |
replace |
发生冲突时,直接代替已有的记录 |
5. 查询操作
查询是数据库中最为繁琐的一部分,因为有些东西得记住。重点来看 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}); // 偏移量
知道了这些参数的意义,使用就变得简单起来,举几个例子。
5.1 获取用户
重点:如何使用 Where
进行条件查询。
// 获取用户
Future<User> searchUserByNameAndPwd(String account, String pwd) async {
var _db = await db;
List<Map<String, dynamic>> result = await _db.query("user",
where: 'name = ? and pwd = ?', whereArgs: [account, pwd]);
Map<String, dynamic> r = result.isNotEmpty ? result.first : null;
if (r == null) return null;
return User.fromJson(r);
}
5.2 获取指定位置和指定品牌的鞋子
重点:如何使用排序。
// 获取指定位置的Shoe列表
Future<List<Shoe>> queryShoeByPosAndBrand(int startPos, int endPos, String brand) async {
var _db = await db;
List<Map<String, dynamic>> result = await _db.query('shoe',
where: ' id > ? and id <= ? and brand = ?',
whereArgs: [startPos, endPos, brand],
orderBy: 'id ASC');
if (result.isEmpty) return List<Shoe>();
return List.generate(result.length, (i) => Shoe.fromJson(result[i]));
}
5.3 处理包含外键的查询
对于 FavShoe
表来说,我希望,当查到收藏记录的时候,我也能够得到 FavShoe
对应的 Shoe
的数据:
// 查询某人的收藏记录
Future<FavShoe> queryFavShoeByUserID(int userId, int shoeId) async {
var _db = await db;
List<Map<String, dynamic>> result = await _db.query('fav_shoe',
where: ' user_id = ? and shoe_id = ?',
whereArgs: [userId,shoeId]);
if (result.isEmpty) return null;
var _favShoe = FavShoe.fromJson(result.first);
if(_favShoe == null)
return null;
// 单独查询鞋子表
var _shoe = await queryShoeById(_favShoe.shoeId);
_favShoe.shoe = _shoe;
return _favShoe;
}
可以看到,我是在进行过一次收藏记录查询的同时,又根据 shoeId
对 Shoe
表进行了一次查询,假设我现在还想获取到用户的信息,那么就需要对 User
表再进行一次查询。
如果我查询的某个用户的是一个列表呢?我们肯定不想对每条记录进行遍历,然后再为每条记录再进行一次查询。
5.4 复合查询
我们的目标是获取某个用户的所有收藏记录,我想获取到所有收藏记录的同时,还能获取到每条搜藏记录对应鞋子的价格和名称,这个时候就可以使用复合查询。
query
方法貌似不支持复合查询,所以我们得使用 rawQuery
:
// 查询某人的收藏记录
Future<List<FavShoe>> queryFavShoesByUserID(int userId) async {
var _db = await db;
List<Map<String, dynamic>> result = await _db.rawQuery("SELECT fav_shoe.id, shoe_id, user_id, name, price "
"FROM fav_shoe LEFT OUTER JOIN ON fav_shoe.shoe_id = shoe.id "
"WHERE user_id = ?;",[userId]);
if (result.isEmpty) return List<FavShoe>();
return List.generate(result.length, (i) => FavShoe.fromJson(result[i]));
}
看一下这里的 map
是如何转化的:
class FavShoe {
final int id;
final int userId;
User user;
final int shoeId;
Shoe shoe;
final int date;
// ...
factory FavShoe.fromJsonAndShoe(Map<String, dynamic> map) {
Shoe shoe =
Shoe(map['name'], null, map['price'], null, null, id: map['shoeId']);
return FavShoe(map['userId'], map['shoeId'], map['date'], id: map['id'], shoe: shoe);
}
//...
}
同样也是创建一个 Shoe
对象,存入我们直接查询的数据,比刚刚再进行一次查询方便多了。
6. 更新和删除
更新和删除都是比较简单的操作,我们放在一起讲。
6.1 更新
// 更新用户
Future<void> updateUser(User user) async {
var _db = await db;
await _db.update("user", user.toMap(), where: "id = ?",whereArgs: [user.id]);
}
6.2 删除
根据用户 id
删除用户:
// 删除
Future<void> deleteUser(int userId) async {
var _db = await db;
await _db.delete("user", where: "id = ?",whereArgs: [userId]);
}
7. 事务
事务的概念很简单,就是一组 SQL 操作要么全执行,要么都不执行,
sqflite 提供了两种处理事务的方法:
- 直接使用事务。
- 使用
Batch
。
7.1 直接调用
Future<void> doTransaction() async {
var _db = await db;
_db.transaction((txn) async {
// 数据库操作一
// 数据库操作二
// ...
});
}
数据库操作直接放在代码块中即可。
7.2 Batch使用
Batch
的操作和前面的增删查改的操作大致上是一直的,不过会有一点区别,使用 Batch
进行增删查改的时候没有返回值,它会在 Batch#commit()
方法获取所有的结果,和前面操作的顺序一致:
Future<void> doTransaction() async {
var _db = await db;
Batch batch = _db.batch();
User one = new User("200722649@qq.com", "123456", "ChenHa", null);
User two = new User("200622649@qq.com", "123456", "WangHa", null);
batch.insert("user", one.toMap());
batch.insert("user", two.toMap());
batch.delete("user",where: "id = ?",whereArgs: [6]);
batch.query("user");
// results 的数量有四个
// 第一个 - 插入用户one的结果
// 第二个 - 插入用户two的结果
// 第三个 - 删除用户id为6的结果
// 第四个 - 查询所有用户的结果
List<dynamic> results = await batch.commit();
}
可以看到,Batch
的理念和事务是一致的,并且它也是用事务实现的,感兴趣的同学可以自行研究。
四、总结
关于数据库的学习到此就结束了,个人觉得还是得先学会SQL语法,因为很多第三方库进行数据库处理的时候最后都转化为SQL语句,后面即使遇到问题,你也能很快定位到问题。
另外,Flutter 中 sqflite
这个库的使用还是比较接近使用原生SQL操作的,这也进一步要求开发者对 SQL 语法的掌握。
如果你有更好的理解,欢迎下方评论交流,如果你觉得本文不错,三连是对我最好的肯定!
![](https://img.haomeiwen.com/i9271486/71afbc1d2bca434e.jpg)