Flutter使用sqflite数据库,批量操作

在pubspec.yaml引入sqlite插件,记得执行flutter packages get

flutter:
    sdk: flutter
sqflite: ^1.2.0

最新版本到这里搜索

接下来,以我的项目为例(小说阅读器)
项目中需要保存小说的数据、书的目录,阅读记录等

1.首先创建一个类,用来管理数据库和表

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class ReadSQL{
  static const String tableShelf = "bookShelf";
  static const String tableCatalog = "bookCatalog";

  final String dropTableShelf = "DROP TABLE IF EXISTS $tableShelf";
  final String createTableShelf = "CREATE TABLE $tableShelf (id INTEGER PRIMARY KEY AUTOINCREMENT, bookId TEXT"
      ",title TEXT,readers REAL,briefIntro TEXT,charactersCount INTEGER"
      ",chaptersCount INTEGER,cover TEXT,serialState INTEGER"
      ",majorCateId TEXT,majorCateName TEXT"
      ",minorCateId TEXT,minorCateName TEXT"
      ",ratio REAL,author TEXT,lastReadTime INTEGER)";


  final String dropTableCatalog = "DROP TABLE IF EXISTS $tableCatalog";
  final String createTableCatalog = "CREATE TABLE $tableCatalog (id INTEGER PRIMARY KEY AUTOINCREMENT, mixCode TEXT"
      ",bookId TEXT,chapterTitle TEXT,chapterId INTEGER,status INTEGER)";

  Database _db;

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

  _initDb() async{
    String basePath = await getDatabasesPath();
    String path = join(basePath,"read.db");
    Database db = await openDatabase(path,version: 2,onCreate: _onCreate,onUpgrade: _onUpgrade);
    return db;

  }

  Future close() async {
    var result = _db.close();
    _db = null;
    return result;
  }

  void _onCreate(Database db, int newVersion) async{
    print("_onCreate newVersion:$newVersion");
    var batch = db.batch();
    batch.execute(dropTableShelf);
    batch.execute(createTableShelf);

    batch.execute(dropTableCatalog);
    batch.execute(createTableCatalog);
    await batch.commit();
  }

  void _onUpgrade(Database db, int oldVersion,int newVersion)async{
    print("_onUpgrade oldVersion:$oldVersion");
    print("_onUpgrade newVersion:$newVersion");

    var batch = db.batch();
    if(oldVersion == 1){
      batch.execute(dropTableCatalog);
      batch.execute(createTableCatalog);
    }

    await batch.commit();

  }
}

该类主要用来创建表和升级表。
说下升级的问题:

1.第一个版本version=1,创建了表bookShelf,用来保存书籍到书架。
2.接着又要添加一个目录表bookCatalog(记得version改为2),所以才有上面的代码
如果是新用户,直接执行是_onCreate方法,日志打印newVersion=2;
升级上来的用户执行_onUpgrade方法,然后做个判断,创建表bookCatalog。
如果要升级修改某个表的字段看这个链接

2.创建具体对象

import 'package:json_annotation/json_annotation.dart';

part 'bookChapter.g.dart';
@JsonSerializable()
class BookChapter{
  int chapterId;
  String chapterTitle;
  int status;
  @JsonKey(ignore: true)
  String mixCode;
  @JsonKey(ignore: true)
  String bookId;

  BookChapter(this.chapterId, this.chapterTitle, this.status,{this.mixCode,this.bookId});

  factory BookChapter.fromJson(Map<String,dynamic> json) => _$BookChapterFromJson(json);
  Map<String, dynamic> toJson() => _$BookChapterToJson(this);

  Map<String, dynamic> toMap() {
    final Map<String, dynamic> data = new Map<String, dynamic>();

    data['mixCode'] = this.mixCode;
    data['bookId'] = this.bookId;
    data['chapterTitle'] = this.chapterTitle;
    data['chapterId'] = this.chapterId;
    data['status'] = this.status;
    return data;
  }

  BookChapter.fromMap(Map<String, dynamic> data) {
    this.mixCode = data['mixCode'];
    this.bookId = data['bookId'];
    this.chapterTitle = data['chapterTitle'];
    this.chapterId = data['chapterId'];
    this.status = data['status'];
  }

  @override
  String toString() {
    return 'BookChapter{chapterId: $chapterId, chapterTitle: $chapterTitle, status: $status}';
  }
}

说明:

类中有些字段使用了@JsonKey关键字,代表这个字段不用使用json自动化写到BookChapterFromJson类中,和表没有关系,关于json相关,看flutter中文网相关介绍

3.写个帮助类,具体对某个表的操作(例如bookCatalog表)


import 'package:sqflite/sqflite.dart';
import 'package:tangerine_read/common/app_constant.dart';
import 'package:tangerine_read/local/database/read_sql.dart';
import 'package:tangerine_read/model/bookChapter.dart';

class BookCatalogHelper{
  ReadSQL _sql;

  static final BookCatalogHelper _instance = new BookCatalogHelper.internal();
  static BookCatalogHelper get instance => _instance;
  BookCatalogHelper.internal(){
    _sql = ReadSQL();
  }

  Future close() async {
    var result = _sql.close();
    return result;
  }


  Future<bool> isBookHasCatalog(String bookId)async{
    var dbClient = await _sql.db;
    int count = Sqflite.firstIntValue(await dbClient.rawQuery("SELECT COUNT(*) FROM ${ReadSQL.tableCatalog} WHERE bookId=$bookId"));

    return count > 0;
  }


  Future<int> getBookCatalogCount(String bookId)async{
    var dbClient = await _sql.db;
    int count = Sqflite.firstIntValue(await dbClient.rawQuery("SELECT COUNT(*) FROM ${ReadSQL.tableCatalog} WHERE bookId=$bookId"));

    return count;
  }

  ///目录保存sql比较复杂
  ///需要考虑该是否 连载或者完结
  ///连载的书每次都要更新目录
  Future<int> saveCatalogs(String bookId,List<BookChapter> catalog)async{
    if(catalog == null || catalog.isEmpty){
      return 0;
    }

    ///因为考虑每一章的信息唯一,所以要给目录加上一个mixCode
    catalog.forEach((bookChapter){
      bookChapter.mixCode = bookId + bookChapter.chapterId.toString();
      bookChapter.bookId = bookId;
    });

    var dbClient = await _sql.db;
    bool has = await isBookHasCatalog(bookId);

    if(has){
      if(AppConstant.DEBUG){
        print("saveCatalogs delete");
      }
      ///delete
      int deleteCount = await dbClient.rawDelete("DELETE FROM ${ReadSQL.tableCatalog} WHERE bookId=$bookId");
      if(AppConstant.DEBUG){
        print("saveCatalogs deleteCount:$deleteCount");
      }
    }

    ///批量往表中加数据
    int startTime = DateTime.now().millisecondsSinceEpoch;
    var batch = dbClient.batch();
    catalog.forEach((log){
      batch.insert(ReadSQL.tableCatalog, log.toMap());
    });

    batch.commit();
    if(AppConstant.DEBUG){
       print("saveCatalogs insertCount:${catalog.length}");
       print("saveCatalogstime:${DateTime.now().millisecondsSinceEpoch - startTime}");
    }

    return catalog.length;
  }
  
  Future<List<BookChapter>> getCatalogs(String bookId)async{
    var dbClient = await _sql.db;
    List<Map> maps = await dbClient.rawQuery("SELECT * FROM ${ReadSQL.tableCatalog} WHERE bookId=$bookId");
    if (maps == null || maps.length == 0) {
      return null;
    }

    List<BookChapter> list = [];
    maps.forEach((it){
      list.add(BookChapter.fromMap(it));
    });

    return list;

  }

  void deleteCatalog(String bookId)async{
    var dbClient = await _sql.db;
    var deleteCount = await dbClient.rawDelete("DELETE FROM ${ReadSQL.tableCatalog} WHERE bookId IN ($bookId)");
    if(AppConstant.DEBUG){
       print("deleteCatalog deleteCount:$deleteCount");
    }
  }
}

目前为止已经对一个表的具体操作已经写完了。
可是并没有完全满足增删改查的需要,因为确实我这个表用不到更新

下面的例子是补充对数据表增删改查的操作,不是对这个bookCatalog表了,但不影响功能演示

1.查询

 ///
  //      var dbClient = await _sql.db;
  //      List<Map> maps = await dbClient.query(ReadSQL.tableShelf,
  //      columns: ["bookId","title","readers","briefIntro","charactersCount",
  //      "chaptersCount","cover","serialState","majorCateId",
  //      "majorCateName","minorCateId","minorCateName","ratio","author","lastReadTime"],
  //      where: "bookId = ?",
  //      whereArgs: [bookId]
  //      );
  ///
  Future<BookShelf> getBook(String bookId)async{
    var dbClient = await _sql.db;
    List<Map> maps = await dbClient.rawQuery("SELECT * FROM ${ReadSQL.tableShelf} WHERE bookId=$bookId");
    if(maps.length > 0){
      return BookShelf.fromMap(maps.first);
    }

    return null;
  }

查询没有使用dbClient.query方法,我觉得没有使用dbClient.rawQuery自己写查询语句来的方便,这个看具体开发人员的习惯

2.更新和插入

Future<int> saveBook(BookShelf bookShelf)async{
    bookShelf.lastReadTime = DateTime.now().millisecondsSinceEpoch;
    var dbClient = await _sql.db;

    List<Map> hasList = await dbClient.rawQuery("SELECT * FROM ${ReadSQL.tableShelf} WHERE bookId=${bookShelf.bookId}");

    var result = 0;
    if(hasList.length > 0){
      if(AppConstant.DEBUG){
         print("hasList.length > 0");
      }
      result = await dbClient.update(ReadSQL.tableShelf, bookShelf.toMap(),where: "bookId = ?",whereArgs: [bookShelf.bookId]);
    }else{
      if(AppConstant.DEBUG){
         print("hasList.length else");
      }
      result =  await dbClient.insert(ReadSQL.tableShelf, bookShelf.toMap());
    }
    return result;
  }

这里我的需求就是保存书籍到书架,如果没有保存过,就直接插入数据,
如果已经在书架,每次需要更新时间,以便书架依据时间排序

3.删除(批量删除,2种方式)

 void deleteBooks(List<BookShelf> bookList)async{
    var dbClient = await _sql.db;
    String in_book = "";
    for(int i=0;i<bookList.length;i++){
      in_book += "${bookList[i].bookId}";
      if(i != bookList.length-1){
        in_book+=",";
      }
    }
    dbClient.rawDelete("DELETE FROM ${ReadSQL.tableShelf} WHERE bookId IN ($in_book)");

  }

 ///该方法证明效率高
 void deleteBooks(List<BookShelf> bookList)async{
    var dbClient = await _sql.db;
    var batch = dbClient.batch();
    bookList.forEach((book){
      batch.rawDelete("DELETE FROM ${ReadSQL.tableShelf} WHERE bookId = ${book.bookId}");
    });
    batch.commit();
  }

这里的删除,删除的可能是一本或者多本书,用到了sql的删除语句的IN操作符, 当然可以使用batch这种批量删,后面我会接着说下,批量操作的效率问题

IN 操作符允许您在 WHERE 子句中规定多个值。
SELECT column_name(s) FROM table_name
 WHERE column_name IN (value1,value2,...);

删除的效率问题

先说结果,使用batch方式最好!!

先说下我的需求,小说要加入本地搜索,解决方法就是第一次运行时,把定义好的书的json数据,批量插入一个表中,用户在搜索界面就可以搜书了(我们的书毕竟不多,不到1200本,没有必要走网络搜索)

最开始我是使用这种方式:

initLocalBooks(List<BookLocalData> books)async{
 var dbClient = await _sql.db;
 int count = Sqflite.firstIntValue(await dbClient.rawQuery("SELECT COUNT(*) FROM ${ReadSQL.tableSearch}"));

 if(books.length == count){
   ///说明数据库和要加入的书的数量相等,不插入数据库
   if(AppConstant.DEBUG){
     print("not need insert");
   }
   return;
 }

 ///先删除
 int deleteCount = await dbClient.rawDelete("DELETE FROM ${ReadSQL.tableSearch}");
 if(AppConstant.DEBUG){
   print("initLocalBooks deleteCount:$deleteCount");
 }

 String values = "";
 int startTime = DateTime.now().millisecondsSinceEpoch;
 for(int i=0;i<books.length;i++){
   var localBook = books[i];
   if(i == books.length-1){
     values += "('${localBook.book_id}','${localBook.author_name}','${localBook.brief_intro}'"
         ",'${localBook.cover}','${localBook.title}','${localBook.book_alias}')";
   }else{
     values += "('${localBook.book_id}','${localBook.author_name}','${localBook.brief_intro}'"
         ",'${localBook.cover}','${localBook.title}','${localBook.book_alias}'),";
   }
 }

 int insertCount = await dbClient.rawInsert("INSERT INTO ${ReadSQL.tableSearch} (book_id,author_name,brief_intro,cover,title,book_alias) "
     "VALUES $values");

 if(AppConstant.DEBUG){
   print("initLocalBooks insertCount:$insertCount");
   print("initLocalBooks time:${DateTime.now().millisecondsSinceEpoch - startTime}");
 }

}

使用的是sql语句中的INSERT INTO

INSERT INTO table_name (column1,column2,column3,...)
VALUES (value1,value2,value3,...);

打印的日志是:

I/flutter (20456): initLocalBooks deleteCount:0
I/flutter (20456): initLocalBooks insertCount:1568
I/flutter (20456): initLocalBooks time:824

后来研究了批量batch操作,代码如下:

initLocalBooks2(List<BookLocalData> books)async{
  var dbClient = await _sql.db;
  int count = Sqflite.firstIntValue(await dbClient.rawQuery("SELECT COUNT(*) FROM ${ReadSQL.tableSearch}"));

  if(books.length == count){
    ///说明数据库和要加入的书的数量相等,不插入数据库
    if(AppConstant.DEBUG){
       print("not need insert");
    }
    return;
  }

  ///先删除
  int deleteCount = await dbClient.rawDelete("DELETE FROM ${ReadSQL.tableSearch}");
  if(AppConstant.DEBUG){
     print("initLocalBooks2 deleteCount:$deleteCount");
  }

  var batch = dbClient.batch();
  int startTime = DateTime.now().millisecondsSinceEpoch;
  books.forEach((book){
    batch.insert(ReadSQL.tableSearch, book.toMap());
  });
  batch.commit();
  if(AppConstant.DEBUG){
    print("initLocalBooks2 insertCount:${books.length}");
    print("initLocalBooks2 time:${DateTime.now().millisecondsSinceEpoch - startTime}");
  }

}

日志如下:

I/flutter (28596): initLocalBooks2 deleteCount:0
I/flutter (28596): initLocalBooks2 insertCount:1568
I/flutter (28596): initLocalBooks2 time:28

效率一目了然。
可能还有使用异步操作,其他代码都是一样,最主要都是下面插入的逻辑:

  dbClient.transaction((txn)async{
    int startTime = DateTime.now().millisecondsSinceEpoch;
    books.forEach((book)async{
      await txn.insert(ReadSQL.tableSearch, book.toMap());
    });

    if(AppConstant.DEBUG){
      print("initLocalBooks2 insertCount:${books.length}");
      print("initLocalBooks2 time:${DateTime.now().millisecondsSinceEpoch - startTime}");
    }
  });

日志:

I/flutter (25929): initLocalBooks2 deleteCount:0
I/flutter (25929): initLocalBooks2 insertCount:1568
I/flutter (25929): initLocalBooks2 time:291

但是这个是异步的,不影响接下来的业务逻辑,看每个项目的具体需求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容