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. // for your app.
final dbFolder = await getApplicationDocumentsDirectory(); final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite')); 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 // #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. // for your app.
final dbFolder = await getApplicationDocumentsDirectory(); final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite')); 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 %} {% 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 ## Preparations
To use the isolate api, first enable the appropriate [build option]({{ "builder_options.md" | pageUrl }}) by 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)); 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 - Add `singleClientMode` to `remote()` and `DriftIsolate` connections to make
the common case with one client more efficient. the common case with one client more efficient.
- Fix a concurrency issues around transactions. - 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) - __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 in which drift will generate multiple smaller files instead of one very large
one with all tables and generated queries. one with all tables and generated queries.

View File

@ -10,7 +10,10 @@
library drift.ffi; library drift.ffi;
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:sqlite3/common.dart'; import 'package:sqlite3/common.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
@ -50,6 +53,53 @@ class NativeDatabase extends DelegatedDatabase {
return NativeDatabase._(_NativeDelegate(file, setup), logStatements); 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. /// Creates an in-memory database won't persist its changes on disk.
/// ///
/// {@macro drift_vm_database_factory} /// {@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 rxdart: ^0.27.0
shelf: ^1.3.0 shelf: ^1.3.0
stack_trace: ^1.10.0 stack_trace: ^1.10.0
test_descriptor: ^2.0.1

View File

@ -1,14 +1,41 @@
@TestOn('vm') @TestOn('vm')
import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.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'; import '../../test_utils/database_vm.dart';
void main() { void main() {
preferLocalSqlite3(); 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', () { group('NativeDatabase.opened', () {
test('disposes the underlying database by default', () async { test('disposes the underlying database by default', () async {
final underlying = sqlite3.openInMemory(); final underlying = sqlite3.openInMemory();

View File

@ -1,63 +1,21 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
/// Obtains a database connection for running drift in a Dart VM. /// 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() { DatabaseConnection connect() {
return DatabaseConnection.delayed(Future.sync(() async { return DatabaseConnection.delayed(Future(() async {
// Background isolates can't use platform channels, so let's use // Background isolates can't use platform channels, so let's use
// `path_provider` in the main isolate and just send the result containing // `path_provider` in the main isolate and just send the result containing
// the path over to the background isolate. // 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 appDir = await getApplicationDocumentsDirectory();
final dbPath = p.join(appDir.path, 'todos.db'); final dbPath = p.join(appDir.path, 'todos.db');
final receiveDriftIsolate = ReceivePort(); return NativeDatabase.createBackgroundConnection(File(dbPath));
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);
})); }));
} }
/// 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);
}