flutter 持久化存储-----数据库sqflite|8月更文挑战

1,918 阅读3分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

Flutter中持久化存储数据有多种方案, 一般常用的有 shared_preferencessqfite

  • shared_preferences: 包含NSUserDefaults(在iOS上)和SharedPreferences(在Android上),为简单数据提供持久存储。数据以异步方式持久保存到磁盘。

  • sqflite: 是一款轻量级的关系型数据库,类似SQLite. 支持iOS和Android。适用于存储数据库 , 表类型的数据.

sqflite的使用

添加依赖:

作者所用版本为1.1.3

dependencies:
  ...
  sqflite: ^1.1.3

为了方便,外面创建一个DatabaseHelper来封装一些数据库的相关操作:

先贴一下代码 ,然后我们逐行分析:

/*
 * author: Created by 李卓原 on 2019/3/12.
 * email: zhuoyuan93@gmail.com
 *
 */

import 'dart:async';

import 'package:path/path.dart';
import 'package:sale_aggregator_app/models/video.dart';
import 'package:sqflite/sqflite.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = new DatabaseHelper.internal();

  factory DatabaseHelper() => _instance;

  final String tableVideo = 'VideoTable';
  final String columnId = 'id';
  final String image = 'image';
  final String url = 'url';
  final String duration = 'duration';
  final String title = 'title';
  final String favoriteStatus = 'favorite_status';

  static Database _db;

  DatabaseHelper.internal();

  Future<Database> get db async {
    if (_db != null) {
      return _db;
    }
    _db = await initDb();

    return _db;
  }

  initDb() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'flashgo.db');

    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
    return db;
  }

  void _onCreate(Database db, int newVersion) async {
    await db.execute(
        'CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT)');
  }

  Future<int> insertVideo(Video video) async {
    var dbClient = await db;
    var result = await dbClient.insert(tableVideo, video.toJson());

    return result;
  }

  Future<List> selectVideos({int limit, int offset}) async {
    var dbClient = await db;
    var result = await dbClient.query(
      tableVideo,
      columns: [columnId, image, url, duration, title, favoriteStatus],
      limit: limit,
      offset: offset,
    );
    List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));
    return videos;
  }

  Future<int> getCount() async {
    var dbClient = await db;
    return Sqflite.firstIntValue(
        await dbClient.rawQuery('SELECT COUNT(*) FROM $tableVideo'));
  }

  Future<Video> getVideo(int id) async {
    var dbClient = await db;
    List<Map> result = await dbClient.query(tableVideo,
        columns: [columnId, image, url, duration, title, favoriteStatus],
        where: '$id = ?',
        whereArgs: [id]);

    if (result.length > 0) {
      return Video.fromSql(result.first);
    }

    return null;
  }

  Future<int> deleteNote(String images) async {
    var dbClient = await db;
    return await dbClient
        .delete(tableVideo, where: '$image = ?', whereArgs: [images]);
  }

  Future<int> updateNote(Video video) async {
    var dbClient = await db;
    return await dbClient.update(tableVideo, video.toJson(),
        where: "$columnId = ?", whereArgs: [video.id]);
  }

  Future close() async {
    var dbClient = await db;
    return dbClient.close();
  }
}


可以看到我们在执行相关方法的时候都会先获得db, db会执行一个initDb方法,用于创建数据库和表


  initDb() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'flashgo.db');

    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
    return db;
  }
  
  void _onCreate(Database db, int newVersion) async {
    await db.execute(
        'CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT)');
  }
  • getDatabasesPath() : 获取默认数据库位置(在Android上,它通常是data/data/<package_name>/databases,在iOS上,它是Documents目录)
  • join(databasesPath, 'flashgo.db'): 相当于在上述方法获取到的位置创建了一个名为flashgo的数据库.
  • openDatabase: 按指定路径打开数据库 , 路径就是上面flash.db的路径,version为数据库版本号,onCreate是创建表的方法

然后是我定义的一个实体类,看一下代码:

class Video {
  int id;
  String image;
  String url;
  int duration;

  String title;
  bool favoriteStatus;

  Video(
      {this.id,
      this.image,
      this.url,
      this.duration,
      this.title,
      this.favoriteStatus});

  Video.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    image = json['image'];
    url = json['url'];
    duration = json['duration'];
    title = json['title'];
    favoriteStatus = json['favorite_status'];
  }

  Video.fromSql(Map<String, dynamic> json) {
    id = json['id'];
    image = json['image'];
    url = json['url'];
    duration = json['duration'];
    title = json['title'];
    favoriteStatus = json['favorite_status'] == 'true';
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['image'] = this.image;
    data['url'] = this.url;
    data['duration'] = this.duration;
    data['title'] = this.title;
    data['favorite_status'] = this.favoriteStatus;
    return data;
  }
}

细心的同学可能看到了一个与众不同的方法: fromSql , 其中的json['favorite_status'] 是字符串类型, 为什么不依然用bool型呢, 因为sqlite不支持bool型.

上文中,CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT) 这个建表方法可以看到 id用的是integer, 其他都是用的text.

我们看一下sqlite都支持哪些数据类型吧:


sql存储数据类型

每个存储在 SQLite 数据库中的值都具有以下存储类之一:

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

数据库和表已经准备就绪了,那么该看一看它的增删改查了

插入数据
  Future<int> insertVideo(Video video) async {
    var dbClient = await db;
    var result = await dbClient.insert(tableVideo, video.toJson());

    return result;
  }

这里是我封装的一个插入数据的方法,参数是video的对象, 但是可以看到insert方法第二个参数是一个json数据,所以其实也可以直接传递json数据.而不是传一个对象再转成json.

查询方法

Future<List> selectVideos({int limit, int offset}) async {
    var dbClient = await db;
    var result = await dbClient.query(
      tableVideo,
      columns: [columnId, image, url, duration, title, favoriteStatus],
      limit: limit,
      offset: offset,
    );
    List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));
    return videos;
  }

主要是调用了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});

有一个必传参数是表名, 然后有很多修饰, 比如 limit : 是要查询多少条数据, offset :是从哪里开始查. columns: 是要查询哪几列 where: 是查询条件,这里我是查询所有的所以没有设置.

limit 和offset 这两个也是最常用的属性,所以我封装方法的时候允许设置这两个参数. 如果你有多张表,多个列需要查询,我建议各自封装方法,不然的话,需要传入的参数过于复杂便失去了封装的意义.

这里的查询方法返回的是json数据,且要记住,是只有integer和text类型的,所以想要bool一定要自己处理

List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));

所以这里,我新建了一个fromSql的方法,把查询出来的json数据转成我想要的类型的对象.

查询单个
  Future<Video> getVideo(int id) async {
    var dbClient = await db;
    List<Map> result = await dbClient.query(tableVideo,
        columns: [columnId, image, url, duration, title, favoriteStatus],
        where: '$id = ?',
        whereArgs: [id]);

    if (result.length > 0) {
      return Video.fromSql(result.first);
    }

    return null;
  }

思路同上,只是要多了一个where,即查询条件,这里我是根据id来查 所以只传入了一个id参数. 返回查询到的结果(json类型) . 如果查询不到,则返回一个null.

更改数据

  Future<int> updateVideo(Video video) async {
    var dbClient = await db;
    return await dbClient.update(tableVideo, video.toJson(),
        where: "$columnId = ?", whereArgs: [video.id]);
  }

这个代码的逻辑是

  1. 传入更改后的数据
  2. 根据传入的数据id找到对应数据
  3. 更新数据

删除数据

  Future<int> deleteVideo(String images) async {
    var dbClient = await db;
    return await dbClient
        .delete(tableVideo, where: '$image = ?', whereArgs: [images]);
  }

其实逻辑和查询是一样的,我这是根据image来查找并删除.也可以用id或者其他数据.

获取数据的数量

  Future<int> getCount() async {
    var dbClient = await db;
    return Sqflite.firstIntValue(
        await dbClient.rawQuery('SELECT COUNT(*) FROM $tableVideo'));
  }

此方法用来查询表中有多少条数据. 这我用了rawQuery方法, 它是支持直接使用sql语句进行查询的. 因为该结果返回一个列表,所以使用Sqflite.firstIntValue来获取其中的第一个值.

关闭方法

  Future close() async {
    var dbClient = await db;
    return dbClient.close();
  }

在操作执行完毕后 , 记得关闭数据库.关闭之后无法再访问数据库.

以上是对代码的分析,下面看一下实际的使用:

  //把视频列表存到数据库以备用
  void saveVideos(List<Video> videos) async {
    var db = DatabaseHelper();
    videos.forEach((v) => db.insertVideo(v));
    db.close();
  }

相关代码尽在[github]flutter_study/sqflite_page.dart at master · lizhuoyuan/flutter_study (github.com))