寫一個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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...