Add DatabaseConnection.delayed constructor

This commit is contained in:
Simon Binder 2020-08-13 21:09:26 +02:00
parent a037de6621
commit a2b28945d1
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 164 additions and 57 deletions

View File

@ -63,6 +63,24 @@ void main() async {
} }
``` ```
If you need to construct the database outside of an `async` context, you can use the new
`DatabaseConnection.delayed` constructor introduced in moor 3.4. In the example above, you
could synchronously obtain a `TodoDb` by using:
```dart
Future<DatabaseConnection> _connectAsync() async {
MoorIsolate isolate = await MoorIsolate.spawn(_backgroundConnection);
return isolate.connect();
}
void main() {
final db = TodoDb.connect(DatabaseConnection.delayed(_connectAsync()));
}
```
This can be helpful when using moor in DI frameworks, since you have the database available
immediately. Internally, moor will connect when the first query is sent to the database.
### Initialization on the main thread ### Initialization on the main thread
Platform channels are not available on background isolates, but sometimes you might want to use Platform channels are not available on background isolates, but sometimes you might want to use

View File

@ -1,3 +1,8 @@
## 3.4.0 (unreleased)
- New `DatabaseConnection.delayed` constructor to synchronously obtain a database connection
that requires async setup. This can be useful when connecting to a `MoorIsolate`.
## 3.3.1 ## 3.3.1
- Support changing `onData` handlers for query streams. - Support changing `onData` handlers for query streams.

View File

@ -26,6 +26,41 @@ class DatabaseConnection {
: typeSystem = SqlTypeSystem.defaultInstance, : typeSystem = SqlTypeSystem.defaultInstance,
streamQueries = StreamQueryStore(); streamQueries = StreamQueryStore();
/// Database connection that is instantly available, but delegates work to a
/// connection only available through a `Future`.
///
/// This can be useful in scenarios where you need to obtain a database
/// instance synchronously, but need an async setup. A prime example here is
/// `MoorIsolate`:
///
/// ```dart
/// @UseMoor(...)
/// class MyDatabase extends _$MyDatabase {
/// MyDatabase._connect(DatabaseConnection c): super.connect(c);
///
/// factory MyDatabase.fromIsolate(MoorIsolate isolate) {
/// return MyDatabase._connect(
/// // isolate.connect() returns a future, but we can still return a
/// // database synchronously thanks to DatabaseConnection.delayed!
/// DatabaseConnection.delayed(isolate.connect()),
/// );
/// }
/// }
/// ```
factory DatabaseConnection.delayed(FutureOr<DatabaseConnection> connection) {
if (connection is DatabaseConnection) {
return connection;
}
final future = connection as Future<DatabaseConnection>;
return DatabaseConnection(
SqlTypeSystem.defaultInstance,
LazyDatabase(() async => (await future).executor),
DelayedStreamQueryStore(future.then((conn) => conn.streamQueries)),
);
}
/// Returns a database connection that is identical to this one, except that /// Returns a database connection that is identical to this one, except that
/// it uses the provided [executor]. /// it uses the provided [executor].
DatabaseConnection withExecutor(QueryExecutor executor) { DatabaseConnection withExecutor(QueryExecutor executor) {

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor/src/runtime/executor/delayed_stream_queries.dart';
import 'package:moor/src/runtime/executor/stream_queries.dart'; import 'package:moor/src/runtime/executor/stream_queries.dart';
part 'batch.dart'; part 'batch.dart';

View File

@ -0,0 +1,50 @@
import 'package:moor/src/runtime/api/runtime_api.dart';
import 'stream_queries.dart';
/// Version of [StreamQueryStore] that delegates work to an asynchronously-
/// available delegate.
/// This class is internal and should not be exposed to moor users. It's used
/// through a delayed database connection.
class DelayedStreamQueryStore implements StreamQueryStore {
Future<StreamQueryStore> _delegate;
StreamQueryStore _resolved;
/// Creates a [StreamQueryStore] that will work after [delegate] is
/// available.
DelayedStreamQueryStore(Future<StreamQueryStore> delegate) {
_delegate = delegate.then((value) => _resolved = value);
}
@override
Future<void> close() async => (await _delegate).close();
@override
void handleTableUpdates(Set<TableUpdate> updates) {
_resolved?.handleTableUpdates(updates);
}
@override
void markAsClosed(QueryStream stream, Function() whenRemoved) {
throw UnimplementedError('The stream will call this on the delegate');
}
@override
void markAsOpened(QueryStream stream) {
throw UnimplementedError('The stream will call this on the delegate');
}
@override
Stream<T> registerStream<T>(QueryStreamFetcher<T> fetcher) {
return Stream.fromFuture(_delegate)
.asyncExpand((resolved) => resolved.registerStream(fetcher))
.asBroadcastStream();
}
@override
Stream<Null> updatesForSync(TableUpdateQuery query) {
return Stream.fromFuture(_delegate)
.asyncExpand((resolved) => resolved.updatesForSync(query))
.asBroadcastStream();
}
}

View File

@ -66,4 +66,13 @@ class LazyDatabase extends QueryExecutor {
@override @override
Future<int> runUpdate(String statement, List args) => Future<int> runUpdate(String statement, List args) =>
_delegate.runUpdate(statement, args); _delegate.runUpdate(statement, args);
@override
Future<void> close() {
if (_delegate != null) {
return _delegate.close();
} else {
return Future.value();
}
}
} }

View File

@ -1,6 +1,6 @@
name: moor name: moor
description: Moor is a safe and reactive persistence library for Dart applications description: Moor is a safe and reactive persistence library for Dart applications
version: 3.3.1 version: 3.4.0-dev
repository: https://github.com/simolus3/moor repository: https://github.com/simolus3/moor
homepage: https://moor.simonbinder.eu/ homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -83,26 +83,25 @@ void main() {
void _runTests( void _runTests(
FutureOr<MoorIsolate> Function() spawner, bool terminateIsolate) { FutureOr<MoorIsolate> Function() spawner, bool terminateIsolate) {
MoorIsolate isolate; MoorIsolate isolate;
DatabaseConnection isolateConnection; TodoDb database;
setUp(() async { setUp(() async {
isolate = await spawner(); isolate = await spawner();
isolateConnection = await isolate.connect(isolateDebugLog: false);
database = TodoDb.connect(
DatabaseConnection.delayed(isolate.connect(isolateDebugLog: false)),
);
}); });
tearDown(() { tearDown(() async {
isolateConnection.executor.close(); await database.close();
if (terminateIsolate) { if (terminateIsolate) {
return isolate.shutdownAll(); await isolate.shutdownAll();
} else {
return Future.value();
} }
}); });
test('can open database and send requests', () async { test('can open database and send requests', () async {
final database = TodoDb.connect(isolateConnection);
final result = await database.select(database.todosTable).get(); final result = await database.select(database.todosTable).get();
expect(result, isEmpty); expect(result, isEmpty);
}); });
@ -110,7 +109,6 @@ void _runTests(
test('can run beforeOpen', () async { test('can run beforeOpen', () async {
var beforeOpenCalled = false; var beforeOpenCalled = false;
final database = TodoDb.connect(isolateConnection);
database.migration = MigrationStrategy(beforeOpen: (details) async { database.migration = MigrationStrategy(beforeOpen: (details) async {
await database.customStatement('PRAGMA foreign_keys = ON'); await database.customStatement('PRAGMA foreign_keys = ON');
beforeOpenCalled = true; beforeOpenCalled = true;
@ -118,26 +116,20 @@ void _runTests(
// run a select statement to verify that the database is open // run a select statement to verify that the database is open
await database.customSelect('SELECT 1').get(); await database.customSelect('SELECT 1').get();
await database.close();
expect(beforeOpenCalled, isTrue); expect(beforeOpenCalled, isTrue);
}); });
test('stream queries work as expected', () async { test('stream queries work as expected', () async {
final database = TodoDb.connect(isolateConnection);
final initialCompanion = TodosTableCompanion.insert(content: 'my content'); final initialCompanion = TodosTableCompanion.insert(content: 'my content');
final stream = database.select(database.todosTable).watchSingle(); final stream = database.select(database.todosTable).watchSingle();
final expectation = expectLater(
stream,
emitsInOrder([null, TodoEntry(id: 1, content: 'my content')]),
);
await expectLater(stream, emits(null));
await database.into(database.todosTable).insert(initialCompanion); await database.into(database.todosTable).insert(initialCompanion);
await expectation; await expectLater(stream, emits(TodoEntry(id: 1, content: 'my content')));
}); });
test('can start transactions', () async { test('can start transactions', () async {
final database = TodoDb.connect(isolateConnection);
final initialCompanion = TodosTableCompanion.insert(content: 'my content'); final initialCompanion = TodosTableCompanion.insert(content: 'my content');
await database.transaction(() async { await database.transaction(() async {
@ -149,15 +141,12 @@ void _runTests(
}); });
test('supports no-op transactions', () async { test('supports no-op transactions', () async {
final database = TodoDb.connect(isolateConnection);
await database.transaction(() { await database.transaction(() {
return Future.value(null); return Future.value(null);
}); });
await database.close();
}); });
test('supports transactions in migrations', () async { test('supports transactions in migrations', () async {
final database = TodoDb.connect(isolateConnection);
database.migration = MigrationStrategy(beforeOpen: (details) async { database.migration = MigrationStrategy(beforeOpen: (details) async {
await database.transaction(() async { await database.transaction(() async {
return await database.customSelect('SELECT 1').get(); return await database.customSelect('SELECT 1').get();
@ -165,15 +154,11 @@ void _runTests(
}); });
await database.customSelect('SELECT 2').get(); await database.customSelect('SELECT 2').get();
await database.close();
}); });
test('transactions have an isolated view on data', () async { test('transactions have an isolated view on data', () async {
// regression test for https://github.com/simolus3/moor/issues/324 // regression test for https://github.com/simolus3/moor/issues/324
final db = TodoDb.connect(isolateConnection); await database
await db
.customStatement('create table tbl (id integer primary key not null)'); .customStatement('create table tbl (id integer primary key not null)');
Future<void> expectRowCount(TodoDb db, int count) async { Future<void> expectRowCount(TodoDb db, int count) async {
@ -182,74 +167,65 @@ void _runTests(
} }
final rowInserted = Completer<void>(); final rowInserted = Completer<void>();
final runTransaction = db.transaction(() async { final runTransaction = database.transaction(() async {
await db.customInsert('insert into tbl default values'); await database.customInsert('insert into tbl default values');
await expectRowCount(db, 1); await expectRowCount(database, 1);
rowInserted.complete(); rowInserted.complete();
// Hold transaction open for expectRowCount() outside the transaction to // Hold transaction open for expectRowCount() outside the transaction to
// finish // finish
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
await db.customStatement('delete from tbl'); await database.customStatement('delete from tbl');
await expectRowCount(db, 0); await expectRowCount(database, 0);
}); });
await rowInserted.future; await rowInserted.future;
await expectRowCount(db, 0); await expectRowCount(database, 0);
await runTransaction; // wait for the transaction to complete await runTransaction; // wait for the transaction to complete
await db.close();
}); });
test("can't run queries on a closed database", () async { test("can't run queries on a closed database", () async {
final db = TodoDb.connect(isolateConnection); await database.customSelect('SELECT 1;').getSingle();
await db.customSelect('SELECT 1;').getSingle();
await db.close(); await database.close();
await expectLater( await expectLater(
() => db.customSelect('SELECT 1;').getSingle(), throwsStateError); () => database.customSelect('SELECT 1;').getSingle(), throwsStateError);
}); });
test('can run deletes, updates and batches', () async { test('can run deletes, updates and batches', () async {
final db = TodoDb.connect(isolateConnection); await database.into(database.users).insert(
await db.into(db.users).insert(
UsersCompanion.insert(name: 'simon.', profilePicture: Uint8List(0))); UsersCompanion.insert(name: 'simon.', profilePicture: Uint8List(0)));
await db await database
.update(db.users) .update(database.users)
.write(const UsersCompanion(name: Value('changed name'))); .write(const UsersCompanion(name: Value('changed name')));
var result = await db.select(db.users).getSingle(); var result = await database.select(database.users).getSingle();
expect(result.name, 'changed name'); expect(result.name, 'changed name');
await db.delete(db.users).go(); await database.delete(database.users).go();
await db.batch((batch) { await database.batch((batch) {
batch.insert( batch.insert(
db.users, database.users,
UsersCompanion.insert(name: 'not simon', profilePicture: Uint8List(0)), UsersCompanion.insert(name: 'not simon', profilePicture: Uint8List(0)),
); );
}); });
result = await db.select(db.users).getSingle(); result = await database.select(database.users).getSingle();
expect(result.name, 'not simon'); expect(result.name, 'not simon');
await db.close();
}); });
test('transactions can be rolled back', () async { test('transactions can be rolled back', () async {
final db = TodoDb.connect(isolateConnection); await expectLater(database.transaction(() async {
await database.into(database.categories).insert(
await expectLater(db.transaction(() async {
await db.into(db.categories).insert(
CategoriesCompanion.insert(description: 'my fancy description')); CategoriesCompanion.insert(description: 'my fancy description'));
throw Exception('expected'); throw Exception('expected');
}), throwsException); }), throwsException);
final result = await db.select(db.categories).get(); final result = await database.select(database.categories).get();
expect(result, isEmpty); expect(result, isEmpty);
await db.close(); await database.close();
}); });
} }

View File

@ -79,4 +79,17 @@ void main() {
verify(inner.ensureOpen(user)); verify(inner.ensureOpen(user));
}); });
test('can close inner executor', () async {
final inner = MockExecutor();
final lazy = LazyDatabase(() => inner);
final user = _LazyQueryUserForTest();
await lazy.close(); // Close before opening, expect no effect
await lazy.ensureOpen(user);
await lazy.close();
verify(inner.close());
});
} }