Skip to content

Commit

Permalink
Add experimental support for type converters (#318)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitusortner authored Oct 16, 2020
1 parent fab7583 commit 5fd7cd8
Show file tree
Hide file tree
Showing 68 changed files with 2,074 additions and 439 deletions.
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ As a consequence, it's necessary to have an understanding of SQL and SQLite in o
- no hidden costs
- iOS, Android, Linux, macOS, Windows

⚠️ The library is on its way to its first stable release!
After integrating type converters and embeddable objects, the API surface won't change until after 1.0.
⚠️ The library is on its way to its first stable release and is open to contributions!

[![pub package](https://img.shields.io/pub/v/floor.svg)](https://pub.dartlang.org/packages/floor)
[![build status](https://github.com/vitusortner/floor/workflows/Continuous%20integration/badge.svg)](https://github.com/vitusortner/floor/actions)
Expand All @@ -37,6 +36,7 @@ After integrating type converters and embeddable objects, the API surface won't
1. [Streams](#streams)
1. [Transactions](#transactions)
1. [Inheritance](#inheritance-1)
1. [Type Converters](#type-converters)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Initialization Callback](#initialization-callback)
Expand Down Expand Up @@ -174,7 +174,7 @@ After integrating type converters and embeddable objects, the API surface won't
final result = await personDao.findPersonById(1);
```

For further examples take a look at the [example](https://github.com/vitusortner/floor/tree/develop/example) and [floor_test](https://github.com/vitusortner/floor/tree/develop/floor_test) directories.
For further examples take a look at the [example](https://github.com/vitusortner/floor/tree/develop/example) and [floor_test](https://github.com/vitusortner/floor/tree/develop/floor/test/integration) directories.

## Architecture
The components for storing and accessing data are **Entity**, **Data Access Object (DAO)** and **Database**.
Expand Down Expand Up @@ -234,6 +234,8 @@ Floor entities can hold values of the following Dart types which map to their co
- `bool` - INTEGER (0 = false, 1 = true)
- `Uint8List` - BLOB

In case you want to store sophisticated Dart objects that can be represented by one of the above types, take a look at [Type Converters](#type-converters).

### Primary Keys
Whenever a compound primary key is required (e.g. *n-m* relationships), the syntax for setting the keys differs from the previously mentioned way of setting primary keys.
Instead of annotating a field with `@PrimaryKey`, the `@Entity` annotation's `primaryKey` attribute is used.
Expand Down Expand Up @@ -563,6 +565,72 @@ await personDao.insertItem(person);
final result = await personDao.findPersonById(1);
```

## Type Converters
⚠️ **This feature is still in an experimental state.
Please use it with caution and file issues for problems you encounter.**

SQLite allows storing values of only a handful types.
Whenever more complex Dart in-memory objects should be stored, there sometimes is the need for converting between Dart and SQLite compatible types.
Dart's `DateTime`, for instance, provides an object-oriented API for handling time.
Objects of this class can simply be represented as `int` values by mapping `DateTime` to its timestamp in milliseconds.
Instead of manually mapping between these types repeatedly, when reading and writing, type converters can be used.
It's sufficient to define the conversion from a database to an in-memory type and vice versa once, which then is reused automatically.

The implementation and usage of the mentioned `DateTime` to `int` converter is described in the following.

1. Create a converter class that implements the abstract `TypeConverter` and supply the in-memory object type and database type as parameterized types.
This class inherits the `decode()` and `encode()` functions which define the conversion from one to the other type.
```dart
class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}
@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
```

2. Apply the created type converter to the database by using the `@TypeConverters` annotation and make sure to additionally import the file of your type converter here.
Importing it in your database file is **always** necessary because the generated code will be `part` of your database file and this is the location where your type converters get instantiated.
```dart
@TypeConverters([DateTimeConverter])
@Database(version: 1, entities: [Order])
abstract class OrderDatabase extends FloorDatabase {
OrderDao get orderDao;
}
```

3. Use the non-default `DateTime` type in an entity.
```dart
@entity
class Order {
@primaryKey
final int id;
final DateTime date;
Order(this.id, this.date);
}
```

### Type converters can be applied to
1. databases
1. DAOs
1. entities/views
1. entity/view fields
1. DAO methods
1. DAO method parameters

The type converter is added to the scope of the element so if you put it on a class, all methods/fields in that class will be able to use the converter.

**The closest type converter wins!**
If you, for example, add a converter on the database level and another one on a DAO method parameter, which takes care of converting the same types, the one declared next to the DAO method parameter will be used.
Please refer to the above list to get more information about the precedence of converters.

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

Expand Down
3 changes: 1 addition & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ analyzer:
todo: ignore
exclude:
- 'bin/cache/**'
- '**.g.dart'

linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
- always_declare_return_types
# - always_put_control_body_on_new_line
# - always_put_control_body_on_new_line
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
# - always_require_non_null_named_parameters
# - always_specify_types
Expand Down
16 changes: 10 additions & 6 deletions example/lib/database.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.7"
dartx:
dependency: transitive
description:
name: dartx
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
fake_async:
dependency: transitive
description:
Expand Down Expand Up @@ -464,6 +471,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
time:
dependency: transitive
description:
name: time
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
timing:
dependency: transitive
description:
Expand Down
74 changes: 71 additions & 3 deletions floor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ As a consequence, it's necessary to have an understanding of SQL and SQLite in o
- no hidden costs
- iOS, Android, Linux, macOS, Windows

⚠️ The library is on its way to its first stable release!
After integrating type converters and embeddable objects, the API surface won't change until after 1.0.
⚠️ The library is on its way to its first stable release and is open to contributions!

[![pub package](https://img.shields.io/pub/v/floor.svg)](https://pub.dartlang.org/packages/floor)
[![build status](https://github.com/vitusortner/floor/workflows/Continuous%20integration/badge.svg)](https://github.com/vitusortner/floor/actions)
Expand All @@ -37,6 +36,7 @@ After integrating type converters and embeddable objects, the API surface won't
1. [Streams](#streams)
1. [Transactions](#transactions)
1. [Inheritance](#inheritance-1)
1. [Type Converters](#type-converters)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Initialization Callback](#initialization-callback)
Expand Down Expand Up @@ -174,7 +174,7 @@ After integrating type converters and embeddable objects, the API surface won't
final result = await personDao.findPersonById(1);
```

For further examples take a look at the [example](https://github.com/vitusortner/floor/tree/develop/example) and [floor_test](https://github.com/vitusortner/floor/tree/develop/floor_test) directories.
For further examples take a look at the [example](https://github.com/vitusortner/floor/tree/develop/example) and [floor_test](https://github.com/vitusortner/floor/tree/develop/floor/test/integration) directories.

## Architecture
The components for storing and accessing data are **Entity**, **Data Access Object (DAO)** and **Database**.
Expand Down Expand Up @@ -234,6 +234,8 @@ Floor entities can hold values of the following Dart types which map to their co
- `bool` - INTEGER (0 = false, 1 = true)
- `Uint8List` - BLOB

In case you want to store sophisticated Dart objects that can be represented by one of the above types, take a look at [Type Converters](#type-converters).

### Primary Keys
Whenever a compound primary key is required (e.g. *n-m* relationships), the syntax for setting the keys differs from the previously mentioned way of setting primary keys.
Instead of annotating a field with `@PrimaryKey`, the `@Entity` annotation's `primaryKey` attribute is used.
Expand Down Expand Up @@ -563,6 +565,72 @@ await personDao.insertItem(person);
final result = await personDao.findPersonById(1);
```

## Type Converters
⚠️ **This feature is still in an experimental state.
Please use it with caution and file issues for problems you encounter.**

SQLite allows storing values of only a handful types.
Whenever more complex Dart in-memory objects should be stored, there sometimes is the need for converting between Dart and SQLite compatible types.
Dart's `DateTime`, for instance, provides an object-oriented API for handling time.
Objects of this class can simply be represented as `int` values by mapping `DateTime` to its timestamp in milliseconds.
Instead of manually mapping between these types repeatedly, when reading and writing, type converters can be used.
It's sufficient to define the conversion from a database to an in-memory type and vice versa once, which then is reused automatically.

The implementation and usage of the mentioned `DateTime` to `int` converter is described in the following.

1. Create a converter class that implements the abstract `TypeConverter` and supply the in-memory object type and database type as parameterized types.
This class inherits the `decode()` and `encode()` functions which define the conversion from one to the other type.
```dart
class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}
@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
```

2. Apply the created type converter to the database by using the `@TypeConverters` annotation and make sure to additionally import the file of your type converter here.
Importing it in your database file is **always** necessary because the generated code will be `part` of your database file and this is the location where your type converters get instantiated.
```dart
@TypeConverters([DateTimeConverter])
@Database(version: 1, entities: [Order])
abstract class OrderDatabase extends FloorDatabase {
OrderDao get orderDao;
}
```

3. Use the non-default `DateTime` type in an entity.
```dart
@entity
class Order {
@primaryKey
final int id;
final DateTime date;
Order(this.id, this.date);
}
```

### Type converters can be applied to
1. databases
1. DAOs
1. entities/views
1. entity/view fields
1. DAO methods
1. DAO method parameters

The type converter is added to the scope of the element so if you put it on a class, all methods/fields in that class will be able to use the converter.

**The closest type converter wins!**
If you, for example, add a converter on the database level and another one on a DAO method parameter, which takes care of converting the same types, the one declared next to the DAO method parameter will be used.
Please refer to the above list to get more information about the precedence of converters.

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

Expand Down
14 changes: 14 additions & 0 deletions floor/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.7"
dartx:
dependency: transitive
description:
name: dartx
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
fake_async:
dependency: transitive
description:
Expand Down Expand Up @@ -464,6 +471,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
time:
dependency: transitive
description:
name: time
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
timing:
dependency: transitive
description:
Expand Down
27 changes: 27 additions & 0 deletions floor/test/integration/type_converter/order.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:floor/floor.dart';

@entity
class Order {
@primaryKey
final int id;

final DateTime date;

Order(this.id, this.date);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Order &&
runtimeType == other.runtimeType &&
id == other.id &&
date == other.date;

@override
int get hashCode => id.hashCode ^ date.hashCode;

@override
String toString() {
return 'Order{id: $id, date: $date}';
}
}
12 changes: 12 additions & 0 deletions floor/test/integration/type_converter/order_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:floor/floor.dart';

import 'order.dart';

@dao
abstract class OrderDao {
@insert
Future<void> insertOrder(Order order);

@Query('SELECT * FROM `Order` WHERE date = :date')
Future<List<Order>> findOrdersByDate(DateTime date);
}
18 changes: 18 additions & 0 deletions floor/test/integration/type_converter/order_database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:async';

import 'package:floor/floor.dart';
import 'package:floor_annotation/floor_annotation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart' as sqflite;

import 'order.dart';
import 'order_dao.dart';
import 'type_converter.dart';

part 'order_database.g.dart';

@Database(version: 1, entities: [Order])
@TypeConverters([DateTimeConverter])
abstract class OrderDatabase extends FloorDatabase {
OrderDao get orderDao;
}
Loading

0 comments on commit 5fd7cd8

Please sign in to comment.