寫一個TODO App學習Flutter資料庫工具Moor

来源:https://www.cnblogs.com/mengdd/archive/2020/04/14/flutter-todo-app-database-using-moor.html
-Advertisement-
Play Games

用Moor做TODO app: * 基本使用: 依賴添加, 資料庫和表的建立, 對錶的基本操作. * 問題解決: 插入數據註意類型; 多個表的文件組織. * 常用功能: 外鍵和join, 資料庫升級, 條件查詢. ...


寫一個TODO App學習Flutter資料庫工具Moor

Flutter的資料庫存儲, 官方文檔: https://flutter.dev/docs/cookbook/persistence/sqlite
中寫的是直接操縱SQLite資料庫的方法.

有沒有什麼package可以像Android的Room一樣, 幫助開發者更加方便地做資料庫存儲呢?

Moor就是這種目的: https://pub.dev/packages/moor.
它的名字是把Room反過來. 它是一個第三方的package.

為了學習一下怎麼用, 我做了一個小的todo app: https://github.com/mengdd/more_todo.

本文是一個工作記錄.

TL;DR

用Moor做TODO app:

  • 基本使用: 依賴添加, 資料庫和表的建立, 對錶的基本操作.
  • 問題解決: 插入數據註意類型; 多個表的文件組織.
  • 常用功能: 外鍵和join, 資料庫升級, 條件查詢.

代碼: Todo app: https://github.com/mengdd/more_todo

Moor基本使用

官方這裡有個文檔:
Moor Getting Started

Step 1: 添加依賴

pubspec.yaml中:

dependencies:
  flutter:
    sdk: flutter

  moor: ^2.4.0
  moor_ffi: ^0.4.0
  path_provider: ^1.6.5
  path: ^1.6.4
  provider: ^4.0.4

dev_dependencies:
  flutter_test:
    sdk: flutter
  moor_generator: ^2.4.0
  build_runner: ^1.8.1

這裡我是用的當前(2020.4)最新版本, 之後請更新各個package版本號到最新的版本.

對各個packages的解釋:

* moor: This is the core package defining most apis
* moor_ffi: Contains code that will run the actual queries
* path_provider and path: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
* moor_generator: Generates query code based on your tables
* build_runner: Common tool for code-generation, maintained by the Dart team

現在推薦使用moor_ffi而不是moor_flutter.

網上的一些例子是使用moor_flutter的, 所以看那些例子的時候有些地方可能對不上了.

Step 2: 定義資料庫和表

新建一個文件, 比如todo_database.dart:

import 'dart:io';

import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

part 'todo_database.g.dart';

// this will generate a table called "todos" for us. The rows of that table will
// be represented by a class called "Todo".
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();

  TextColumn get title => text().withLength(min: 1, max: 50)();

  TextColumn get content => text().nullable().named('description')();

  IntColumn get category => integer().nullable()();

  BoolColumn get completed => boolean().withDefault(Constant(false))();
}

@UseMoor(tables: [Todos])
class TodoDatabase extends _$TodoDatabase {
  // we tell the database where to store the data with this constructor
  TodoDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;
}

LazyDatabase _openConnection() {
  // the LazyDatabase util lets us find the right location for the file async.
  return LazyDatabase(() async {
    // put the database file, called db.sqlite here, into the documents folder
    // for your app.
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return VmDatabase(file, logStatements: true);
  });
}

幾個知識點:

  • 要加part 'todo_database.g.dart';, 等一下要生成這個文件.
  • 這裡定義的class是Todos, 生成的具體實體類會去掉s, 也即Todo. 如果想指定生成的類名, 可以在類上加上註解, 比如: @DataClassName("Category"), 生成的類就會叫"Category".
  • 慣例: $是生成類類名首碼. .g.dart是生成文件.

Step 3: 生成代碼

運行:

flutter packages pub run build_runner build

or:

flutter packages pub run build_runner watch

來進行一次性(build)或者持續性(watch)的構建.

如果不順利, 有可能還需要加上--delete-conflicting-outputs:

flutter packages pub run build_runner watch --delete-conflicting-outputs

運行成功之後, 生成todo_database.g.dart文件.

所有的代碼中報錯應該消失了.

Step 4: 添加增刪改查方法

對於簡單的例子, 把方法直接寫在資料庫類里:

@UseMoor(tables: [Todos])
class TodoDatabase extends _$TodoDatabase {
  // we tell the database where to store the data with this constructor
  TodoDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  Future<List<Todo>> getAllTodos() => select(todos).get();

  Stream<List<Todo>> watchAllTodos() => select(todos).watch();

  Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);

  Future updateTodo(Todo todo) => update(todos).replace(todo);

  Future deleteTodo(Todo todo) => delete(todos).delete(todo);
}

資料庫的查詢不但可以返回Future還可以返回Stream, 保持對數據的持續觀察.

這裡註意插入的方法用了Companion對象. 後面會說為什麼.

上面這種做法把資料庫操作方法都寫在一起, 代碼多了之後顯然不好.
改進的方法就是寫DAO:
https://moor.simonbinder.eu/docs/advanced-features/daos/

後面會改.

Step 5: 把數據提供到UI中使用

提供數據訪問方法涉及到程式的狀態管理.
方法很多, 之前寫過一個文章: https://www.cnblogs.com/mengdd/p/flutter-state-management.html

這裡先選一個簡單的方法用Provider直接提供資料庫對象, 包在程式外層:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => TodoDatabase(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

需要的時候:

TodoDatabase database = Provider.of<TodoDatabase>(context, listen: false);

就拿到database對象, 然後可以調用它的方法了.

之後就是UI怎麼用的問題了, 這裡不再多說.

我的代碼中tag: v0.1.1就是這種最簡單的方法.
可以checkout過去看這個最簡單版本的實現.

Step 6: 改進: 抽取方法到DAO, 重構

增刪改查的方法從資料庫中抽取出來, 寫在DAO里:

part 'todos_dao.g.dart';

// the _TodosDaoMixin will be created by moor. It contains all the necessary
// fields for the tables. The <MyDatabase> type annotation is the database class
// that should use this dao.
@UseDao(tables: [Todos])
class TodosDao extends DatabaseAccessor<TodoDatabase> with _$TodosDaoMixin {
  // this constructor is required so that the main database can create an instance
  // of this object.
  TodosDao(TodoDatabase db) : super(db);

  Future<List<Todo>> getAllTodos() => select(todos).get();

  Stream<List<Todo>> watchAllTodos() => select(todos).watch();

  Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);

  Future updateTodo(Todo todo) => update(todos).replace(todo);

  Future deleteTodo(Todo todo) => delete(todos).delete(todo);
}

運行命令行重新生成一下(如果是watch就不用).

其實就生成了個這:

part of 'todos_dao.dart';

mixin _$TodosDaoMixin on DatabaseAccessor<TodoDatabase> {
  $TodosTable get todos => db.todos;
}

這裡的todos是其中的table對象.

所以如果不是改table, 只改變DAO中的方法實現的話, 不用重新生成.

這時候我們提供給UI的部分也要改了.

之前是Provider直接提供了database對象, 雖然可以直接換成提供DAO對象, 但是DAO會有很多個, 硬要這麼提供的話代碼很快就亂了.

怎麼解決也有多種方法, 這是一個架構設計問題, 百花齊放, 答案很多.

我這裡簡單封裝一下:

class DatabaseProvider {
  TodosDao _todosDao;

  TodosDao get todosDao => _todosDao;

  DatabaseProvider() {
    TodoDatabase database = TodoDatabase();
    _todosDao = TodosDao(database);
  }
}

最外層改成提供這個:

    return Provider(
      create: (_) => DatabaseProvider(),
//...
    );

用的時候把DAO get出來用就可以了.

如果有其他DAO也可以加進去.

Troubleshooting

插入的時候應該用Companion對象

插入數據的方法:
如果這樣寫:

Future insertTodo(Todo todo) => into(todos).insert(todo);

就坑了.

因為按照定義, 我們的id是自動生成並自增的:

IntColumn get id => integer().autoIncrement()();

但是生成的這個Todo類, 裡面所有非空的欄位都是@required的:

Todo(
  {@required this.id,
  @required this.title,
  this.content,
  this.category,
  @required this.completed});

要新建一個實例並插入, 我自己是無法指定這個遞增的id的. (先查詢再自己手動遞增是不是太tricky了. 一般不符合直覺的古怪的做法都是不對的.)

可以看這兩個issue中, 作者的解釋也是用Companion對象:

所以insert方法最後寫成了這樣:

Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);

還有一種寫法是這樣:

 Future insertTodo(Insertable<Todo> todo) => into(todos).insert(todo);

添加數據:

final todo = TodosCompanion(
  title: Value(input),
  completed: Value(false),
);
todosDao.insertTodo(todo);

這裡構建對象的時候, 只需要把需要的值用Value包裝起來. 沒有提供的會是Value.absent().

表定義必須和資料庫類寫在一起? 多個表怎麼辦?

實際的項目中肯定有多個表, 我想著一個表一個文件這樣比較好.

於是當我天真地為我的新數據表, 比如Category, 新建一個categories.dart文件, 裡面繼承了Table類, 也指定了生成文件的名字.

part 'categories.g.dart';

@DataClassName('Category')
class Categories extends Table {
//...
}

運行生成build之後代碼中這行是紅的:

part 'categories.g.dart';

沒有生成這個文件.

查看後發現Category類仍然被生成在了databse的.g.dart文件中.

關於這個問題的討論: https://github.com/simolus3/moor/issues/480

解決方法有兩種思路:

  • 簡單解決: 源碼仍然分開寫, 只不過所有的生成代碼放一起.

去掉part語句.

@DataClassName('Category')
class Categories extends Table {
//...
}

生成的代碼仍然是方法database的生成文件中, 但是我們的源文件看起來是分開了.
之後使用具體數據類型的時候, import的還是database文件對應類.

  • 使用.moor文件.

進階需求

外鍵和join

把兩個表關聯起來這個需求還挺常見的.

比如我們的todo實例, 增加了Category類之後, 想把todo放在不同的category中, 沒有category的就放在inbox里, 作為未分類.

moor對外鍵不是直接支持, 而是通過customStatement來實現的.

這裡Todos類里的這一列, 加上自定義限制, 關聯到categories表:

IntColumn get category => integer()
  .nullable()
  .customConstraint('NULL REFERENCES categories(id) ON DELETE CASCADE')();

要用主鍵id.

這裡指定了兩遍可以null: 一次是nullable(), 另一次是在語句中.

實際上customConstraint中的會覆蓋前者. 但是我們仍然需要前者, 用來表明在生成類中改欄位是可以為null的.

另外還指定了刪除category的時候刪除對應的todo.

外鍵預設不開啟, 需要運行:

customStatement('PRAGMA foreign_keys = ON');

join查詢的部分, 先把兩個類包裝成第三個類.

class TodoWithCategory {
  final Todo todo;
  final Category category;

  TodoWithCategory({@required this.todo, @required this.category});
}

之後更改TODO的DAO, 註意這裡添加了一個table, 所以要重新生成一下.

之前的查詢方法改成這樣:

Stream<List<TodoWithCategory>> watchAllTodos() {
final query = select(todos).join([
  leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
]);

return query.watch().map((rows) {
  return rows.map((row) {
    return TodoWithCategory(
      todo: row.readTable(todos),
      category: row.readTable(categories),
    );
  }).toList();
});
}

join返回的結果是List<TypedResult>, 這裡用map操作符轉換一下.

資料庫升級

資料庫升級, 在資料庫升級的時候添加新的表和列.

由於外鍵預設是不開啟的, 所以也要開啟一下.

PS: 這裡Todo中的category之前已經建立過了.
遷移的時候不能修改已經存在的列. 所以只能棄表重建了.

@UseMoor(tables: [Todos, Categories])
class TodoDatabase extends _$TodoDatabase {
  // we tell the database where to store the data with this constructor
  TodoDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 2;

  @override
  MigrationStrategy get migration => MigrationStrategy(
        onUpgrade: (migrator, from, to) async {
          if (from == 1) {
            migrator.deleteTable(todos.tableName);
            migrator.createTable(todos);
            migrator.createTable(categories);
          }
        },
        beforeOpen: (details) async {
          await customStatement('PRAGMA foreign_keys = ON');
        },
      );
}

沒想到報錯了: Unhandled Exception: SqliteException: near "null": syntax error,
出錯的是drop table的這句:

Moor: Sent DROP TABLE IF EXISTS null; with args []

說todos.tableName是null.

這個get的設計用途原來是用來指定自定義名稱的:
https://pub.dev/documentation/moor/latest/moor_web/Table/tableName.html

因為我沒有設置自定義名稱, 所以這裡返回了null.

這裡我改成了:

migrator.deleteTable(todos.actualTableName);

條件查詢

查某個分類下:

  Stream<List<TodoWithCategory>> watchTodosInCategory(Category category) {
    final query = select(todos).join([
      leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
    ]);

    if (category != null) {
      query.where(categories.id.equals(category.id));
    } else {
      query.where(isNull(categories.id));
    }

    return query.watch().map((rows) {
      return rows.map((row) {
        return TodoWithCategory(
          todo: row.readTable(todos),
          category: row.readTable(categories),
        );
      }).toList();
    });
  }

多個條件的組合用&, 比如上面的查詢組合未完成:

query.where(
        categories.id.equals(category.id) & todos.completed.equals(false));

總結

Moor是一個第三方的package, 用來幫助Flutter程式的本地存儲. 由於開放了SQL語句查詢, 所以怎麼定製都行. 作者很熱情, 可以看到很多issue下都有他詳細的回覆.

本文是做一個TODO app來練習使用moor.
包括了基本的增刪改查, 外鍵, 資料庫升級等.

代碼: https://github.com/mengdd/more_todo

參考

最後, 歡迎關註微信公眾號: 聖騎士Wind
微信公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SQL列的數據類型分類: Unicode數據: 1.nchar 2.nvarchar 3.ntext 說明: Unicode支持的字元範圍更大。存儲 Unicode 字元所需要的空間更大。 傻瓜式教程(我初學者) ...
  • 在創建時間欄位的時候 表示當插入數據的時候,該欄位預設值為當前時間 表示每次更新這條數據的時候,該欄位都會更新成當前時間 這兩個操作是mysql資料庫本身在維護,所以可以根據這個特性來生成【創建時間】和【更新時間】兩個欄位,且不需要代碼來維護 如下: ~~~mysql CREATE TABLE ( ...
  • DM JDBC 介紹 DM JDBC 驅動程式是 DM資料庫的 JDBC 驅動程式,它是一個能夠支持基本 SQL 功能 的通用應用程式編程介面,支持一般的 SQL 資料庫訪問。 通過 JDBC 驅動程式,用戶可以在應用程式中實現對 DM 資料庫的連接與訪問,JDBC 驅動程式的主要功能包括: 1. ...
  • 1.前言 2.select簡單查詢 3.單值函數 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 前言 ...
  • 前言 文章首發於微信公眾號【碼猿技術專欄】。 在實際的開發中一定會碰到根據某個欄位進行排序後來顯示結果的需求,但是你真的理解order by在 Mysql 底層是如何執行的嗎? 假設你要查詢城市是蘇州的所有人名字,並且按照姓名進行排序返回前 1000 個人的姓名、年齡,這條 sql 語句應該如何寫? ...
  • 作為全球新冠疫情數據的實時統計的權威,約翰斯—霍普金斯大學的實時數據一直是大家實時關註的,也是各大媒體的主要數據來源。在今天早上的相當一段長的時間,霍普金斯大學的全球疫情分佈大屏中顯示,全球確診人數已經突破200萬。 有圖有真相 隨後相關媒體也進行了轉發,不過這個數據明顯波動太大,隨後該網站也修改了 ...
  • 一、啟動mongo shell 安裝好MongoDB後,直接在命令行終端執行下麵的命令: mongo 如下圖所示: 可選參數如下: 也可以簡寫為: 在mongo shell中使用外部編輯器,如:vi,只需設置環境變數: export EDITOR=vi 啟動mongo shel即可。下麵我們在mon ...
  • 老孟導讀:沒有接觸過音樂字幕方面知識的話,會對字幕的實現比較迷茫,什麼時候轉到下一句?看了這篇文章,你就會明白字幕so easy。 先來一張效果圖: 字幕格式 目前市面上有很多種字幕格式,比如srt, ssa, ass(文本形式)和idx+sub(圖形格式),但不管哪一種格式都會包含2個屬性:時間戳 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...