SQLite
Finch 使用 sqlite3 包来支持 SQLite。SQLite 是一种基于文件的数据库 —— 不需要管理单独的数据库服务器。所有数据都存储在磁盘上的单个文件中。
在以下场景中,SQLite 是不错的选择:
- 你正在构建一个不需要高并发的小型或中型应用
- 你希望零依赖部署(不需要外部数据库服务器)
- 你正在进行本地开发,希望能快速搭建环境
Finch 中的 SQLite 集成使用与 MySQL 相同的 MTable、MField* 和 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 完全相同,都使用 MTable 和 MField*。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 迁移 —— 但仅限于--init。migrate --rollback --sqlite和migrate --list --sqlite会静默忽略--sqlite标志,只对 MySQL 生效;这两种操作请改用migrate_sqlite --rollback/migrate_sqlite --list。完整的命令参考和迁移文件格式请参阅 Database Migration。
SQLite 非常适合开发、测试,以及不需要独立数据库服务器的小型生产部署。