Flutter 数据库指南

4,640 阅读12分钟

技术不止,文章有料,关注公众号 九心说,每周一篇高质好文,和九心在大厂路上肩并肩。

最近一段时间,在学 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教程

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

SQL语言

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 语法的掌握。

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

打工人