2019-03-09 08:01:55 -08:00
|
|
|
|
# Moor
|
|
|
|
|
[![Build Status](https://travis-ci.com/simolus3/moor.svg?token=u4VnFEE5xnWVvkE6QsqL&branch=master)](https://travis-ci.com/simolus3/moor)
|
2019-03-27 10:56:14 -07:00
|
|
|
|
[![codecov](https://codecov.io/gh/simolus3/moor/branch/master/graph/badge.svg)](https://codecov.io/gh/simolus3/moor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| Core API | Flutter | Generator |
|
|
|
|
|
|:-------------:|:-------------:|:-----:|
|
|
|
|
|
| [![Generator version](https://img.shields.io/pub/v/moor.svg)](https://pub.dartlang.org/packages/moor) | [![Flutter version](https://img.shields.io/pub/v/moor_flutter.svg)](https://pub.dartlang.org/packages/moor_flutter) | [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](https://pub.dartlang.org/packages/moor_generator) |
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
2019-03-09 11:35:29 -08:00
|
|
|
|
Moor is an easy to use and safe way to persist data for Flutter apps. It features
|
2019-02-18 11:51:37 -08:00
|
|
|
|
a fluent Dart DSL to describe tables and will generate matching database code that
|
|
|
|
|
can be used to easily read and store your app's data. It also features a reactive
|
|
|
|
|
API that will deliver auto-updating streams for your queries.
|
|
|
|
|
|
2019-03-09 11:35:29 -08:00
|
|
|
|
- [Moor](#moor)
|
2019-02-18 11:51:37 -08:00
|
|
|
|
* [Getting started](#getting-started)
|
|
|
|
|
+ [Adding the dependency](#adding-the-dependency)
|
|
|
|
|
+ [Declaring tables](#declaring-tables)
|
|
|
|
|
+ [Generating the code](#generating-the-code)
|
|
|
|
|
* [Writing queries](#writing-queries)
|
|
|
|
|
+ [Select statements](#select-statements)
|
|
|
|
|
- [Where](#where)
|
|
|
|
|
- [Limit](#limit)
|
2019-02-20 07:37:09 -08:00
|
|
|
|
- [Ordering](#ordering)
|
2019-02-18 11:51:37 -08:00
|
|
|
|
+ [Updates and deletes](#updates-and-deletes)
|
|
|
|
|
+ [Inserts](#inserts)
|
2019-03-01 11:58:38 -08:00
|
|
|
|
+ [Custom statements](#custom-statements)
|
2019-02-18 11:51:37 -08:00
|
|
|
|
* [Migrations](#migrations)
|
|
|
|
|
* [TODO-List and current limitations](#todo-list-and-current-limitations)
|
2019-02-20 07:37:09 -08:00
|
|
|
|
+ [Limitations (at the moment)](#limitations-at-the-moment)
|
2019-02-18 11:51:37 -08:00
|
|
|
|
+ [Planned for the future](#planned-for-the-future)
|
|
|
|
|
+ [Interesting stuff that would be nice to have](#interesting-stuff-that-would-be-nice-to-have)
|
|
|
|
|
|
|
|
|
|
## Getting started
|
|
|
|
|
### Adding the dependency
|
2019-03-10 07:21:42 -07:00
|
|
|
|
First, let's add moor to your project's `pubspec.yaml`.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
```yaml
|
|
|
|
|
dependencies:
|
2019-03-27 10:56:14 -07:00
|
|
|
|
moor_flutter: # use the latest version
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
|
|
|
|
dev_dependencies:
|
2019-03-27 10:56:14 -07:00
|
|
|
|
moor_generator: # use the latest versions
|
2019-03-10 07:21:42 -07:00
|
|
|
|
build_runner:
|
2019-02-18 11:51:37 -08:00
|
|
|
|
```
|
2019-03-09 08:01:55 -08:00
|
|
|
|
We're going to use the `moor_flutter` library to specify tables and access the database. The
|
|
|
|
|
`moor_generator` library will take care of generating the necessary code so the
|
2019-02-18 11:51:37 -08:00
|
|
|
|
library knows how your table structure looks like.
|
|
|
|
|
|
|
|
|
|
### Declaring tables
|
|
|
|
|
You can use the DSL included with this library to specify your libraries with simple
|
|
|
|
|
dart code:
|
|
|
|
|
```dart
|
2019-03-09 08:01:55 -08:00
|
|
|
|
import 'package:moor_flutter/moor_flutter.dart';
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
|
|
|
|
// assuming that your file is called filename.dart. This will give an error at first,
|
2019-03-09 08:01:55 -08:00
|
|
|
|
// but it's needed for moor to know about the generated code
|
2019-02-18 11:51:37 -08:00
|
|
|
|
part 'filename.g.dart';
|
|
|
|
|
|
|
|
|
|
// this will generate a table called "todos" for us. The rows of that table will
|
|
|
|
|
// be represented by a class called "Todo".
|
|
|
|
|
class Todos extends Table {
|
|
|
|
|
IntColumn get id => integer().autoIncrement()();
|
|
|
|
|
TextColumn get title => text().withLength(min: 6, max: 10)();
|
|
|
|
|
TextColumn get content => text().named('body')();
|
|
|
|
|
IntColumn get category => integer().nullable()();
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-09 08:01:55 -08:00
|
|
|
|
// This will make moor generate a class called "Category" to represent a row in this table.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
// By default, "Categorie" would have been used because it only strips away the trailing "s"
|
|
|
|
|
// in the table name.
|
|
|
|
|
@DataClassName("Category")
|
|
|
|
|
class Categories extends Table {
|
|
|
|
|
|
|
|
|
|
IntColumn get id => integer().autoIncrement()();
|
|
|
|
|
TextColumn get description => text()();
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-09 08:01:55 -08:00
|
|
|
|
// this annotation tells moor to prepare a database class that uses both of the
|
2019-02-18 11:51:37 -08:00
|
|
|
|
// tables we just defined. We'll see how to use that database class in a moment.
|
2019-03-10 04:00:25 -07:00
|
|
|
|
@UseMoor(tables: [Todos, Categories])
|
2019-02-18 11:51:37 -08:00
|
|
|
|
class MyDatabase {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2019-03-15 12:09:51 -07:00
|
|
|
|
__⚠️ Note:__ The column definitions, the table name and the primary key must be known at
|
|
|
|
|
compile time. For column definitions and the primary key, the function must use the `=>`
|
|
|
|
|
operator and can't contain anything more than what's included in this `readme` and the
|
|
|
|
|
examples. Otherwise, the generator won't be able to know what's going on.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
|
|
|
|
### Generating the code
|
2019-03-09 11:35:29 -08:00
|
|
|
|
Moor integrates with the dart `build` system, so you can generate all the code needed with
|
|
|
|
|
`flutter packages pub run build_runner build`. If you want to continously rebuild the generated code
|
2019-02-18 11:51:37 -08:00
|
|
|
|
whever you change your code, run `flutter packages pub run build_runner watch` instead.
|
2019-03-09 11:35:29 -08:00
|
|
|
|
After running either command once, the moor generator will have created a class for your
|
2019-02-18 11:51:37 -08:00
|
|
|
|
database and data classes for your entities. To use it, change the `MyDatabase` class as
|
|
|
|
|
follows:
|
|
|
|
|
```dart
|
2019-03-09 11:35:29 -08:00
|
|
|
|
@UseMoor(tables: [Todos, Categories])
|
2019-02-18 11:51:37 -08:00
|
|
|
|
class MyDatabase extends _$MyDatabase {
|
|
|
|
|
// we tell the database where to store the data with this constructor
|
|
|
|
|
MyDatabase() : super(FlutterQueryExecutor.inDatabaseFolder(path: 'db.sqlite'));
|
|
|
|
|
|
|
|
|
|
// you should bump this number whenever you change or add a table definition. Migrations
|
|
|
|
|
// are covered later in this readme.
|
|
|
|
|
@override
|
|
|
|
|
int get schemaVersion => 1;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
You can ignore the `schemaVersion` at the moment, the important part is that you can
|
|
|
|
|
now run your queries with fluent Dart code:
|
|
|
|
|
## Writing queries
|
|
|
|
|
```dart
|
2019-03-09 11:35:29 -08:00
|
|
|
|
// inside the database class:
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
|
|
|
|
// loads all todo entries
|
|
|
|
|
Future<List<Todo>> get allTodoEntries => select(todos).get();
|
|
|
|
|
|
|
|
|
|
// watches all todo entries in a given category. The stream will automatically
|
|
|
|
|
// emit new items whenever the underlying data changes.
|
|
|
|
|
Stream<List<TodoEntry>> watchEntriesInCategory(Category c) {
|
|
|
|
|
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
### Select statements
|
|
|
|
|
You can create `select` statements by starting them with `select(tableName)`, where the
|
|
|
|
|
table name
|
2019-03-09 08:01:55 -08:00
|
|
|
|
is a field generated for you by moor. Each table used in a database will have a matching field
|
2019-03-09 11:35:29 -08:00
|
|
|
|
to run queries against. Any query can be run once with `get()` or be turned into an auto-updating
|
2019-02-18 11:51:37 -08:00
|
|
|
|
stream using `watch()`.
|
|
|
|
|
#### Where
|
|
|
|
|
You can apply filters to a query by calling `where()`. The where method takes a function that
|
|
|
|
|
should map the given table to an `Expression` of boolean. A common way to create such expression
|
|
|
|
|
is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan`
|
|
|
|
|
and `isSmallerThan`. You can compose expressions using `and(a, b), or(a, b)` and `not(a)`.
|
|
|
|
|
#### Limit
|
|
|
|
|
You can limit the amount of results returned by calling `limit` on queries. The method accepts
|
|
|
|
|
the amount of rows to return and an optional offset.
|
2019-02-19 02:17:21 -08:00
|
|
|
|
#### Ordering
|
|
|
|
|
You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual
|
|
|
|
|
ordering terms from the table.
|
|
|
|
|
```dart
|
|
|
|
|
Future<List<TodoEntry>> sortEntriesAlphabetically() {
|
|
|
|
|
return (select(todos)..orderBy([(t) => OrderingTerm(expression: t.title)])).get();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
You can also reverse the order by setting the `mode` property of the `OrderingTerm` to
|
|
|
|
|
`OrderingMode.desc`.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
### Updates and deletes
|
|
|
|
|
You can use the generated `row` class to update individual fields of any row:
|
|
|
|
|
```dart
|
|
|
|
|
Future moveImportantTasksIntoCategory(Category target) {
|
2019-03-09 07:20:27 -08:00
|
|
|
|
// use update(...).write when you have a custom where clause and want to update
|
|
|
|
|
// only the columns that you specify (here, only "category" will be updated, the
|
|
|
|
|
// title and description of the rows affected will be left unchanged).
|
|
|
|
|
// Notice that you can't set fields back to null with this method.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
return (update(todos)
|
|
|
|
|
..where((t) => t.title.like('%Important%'))
|
|
|
|
|
).write(TodoEntry(
|
|
|
|
|
category: target.id
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-09 07:20:27 -08:00
|
|
|
|
Future update(TodoEntry entry) {
|
|
|
|
|
// using replace will update all fields from the entry that are not marked as a primary key.
|
|
|
|
|
// it will also make sure that only the entry with the same primary key will be updated.
|
|
|
|
|
// Here, this means that the row that has the same id as entry will be updated to reflect
|
|
|
|
|
// the entry's title, content and category. Unlike write, this supports setting columns back
|
2019-03-09 11:35:29 -08:00
|
|
|
|
// to null. As it set's its where clause automatically, it can not be used together with where.
|
2019-03-09 07:20:27 -08:00
|
|
|
|
return update(todos).replace(entry);
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 11:51:37 -08:00
|
|
|
|
Future feelingLazy() {
|
2019-02-20 09:03:45 -08:00
|
|
|
|
// delete the oldest nine entries
|
|
|
|
|
return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
|
2019-02-18 11:51:37 -08:00
|
|
|
|
}
|
|
|
|
|
```
|
2019-02-19 02:17:21 -08:00
|
|
|
|
__⚠️ Caution:__ If you don't explicitly add a `where` clause on updates or deletes,
|
2019-02-18 11:51:37 -08:00
|
|
|
|
the statement will affect all rows in the table!
|
|
|
|
|
|
|
|
|
|
### Inserts
|
|
|
|
|
You can very easily insert any valid object into tables:
|
|
|
|
|
```dart
|
|
|
|
|
// returns the generated id
|
|
|
|
|
Future<int> addTodoEntry(Todo entry) {
|
|
|
|
|
return into(todos).insert(entry);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
All row classes generated will have a constructor that can be used to create objects:
|
|
|
|
|
```dart
|
|
|
|
|
addTodoEntry(
|
|
|
|
|
Todo(
|
|
|
|
|
title: 'Important task',
|
|
|
|
|
content: 'Refactor persistence code',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
If a column is nullable or has a default value (this includes auto-increments), the field
|
|
|
|
|
can be omitted. All other fields must be set and non-null. The `insert` method will throw
|
|
|
|
|
otherwise.
|
|
|
|
|
|
2019-02-28 13:28:46 -08:00
|
|
|
|
### Custom statements
|
|
|
|
|
You can also issue custom queries by calling `customUpdate` for update and deletes and
|
|
|
|
|
`customSelect` or `customSelectStream` for select statements. Using the todo example
|
|
|
|
|
above, here is a simple custom query that loads all categories and how many items are
|
|
|
|
|
in each category:
|
|
|
|
|
```dart
|
|
|
|
|
class CategoryWithCount {
|
|
|
|
|
final Category category;
|
|
|
|
|
final int count; // amount of entries in this category
|
|
|
|
|
|
|
|
|
|
CategoryWithCount(this.category, this.count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// then, in the database class:
|
|
|
|
|
Stream<List<CategoryWithCount>> categoriesWithCount() {
|
|
|
|
|
// select all categories and load how many associated entries there are for
|
|
|
|
|
// each category
|
|
|
|
|
return customSelectStream(
|
|
|
|
|
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;',
|
2019-03-09 11:35:29 -08:00
|
|
|
|
readsFrom: {todos, categories}).map((rows) {
|
2019-02-28 13:28:46 -08:00
|
|
|
|
// when we have the result set, map each row to the data class
|
|
|
|
|
return rows
|
|
|
|
|
.map((row) => CategoryWithCount(Category.fromData(row.data, this), row.readInt('amount')))
|
|
|
|
|
.toList();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
```
|
2019-03-09 11:35:29 -08:00
|
|
|
|
For custom selects, you should use the `readsFrom` parameter to specify from which tables the query is
|
|
|
|
|
reading. When using a `Stream`, moor will be able to know after which updates the stream should emit
|
|
|
|
|
items. If you're using a custom query for updates or deletes with `customUpdate`, you should also
|
|
|
|
|
use the `updates` parameter to let moor know which tables you're touching.
|
2019-02-28 13:28:46 -08:00
|
|
|
|
|
2019-02-18 11:51:37 -08:00
|
|
|
|
## Migrations
|
2019-03-09 11:35:29 -08:00
|
|
|
|
Moor provides a migration API that can be used to gradually apply schema changes after bumping
|
2019-02-18 11:51:37 -08:00
|
|
|
|
the `schemaVersion` getter inside the `Database` class. To use it, override the `migration`
|
|
|
|
|
getter. Here's an example: Let's say you wanted to add a due date to your todo entries:
|
|
|
|
|
```dart
|
|
|
|
|
class Todos extends Table {
|
|
|
|
|
IntColumn get id => integer().autoIncrement()();
|
|
|
|
|
TextColumn get title => text().withLength(min: 6, max: 10)();
|
|
|
|
|
TextColumn get content => text().named('body')();
|
|
|
|
|
IntColumn get category => integer().nullable()();
|
|
|
|
|
DateTimeColumn get dueDate => dateTime().nullable()(); // we just added this column
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
We can now change the `database` class like this:
|
|
|
|
|
```dart
|
|
|
|
|
@override
|
2019-02-20 09:03:45 -08:00
|
|
|
|
int get schemaVersion => 2; // bump because the tables have changed
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
MigrationStrategy get migration => MigrationStrategy(
|
|
|
|
|
onCreate: (Migrator m) {
|
|
|
|
|
return m.createAllTables();
|
|
|
|
|
},
|
|
|
|
|
onUpgrade: (Migrator m, int from, int to) async {
|
|
|
|
|
if (from == 1) {
|
2019-02-20 09:03:45 -08:00
|
|
|
|
// we added the dueDate property in the change from version 1
|
2019-02-18 11:51:37 -08:00
|
|
|
|
await m.addColumn(todos, todos.dueDate);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// rest of class can stay the same
|
|
|
|
|
```
|
2019-03-09 11:35:29 -08:00
|
|
|
|
You can also add individual tables or drop them. You can't use the high-level query API in
|
|
|
|
|
migrations. If you need to use it, please specify the `onFinished` method on the
|
|
|
|
|
`MigrationStrategy`. It will be called after a migration happened and it's safe to call methods
|
|
|
|
|
on your database from inside that method.
|
2019-02-18 11:51:37 -08:00
|
|
|
|
|
2019-03-05 11:36:06 -08:00
|
|
|
|
### Extracting functionality with DAOs
|
|
|
|
|
When you have a lot of queries, putting them all into one class quickly becomes
|
|
|
|
|
tedious. You can avoid this by extracting some queries into classes that are
|
|
|
|
|
available from your main database class. Consider the following code:
|
|
|
|
|
```dart
|
|
|
|
|
part 'todos_dao.g.dart';
|
|
|
|
|
|
2019-03-09 08:01:55 -08:00
|
|
|
|
// the _TodosDaoMixin will be created by moor. It contains all the necessary
|
2019-03-05 11:36:06 -08:00
|
|
|
|
// fields for the tables. The <MyDatabase> type annotation is the database class
|
|
|
|
|
// that should use this dao.
|
|
|
|
|
@UseDao(tables: [Todos])
|
|
|
|
|
class TodosDao extends DatabaseAccessor<MyDatabase> with _TodosDaoMixin {
|
|
|
|
|
// this constructor is required so that the main database can create an instance
|
|
|
|
|
// of this object.
|
2019-03-09 11:35:29 -08:00
|
|
|
|
TodosDao(MyDatabase db) : super(db);
|
2019-03-05 11:36:06 -08:00
|
|
|
|
|
|
|
|
|
Stream<List<TodoEntry>> todosInCategory(Category category) {
|
|
|
|
|
if (category == null) {
|
|
|
|
|
return (select(todos)..where((t) => isNull(t.category))).watch();
|
|
|
|
|
} else {
|
|
|
|
|
return (select(todos)..where((t) => t.category.equals(category.id)))
|
|
|
|
|
.watch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2019-03-09 11:35:29 -08:00
|
|
|
|
If we now change the annotation on the `MyDatabase` class to `@UseMoor(tables: [Todos, Categories], daos: [TodosDao])`
|
|
|
|
|
and re-run the code generation, a generated getter `todosDao` can be used to access the instance of that dao.
|
2019-03-05 11:36:06 -08:00
|
|
|
|
|
2019-02-18 11:51:37 -08:00
|
|
|
|
## TODO-List and current limitations
|
2019-02-20 07:37:09 -08:00
|
|
|
|
### Limitations (at the moment)
|
2019-02-28 13:28:46 -08:00
|
|
|
|
Please note that a workaround for most on this list exists with custom statements.
|
|
|
|
|
|
2019-02-18 11:51:37 -08:00
|
|
|
|
- No joins
|
|
|
|
|
- No `group by` or window functions
|
|
|
|
|
|
|
|
|
|
### Planned for the future
|
|
|
|
|
These aren't sorted by priority. If you have more ideas or want some features happening soon,
|
2019-03-09 11:35:29 -08:00
|
|
|
|
let me know by creating an issue!
|
2019-02-18 11:51:37 -08:00
|
|
|
|
- Simple `COUNT(*)` operations (group operations will be much more complicated)
|
|
|
|
|
- Support default values and expressions
|
|
|
|
|
- Support Dart VM apps
|
|
|
|
|
- References
|
|
|
|
|
- DSL API
|
|
|
|
|
- Support in generator
|
|
|
|
|
- Validation
|
|
|
|
|
- Table joins
|
|
|
|
|
- Bulk inserts
|
2019-03-10 04:38:53 -07:00
|
|
|
|
- When inserts / updates fail, explain why that happened
|
2019-02-18 11:51:37 -08:00
|
|
|
|
### Interesting stuff that would be nice to have
|
|
|
|
|
Implementing this will very likely result in backwards-incompatible changes.
|
|
|
|
|
|
|
|
|
|
- Find a way to hide implementation details from users while still making them
|
|
|
|
|
accessible for the generated code
|
|
|
|
|
- `GROUP BY` grouping functions
|
|
|
|
|
- Support for different database engines
|
2019-02-20 08:54:00 -08:00
|
|
|
|
- Support webapps via `AlaSQL` or a different engine
|