# 数据持久化-Sqflite

有一些应用场景需要不仅持久化存储数据,还需要对数据进行:

  • 索引
  • 大量数据
  • 图片
  • ...

# sqflite介绍

就是Flutter的SQLite (opens new window)插件,支持iOS、Android和MacOS。

说明:SQLite 是一个软件库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。

安装:

dependencies:
  sqflite: ^2.0.2

使用flutter pub get进行安装

接下来,需要有一些前置的知识:

  1. 常见的SQL语句
  2. 如何创建表(数据存储的一种称谓,一个形式,类似于Excel)
  3. 如何使用sqflite进行数据的增删改查

# 常见的SQL语句

# 插入

语法

INSERT INTO 语句有两种基本语法,如下所示:

INSERT INTO TABLE_NAME [(column1, column2, column3,...columnN)]  
VALUES (value1, value2, value3,...valueN);

在这里,column1, column2,...columnN 是要插入数据的表中的列的名称。

如果要为表中的所有列添加值,您也可以不需要在 SQLite 查询中指定列名称。但要确保值的顺序与列在表中的顺序一致。SQLite 的 INSERT INTO 语法如下:

INSERT INTO TABLE_NAME VALUES (value1,value2,value3,...valueN);

# Where 子句

SQLite的 WHERE 子句用于指定从一个表或多个表中获取数据的条件。

如果满足给定的条件,即为真(true)时,则从表中返回特定的值。您可以使用 WHERE 子句来过滤记录,只获取需要的记录。

WHERE 子句不仅可用在 SELECT 语句中,它也可用在 UPDATE、DELETE 语句中,等等

# 查询

语法

SQLite 的 SELECT 语句的基本语法如下:

SELECT column1, column2, columnN FROM table_name;

在这里,column1, column2...是表的字段,他们的值即是您要获取的。如果您想获取所有可用的字段,那么可以使用下面的语法:

SELECT * FROM table_name;

# 编辑(更新)

语法

带有 WHERE 子句的 UPDATE 查询的基本语法如下:

UPDATE table_name
SET column1 = value1, column2 = value2...., columnN = valueN
WHERE [condition];

您可以使用 AND 或 OR 运算符来结合 N 个数量的条件。

# 删除

语法

带有 WHERE 子句的 DELETE 查询的基本语法如下:

DELETE FROM table_name
WHERE [condition];

可以使用 AND 或 OR 运算符来结合 N 个数量的条件。

# 初始化工具类

使用sqflite,需要对其初始化,此时应该思考:

  1. 是否需要单例?
  2. 初始化和什么东西有关?
    • 路径
    • 数据库文件
    • 数据库表

如果不清楚还可以,参考官方:

// Get a location using getDatabasesPath
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');

// Delete the database
await deleteDatabase(path);

// open the database
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  // When creating the db, create the table
  await db.execute(
      'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});

// Insert some records in a transaction
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');
});

// Update some record
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, value = ? WHERE name = ?',
    ['updated name', '9876', 'some name']);
print('updated: $count');

// Get the records
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 the records
count = Sqflite
    .firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);

// Delete a record
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// Close the database
await database.close();

但是,上面除了部分有价值的内容以外,没有单例的说明。

// 获取数据库的地址
var databasesPath = await getDatabasesPath();
// 创建数据库及名称
String path = join(databasesPath, 'demo.db');

// or,删除数据库,一般用于更新
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)');
});

单例可以通过构建方法来实现,具体的实现:

import 'package:my_app/config.dart';
import 'package:my_app/entity/user_info.dart';
import 'package:my_app/storage.dart';
import 'package:my_app/utils/functions.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sqlite_api.dart';

class BaseProvider {
  // 1.初始化
  // 2.提供db实例 -> path -> openDatabase
  BaseProvider();

  static final BaseProvider db = BaseProvider();

  late Database _database;

  Future<Database> get database async {
    if (_database != null) {
      return _database;
    } else {
      _database = await initDB();
    }
    return _database;
  }

  initDB() async {
    // 判断路径是否存在,不存在需要创建
    String path = '';
    UserInfo? userInfo = Storage().getUserInfo();
    String databasePath = await getDatabasesPath();
    // 根据不同的场景:public, user
    if (userInfo != null) {
      path = join(databasePath, userInfo.id, Config.privateDBName);
      await makeDir(path);
    } else {
      // 公共的场景
      path = join(databasePath, Config.publicDBName);
    }
    // onCreate -> 创建数据表格
    Database database = await openDatabase(path);
    return database;
  }
}

# 创建表格

sqlite支持的数据类型:

储类 描述
NULL 值是一个 NULL 值。
INTEGER 值是一个带符号的整数,根据值的大小存储在 1、2、3、4、6 或 8 字节中。
REAL 值是一个浮点值,存储为 8 字节的 IEEE 浮点数字。
TEXT 值是一个文本字符串,使用数据库编码(UTF-8、UTF-16BE 或 UTF-16LE)存储。
BLOB 值是一个 blob 数据,完全根据它的输入存储。

其他的类型可以参考:官方文档 (opens new window)

增加代码:

// onCreate -> 创建数据表格
Database database = await openDatabase(
  path,
  version: 1,
  onCreate: (db, version) async {
    // sqlite四种类型 -> NULL, TEXT, INTEGER, BLOB, REAL
    // onCreate -> 创建数据表格
    await db.execute('''CREATE TABLE IF NOT EXISTS posts (
      _id TEXT PRIMARY KEY,
      uid TEXT,
      title TEXT,
      content TEXT,
      created TEXT,
      catalog TEXT,
      fav TEXT,
      isEnd TEXT DEFAULT '0',
      reads INTEGER DEFAULT 0,
      answer INTEGER DEFAULT 0,
      status TEXT DEFAULT '0',
      isTop TEXT DEFAULT '0',
      sort TEXT DEFAULT 100,
      tags TEXT)
      ''');
    // await db.execute ...
  },
);

同时,增加几个实体文件,见课程的资源文件夹(通过icode下载)。

image-20220122181244488

# 业务相关

创建一个首页列表数据的provider类,实现SQL的增删改查:

// 扩展基类
class ContentProvider extends BaseProvider {
  
  // 创建一个静态的方法,这个静态方法与 ContentProvider() 等价
  // 一个方法与构造函数等价,后续的使用方法:ContentProvider.db.[methods]
  static final ContentProvider db = ContentProvider();

  // 这里使用super关键字,调用了基类中的database,这样ContentProvider与基类中的databse为同一个实例,即同一个数据库
  Future<Database> get database async => super.database;
  // ...

创建lib/database/content_provider.dart文件:

import 'package:my_app/database/base_provider.dart';
import 'package:my_app/entity/content/post.dart';
import 'package:sqflite/sqlite_api.dart';

// ContentProvider().db -> public属性或者方法
//
class ContentProvider extends BaseProvider {
  static final ContentProvider db = ContentProvider();

  Future<Database> get database async => super.database;

  // 增
  // list -> 写入到posts -> for性能非常低下,batch insert
  addPostBatch(List<Post> lists) async {
    try {
      final db = await database;
      Batch batch = db.batch();
      lists.forEach((element) {
        batch.insert('posts', element.toJson());
      });
      var res = await batch.commit();
      return res;
    } catch (err) {
      print('err is 👉 $err');
    }
  }

  // 删
  deleteBatch(List<String> ids) async {
    try {
      final db = await database;
      Batch batch = db.batch();
      ids.forEach((element) {
        batch.delete('posts', where: '[_id] = ?', whereArgs: [element]);
      });
      var res = await batch.commit();
      return res;
    } catch (err) {
      print('err is 👉 $err');
    }
  }

  // 查
  getPost(String id) async {
    try {
      final db = await database;
      var res = await db.query('posts', where: '[_id] = ?', whereArgs: [id]);
      // List
      if (res.length == 0) return null;
      List<Post> lists = res.map((e) => Post.fromJson(e)).toList();
      return lists[0];
    } catch (err) {
      print('err is 👉 $err');
    }
  }

  getPostsById(List<String> ids) async {
    try {
      final db = await database;
      // SELECT * FROM posts WHERER [_id] IN ("id1","id2","id3")
      // ->rawQuery
      String sql = 'SELECT * FROM posts WHERER [_id] IN ("' +
          ids.join(',').replaceAll(',', '","') +
          '")';
      var res = await db.rawQuery(sql);
      if (res.length == 0) return null;
      List<Post> lists = res.map((e) => Post.fromJson(e)).toList();
      return lists;
    } catch (err) {
      print('err is 👉 $err');
    }
  }

  // 改
  // toJson -> Array & Object -> SQLite
  updatePost(List<Post> lists) async {
    try {
      final db = await database;
      Batch batch = db.batch();
      lists.forEach((element) {
        batch.update('posts', element.toJson(), where: '[_id] = ?', whereArgs: [element.id]);
      });
      var res = await batch.commit();
      return res;
    } catch (err) {
      print('err is 👉 $err');
    }
  }
}

# 调试sqflite

打印日志:

  • 数据库路径
  • 打印请求的列表数据

注意本地的数据库路径会在重新启动了之后,发生变化,所以最好平时开始的时候进行打印,如果需要查看数据库中的内容,可以使用navicat一类的数据库查看软件进行查看。

image-20220122203933993

无需配置密码。

使用在线工具jsonto

image-20220122203849286

原因是由于uid可能是null,可能是一个map数据。map数据不能直接入库,库中只能存放TEXT类型的数据!

定义三个工具方法:

encodeListOrMap<T>(data) {
  return data is T ? json.encode(data) : data;
}

decodeList<T>(data) {
  return data is List
      ? data.cast<T>()
      : json.decode(data).map<T>((e) => e as T).toList();
}

decodeMap(data) {
  return data is Map
      ? data as Map<String, dynamic>
      : json.decode(data) as Map<String, dynamic>;
  // jsonData['uid'] is Map
  //         ? jsonData['uid'] as Map<String, dynamic>
  //         : json.decode(jsonData['uid']) as Map<String, dynamic>
}

修改post.dart文件:

import 'dart:convert';

import 'package:json_annotation/json_annotation.dart';
import 'package:my_app/utils/functions.dart';

part 'post.g.dart';


()
class Post extends Object {
  (name: 'isEnd')
  String isEnd;

  (name: 'reads')
  int reads;

  (name: 'answer')
  int answer;

  (name: 'status')
  String status;

  (name: 'isTop')
  String isTop;

  (name: 'sort')
  String sort;

  (name: 'tags')
  List<String> tags;

  (name: '_id')
  String id;

  (name: 'title')
  String title;

  (name: 'content')
  String content;

  (name: 'catalog')
  String catalog;

  (name: 'fav')
  String fav;

  (name: 'uid')
  Uid uid;

  (name: 'created')
  String created;


  Post(
    this.isEnd,
    this.reads,
    this.answer,
    this.status,
    this.isTop,
    this.sort,
    this.tags,
    this.id,
    this.title,
    this.content,
    this.catalog,
    this.fav,
    this.uid,
    this.created,
  );

  factory Post.fromJson(Map<String, dynamic> srcJson) =>
      _$PostFromJson(srcJson);

  Map<String, dynamic> toJson() => _$PostToJson(this);
}

()
class Uid extends Object {
  (name: 'pic')
  String pic;

  (name: 'isVip')
  String isVip;

  (name: '_id')
  String id;

  (name: 'name')
  String name;

  Uid(
    this.pic,
    this.isVip,
    this.id,
    this.name,
  );

  factory Uid.fromJson(Map<String, dynamic> srcJson) => _$UidFromJson(srcJson);

  Map<String, dynamic> toJson() => _$UidToJson(this);
}

修改post.g.dart文件:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'post.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Post _$PostFromJson(Map<String, dynamic> jsonData) => Post(
      jsonData['isEnd'] as String,
      jsonData['reads'] as int,
      jsonData['answer'] as int,
      jsonData['status'] as String,
      jsonData['isTop'] as String,
      jsonData['sort'] as String,
      decodeList<String>(jsonData['tags']),
      jsonData['_id'] as String,
      jsonData['title'] as String,
      jsonData['content'] as String,
      jsonData['catalog'] as String,
      jsonData['fav'] as String,
      Uid.fromJson(decodeMap(jsonData['uid'])),
      jsonData['created'] as String,
    );

Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
      'isEnd': instance.isEnd,
      'reads': instance.reads,
      'answer': instance.answer,
      'status': instance.status,
      'isTop': instance.isTop,
      'sort': instance.sort,
      'tags': encodeListOrMap<List>(instance.tags),
      '_id': instance.id,
      'title': instance.title,
      'content': instance.content,
      'catalog': instance.catalog,
      'fav': instance.fav,
      'uid': encodeListOrMap<Uid>(instance.uid),
      'created': instance.created,
    };

Uid _$UidFromJson(Map<String, dynamic> json) => Uid(
      json['pic'] as String,
      json['isVip'] as String,
      json['_id'] as String,
      json['name'] as String,
    );

Map<String, dynamic> _$UidToJson(Uid instance) => <String, dynamic>{
      'pic': instance.pic,
      'isVip': instance.isVip,
      '_id': instance.id,
      'name': instance.name,
    };