Add helper method to setup isolates more easily

This commit is contained in:
Simon Binder 2022-11-25 17:54:38 +01:00
parent 0231225733
commit cd00c6899e
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
10 changed files with 146 additions and 49 deletions

View File

@ -27,6 +27,7 @@ LazyDatabase _openConnection() {
// for your app.
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
return NativeDatabase.createInBackground(file);
});
}

View File

@ -127,3 +127,16 @@ DatabaseConnection createDriftIsolateAndConnect() {
}));
}
// #enddocregion init_connect
// #docregion simple
QueryExecutor createSimple() {
return LazyDatabase(() async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, 'db.sqlite'));
// Using createInBackground creates a drift isolate with the recommended
// options behind the scenes.
return NativeDatabase.createInBackground(file);
});
}
// #enddocregion simple

View File

@ -60,7 +60,7 @@ LazyDatabase _openConnection() {
// for your app.
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
return NativeDatabase.createInBackground(file);
});
}

View File

@ -36,6 +36,28 @@ a background isolate. Zero code changes are needed for queries!
{% assign snippets = 'package:drift_docs/snippets/isolates.dart.excerpt.json' | readString | json_decode %}
## Simple setup
Starting from Drift version 2.3.0, using drift isolates has been greatly
simplified. Simply use `NativeDatabase.createInBackground` as a drop-in
replacement for the `NativeDatabase` you've been using before:
{% include "blocks/snippet" snippets = snippets name = 'simple' %}
In the common case where you only need a isolate for performance reasons, this
is as simple as it gets.
The rest of this article explains a more complex setup giving you full control
over the internal components making up a drift isolate. This is useful for
advanced use cases, including:
- Having two databases on different isolates which need to stay in sync.
- Sharing a drift database connection across different Dart or Flutter engines,
like for a background service on Android.
In most other cases, simply using `NativeDatabase.createInbackground` works
great! It implements the same approach shared in this article, except that all
the complicated bits are hidden behind a simple method.
## Preparations
To use the isolate api, first enable the appropriate [build option]({{ "builder_options.md" | pageUrl }}) by

View File

@ -60,7 +60,7 @@ LazyDatabase _openConnection() {
await file.writeAsBytes(buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes));
}
return NativeDatabase(file);
return NativeDatabase.createInBackground(file);;
});
}
```

View File

@ -9,6 +9,9 @@
- Add `singleClientMode` to `remote()` and `DriftIsolate` connections to make
the common case with one client more efficient.
- Fix a concurrency issues around transactions.
- Add `NativeDatabase.createInBackground` as a drop-in replacement for
`NativeDatabase`. It creates a drift isolate behind the scenes, avoiding all
of the boilerplate usually involved with drift isolates.
- __Experimental__: Add a [modular generation mode](https://drift.simonbinder.eu/docs/advanced-features/builder_options/#enabling-modular-code-generation)
in which drift will generate multiple smaller files instead of one very large
one with all tables and generated queries.

View File

@ -10,7 +10,10 @@
library drift.ffi;
import 'dart:io';
import 'dart:isolate';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:meta/meta.dart';
import 'package:sqlite3/common.dart';
import 'package:sqlite3/sqlite3.dart';
@ -50,6 +53,53 @@ class NativeDatabase extends DelegatedDatabase {
return NativeDatabase._(_NativeDelegate(file, setup), logStatements);
}
/// Creates a database storing its result in [file].
///
/// This method will create the same database as the default constructor of
/// the [NativeDatabase] class. It also behaves the same otherwise: The [file]
/// is created if it doesn't exist, [logStatements] can be used to print
/// statements and [setup] can be used to perform a one-time setup work when
/// the database is created.
///
/// The big distinction of this method is that the database is implicitly
/// created on a background isolate, freeing up your main thread accessing the
/// database from I/O work needed to run statements.
/// When the database returned by this method is closed, the background
/// isolate will shut down as well.
///
/// __Important limitations__: If the [setup] parameter is given, it must be
/// a static or top-level function. The reason is that it is executed on
/// another isolate.
static QueryExecutor createInBackground(File file,
{bool logStatements = false, DatabaseSetup? setup}) {
return createBackgroundConnection(file,
logStatements: logStatements, setup: setup)
.executor;
}
/// Like [createInBackground], except that it returns the whole
/// [DatabaseConnection] instead of just the executor.
///
/// This creates a database writing data to the given [file]. The database
/// runs in a background isolate and is stopped when closed.
static DatabaseConnection createBackgroundConnection(File file,
{bool logStatements = false, DatabaseSetup? setup}) {
return DatabaseConnection.delayed(Future.sync(() async {
final receiveIsolate = ReceivePort();
await Isolate.spawn(
_NativeIsolateStartup.start,
_NativeIsolateStartup(
file.absolute.path, logStatements, setup, receiveIsolate.sendPort),
debugName: 'Drift isolate worker for ${file.path}',
);
final driftIsolate = await receiveIsolate.first as DriftIsolate;
receiveIsolate.close();
return driftIsolate.connect(singleClientMode: true);
}));
}
/// Creates an in-memory database won't persist its changes on disk.
///
/// {@macro drift_vm_database_factory}
@ -204,3 +254,25 @@ class _NativeDelegate extends Sqlite3Delegate<Database> {
}
}
}
class _NativeIsolateStartup {
final String path;
final bool enableLogs;
final DatabaseSetup? setup;
final SendPort sendServer;
_NativeIsolateStartup(
this.path, this.enableLogs, this.setup, this.sendServer);
static void start(_NativeIsolateStartup startup) {
final isolate = DriftIsolate.inCurrent(() {
return DatabaseConnection(NativeDatabase(
File(startup.path),
logStatements: startup.enableLogs,
setup: startup.setup,
));
});
startup.sendServer.send(isolate);
}
}

View File

@ -35,3 +35,4 @@ dev_dependencies:
rxdart: ^0.27.0
shelf: ^1.3.0
stack_trace: ^1.10.0
test_descriptor: ^2.0.1

View File

@ -1,14 +1,41 @@
@TestOn('vm')
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import '../../generated/todos.dart';
import '../../test_utils/database_vm.dart';
void main() {
preferLocalSqlite3();
group('implicit isolates', () {
test('work with direct executors', () async {
final file = File(d.path('test.db'));
final db = TodoDb(NativeDatabase.createInBackground(file));
await db.todosTable.select().get(); // Open the database
await db.close();
await d.file('test.db', anything).validate();
});
test('work with connections', () async {
final file = File(d.path('test.db'));
final db =
TodoDb.connect(NativeDatabase.createBackgroundConnection(file));
await db.todosTable.select().get(); // Open the database
await db.close();
await d.file('test.db', anything).validate();
});
});
group('NativeDatabase.opened', () {
test('disposes the underlying database by default', () async {
final underlying = sqlite3.openInMemory();

View File

@ -1,63 +1,21 @@
import 'dart:io';
import 'dart:isolate';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
/// Obtains a database connection for running drift in a Dart VM.
///
/// The [NativeDatabase] from drift will synchronously use sqlite3's C APIs.
/// To move synchronous database work off the main thread, we use a
/// [DriftIsolate], which can run queries in a background isolate under the
/// hood.
DatabaseConnection connect() {
return DatabaseConnection.delayed(Future.sync(() async {
return DatabaseConnection.delayed(Future(() async {
// Background isolates can't use platform channels, so let's use
// `path_provider` in the main isolate and just send the result containing
// the path over to the background isolate.
// We use `path_provider` to find a suitable path to store our data in.
final appDir = await getApplicationDocumentsDirectory();
final dbPath = p.join(appDir.path, 'todos.db');
final receiveDriftIsolate = ReceivePort();
await Isolate.spawn(_entrypointForDriftIsolate,
_IsolateStartRequest(receiveDriftIsolate.sendPort, dbPath));
final driftIsolate = await receiveDriftIsolate.first as DriftIsolate;
// Each connect() spawns a new isolate which is only used for one
// connection, so we shutdown the isolate when the database is closed.
return driftIsolate.connect(singleClientMode: true);
return NativeDatabase.createBackgroundConnection(File(dbPath));
}));
}
/// The entrypoint of isolates can only take a single message, but we need two
/// (a send port to reach the originating isolate and the database's path that
/// should be opened on the background isolate). So, we bundle this information
/// in a single class.
class _IsolateStartRequest {
final SendPort talkToMain;
final String databasePath;
_IsolateStartRequest(this.talkToMain, this.databasePath);
}
/// The entrypoint for a background isolate launching a drift server.
///
/// The main isolate can then connect to that isolate server to transparently
/// run queries in the background.
void _entrypointForDriftIsolate(_IsolateStartRequest request) {
// The native database synchronously uses sqlite3's C API with `dart:ffi` for
// a fast database implementation that doesn't require platform channels.
final databaseImpl = NativeDatabase(File(request.databasePath));
// We can use DriftIsolate.inCurrent because this function is the entrypoint
// of a background isolate itself.
final driftServer =
DriftIsolate.inCurrent(() => DatabaseConnection(databaseImpl));
// Inform the main isolate about the server we just created.
request.talkToMain.send(driftServer);
}