sqflite-open-database
GitHubFlutter中使用sqflite插件操作SQLite数据库,涵盖打开、创建、迁移、关闭及删除等操作,处理版本迁移与常见陷阱。
Trigger Scenarios
Install
npx skills add tekartik/sqflite --skill sqflite-open-database -g -y
SKILL.md
Frontmatter
{
"name": "sqflite-open-database",
"description": "Use when opening, creating, migrating, closing or deleting a SQLite database with package:sqflite in a Flutter app (Android, iOS, macOS): openDatabase, openReadOnlyDatabase, OpenDatabaseOptions, version, onConfigure, onCreate, onUpgrade, onDowngrade, onDatabaseDowngradeDelete, onOpen, getDatabasesPath, deleteDatabase, databaseExists, inMemoryDatabasePath, singleInstance, readOnly, databaseFactory, getVersion, setJournalMode (WAL), foreign keys, copying an asset database, and the \"database is locked\" pitfalls."
}
sqflite: opening and migrating a database
package:sqflite is the Flutter plugin for SQLite on Android, iOS and macOS.
A database is a file identified by a path; a relative path is resolved against
getDatabasesPath(). openDatabase runs a small version-based migration
mechanism (onCreate / onUpgrade / onDowngrade) inside a transaction and
returns a Database that you keep open for the life of the app.
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<Database> openAppDatabase() async {
final path = join(await getDatabasesPath(), 'app.db');
return openDatabase(
path,
version: 1,
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE Todo (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL)',
);
},
);
}
Guidelines
Imports and platforms
import 'package:sqflite/sqflite.dart';gives the global functions (openDatabase,openReadOnlyDatabase,getDatabasesPath,deleteDatabase,databaseExists), the globaldatabaseFactory, theSqflitehelper class and every type ofpackage:sqflite/sqlite_api.dart(Database,Transaction,Batch,DatabaseFactory,OpenDatabaseOptions,ConflictAlgorithm,DatabaseException,inMemoryDatabasePath, ...).sqfliteonly works on Android, iOS and macOS. For Linux, Windows, the Dart VM and unit tests usepackage:sqflite_common_ffi; for the web usepackage:sqflite_common_ffi_web; both plug in throughdatabaseFactory = ...and the code below stays unchanged. Code that must not depend on Flutter should importpackage:sqflite_common/sqlite_api.dartand receive aDatabaseFactory.- Build paths with
joinfrompackage:path, never with string concatenation.getDatabasesPath()isdata/data/<package>/databaseson Android and the Documents directory on iOS/macOS; on iOS the Library directory frompath_provider(getLibraryDirectory()) is the recommended location instead. - The plugin creates the parent directory of a read-write database on open.
When you write the file yourself (asset copy) create the directory first
with
Directory(dirname(path)).create(recursive: true).
Versioning callbacks
- Pass
version(anint> 0) to enable migrations. Callbacks run in this order:onConfigure, then exactly one ofonCreate/onUpgrade/onDowngrade, thenonOpen. WithoutversiononlyonConfigureandonOpenrun. onCreate(db, version)runs when the file does not exist.onUpgrade(db, oldVersion, newVersion)runs when the stored version is lower thanversion(and also instead ofonCreate, witholdVersion == 0, when noonCreateis given).onDowngraderuns when the stored version is higher; passonDatabaseDowngradeDeleteto delete and recreate the database in that case, oronDatabaseVersionChangeErrorto fail.onCreate,onUpgradeandonDowngradealready run inside a transaction: use thedbthey receive directly (ordb.batch()+commit()), never calldb.transaction()inside them. The version is stored (PRAGMA user_version) when the callback completes without throwing.- Write migrations as a chain:
if (oldVersion < 2) {...} if (oldVersion < 3) {...}so any old version reaches the newest schema. Put schema statements in aBatch, one statement perexecute(multi-statement strings separated by;are not supported). onConfigureis the place forPRAGMA foreign_keys = ON,db.setJournalMode('WAL')(extensionSqfliteDatabaseExt, handles the Android quirk whereexecutefails),PRAGMA auto_vacuum(only whenawait db.getVersion() == 0, i.e. a new file) and, on Android,db.androidSetLocale('fr-FR')(extensionSqfliteDatabaseAndroidExt). They must be re-applied at every open, which is why they belong there.db.getVersion()/db.setVersion()exist (extensionSqfliteDatabaseExecutorExt) but do not drive migrations with them; useversionand the callbacks.openDatabase(path, options: OpenDatabaseOptions(...))is equivalent to the named parameters; whenoptionsis given all other parameters are ignored.DatabaseFactory.openDatabaseonly takesoptions.
Instances, closing, deleting
- Open the database once and keep the
Database; store theFuture<Database>(not theDatabase) in a field so concurrent callers share the same open call. Many apps never close it. singleInstance: true(default) returns the sameDatabasefor the same path; a secondopenDatabaseon that path returns the existing instance and ignores its callbacks. It is forced tofalseforinMemoryDatabasePath(':memory:'). Opening the same file twice withsingleInstance: falsecauses "database is locked" errors on Android.rollbackActiveTransactionOnOpen(OpenDatabaseOptions, default: true in debug, false in release) rolls back a transaction left open by a previous isolate/hot restart whensingleInstanceis true. Keep the default unless you deliberately use several isolates.readOnly: true(oropenReadOnlyDatabase(path)) ignores every callback, never starts a transaction and fails on the first write. Use it for shipped asset databases.- Delete with
deleteDatabase(path), never withFile(path).delete(): it closes the open instance, handles the hot-restart state and removes the-wal,-shmand-journalside files. databaseExists(path)checks the file;db.isOpentells ifclose()was called;db.pathis the resolved absolute path.databaseFactory.readDatabaseBytes(path)/writeDatabaseBytes(path, bytes)copy a whole database file (backup, restore, asset import) in a way that also works with the ffi and web factories.factory.sandbox(path: root)(extensionSqfliteDatabaseFactorySandboxExtension) returns aDatabaseFactorywhose relative paths live underrootand whose absolute paths must stay inside it. Use it to isolate tests or per-user data.
Isolates and hot restart
- Use the database from the main isolate: native calls already run on a
background thread and the transaction lock is not cross-isolate. If a
background isolate (push notification, work manager) must read the database,
open it there with
singleInstance: falseand do not close it. - After changing the schema during development restart the app; a hot reload keeps the native connection open with the old schema.
Examples
Migration chain with batches and a downgrade policy
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
const _version = 2;
void _createV1(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute(
'CREATE TABLE Company (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',
);
}
void _upgradeV1ToV2(Batch batch) {
batch.execute('ALTER TABLE Company ADD description TEXT');
batch.execute('''CREATE TABLE Employee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
companyId INTEGER,
FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE)''');
}
Future<Database> openCompanyDb() async {
final path = join(await getDatabasesPath(), 'company.db');
return openDatabase(
path,
version: _version,
onConfigure: (db) async {
await db.execute('PRAGMA foreign_keys = ON');
},
onCreate: (db, version) async {
// Fresh install: build the latest schema through the same steps.
final batch = db.batch();
_createV1(batch);
_upgradeV1ToV2(batch);
await batch.commit();
},
onUpgrade: (db, oldVersion, newVersion) async {
final batch = db.batch();
if (oldVersion < 2) {
_upgradeV1ToV2(batch);
}
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete,
);
}
One shared instance for the whole app
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
class AppDatabase {
Future<Database>? _db;
/// Safe to call concurrently: the first call starts the open, others await it.
Future<Database> get database => _db ??= _open();
Future<Database> _open() async {
final path = join(await getDatabasesPath(), 'app.db');
return openDatabase(
path,
version: 1,
onConfigure: (db) => db.setJournalMode('WAL'),
onCreate: (db, _) => db.execute(
'CREATE TABLE Note (id INTEGER PRIMARY KEY, content TEXT)',
),
);
}
Future<void> close() async {
final db = await _db;
_db = null;
await db?.close();
}
}
Copy a bundled asset database on first launch
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<Database> openAssetDatabase() async {
final path = join(await getDatabasesPath(), 'catalog.db');
if (!await databaseExists(path)) {
await Directory(dirname(path)).create(recursive: true);
final data = await rootBundle.load(url.join('assets', 'catalog.db'));
await databaseFactory.writeDatabaseBytes(
path,
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
);
}
// Shipped data: open read-only, no callbacks run.
return openReadOnlyDatabase(path);
}
Delete and recreate, in-memory database
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<void> resetDatabase() async {
final path = join(await getDatabasesPath(), 'app.db');
await deleteDatabase(path); // also closes the open instance
}
Future<Database> openScratchDb() =>
openDatabase(inMemoryDatabasePath); // singleInstance is forced to false
Opening through an explicit factory
import 'package:sqflite/sqflite.dart';
/// Works with databaseFactory (sqflite), databaseFactoryFfi, databaseFactoryFfiWeb...
Future<Database> openWith(DatabaseFactory factory, String path) {
return factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) =>
db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)'),
),
);
}
Common mistakes
- Calling
db.transaction()insideonCreate/onUpgrade: they are already in a transaction, usedbdirectly or a batch. - Creating two tables in one
executestring. One statement per call. - Forgetting to bump
versionafter changingonCreate; existing installs never see the new schema. Handle it inonUpgradetoo. - Deleting the file with
dart:ioinstead ofdeleteDatabase, then wondering whyonCreatedoes not run after a hot restart. - Opening the database in every widget/repository call with
singleInstance: false, leading todatabase is locked (code 5). - Putting
PRAGMA foreign_keys/setJournalModeinonCreate: they are per connection and must go inonConfigure. - Calling
openDatabaseon Linux/Windows/web without first settingdatabaseFactoryfromsqflite_common_ffi/sqflite_common_ffi_web(StateError: databaseFactory not initialized).
More
Queries, transactions and batches: see the sqflite-crud-and-transactions
skill. Unit tests, desktop/web factories and logging: see the
sqflite-testing-and-platforms skill.
Version History
- aaabf90 Current 2026-09-22 03:38


