Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add support for @DatabaseView annotations #262

Merged
merged 7 commits into from
Mar 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This package is still in an early phase and the API will likely change.
1. [Primary Keys](#primary-keys)
1. [Indices](#indices)
1. [Ignoring Fields](#ignoring-fields)
1. [Database Views](#database-views)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Callback](#callback)
Expand Down Expand Up @@ -275,6 +276,9 @@ StreamBuilder<List<Person>>(
);
```

NOTE: It is currently not possible to return a `Stream` if the function queries a database view. This is mostly due
to the complexity of detecting which entities are involved in a database view.

## Transactions
Whenever you want to perform some operations in a transaction you have to add the `@transaction` annotation to the method.
It's also required to add the `async` modifier. These methods can only return `Future<void>`.
Expand Down Expand Up @@ -414,9 +418,47 @@ class Person {
Person(this.id, this.name);
}
```
## Database Views
If you want to define static `SELECT`-statements which return different types than your entities, your best option is
to use `@DatabaseView`. A database view can be understood as a virtual table, which can be queried like a real table.

A database view in floor is defined and used similarly to entities, with the main difference being that
access is read-only, which means that update, insert and delete functions are not possible. Similarly to
entities, the class name is used if no `viewName` was set.

```dart
@DatabaseView('SELECT distinct(name) as name FROM person', viewName: 'name')
class Name {
final String name;

Name(this.name);
}
```

Database views do not have any foreign/primary keys or indices. Instead, you should manually define indices which fit to
your statement and put them into the `@Entity` annotation of the involved entities.

Setters, getters and static fields are automatically ignored (like in entities), you can specify additional fields
to ignore by annotating them with `@ignore`.

After defining a database view in your code, you have to add it to your database by adding it to the `views` field of
the `@Database` annotation:

```dart
@Database(version: 1, entities: [Person], views:[Name])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}

```

You can then query the view via a DAO function like an entity.

NOTE: Be aware that it is currently not possible to return a
`Stream<>` object from a function which queries a database view.

## Migrations
Whenever are doing changes to your entities, you're required to also migrate the old data.
Whenever you are doing changes to your entities, you're required to also migrate the old data.

First, update your entity.
Next, Increase the database version.
Expand Down
44 changes: 43 additions & 1 deletion floor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This package is still in an early phase and the API will likely change.
1. [Primary Keys](#primary-keys)
1. [Indices](#indices)
1. [Ignoring Fields](#ignoring-fields)
1. [Database Views](#database-views)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Callback](#callback)
Expand Down Expand Up @@ -275,6 +276,9 @@ StreamBuilder<List<Person>>(
);
```

NOTE: It is currently not possible to return a `Stream` if the function queries a database view. This is mostly due
to the complexity of detecting which entities are involved in a database view.

## Transactions
Whenever you want to perform some operations in a transaction you have to add the `@transaction` annotation to the method.
It's also required to add the `async` modifier. These methods can only return `Future<void>`.
Expand Down Expand Up @@ -414,9 +418,47 @@ class Person {
Person(this.id, this.name);
}
```
## Database Views
If you want to define static `SELECT`-statements which return different types than your entities, your best option is
to use `@DatabaseView`. A database view can be understood as a virtual table, which can be queried like a real table.

A database view in floor is defined and used similarly to entities, with the main difference being that
access is read-only, which means that update, insert and delete functions are not possible. Similarly to
entities, the class name is used if no `viewName` was set.

```dart
@DatabaseView('SELECT distinct(name) as name FROM person', viewName: 'name')
class Name {
final String name;

Name(this.name);
}
```

Database views do not have any foreign/primary keys or indices. Instead, you should manually define indices which fit to
your statement and put them into the `@Entity` annotation of the involved entities.

Setters, getters and static fields are automatically ignored (like in entities), you can specify additional fields
to ignore by annotating them with `@ignore`.

After defining a database view in your code, you have to add it to your database by adding it to the `views` field of
the `@Database` annotation:

```dart
@Database(version: 1, entities: [Person], views:[Name])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}

```

You can then query the view via a DAO function like an entity.

NOTE: Be aware that it is currently not possible to return a
`Stream<>` object from a function which queries a database view.

## Migrations
Whenever are doing changes to your entities, you're required to also migrate the old data.
Whenever you are doing changes to your entities, you're required to also migrate the old data.

First, update your entity.
Next, Increase the database version.
Expand Down
15 changes: 15 additions & 0 deletions floor/test/integration/dao/name_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:floor/floor.dart';

import '../model/name.dart';

@dao
abstract class NameDao {
mqus marked this conversation as resolved.
Show resolved Hide resolved
@Query('SELECT * FROM names ORDER BY name ASC')
Future<List<Name>> findAllNames();

@Query('SELECT * FROM names WHERE name = :name')
Future<Name> findExactName(String name);

@Query('SELECT * FROM names WHERE name LIKE :suffix ORDER BY name ASC')
Future<List<Name>> findNamesLike(String suffix);
}
23 changes: 23 additions & 0 deletions floor/test/integration/model/name.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:floor/floor.dart';

@DatabaseView(
'SELECT custom_name as name FROM person UNION SELECT name from dog',
viewName: 'names')
class Name {
final String name;

Name(this.name);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Name && runtimeType == other.runtimeType && name == other.name;

@override
int get hashCode => name.hashCode;

@override
String toString() {
return 'Name{name: $name}';
}
}
87 changes: 87 additions & 0 deletions floor/test/integration/view_test/view_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'dart:async';

import 'package:floor/floor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' hide equals;
import 'package:sqflite/sqflite.dart' as sqflite;
import 'package:sqflite_ffi_test/sqflite_ffi_test.dart';

import '../dao/dog_dao.dart';
import '../dao/name_dao.dart';
import '../dao/person_dao.dart';
import '../model/dog.dart';
import '../model/name.dart';
import '../model/person.dart';

part 'view_test.g.dart';

@Database(version: 1, entities: [Person, Dog], views: [Name])
abstract class ViewTestDatabase extends FloorDatabase {
PersonDao get personDao;

DogDao get dogDao;

NameDao get nameDao;
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
sqfliteFfiTestInit();

group('database tests', () {
ViewTestDatabase database;
PersonDao personDao;
DogDao dogDao;
NameDao nameDao;

setUp(() async {
database = await $FloorViewTestDatabase.inMemoryDatabaseBuilder().build();

personDao = database.personDao;
dogDao = database.dogDao;
nameDao = database.nameDao;
});

tearDown(() async {
await database.close();
});

group('Query Views', () {
test('query view with exact value', () async {
final person = Person(1, 'Frank');
await personDao.insertPerson(person);

final actual = await nameDao.findExactName('Frank');

final expected = Name('Frank');
expect(actual, equals(expected));
});

test('query view with LIKE', () async {
final persons = [Person(1, 'Leo'), Person(2, 'Frank')];
await personDao.insertPersons(persons);

final dog = Dog(1, 'Romeo', 'Rome', 1);
await dogDao.insertDog(dog);

final actual = await nameDao.findNamesLike('%eo');

final expected = [Name('Leo'), Name('Romeo')];
expect(actual, equals(expected));
});

test('query view with all values', () async {
final persons = [Person(1, 'Leo'), Person(2, 'Frank')];
await personDao.insertPersons(persons);

final dog = Dog(1, 'Romeo', 'Rome', 1);
await dogDao.insertDog(dog);

final actual = await nameDao.findAllNames();

final expected = [Name('Frank'), Name('Leo'), Name('Romeo')];
expect(actual, equals(expected));
});
});
});
}
1 change: 1 addition & 0 deletions floor_annotation/lib/floor_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ library floor_annotation;
export 'src/column_info.dart';
export 'src/dao.dart';
export 'src/database.dart';
export 'src/database_view.dart';
export 'src/delete.dart';
export 'src/entity.dart';
export 'src/foreign_key.dart';
Expand Down
9 changes: 8 additions & 1 deletion floor_annotation/lib/src/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ class Database {
/// The entities the database manages.
final List<Type> entities;

/// The views the database manages.
final List<Type> views;

/// Marks a class as a FloorDatabase.
const Database({@required this.version, @required this.entities});
const Database({
@required this.version,
@required this.entities,
this.views = const [],
});
}
14 changes: 14 additions & 0 deletions floor_annotation/lib/src/database_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Marks a class as a database view (a fixed select statement).
class DatabaseView {
/// The table name of the SQLite view.
final String viewName;

/// The SELECT query on which the view is based on.
final String query;

/// Marks a class as a database view (a fixed select statement).
const DatabaseView(
this.query, {
this.viewName,
});
}
4 changes: 4 additions & 0 deletions floor_generator/lib/misc/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ abstract class AnnotationField {

static const DATABASE_VERSION = 'version';
static const DATABASE_ENTITIES = 'entities';
static const DATABASE_VIEWS = 'views';

static const COLUMN_INFO_NAME = 'name';
static const COLUMN_INFO_NULLABLE = 'nullable';
Expand All @@ -13,6 +14,9 @@ abstract class AnnotationField {
static const ENTITY_FOREIGN_KEYS = 'foreignKeys';
static const ENTITY_INDICES = 'indices';
static const ENTITY_PRIMARY_KEYS = 'primaryKeys';

static const VIEW_NAME = 'viewName';
static const VIEW_QUERY = 'query';
}

abstract class ForeignKeyField {
Expand Down
12 changes: 9 additions & 3 deletions floor_generator/lib/processor/dao_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:floor_generator/processor/query_method_processor.dart';
import 'package:floor_generator/processor/transaction_method_processor.dart';
import 'package:floor_generator/processor/update_method_processor.dart';
import 'package:floor_generator/value_object/dao.dart';
import 'package:floor_generator/value_object/view.dart';
import 'package:floor_generator/value_object/deletion_method.dart';
import 'package:floor_generator/value_object/entity.dart';
import 'package:floor_generator/value_object/insertion_method.dart';
Expand All @@ -21,20 +22,24 @@ class DaoProcessor extends Processor<Dao> {
final String _daoGetterName;
final String _databaseName;
final List<Entity> _entities;
final List<View> _views;

DaoProcessor(
final ClassElement classElement,
final String daoGetterName,
final String databaseName,
final List<Entity> entities,
final List<View> views,
) : assert(classElement != null),
assert(daoGetterName != null),
assert(databaseName != null),
assert(entities != null),
assert(views != null),
_classElement = classElement,
_daoGetterName = daoGetterName,
_databaseName = databaseName,
_entities = entities;
_entities = entities,
_views = views;

@override
Dao process() {
Expand Down Expand Up @@ -66,7 +71,8 @@ class DaoProcessor extends Processor<Dao> {
List<QueryMethod> _getQueryMethods(final List<MethodElement> methods) {
return methods
.where((method) => method.hasAnnotation(annotations.Query))
.map((method) => QueryMethodProcessor(method, _entities).process())
.map((method) =>
QueryMethodProcessor(method, _entities, _views).process())
.toList();
}

Expand Down Expand Up @@ -119,7 +125,7 @@ class DaoProcessor extends Processor<Dao> {
List<Entity> _getStreamEntities(final List<QueryMethod> queryMethods) {
return queryMethods
.where((method) => method.returnsStream)
.map((method) => method.entity)
.map((method) => method.queryable as Entity)
.toList();
}
}
Loading