Improve docs a bit, refactor onOpen callback

This commit is contained in:
Simon Binder 2019-06-20 11:41:00 +02:00
parent 373d7c6478
commit 8204f6b393
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 209 additions and 27 deletions

View File

@ -7,6 +7,7 @@ analyzer:
unused_local_variable: error
dead_code: error
override_on_non_overriding_method: error
deprecated_member_use_from_same_package: ignore
exclude:
- "**/*.g.dart"
# Will be analyzed anyway, nobody knows why ¯\_(ツ)_/¯. We're only analyzing lib/ and test/ as a workaround

View File

@ -72,7 +72,7 @@
<span class="fs-2">
Was this page helpful? Please
{% assign url = page.url | absolute_url %}
{% assign body = 'From documentation URL: ' | append: url %}
{% assign body = 'Refers to the documentation: ' | append: url %}
<a href="{{site.github_link}}/issues/new?body={{body | url_encode}}">report an issue</a>
if you have questions or run into problems.
</span>

41
docs/docs/faq.md Normal file
View File

@ -0,0 +1,41 @@
---
title: Frequently asked questions
nav_order: 6
permalink: /faq/
---
## Using the database
If you've created a `MyDatabase` class by following the [getting started guide]({{site.url}}/getting-started/), you
still need to somehow obtain an instance of it. It's recommended to only have one (singleton) instance of your database,
so you could store that instance in a global variable:
### Vanilla flutter
```dart
MyDatabase database;
void main() {
database = MyDatabase();
runApp(MyFlutterApp());
}
```
It would be cleaner to use `InheritedWidgets` for that, and the `provider` package helps here:
### Provider
If you're using the [provider](https://pub.dev/packages/provider) package, you can wrap your top-level widget in a
provider that manages the database instance:
```dart
void main() {
runApp(
Provider<MyDatabase>(
builder: (context) => MyDatabase(),
child: MyFlutterApp(),
),
);
}
```
Your widgets would then have access to the database using `Provider.of<MyDatabase>(context)`.
### A more complex architecture
If you're strict on keeping your business logic out of the widget layer, you probably use some dependency injection
framework like `kiwi` or `get_it` to instantiate services and view models. Creating a singleton instance of `MyDatabase`
in your favorite dependency injection framework for flutter hence solves this problem for you.

View File

@ -8,4 +8,8 @@ permalink: /getting-started/
# Getting started
{% include content/getting_started.md %}
Congrats, you are now ready to fully use moor and [write queries]({{site.url}}/queries/).
Congratulations, you now have a class which you can use to easily write queries.
A detailed guide on how to do that is written [here]({{site.url}}/queries/).
PS: You might be asking how you would actually obtain an instance of `MyDatabase` for
your widgets. If so, [here]({{site.url}}/faq/#using-the-database) is some guidance.

View File

@ -42,4 +42,3 @@ let me know by [creating an issue]({{site.github_link}}/issues/new)!
- VM apps
- Web apps via `AlaSQL` or a different engine?
- References (can be expressed via custom constraints, see issue [#14](https://github.com/simolus3/moor/issues/14))
- When inserts / updates fail due to invalid data, explain why that happened

View File

@ -1,6 +1,7 @@
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/executor/before_open.dart';
import 'package:moor/src/runtime/executor/stream_queries.dart';
import 'package:moor/src/types/type_system.dart';
import 'package:moor/src/runtime/statements/delete.dart';
@ -199,6 +200,8 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
/// changes in your schema, you'll need a custom migration strategy to create
/// the new tables or change the columns.
MigrationStrategy get migration => MigrationStrategy();
MigrationStrategy _cachedMigration;
MigrationStrategy get _resolvedMigration => _cachedMigration ??= migration;
/// A list of tables specified in this database.
List<TableInfo> get allTables;
@ -218,7 +221,7 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
/// strategy. This method should not be called by users.
Future<void> handleDatabaseCreation({@required SqlExecutor executor}) {
final migrator = _createMigrator(executor);
return migration.onCreate(migrator);
return _resolvedMigration.onCreate(migrator);
}
/// Handles database updates by delegating the work to the [migration]
@ -226,6 +229,21 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
Future<void> handleDatabaseVersionChange(
{@required SqlExecutor executor, int from, int to}) {
final migrator = _createMigrator(executor);
return migration.onUpgrade(migrator, from, to);
return _resolvedMigration.onUpgrade(migrator, from, to);
}
/// Handles the before opening callback as set in the [migration]. This method
/// is used internally by database implementations and should not be called by
/// users.
Future<void> beforeOpenCallback(
QueryExecutor executor, OpeningDetails details) async {
final migration = _resolvedMigration;
if (migration.onFinished != null) {
await migration.onFinished();
}
if (migration.beforeOpen != null) {
final engine = BeforeOpenEngine(this, executor);
await migration.beforeOpen(engine, details);
}
}
}

View File

@ -0,0 +1,25 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/executor/stream_queries.dart';
/// Used internally by moor.
class BeforeOpenEngine extends DatabaseConnectionUser with QueryEngine {
BeforeOpenEngine(DatabaseConnectionUser other, QueryExecutor executor)
: super.delegate(
other,
executor: executor,
streamQueries: _IgnoreStreamQueries(),
);
}
class _IgnoreStreamQueries extends StreamQueryStore {
@override
Stream<T> registerStream<T>(QueryStreamFetcher<T> statement) {
throw StateError('Streams cannot be created inside a transaction. See the '
'documentation of GeneratedDatabase.transaction for details.');
}
@override
Future handleTableUpdates(Set<TableInfo> tables) {
return Future.value(null);
}
}

View File

@ -10,8 +10,17 @@ typedef Future<void> OnUpgrade(Migrator m, int from, int to);
/// Signature of a function that's called after a migration has finished and the
/// database is ready to be used. Useful to populate data.
@deprecated
typedef Future<void> OnMigrationFinished();
/// Signature of a function that's called before a database is marked opened by
/// moor, but after migrations took place. This is a suitable callback to to
/// populate initial data or issue `PRAGMA` statements that you want to use.
/// All queries must be sent to [db] directly, otherwise your code will hang.
/// See the discussion at [QueryEngine.transaction] for details.
typedef OnBeforeOpen = Future<void> Function(
QueryEngine db, OpeningDetails details);
Future<void> _defaultOnCreate(Migrator m) => m.createAllTables();
Future<void> _defaultOnUpdate(Migrator m, int from, int to) async =>
throw Exception("You've bumped the schema version for your moor database "
@ -29,12 +38,21 @@ class MigrationStrategy {
/// Executes after the database is ready and all migrations ran, but before
/// any other queries will be executed, making this method suitable to
/// populate data.
@Deprecated('Use beforeOpen instead')
final OnMigrationFinished onFinished;
/// Executes after the database is ready to be used (ie. it has been opened
/// and all migrations ran), but before any other queries will be sent. This
/// makes it a suitable place to populate data after the database has been
/// created or set sqlite `PRAGMAS` that you need.
final OnBeforeOpen beforeOpen;
MigrationStrategy({
this.onCreate = _defaultOnCreate,
this.onUpgrade = _defaultOnUpdate,
this.onFinished,
this.beforeOpen,
@Deprecated('This callback is broken. Use beforeOpen instead')
this.onFinished,
});
}
@ -123,6 +141,25 @@ class Migrator {
}
}
/// Provides information about whether migrations ran before opening the
/// database.
class OpeningDetails {
/// The schema version before the database has been opened, or `null` if the
/// database has just been created.
final int versionBefore;
/// The schema version after running migrations.
final int versionNow;
/// Whether the database has been created during this session.
bool get wasCreated => versionBefore == null;
/// Whether a schema upgrade was performed while opening the database.
bool get hadUpgrade => !wasCreated && versionBefore != versionNow;
const OpeningDetails(this.versionBefore, this.versionNow);
}
class _SimpleSqlAsQueryExecutor extends QueryExecutor {
final SqlExecutor executor;

View File

@ -8,7 +8,7 @@
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="example"
android:label="Moor example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"

View File

@ -45,7 +45,7 @@ class Database extends _$Database {
path: 'db.sqlite', logStatements: true));
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration {
@ -58,6 +58,27 @@ class Database extends _$Database {
await m.addColumn(todos, todos.targetDate);
}
},
beforeOpen: (db, details) async {
if (details.wasCreated) {
// create default categories and entries
final workId =
await db.into(categories).insert(Category(description: 'Work'));
await db.into(todos).insert(TodoEntry(
content: 'A first todo entry',
category: null,
targetDate: DateTime.now(),
));
await db.into(todos).insert(
TodoEntry(
content: 'Rework persistence code',
category: workId,
targetDate: DateTime.now().add(const Duration(days: 4)),
),
);
}
},
);
}

View File

@ -55,15 +55,16 @@ class HomeScreenState extends State<HomeScreen> {
sizeFactor: animation,
axis: Axis.vertical,
child: AnimatedBuilder(
animation:
CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: TodoCard(item.entry),
builder: (context, child) {
return Opacity(
opacity: animation.value,
child: child,
);
}),
animation:
CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: TodoCard(item.entry),
builder: (context, child) {
return Opacity(
opacity: animation.value,
child: child,
);
},
),
);
},
),

View File

@ -80,7 +80,9 @@ class FlutterQueryExecutor extends _DatabaseOwner {
@override
s.Database db;
Completer<void> _openingCompleter;
bool _hadMigration = false;
int _versionBefore;
FlutterQueryExecutor({@required this.path, bool logStatements})
: _inDbPath = false,
@ -93,10 +95,29 @@ class FlutterQueryExecutor extends _DatabaseOwner {
@override
Future<bool> ensureOpen() async {
// mechanism to ensure that _openDatabase is only called once, even if we
// have many queries calling ensureOpen() repeatedly. _openingCompleter is
// set if we're currently in the process of opening the database.
if (_openingCompleter != null) {
// already opening, wait for that to finish and don't open the database
// again
await _openingCompleter.future;
return true;
}
if (db != null && db.isOpen) {
// database is opened and ready
return true;
}
// alright, opening the database
_openingCompleter = Completer();
await _openDatabase();
_openingCompleter.complete();
return true;
}
Future _openDatabase() async {
String resolvedPath;
if (_inDbPath) {
resolvedPath = join(await s.getDatabasesPath(), path);
@ -112,19 +133,17 @@ class FlutterQueryExecutor extends _DatabaseOwner {
);
}, onUpgrade: (db, from, to) {
_hadMigration = true;
_versionBefore = from;
return databaseInfo.handleDatabaseVersionChange(
executor: _migrationExecutor(db), from: from, to: to);
}, onOpen: (db) async {
db = db;
// the openDatabase future will resolve later, so we can get an instance
// where we can send the queries from the onFinished operation;
final fn = databaseInfo.migration.onFinished;
if (fn != null && _hadMigration) {
await fn();
}
});
final versionNow = await db.getVersion();
final resolvedPrevious = _hadMigration ? _versionBefore : versionNow;
final details = OpeningDetails(resolvedPrevious, versionNow);
return true;
await databaseInfo.beforeOpenCallback(
_BeforeOpenExecutor(db, logStatements), details);
});
}
SqlExecutor _migrationExecutor(s.Database db) {
@ -189,3 +208,19 @@ class _SqfliteTransactionExecutor extends _DatabaseOwner
return _sendFuture;
}
}
class _BeforeOpenExecutor extends _DatabaseOwner {
@override
final s.DatabaseExecutor db;
_BeforeOpenExecutor(this.db, bool logStatements) : super(logStatements);
@override
TransactionExecutor beginTransaction() {
throw UnsupportedError(
"Transactions can't be started in the befoeOpen callback");
}
@override
Future<bool> ensureOpen() => Future.value(true);
}

View File

@ -59,7 +59,7 @@ packages:
path: "../moor"
relative: true
source: path
version: "1.4.0"
version: "1.4.0+1"
path:
dependency: "direct main"
description: