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

Final fields can't be stored #193

Closed Abhilash-Chandran closed 3 years ago

Abhilash-Chandran commented 3 years ago

Description The scenario is adapting an existing model class to be compatible with ObjectBox. Assume the following ToDo.dart file which represents the model of ToDo item. Existing model already uses the reserved keyword id as a String id for another form of backend store such as Hive or InMemory list. Now to adapt this model to be compatible with ObjectBox terminology, I created a new field called oboxId with the @Id annottation.

ToDo.dart

import 'package:flutter_todo/commons/backend/base_store.dart';
import 'package:meta/meta.dart';
import 'package:objectbox/objectbox.dart';
import 'package:uuid/uuid.dart';

@Entity()

/// Model class to hold the ToDo Object.
class ToDo {
  /// Unique id of this todo to store in backend.
  /// Already used in Hive and InMemory lists.
  final String id;

  /// Id specifically required for ObjectBox, which must be of type int.
  @Id()
  int oboxId;

  /// Description of this todo.
  String description;

  /// A flag to denote if the to activity is complete
  bool completed;

  /// Due Date for this todo.
  DateTime dueDate;

  /// id of the user who created this todo.
  String userId;

  ToDo({
    @required this.description,
    this.dueDate,
    this.completed = false,
    @required this.userId,
  }) : id = Uuid().v4(); // Initializes id with a unique value.

  @override
  String toString() =>
      'Todo of user $userId with description $description and completed $completed';

  @override
  bool operator ==(o) =>
      o is ToDo &&
      o.description == description &&
      o.completed == completed &&
      o.dueDate == dueDate &&
      o.id == id &&
      o.oboxId == oboxId;

  @override
  int get hashCode =>
      description.hashCode ^
      userId.hashCode ^
      completed.hashCode ^
      dueDate.hashCode ^
      oboxId.hashCode;
}

Basic info (please complete the following information):

To Reproduce Steps to reproduce the behavior:

  1. Use the ToDo.dart as model class.
  2. Run flutter pub run build_runner build
  3. Then try to write a query in following format.
@override
  Future<ToDo> get(String id) async {
    final query = objBox.query(ToDo_.id.equals(id)).build(); // compiler throws error stating id is not  present in `ToDo_` type.

  }

Expected behavior It is expected that the id field is still available for querying as it is required to fulfill already existing contracts in the code base.

Additional Information

pubspec.yaml ```yml name: flutter_command_todo description: A new Flutter project. publish_to: "none" version: 1.0.0+1 environment: sdk: ">=2.7.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 flutter_command: ^0.9.3 get_it: ^5.0.6 uuid: ^2.2.2 objectbox: ^0.11.0 objectbox_flutter_libs: any dev_dependencies: flutter_test: sdk: flutter build_runner: objectbox_generator: any flutter: uses-material-design: true ```

Flutter information

flutter doctor Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel beta, 1.25.0-8.1.pre, on Microsoft Windows [Version 10.0.18363.1316], locale de-DE) [√] Android toolchain - develop for Android devices (Android SDK version 29.0.3) [√] Chrome - develop for the web [√] Android Studio (version 4.1.0) [√] VS Code (version 1.53.2) [√] Connected device (1 available) • No issues found!
RTrackerDev commented 3 years ago

@Abhilash-Chandran You marked id as final... Remove final and it will work ...

Abhilash-Chandran commented 3 years ago

@RTrackerDev thanks for your quick feedback. I will check this. But final field serves the purpose that this custom ID is not manipulated anywhere else. I am not clear why final fields can't be queried. I assume it has to do with the generated code then.

vaind commented 3 years ago

Final fields can't be currently set because of the way objects are constructed in the generated code when read from the database. Let's see what we can do about that

RTrackerDev commented 3 years ago

@Abhilash-Chandran Yes the whole reason is in the code that is generated. You could look on objectbox.g.dart file

RTrackerDev commented 3 years ago

@vaind maybe you could use entity data class constructor, when generate code?

vaind commented 3 years ago

maybe you could use entity data class constructor, when generate code?

Yes, but now that I've had a proper look at the class definition, ObjectBox is actually doing the only possible thing...


@Abhilash-Chandran - For ObjectBox to be able to load the id (the one you initialize to Uuid().v4()) field from the database, it needs to set it somehow on the constructed object. Your only constructor doesn't accept the field and it's marked final, so it can't even be set after the object is constructed. Therefore, any time ObjectBox reads the ToDo object from the database, the (only) constructor you've provided initializes with a new UUID.

  ToDo({
    @required this.description,
    this.dueDate,
    this.completed = false,
    @required this.userId,
  }) : id = Uuid().v4(); // Initializes id with a unique value.

To understand what's happening, see the code in objectbox.g.dart, more specifically look for objectFromFB


To actually do something about this, your constructor would need to look for example like this, i.e. provide an option to specify the id or it's generated.

  ToDo({
   this.id = Uuid().v4(),
    @required this.description,
    this.dueDate,
    this.completed = false,
    @required this.userId,
  })

Then, we could modify the code generator to recognize this use case (final fields initialized in a constructor) and construct the object appropriately.

Abhilash-Chandran commented 3 years ago

@Abhilash-Chandran - For ObjectBox to be able to load the id (the one you initialize to Uuid().v4()) field from the database, it needs to set it somehow on the constructed object. Your only constructor doesn't accept the field and it's marked final, so it can't even be set after the object is constructed. Therefore, any time ObjectBox reads the ToDo object from the database, the (only) constructor you've provided initializes with a new UUID.

Thanks for pointing this out. I guess this could be an issue even while de-serializing using HIVE.

To understand what's happening, see the code in objectbox.g.dart, more specifically look for objectFromFB

I looked into this and realized flatbuffers are used internally. Now this is no more a black box for me. 😃

To actually do something about this, your constructor would need to look for example like this, i.e. provide an option to specify the id or it's generated.

  ToDo({
   this.id = Uuid().v4(),
    @required this.description,
    this.dueDate,
    this.completed = false,
    @required this.userId,
  })

I will try to change the constructors accordingly. My current approach is clearly invalid. 😐

BTW. If it helps in anyway following is the repo I am working on. Basically I am trying to create a ToDo app demo for flutter_command package using different types store as backend.

This is the repo. https://github.com/Abhilash-Chandran/flutter_command_todo

vaind commented 3 years ago

Update: recent code generation changes (0.13.0-nullsafety.0 prerelease) now try to use a constructor if available, so maybe give it a try.

Abhilash-Chandran commented 3 years ago

Update: recent code generation changes (0.13.0-nullsafety.0 prerelease) now try to use a constructor if available, so maybe give it a try.

Oh Cool.. Thanks a lot for the heads up. Will check and let you know..

vaind commented 3 years ago

A note from a separate SO issue - to keep in mind when looking into this. This is why a user had to create bridge classes and do conversions:

Regarding the question about Bridge class , since I made my Event class by extending the Equatable and all the properties are defined as final https://stackoverflow.com/questions/67013723/flutter-objectbox-performance-issue/67017621?noredirect=1#comment118509013_67017621