SQLite

Finch 使用 sqlite3 包来支持 SQLite。SQLite 是一种基于文件的数据库 —— 不需要管理单独的数据库服务器。所有数据都存储在磁盘上的单个文件中。

在以下场景中,SQLite 是不错的选择:

  • 你正在构建一个不需要高并发的小型或中型应用
  • 你希望零依赖部署(不需要外部数据库服务器)
  • 你正在进行本地开发,希望能快速搭建环境

Finch 中的 SQLite 集成使用与 MySQL 相同的 MTableMField*Sqler API(完整的模式/查询构建器参考请参阅 MySQL)—— 同一段 Sqler 调用会为其所持有的实际数据库生成有效的 SQL,无论 DatabaseDriver 具体持有哪种数据库,因为 DatabaseDriver 会在运行时检测底层连接的类型。不同之处在于:配置方式、驱动的访问属性,以及 —— 尤为重要的 —— 从结果集中读取值的方式(参见下方的结果处理)。

配置

FinchConfigs 中添加 sqliteConfig。唯一必需的参数是 .sqlite 文件的路径:

FinchConfigs configs = FinchConfigs(
  sqliteConfig: FinchSqliteConfig(
    enable: true,
    filePath: env.get('SQLITE_PATH', './app.sqlite'),
  ),
);

如果该文件不存在,会被自动创建。路径可以是绝对路径,也可以是相对于工作目录的相对路径。

访问驱动

// DatabaseDriver<Database> — 用于 Sqler 查询
var driver = app.sqliteDriver;

// 直接使用 sqlite3.Database — 仅在需要底层访问时使用
var db = app.sqliteDb;

请在数据层类中使用 app.sqliteDriver。将它作为构造函数参数传入,以保持控制器的轻量:

class BooksData {
  final DatabaseDriver db;
  BooksData(this.db);

  // 你的查询方法……
}

// 在控制器中:
var books = BooksData(app.sqliteDriver);

定义表

表模式的定义方式与 MySQL 完全相同,都使用 MTableMField*。Finch 会将它们一并从 finch_mysql.dart 中导入 —— 它们由 MySQL 和 SQLite 共享:

import 'package:finch/finch_mysql.dart'; // MTable、MField* 由 MySQL 和 SQLite 共享
import 'package:finch/finch_ui.dart';    // FieldValidator

final table = MTable(
  name: 'books',
  fields: [
    MFieldInt(name: 'id', isPrimaryKey: true, isAutoIncrement: true, isNullable: false),
    MFieldVarchar(
      name: 'title',
      isNullable: false,
      validators: [
        FieldValidator.requiredField().toSimple(),
        FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
      ],
    ),
    MFieldVarchar(name: 'author', isNullable: false),
    MFieldDate(name: 'published_date', isNullable: false),
    MFieldInt(name: 'category_id', isNullable: true),
  ],
);

在为 SQLite 连接生成 SQL 时(toSQL<Sqlite>() 而不是 toSQL<Mysql>()),MFieldInt 会自动映射到 SQLite 的 INTEGER —— 你不需要一个 SQLite 专用的字段类型。完整的类型列表请参阅 MySQL — Available Field Types,其中包括 MFieldBoolean(而不是 MFieldBool)以及 MFieldDecimal/MFieldFloat(不存在 MFieldDouble)。

使用 Sqler 查询

查询使用与 MySQL 相同的 Sqler 流式 API 构建。Finch 会在内部处理 SQL 方言的差异:

import 'package:finch/finch_mysql.dart';

Future<SqlDatabaseResult> getAllBooks(DatabaseDriver db) async {
  var query = Sqler()
    ..from(QField(table.name, as: 'b'))
    ..selects([
      QSelect('b.id'),
      QSelect('b.title'),
      QSelect('b.author'),
    ])
    ..orderBy(QOrder('b.id', desc: true))
    ..limit(20);

  return db.execute(query);
}

插入

Sqler.insert() 接受目标表以及一个列表形式的行 map:

await db.execute(
  Sqler().insert(QField(table.name), [
    {'title': QVar('Dart in Action'), 'author': QVar('Alice')},
  ]),
);

更新

使用 .update(table),然后对每个字段调用 .updateSet(field, value),最后调用 .where()

await db.execute(
  Sqler()
    ..update(QField(table.name))
    ..updateSet('title', QVar('Updated Title'))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

删除

.delete().from() 组合使用:

await db.execute(
  Sqler()
    ..delete()
    ..from(QField(table.name))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

同样可用的还有相同的 MTable 便捷方法,以及(与 MysqlTable 对应的)SqliteTable 仓储基类,它同样提供 deleteBy/deleteById/findById/countBy —— 完整方法列表请参阅 MySQL — Table Convenience Methods;其中的一切在 app.sqliteDriver 上都可以原样使用。

结果处理

这是 SQLite 与 MySQL 真正存在差异的地方:SQLite 的 SqlDatabaseResult.rows 是一个普通的 List<List<Object?>>(每行是一组按位置排列的值)—— 不是像 MySQL 结果行那样带有 .colByName() 方法的对象。请始终使用 .assoc/.assocFirst 按列名读取数据,而不要按位置索引 .rows

var result = await getAllBooks(app.sqliteDriver);

for (var row in result.assoc) {
  var title = row['title']; // Map<String, String?> — 每个值都会以 String 形式返回
}

var firstRow = result.assocFirst; // Map<String, String?>? — 第一行,不存在则为 null
var numRows  = result.numRows;
var newId    = result.insertId;     // sqlite3 最近一次插入的行 ID
var affected = result.affectedRows; // 最近一条语句改变的行数

注意: 与 MySQL 的 .assoc(保留原生 Dart 类型)不同,SQLite 的 .assoc/.assocFirst 会将每个值字符串化(row[i]?.toString())。如果需要带类型的值,请自行解析数字/布尔值(例如 int.parse(row['id']!))。

对于总行数统计这类分页查询,与 MySQL 完全一样,请把 COUNT(*) 起别名为 count_records,再读取 .countRecords

var countQuery = Sqler()..from(table.qName)..addSelect(SQL.count(QField('id', as: 'count_records')));
var total = (await table.execute(driver, countQuery)).countRecords;

迁移

SQLite 的迁移与 MySQL 的迁移是分开跟踪的,拥有自己专用的应用运行时命令 migrate_sqlite

# 应用所有待处理的 SQLite 迁移
finch run --args="migrate_sqlite --init"

# 创建一个新的 SQLite 迁移文件
finch migrate --create --name add_books_table --sqlite

finch migrate --init --sqlite(带 --sqlite 标志的组合式 MySQL 命令)也会在与 MySQL 同一次调用中一并应用待处理的 SQLite 迁移 —— 但限于 --initmigrate --rollback --sqlitemigrate --list --sqlite 会静默忽略 --sqlite 标志,只对 MySQL 生效;这两种操作请改用 migrate_sqlite --rollback / migrate_sqlite --list。完整的命令参考和迁移文件格式请参阅 Database Migration

SQLite 非常适合开发、测试,以及不需要独立数据库服务器的小型生产部署。