Flutter 二次封装Sqlite 踩坑

1,482 阅读3分钟

安装 Sqlite 插件

首先我们需要安装 Sqlite 插件,我使用的是 2.0.2 版本,目前这个插件下载量挺高的

sqflite: ^2.0.2

创建基类,用来实例化数据库

import 'package:sqflite/sqflite.dart';

abstract class EntityPlus {
	static const String _dbName = "xxx";//数据库名称
  static const int _newVersion = 1;//数据库版本
  static int _oldVersion = 0;//数据库上一个版本
  static String? _dbBasePath;//数据库地址
  static Database? _database;//数据库实例

  EntityPlus() {
  	_initDatabase();
  }

  ///初始化数据库
  Future<Database> _initDatabase() async {
    //获取数据库的位置
    _dbBasePath ??= await getDatabasesPath() + "/$_dbName.db";

    //打开数据库
    _database ??= await openDatabase(
      _dbBasePath!,
      version: _newVersion,
      // onConfigure: (db) { },//数据库初始化时触发的回调
      // onOpen: (db) { },//数据库被打开时触发的回调
      // onCreate: (db, version){},//创建数据库时触发的回调
      onUpgrade: (db, oldVersion, newVersion){//数据库升级时触发的回调
        /*
        	这里需要注意, 在后面时会用到_oldVersion, _oldVersion的变化会触发子类的某些方法
        */
        _oldVersion = old;
      },
      onDowngrade: (db, oldVersion, newVersion){//数据库降级时触发的回调
        /*
        	这里需要注意, 在后面时会用到_oldVersion, _oldVersion的变化会触发子类的某些方法
        */
        _oldVersion = old;
      },
    );

    return _database!;
  }
}

基类已经做好了初始化数据库的准备, 当子类继承基类时会触发初始化数据库事件, 初始化数据库完成后基类还需要做哪些事情?

  1. 建表, 触发子类建表事件, 但是表如果已经存在了, 重复创建会报错, 所以这个函数只能触发一次;
  2. 数据库升级或降级,触发子类的升级或降级事件, 并且也只能触发一次;

添加建表功能

如何触发子类建表事件, 并且只触发一次? 我们可以在基类中定义创建表的函数,并且该函数在子类中必须重写, 判断该表是否存在,如果不存在则创建

import 'package:sqflite/sqflite.dart';

abstract class EntityPlus {
	//...代码省略
  
  abstract String tableName;//数据表名称,在子类中必须要重写的字段
  bool exists = false;//数据表是否存在

  ///建表函数, 在子类中必须重写
	Future<void> onCreate(Database db, int version);

  EntityPlus() {
  	_initDatabase();
  }

  ///初始化数据库
  Future<Database> _initDatabase() async {
    //...省略获取数据库的位置代码
    //...省略打开数据库代码

    //判断表是否存在
    exists = await tableExists();
    if(!exists){
      //表不存在时调用建表函数
      await onCreate(_database!, _newVersion);
      exists = true;
    }

    return _database!;
  }

  ///判断表是否存在
  Future<bool> tableExists() async {
    //内建表sqlite_master
    var res = await _database!.rawQuery(
      "SELECT * FROM sqlite_master WHERE TYPE = 'table' AND NAME = '$tableName'",
    );
    return res.isNotEmpty;
  }
}

数据库升级或降级

在基类中我们实现了建表的功能, 同理数据库升级或降级也可以这样写

import 'package:sqflite/sqflite.dart';

abstract class EntityPlus {
	//...代码省略
  
  abstract String tableName;//数据表名称,在子类中必须要重写的字段
  bool exists = false;//数据表是否存在

  ///建表函数, 在子类中必须重写
	Future<void> onCreate(Database db, int version);

  ///数据库升级时触发的函数,子类中可以根据需要时进行重写
  onUpgrade(Database db, int oldVersion, int newVersion) {}

  ///数据库降级触发的函数,子类中可以根据需要时进行重写
  onDowngrade(Database db, int oldVersion, int newVersion) {}

  EntityPlus() {
  	_initDatabase();
  }

  ///初始化数据库
  Future<Database> _initDatabase() async {
    //...省略获取数据库的位置代码
    //...省略打开数据库代码
    //...省略建表代码

    //数据第一次创建时_oldVersion等于0, 所以忽略
    if (_oldVersion != 0) {
      if (_oldVersion > _newVersion) {							//判断是否降级了
        print("_oldVersion === $_oldVersion");
        print("_newVersion === $_newVersion");
        //数据库降级了,如果子类重写了onDowngrade方法, 则调用的是子类的;
        await onDowngrade(
          _database!,
          await _database!.getVersion(),
          _newVersion,
        );
      } else if (_oldVersion < _newVersion) {			//判断是否升级了
        print("_oldVersion === $_oldVersion");
        print("_newVersion === $_newVersion");
        //数据库升级了,如果子类重写了onUpgrade方法, 则调用的是子类的;
        await onUpgrade(
          _database!,
          await _database!.getVersion(),
          _newVersion,
        );
      }
    }

    return _database!;
  }
}

简易版增删改查

好了现在我们有了建表的功能, 但是我们还需要对表进行增删改查, 所以接下来我们封装一个简易的增删改查功能

import 'package:sqflite/sqflite.dart';

abstract class EntityPlus {
	//...代码省略

  Database get database {
    return _database!;
  }

  ///插入数据
  insert(Map<String, Object?> values) async {
    return database.insert(tableName, values);
  }

  ///删除数据
  remove(Map<String, Object?> json) async {
    var database = await getDatabase();

    List<String> keys = json.keys.toList();
    List<String> where = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      where.add("$key=${json[key]}");
    }

    return database.delete(
      tableName,
      where: where.join(" and "),
    );
  }

  ///修改数据
  update(Map<String, Object?> json1, Map<String, Object?> json2) async {
    List<String> keys = json1.keys.toList();
    List<String> where = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      if (json1[key].runtimeType == String) {
        where.add("$key='${json1[key]}'");
      } else {
        where.add("$key=${json1[key]}");
      }
    }

    return database.update(
      tableName,
      json2,
      where: where.isEmpty ? null : where.join(" and "),
    );
  }

  ///缓存的数据
  static final Map<String, List<Map<String, Object?>>> _findCache = {};

  ///查找数据
  Future<List<Map<String, Object?>>> find({
    Map<String, dynamic>? where,
    int? page,
    int? pageSize,
  }) async {
    List<String> keys = where?.keys.toList() ?? [];

    List<String> whereList = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      if (where![key].runtimeType == String) {
        whereList.add("$key='${where[key]}'");
      } else {
        whereList.add("$key=${where[key]}");
      }
    }

    String sql = whereList.join(" and ");
    String mapKey = "${tableName}_${sql}_page=${page}_pageSize=$pageSize";

    List data = sql.isEmpty ? [] : (_findCache[mapKey] ?? []);
    if (data.isNotEmpty) {
      return _findCache[mapKey]!;
    }

    var result = await database.query(
      tableName,
      where: sql.isEmpty ? null : sql,
      offset: page == null ? null : (page - 1) * (pageSize ?? 1),
      limit: pageSize,
    );
    if (sql.isNotEmpty) {
      _findCache[mapKey] = result;
    }
    return result;
  }

  rawQuery(String sql) async {
    return database.rawQuery(sql);
  }
}

开始实验

新建一个user_info实体类, 继承EntityPlus

class UserInfoEntity extends EntityPlus {
  @override
  String tableName = "user_info";

  //建表函数,当数据库中没有这个表时,基类会触发这个函数
  @override
  onCreate(db, version) async {
    print("创建 $tableName 数据表");
    await db.execute("""
      CREATE TABLE $tableName (
        id integer primary key autoincrement,
        name TEXT,
        sex INTEGER,
        phone TEXT
      )
    """);
  }

  ///当数据库升级时,基类会触发的函数
  @override
  onUpgrade(db, oldVersion, newVersion) {}

  ///当数据库降级,基类会触发的函数
  @override
  onDowngrade(db, oldVersion, newVersion) {}
}

页面中操作user_info表

class Test extends StatefulWidget {
  const Test({Key? key}) : super(key: key);

  @override
  State<Test> createState() => _TestState();
}

class _TestState extends State<Test> {
  @override
  void initState() {
    super.initState();

    try {
      UserInfoEntity userInfoEntity = UserInfoEntity();
      userInfoEntity.find();
    } catch (err) {
      //注意由于打开数据库属于异步操作, 虽然UserInfoEntity已经实例化了, 但是数据库的初始化并没有完成
      print("报错了: $err");
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

此时我们需要考虑一些问题:

  1. 打开数据库属于异步操作, 直接实例化时, 数据库并没有初始化完成, 此时操作数据库肯定会发生异常;
  2. 每次操作数据库都要实例化一次是不是太浪费内存了;

基于以上问题, 我们可以将 UserInfoEntity 改成单例模式

class UserInfoEntity extends EntityPlus {
  static UserInfoEntity? _ins;

  UserInfoEntity._();

  static UserInfoEntity instan() {
    if (_ins == null) {
      print("实例化");
    }
    return _ins ??= UserInfoEntity._();
  }
  
  @override
  String tableName = "user_info";

  //建表函数,当数据库中没有这个表时,基类会触发这个函数
  @override
  onCreate(db, version) async {
    print("创建 $tableName 数据表");
    await db.execute("""
      CREATE TABLE $tableName (
        id integer primary key autoincrement,
        name TEXT,
        sex INTEGER,
        phone TEXT
      )
    """);
  }

  ///当数据库升级时,基类会触发的函数
  @override
  onUpgrade(db, oldVersion, newVersion) {}

  ///当数据库降级,基类会触发的函数
  @override
  onDowngrade(db, oldVersion, newVersion) {}
}

然后再新建一个类,用来存放所有实体类, 将所有实体类先进行实例化

import 'entitys/user_info.entity.dart';

class MySqlite {
  static forFeature() async {
    var list = [
      UserInfoEntity.instan(),
      //...其他的表实体类
    ];
    for (int i = 0; i < list.length; i++) {
      var entity = list[i];

      //是否还记得基类中定义的 exists 字段,这是用来判断表是否创建完成
      while (!entity.exists) {
        //等待数据表创建完成
        await Future.delayed(const Duration(milliseconds: 60), () {});
      }
    }
  }
}

由于第一次安装应用, 设备并不存在我们需要的数据表, 所以还需要等待数据表的创建完成, 防止进入页面时直接对表进行操作时发生异常

在main函数中执行 MySqlite.forFeature, 等数据库初始化完成, 并且所有的表都已经创建完成, 再执行下面的代码

void main() async {
  await MySqlite.forFeature();
  runApp(const MyApp());
}

需要注意一个问题, 等待数据库和表的初始化完成是需要时间的, 这种方式会阻塞页面, 如果表很多的话白屏时间会加长, 可以将这个方法放在启动页中, 等初始化完毕后再进入主页面, 因为我的程序数据表并不多, 所以我就直接写在mian函数中了;

此时再页面中可以直接对数据库进行增删改查的操作了

UserInfoEntity userInfoEntity = UserInfoEntity.instan();

userInfoEntity.insert({
  "name": "刘小明",
  "sex": 1,
  "phone": "123456789",
});
userInfoEntity.find();
userInfoEntity.update();
userInfoEntity.remove({"id": 1});

基类的完整代码

import 'package:sqflite/sqflite.dart';

abstract class EntityPlus {
  static const String _dbName = "zjdk_plus";
  static const int _newVersion = 1;
  static int _oldVersion = 0;
  static String? _dbBasePath;

  static Database? _database;

  ///表名称
  abstract String tableName;

  ///表是否存在
  bool exists = false;

  ///数据库实例化完成
  onReload(Database db, int version) {}

  ///创建表
  Future<void> onCreate(Database db, int version);

  ///更新表
  onUpgrade(Database db, int oldVersion, int newVersion) {}

  ///数据库降级
  onDowngrade(Database db, int oldVersion, int newVersion) {}

  EntityPlus() {
    _initDatabase();
  }

  ///创建数据库
  Future<Database> _initDatabase() async {
    _dbBasePath ??= await getDatabasesPath() + "/$_dbName.db";
    _database ??= await openDatabase(
      _dbBasePath!,
      version: _newVersion,
      // onConfigure: (db) { },
      // onCreate: onCreate,
      onUpgrade: (db, old, newV) {
        _oldVersion = old;
      },
      onDowngrade: (db, old, newV) {
        _oldVersion = old;
      },
      // onOpen: onOpen,
    );

    onReload(_database!, _newVersion);

    //判断表是否存在
    exists = await tableExists();
    if (!exists) {
      await onCreate(_database!, _newVersion);
      exists = true;
    }

    if (_oldVersion != 0) {
      if (_oldVersion > _newVersion) {
        print("_oldVersion === $_oldVersion");
        print("_newVersion === $_newVersion");
        //数据库降级了
        await onDowngrade(
          _database!,
          await _database!.getVersion(),
          _newVersion,
        );
      } else if (_oldVersion < _newVersion) {
        print("_oldVersion === $_oldVersion");
        print("_newVersion === $_newVersion");
        //数据库升级了
        await onUpgrade(
          _database!,
          await _database!.getVersion(),
          _newVersion,
        );
      }
    }

    return _database!;
  }

  ///表是否存在
  Future<bool> tableExists() async {
    //内建表sqlite_master
    var res = await _database!.rawQuery(
      "SELECT * FROM sqlite_master WHERE TYPE = 'table' AND NAME = '$tableName'",
    );
    return res.isNotEmpty;
  }

  ///表列是否存在
  Future<bool> columnExists(String columnName) async {
    var result = await _database!.rawQuery("""
      SELECT sql FROM sqlite_master WHERE type='table' AND name='$tableName' COLLATE NOCASE limit 1
    """);
    String sql = result[0]["sql"] as String;
    int startIndex = sql.indexOf("(") + 1;
    int endIndex = sql.indexOf(")");
    sql = sql.substring(startIndex, endIndex);

    List<String> sqlList = sql.split(",").map((e) => e.trim()).toList();
    bool exists = false;
    for (int j = 0; j < sqlList.length; j++) {
      var rowStr = sqlList[j].trim().split(",").join("");
      var colName = rowStr.split(" ")[0].trim();
      if (colName == columnName) {
        exists = true;
        break;
      }
    }
    return exists;
  }

  ///新增列
  Future addColumn(String columnName, String type) async {
    return await _database!.rawQuery("""
      ALTER TABLE $tableName ADD  $columnName $type
    """);
  }

  ///删表
  dropTable() async {
    if (_database == null) {
      await _initDatabase();
    }
    await _database!.execute("""
      drop table if exists $tableName;
    """);
  }

  Database get database => _database!;

  ///插入数据
  insert(Map<String, Object?> values) async {
    return database.insert(tableName, values);
  }

  ///删除数据
  remove(Map<String, Object?> json) async {
    List<String> keys = json.keys.toList();
    List<String> where = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      where.add("$key=${json[key]}");
    }

    return database.delete(
      tableName,
      where: where.join(" and "),
    );
  }

  ///修改数据
  update(Map<String, Object?> json1, Map<String, Object?> json2) async {
    List<String> keys = json1.keys.toList();
    List<String> where = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      if (json1[key].runtimeType == String) {
        where.add("$key='${json1[key]}'");
      } else {
        where.add("$key=${json1[key]}");
      }
    }

    return database.update(
      tableName,
      json2,
      where: where.isEmpty ? null : where.join(" and "),
    );
  }

  ///缓存的数据
  static final Map<String, List<Map<String, Object?>>> _findCache = {};

  ///查找数据
  Future<List<Map<String, Object?>>> find({
    Map<String, dynamic>? where,
    int? page,
    int? pageSize,
  }) async {
    List<String> keys = where?.keys.toList() ?? [];

    List<String> whereList = [];
    for (int i = 0; i < keys.length; i++) {
      String key = keys[i];
      if (where![key].runtimeType == String) {
        whereList.add("$key='${where[key]}'");
      } else {
        whereList.add("$key=${where[key]}");
      }
    }

    String sql = whereList.join(" and ");
    String mapKey = "${tableName}_${sql}_page=${page}_pageSize=$pageSize";

    List data = sql.isEmpty ? [] : (_findCache[mapKey] ?? []);
    if (data.isNotEmpty) {
      return _findCache[mapKey]!;
    }

    var result = await database.query(
      tableName,
      where: sql.isEmpty ? null : sql,
      offset: page == null ? null : (page - 1) * (pageSize ?? 1),
      limit: pageSize,
    );
    if (sql.isNotEmpty) {
      _findCache[mapKey] = result;
    }
    return result;
  }

  rawQuery(String sql) async {
    return database.rawQuery(sql);
  }
}

总结

鉴于客户端的特殊性与服务端不同,服务端数据库是提前创建好的,在接口处再进行操作;而客户端应用被打开时可能就会对数据库进行操作,并且每个客户端都需要有数据库,以及数据表,所以我们需要等待数据库的初始化完成,以及表的创建完成,才能进行其他操作,在此之前不能对数据库进行任何操作,否则发生报错,而这中间等待的过程我们可以先显示一个启动页,等所有的初始化完成后再显示业务页面;