mirror of https://github.com/AMT-Cheif/drift.git
Add helper method to setup isolates more easily
This commit is contained in:
parent
0231225733
commit
cd00c6899e
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue