mirror of https://github.com/AMT-Cheif/drift.git
254 lines
8.1 KiB
Dart
254 lines
8.1 KiB
Dart
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:drift/drift.dart';
|
|
// #docregion isolate
|
|
import 'package:drift/isolate.dart';
|
|
// #enddocregion isolate
|
|
import 'package:drift/native.dart';
|
|
// #docregion initialization
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
// #enddocregion initialization
|
|
|
|
part 'isolates.g.dart';
|
|
|
|
QueryExecutor _openConnection() {
|
|
return NativeDatabase.memory();
|
|
}
|
|
|
|
class SomeTable extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get content => text()();
|
|
}
|
|
|
|
// Copying the definitions here because we can't import Flutter in documentation
|
|
// snippets.
|
|
class RootIsolateToken {
|
|
static final instance = RootIsolateToken();
|
|
}
|
|
|
|
class BackgroundIsolateBinaryMessenger {
|
|
static void ensureInitialized(RootIsolateToken token) {}
|
|
}
|
|
|
|
// #docregion isolate, database-definition
|
|
|
|
@DriftDatabase(tables: [SomeTable] /* ... */)
|
|
class MyDatabase extends _$MyDatabase {
|
|
// A constructor like this can use the default connection as described in the
|
|
// getting started guide, but also allows overriding the connection.
|
|
MyDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
|
|
|
@override
|
|
int get schemaVersion => 1;
|
|
}
|
|
// #enddocregion isolate, database-definition
|
|
|
|
// #docregion driftisolate-spawn
|
|
Future<DriftIsolate> createIsolateWithSpawn() async {
|
|
final token = RootIsolateToken.instance;
|
|
return await DriftIsolate.spawn(() {
|
|
// This function runs in a new isolate, so we must first initialize the
|
|
// messenger to use platform channels.
|
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
|
|
|
// The callback to DriftIsolate.spawn() must return the database connection
|
|
// to use.
|
|
return LazyDatabase(() async {
|
|
// Note that this runs on a background isolate, which only started to
|
|
// support platform channels in Flutter 3.7. For earlier Flutter versions,
|
|
// a workaround is described later in this article.
|
|
final dbFolder = await getApplicationDocumentsDirectory();
|
|
final path = p.join(dbFolder.path, 'app.db');
|
|
|
|
return NativeDatabase(File(path));
|
|
});
|
|
});
|
|
}
|
|
// #enddocregion driftisolate-spawn
|
|
|
|
// #docregion custom-spawn
|
|
Future<DriftIsolate> createIsolateManually() async {
|
|
final receiveIsolate = ReceivePort('receive drift isolate handle');
|
|
await Isolate.spawn<SendPort>((message) async {
|
|
final server = DriftIsolate.inCurrent(() {
|
|
// Again, this needs to return the LazyDatabase or the connection to use.
|
|
// #enddocregion custom-spawn
|
|
throw 'stub';
|
|
// #docregion custom-spawn
|
|
});
|
|
|
|
// Now, inform the original isolate about the created server:
|
|
message.send(server);
|
|
}, receiveIsolate.sendPort);
|
|
|
|
final server = await receiveIsolate.first as DriftIsolate;
|
|
receiveIsolate.close();
|
|
return server;
|
|
}
|
|
// #enddocregion custom-spawn
|
|
|
|
Future<DriftIsolate> createIsolate() => createIsolateWithSpawn();
|
|
|
|
// #docregion isolate
|
|
void main() async {
|
|
final isolate = await createIsolate();
|
|
|
|
// After creating the isolate, calling connect() will return a connection
|
|
// which can be used to create a database.
|
|
// As long as the isolate is used by only one database (it is here), we can
|
|
// use `singleClientMode` to dispose the isolate after closing the connection.
|
|
final database = MyDatabase(await isolate.connect(singleClientMode: true));
|
|
|
|
// you can now use your database exactly like you regularly would, it
|
|
// transparently uses a background isolate to execute queries.
|
|
// #enddocregion isolate
|
|
// Just using the db to avoid an analyzer error, this isn't part of the docs.
|
|
database.customSelect('SELECT 1');
|
|
// #docregion isolate
|
|
}
|
|
// #enddocregion isolate
|
|
|
|
void connectSynchronously() {
|
|
// #docregion delayed
|
|
MyDatabase(
|
|
DatabaseConnection.delayed(Future.sync(() async {
|
|
final isolate = await createIsolate();
|
|
return isolate.connect(singleClientMode: true);
|
|
})),
|
|
);
|
|
// #enddocregion delayed
|
|
}
|
|
|
|
// #docregion initialization
|
|
|
|
Future<DriftIsolate> _createDriftIsolate() async {
|
|
// this method is called from the main isolate. Since we can't use
|
|
// getApplicationDocumentsDirectory on a background isolate, we calculate
|
|
// the database path in the foreground isolate and then inform the
|
|
// background isolate about the path.
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final path = p.join(dir.path, 'db.sqlite');
|
|
final receivePort = ReceivePort();
|
|
|
|
await Isolate.spawn(
|
|
_startBackground,
|
|
_IsolateStartRequest(receivePort.sendPort, path),
|
|
);
|
|
|
|
// _startBackground will send the DriftIsolate to this ReceivePort
|
|
return await receivePort.first as DriftIsolate;
|
|
}
|
|
|
|
void _startBackground(_IsolateStartRequest request) {
|
|
// this is the entry point from the background isolate! Let's create
|
|
// the database from the path we received
|
|
final executor = NativeDatabase(File(request.targetPath));
|
|
// we're using DriftIsolate.inCurrent here as this method already runs on a
|
|
// background isolate. If we used DriftIsolate.spawn, a third isolate would be
|
|
// started which is not what we want!
|
|
final driftIsolate = DriftIsolate.inCurrent(
|
|
() => DatabaseConnection(executor),
|
|
);
|
|
// inform the starting isolate about this, so that it can call .connect()
|
|
request.sendDriftIsolate.send(driftIsolate);
|
|
}
|
|
|
|
// used to bundle the SendPort and the target path, since isolate entry point
|
|
// functions can only take one parameter.
|
|
class _IsolateStartRequest {
|
|
final SendPort sendDriftIsolate;
|
|
final String targetPath;
|
|
|
|
_IsolateStartRequest(this.sendDriftIsolate, this.targetPath);
|
|
}
|
|
// #enddocregion initialization
|
|
|
|
// #docregion init_connect
|
|
DatabaseConnection createDriftIsolateAndConnect() {
|
|
return DatabaseConnection.delayed(Future.sync(() async {
|
|
final isolate = await _createDriftIsolate();
|
|
return await isolate.connect(singleClientMode: true);
|
|
}));
|
|
}
|
|
// #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
|
|
|
|
// #docregion invalid
|
|
Future<void> invalidIsolateUsage() async {
|
|
final database = MyDatabase(NativeDatabase.memory());
|
|
|
|
// Unfortunately, this doesn't work: Drift databases contain references to
|
|
// async primitives like streams and futures that can't be serialized across
|
|
// isolates like this.
|
|
await Isolate.run(() async {
|
|
await database.batch((batch) {
|
|
// ...
|
|
});
|
|
});
|
|
}
|
|
// #enddocregion invalid
|
|
|
|
Future<List<SomeTableData>> _complexAndExpensiveOperationToFetchRows() async {
|
|
throw 'stub';
|
|
}
|
|
|
|
// #docregion compute
|
|
Future<void> insertBulkData(MyDatabase database) async {
|
|
// computeWithDatabase is an extension provided by package:drift/isolate.dart
|
|
await database.computeWithDatabase(
|
|
computation: (database) async {
|
|
// Expensive computation that runs on its own isolate but talks to the
|
|
// main database.
|
|
final rows = await _complexAndExpensiveOperationToFetchRows();
|
|
await database.batch((batch) {
|
|
batch.insertAll(database.someTable, rows);
|
|
});
|
|
},
|
|
connect: (connection) {
|
|
// This function is responsible for creating a second instance of your
|
|
// database class with a short-lived [connection].
|
|
// For this to work, your database class needs to have a constructor that
|
|
// allows taking a connection as described above.
|
|
return MyDatabase(connection);
|
|
},
|
|
);
|
|
}
|
|
// #enddocregion compute
|
|
|
|
// #docregion custom-compute
|
|
Future<void> customIsolateUsage(MyDatabase database) async {
|
|
final connection = await database.serializableConnection();
|
|
|
|
await Isolate.run(
|
|
() async {
|
|
// We can't share the [database] object across isolates, but the connection
|
|
// is fine!
|
|
final databaseForIsolate = MyDatabase(await connection.connect());
|
|
|
|
try {
|
|
await databaseForIsolate.batch((batch) {
|
|
// (...)
|
|
});
|
|
} finally {
|
|
databaseForIsolate.close();
|
|
}
|
|
},
|
|
debugName: 'My custom database task',
|
|
);
|
|
}
|
|
// #enddocregion custom-compute
|