objectbox / objectbox-dart

Flutter database for super-fast Dart object persistence
https://docs.objectbox.io/getting-started
Apache License 2.0
927 stars 115 forks source link

Support `freezed` entities #229

Closed msxenon closed 2 years ago

msxenon commented 3 years ago

Hello, as far as I know in objectbox-dart we can't create an immutable class.

I have used @freezed with "HiveDB" but I wish we can achieve that with object box

Example:

part 'priority_entity.freezed.dart';
part 'priority_entity.g.dart';

@freezed
abstract class PriorityEntity with _$PriorityEntity {
  @HiveType(typeId: 2, adapterName: 'PriorityEntityAdapter')
  const factory PriorityEntity({
    @required @HiveField(0) String id,
    @required @HiveField(1) @JsonKey(name: 'org_id') String orgId,
    @required @HiveField(2) String name,
    @required @HiveField(3) bool active,
    @required @HiveField(4) List<PriorityStates> states,
  }) = _PriorityEntity;

  factory PriorityEntity.fromJson(Map<String, dynamic> json) =>
      _$PriorityEntityFromJson(json);
}

edit by @vaind: fix the code block

vaind commented 3 years ago

It seems like it could almost work (with the new yet-unreleased support for final fields), with the following declaration:

@freezed
class Person with _$Person {
  @Entity()
  factory Person({ @Id(assignable: true) int? id, String? name, int? age }) = _Person;
}

However, with the freezed generated class being private (_$_Person), it currently cannot be accessed from the objectbox.g.dart. In order to support this, we'd need to switch generated code to use part/part of. And because that would also be necessary if users wanted to store private fields, it might just make sense. Let's consider (the prep work) for 1.0.

vaind commented 3 years ago

I've had a look at what's available in the context when the generator runs... There doesn't seem to be any easy-to-get-to info whether the file is a library with parts or a single-file library, but there is file content so we can check manually - for each entity, the following should lead to the source file: element.enclosingElement.source.

Therefore, we can do this post 1.0 without breaking compatibility for existing users. If others would like to see this feature, please upvote the original issue description (not this comment :) ).

Buggaboo commented 3 years ago

freezed? frozen? Or shall we let grammar go...

vaind commented 3 years ago

freezed? frozen? Or shall we let grammar go...

That's the name of an existing package, not an annotation we'd add to ObjectBox. But yeah, I was a bit flabbergasted by the name myself...

greenrobot commented 3 years ago

freezed? frozen? Or shall we let grammar go...

:+1: Definitely! https://www.youtube.com/watch?v=L0MK7qz13bU

vaind commented 2 years ago

In the end, this didn't need to use part/part of but only use the right type in the generated code. PR #255 adds support to do that, e.g.:

@freezed
class Person with _$Person {
  @Entity(realClass: Person)
  factory Person({ @Id(assignable: true) int? id, String? name, int? age }) = _Person;
}
Prn-Ice commented 2 years ago

@vaind I have the following freezed classes

// ignore_for_file: invalid_annotation_target

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

import 'currency.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  @Entity(realClass: User)
  factory User({
    @Id(assignable: true) int? localId,
    @Unique(onConflict: ConflictStrategy.replace) String? id,
    @JsonKey(name: 'first_name') String? firstName,
    @JsonKey(name: 'last_name') String? lastName,
    @JsonKey(name: 'middle_name') String? middleName,
    String? email,
    String? username,
    String? country,
    @JsonKey(name: 'email_verified_at') String? emailVerifiedAt,
    @JsonKey(name: 'phone_number') String? phoneNumber,
    @JsonKey(name: 'phone_number_verified_at') String? phoneNumberVerifiedAt,
    @JsonKey(name: 'google_id') String? googleId,
    @JsonKey(name: 'facebook_id') String? facebookId,
    @JsonKey(name: 'pin_set_at') String? pinSetAt,
    @JsonKey(name: 'referral_code') String? referralCode,
    @JsonKey(name: 'referred_by') String? referredBy,
    @JsonKey(name: 'last_logged_in_at') String? lastLoggedInAt,
    String? status,
    @JsonKey(name: 'created_at') String? createdAt,
    @JsonKey(name: 'updated_at') String? updatedAt,
    ToOne<Currency>? currency,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

part 'currency.freezed.dart';
part 'currency.g.dart';

@freezed
class Currency with _$Currency {
  @Entity(realClass: Currency)
  factory Currency({
    @Id(assignable: true) int? id,
    String? code,
    int? decimals,
    String? name,
    String? number,
    String? symbol,
  }) = _Currency;

  factory Currency.fromJson(Map<String, dynamic> json) =>
      _$CurrencyFromJson(json);
}

dart run build_runner build --delete-conflicting-outputs fails with the following error

➜ dart run build_runner build --delete-conflicting-outputs
[INFO] Generating build script completed, took 630ms
[INFO] Reading cached asset graph completed, took 192ms
[INFO] Checking for updates since last build completed, took 717ms
[SEVERE] json_serializable:json_serializable on lib/src/models/account/user.dart:

Could not generate `fromJson` code for `currency`.
To support the type `ToOne` you can:
* Use `JsonConverter`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
* Use `JsonKey` fields `fromJson` and `toJson`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
package:test2/src/models/account/user.freezed.dart:510:26
    ╷
510 │   final ToOne<Currency>? currency;
    │                          ^^^^^^^^
    ╵
[INFO] Running build completed, took 6.2s
[INFO] Caching finalized dependency graph completed, took 47ms
[SEVERE] Failed after 6.3s
greenrobot-team commented 2 years ago

@Prn-Ice Have a look at our test code for an example using ToOne and ToMany with freezed: https://github.com/objectbox/objectbox-dart/blob/main/generator/integration-tests/part-partof/lib/frozen.dart

definitelyme commented 2 years ago

@vaind I have the following freezed classes

// ignore_for_file: invalid_annotation_target

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

import 'currency.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  @Entity(realClass: User)
  factory User({
    @Id(assignable: true) int? localId,
    @Unique(onConflict: ConflictStrategy.replace) String? id,
    @JsonKey(name: 'first_name') String? firstName,
    @JsonKey(name: 'last_name') String? lastName,
    @JsonKey(name: 'middle_name') String? middleName,
    String? email,
    String? username,
    String? country,
    @JsonKey(name: 'email_verified_at') String? emailVerifiedAt,
    @JsonKey(name: 'phone_number') String? phoneNumber,
    @JsonKey(name: 'phone_number_verified_at') String? phoneNumberVerifiedAt,
    @JsonKey(name: 'google_id') String? googleId,
    @JsonKey(name: 'facebook_id') String? facebookId,
    @JsonKey(name: 'pin_set_at') String? pinSetAt,
    @JsonKey(name: 'referral_code') String? referralCode,
    @JsonKey(name: 'referred_by') String? referredBy,
    @JsonKey(name: 'last_logged_in_at') String? lastLoggedInAt,
    String? status,
    @JsonKey(name: 'created_at') String? createdAt,
    @JsonKey(name: 'updated_at') String? updatedAt,
    ToOne<Currency>? currency,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

part 'currency.freezed.dart';
part 'currency.g.dart';

@freezed
class Currency with _$Currency {
  @Entity(realClass: Currency)
  factory Currency({
    @Id(assignable: true) int? id,
    String? code,
    int? decimals,
    String? name,
    String? number,
    String? symbol,
  }) = _Currency;

  factory Currency.fromJson(Map<String, dynamic> json) =>
      _$CurrencyFromJson(json);
}

dart run build_runner build --delete-conflicting-outputs fails with the following error

➜ dart run build_runner build --delete-conflicting-outputs
[INFO] Generating build script completed, took 630ms
[INFO] Reading cached asset graph completed, took 192ms
[INFO] Checking for updates since last build completed, took 717ms
[SEVERE] json_serializable:json_serializable on lib/src/models/account/user.dart:

Could not generate `fromJson` code for `currency`.
To support the type `ToOne` you can:
* Use `JsonConverter`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
* Use `JsonKey` fields `fromJson` and `toJson`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
package:test2/src/models/account/user.freezed.dart:510:26
    ╷
510 │   final ToOne<Currency>? currency;
    │                          ^^^^^^^^
    ╵
[INFO] Running build completed, took 6.2s
[INFO] Caching finalized dependency graph completed, took 47ms
[SEVERE] Failed after 6.3s

Any solution to this yet? Json serializable does not work well with ToOne & ToMany. Can you paste an example using json_serializable? Preferably with JsonConverter

Is this correct?

class AuthProviderSerializer implements JsonConverter<ToOne<AuthProvider>?, String?> {
  const AuthProviderSerializer();

  @override
  ToOne<AuthProvider> fromJson(String? value) => ToOne(target: AuthProvider.valueOf('$value'));

  @override
  String? toJson(ToOne<AuthProvider>? instance) => instance?.target?.name;
}
greenrobot-team commented 2 years ago

There is also test code that uses json_serializable: https://github.com/objectbox/objectbox-dart/blob/main/generator/integration-tests/part-partof/lib/json.dart It creates a JsonSerializer for each ToOne/ToMany that uses the special constructors of ToOne and ToMany.

And your example looks almost right, it probably should handle the null case for fromJson.