Flutter 二次封装Sqlite基类
- 发表于
- flutter
安装 Sqlite 插件
首先我们需要安装 Sqlite 插件
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!;
}
}
基类已经做好了初始化数据库的准备, 当子类继承基类时会触发初始化数据库事件, 初始化数据库完成后基类还需要做哪些事情?
- 建表, 触发子类建表事件, 但是表如果已经存在了, 重复创建会报错, 所以这个函数只能触发一次;
- 数据库升级或降级,触发子类的升级或降级事件, 并且也只能触发一次;
添加建表功能
如何触发子类建表事件, 并且只触发一次? 我们可以在基类中定义创建表的函数,并且该函数在子类中必须重写, 判断该表是否存在,如果不存在则创建
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();
}
}
此时我们需要考虑一些问题:
- 打开数据库属于异步操作, 直接实例化时, 数据库并没有初始化完成, 此时操作数据库肯定会发生异常;
- 每次操作数据库都要实例化一次是不是太浪费内存了;
基于以上问题, 我们可以将 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);
}
}
作者:向往全栈的Web小白
原文连接:Flutter 二次封装Sqlite基类
所有媒体,可在保留署名、
原文连接
的情况下转载,若非则不得使用我方内容。