Flutter 数据库指南

微信扫一扫,分享到朋友圈

Flutter 数据库指南

最近一段时间,在学 Flutter,打算把自己写过的项目 HOO 用 Flutter 实现。

里面用到了一些数据库的知识,正好这周我在组内也是分享的SQLite,所以我们就来聊一聊 SQLite 的基础,以及怎么在 Flutter 中去使用数据库。

目录

一、SQLite 基础

1. SQLite 定义

SQLite 是一个进程内的库,实现了自给自足、无服务器的、零配置的、事务性的SQL数据库引擎。

2. SQLite 特点

从上面的定义中,我们已经看到 SQLite 的一些特点,我在这里做一下详细的解释:

  • 嵌入式数据库:与MySQL、Oracle 这类数据库服务器相比,SQLite 无需单独的服务器进程或者操作系统,并且可以和应用的进程处在同一个进程。
  • 无需配置:不需要进行一大堆的配置。
  • 存储在磁盘文件:可以跨平台的存储在一个磁盘文件中。
  • 轻量级:完全配置时小于400kb,省略可选功能时低于250kb。
  • 无需外部依赖。
  • 跨平台:可以在 Unix 和 Windows 中运行。

所以,Android 和 ios 设备一般都会采用 SQLite 作为应用的本地数据库。

二、SQL 语法

SQL 语法是比较重要的,因为无论是什么基于 SQLite 的第三方库,它们在进行数据库操作的时候,最后都会转化为 SQL 语句,这个时候即使遇到 bug,我们也能够迎刃而解。

推荐看一下基础教程: SQLite教程

数据库的语言学习可以分为三个部分:

1. 创建数据库

在进行学习基础语言之前,我们需要了解一下如何创建一个数据库,它通常对应着一个 .db 文件。

如果是在项目中,我们通常会在代码中进行配置,在进入 App 中就会生成对应的 .db 文件。

如果是在电脑中,我们以 mac 为例,需要在系统中安装 SQLite,成功以后,输入命令:

sqlite3 数据库名
复制代码

就可以成功创建一个数据库。

2. 数据库定义语言

先说一下我们的目标,要建一个用户收藏商品的关系,思考一下,我们要建立几张表?

没错需要建立三张表,分别是 User (用户表)、 Goods (商品表) 和 Fav_Goods (收藏表)。 UserGoods 是独立的表, 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. 数据库查询语言

最复杂的就是数据库的查询,里面使用了很多关键字,我用了一张脑图来表达:

对应的命令是:

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语法的时候,使用的用户收藏商品,这里我改成了用户收藏鞋子,三张表依次是 usershoefav_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 实体中的方法来看,显然是不会的,如果我们想要在得到结果的时候也要获取到外键相关的对象,我们就要做更多的事情,我们在设计实体的时候,就可以考虑把这些东西加进来了。

除了创建基础的属性以外,我还创建了跟外键相关的对象属性 usershoe

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. 插入数据

插入数据有两种方法, rawInsertinsert 方法, 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 语句的时候,很容易发生小问题,更推荐直接使用第二种方法。

后面 rawUpdaterawDeleterawQuery 就不讲解了。

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
};
}
}
复制代码

上面的 usershoe 对象就不用管了。

再谈三,当相同主键的记录再一次插入的时候,我们处理冲突规则如下:

规则 解释
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;
}
复制代码

可以看到,我是在进行过一次收藏记录查询的同时,又根据 shoeIdShoe 表进行了一次查询,假设我现在还想获取到用户的信息,那么就需要对 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 语法的掌握。

如果你有更好的理解,欢迎下方评论交流,如果你觉得本文不错,三连是对我最好的肯定!

微信扫一扫,分享到朋友圈

Flutter 数据库指南

(十四) 整合spring cloud云架构 - Spring Cloud构建分布式电子商务平台

上一篇

物理内存充足,但是为什么用代码总申请不到内存呢?

下一篇

你也可能喜欢

Flutter 数据库指南

长按储存图像,分享给朋友