1

I have the following classes:

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


@Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created.  Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Foo extends Equatable {
  int id;
  final String fooProp;

  // I don't need a backlink yet, but very likely will in the future
  // @Backlink()
  // final ToMany<Bar> bars;

  Foo(
    this.fooProp,
    {
      this.id=0,
    }
  );

  @override
  List<Object> get props => [fooProp];
}
import 'package:equatable/equatable.dart';
import 'package:objectbox/objectbox.dart';


@Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created.  Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Bar extends Equatable {
  int id;
  final String barProp;
  final ToMany<Foo> foos;

  Bar(
    this.barProp,
    this.foos,
    {
      this.id=0,
    }
  );

  @override
  List<Object> get props => [barProp, foos];
}

And here is what I'm trying to do:

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

// Get previously stored instance of Foo
Foo foo = ObjectBox.fooBox.get(1);

// Print foo.fooProp
print(foo.fooProp);  // Prints "asdf"

// Change foo.fooProp to something else
foo.fooProp = 'fdsa';

// Update foo
ObjectBox.fooBox.put(foo);

// Get the same instance of Foo again
foo = ObjectBox.fooBox.get(1);

// Check foo.fooProp to make sure it updated
print(foo.fooProp);  // Prints "fdsa", good so far

// Get previously stored instance of Bar which has Foo instance with ID of 1 in its foos
Bar bar = ObjectBox.barBox.get(1);

// Get our foo from bar.foos
foo = bar.foos[0];

// Verify the ID of foo to make sure it is the same object
print(foo.id);  // Prints "1", exactly what we expect

// Print foo.fooProp
print(foo.fooProp); // Prints "asdf", not the expected "fdsa"

The documentation has the following to say on the subject:

Note that to-many relations are resolved lazily on first access, and then cached in the source entity inside the ToMany object. So subsequent calls to any method, like size() of the ToMany, do not query the database, even if the relation was changed elsewhere. To get the latest data fetch the source entity again or call reset() on the ToMany.

The reset() method doesn't appear to be available in the Flutter flavor of ObjectBox, and we can see from my example that even fetching both sides of the ToMany relationship did not result in the expected update.

What am I missing here?

Failed Workaround:

I tried to workaround this problem with the following awful bit of code, but even this does not work. ObjectBox just completely ignores the actual bar.foos and whatever was persisted for foos remains there and doesn't get updated.

final List<Bar> oldBars = ObjectBox.barBox.getAll();
List<Bar> newBars = [];
for(Bar oldBar in oldBars) {
  if(oldBar.foos.isNotEmpty) {
    List<int> oldFooIds = oldBar.foos.map((foo) => foo.id).toList();
    List<Foo> newFoos = foos.where((foo) => oldFooIds.contains(foo.id)).toList();
    Bar newBar = oldBar.copy(foos: ToMany<Foo>(items: newFoos));
    newBars.add(newBar);
  }
}

ObjectBox.barBox.putMany(newBars);

This makes me think there is something wrong with the way I have the relationship setup, but there are no errors when the ObjectBox generator runs

CALL flutter pub run build_runner build --delete-conflicting-outputs

Update:

I have this working now, but clean it is not. I had my Bar constructor set up to accept a collection of Foo objects, but passing the instances of Foo in is what was causing the relations to break. If I instead create an instance of Bar, then use bar.foos.add(foo), the result is as expected. For what it is worth, that is how the examples in the docs show interactions with relations happening, I just didn't think it was that literal, because creating new objects with relations in this manner is a hassle. I think some work can be done in the constructor to make things a bit easier still.

ubiquibacon
  • 10,451
  • 28
  • 109
  • 179

1 Answers1

1

A "solution" (and I use that term loosely) to my problem was the following modification to the Bar class. This allows me to initialize instances of Bar with a pre-built list of instances of Foo.

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


@Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created.  Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Bar extends Equatable {
  int id;
  final String barProp;
  final ToMany<Foo> foos = ToMany<Foo>();

  Bar(
    this.barProp,
    {
      this.id=0,
      foos=const <Foo>[],
    }
  ) {
    this.foos.addAll(foos);
  }

  @override
  List<Object> get props => [barProp, foos];
}

This works fine, for creating new objects, but because I want to use Equatable I'm must make all properties which are used to determine equality final. When a class is an @Entity which will be persisted to ObjectBox, most of its properties will typically be used to determine equality, so this requirement of Equatable makes it at odds with ObjectBox when it is time to update an object. For instance, if I have an instance of Bar, I can't update barProp; I have to create a new instance of Bar which is initializes barProp to the desired value. If I create a new instance of Bar which had the desired value for barProp, foos, and has the same ID as an already persisted instance of Bar, then I try to persist that new instance, barProp will be updated as expected, but foos will not. All that to say, I first have to take the heavy handed approach of calling ObjectBox.barBox.remove() or ObjectBox.barBox.removeAll() (depending on the application) before persisting the new instance of Bar.

Foo fooA = Foo('this is Foo A');
Foo fooB = Foo('this is Foo B');
List<Foo> firstFooList = <Foo>[fooA, fooB];
ObjectBox.fooBox.putMany(firstFooList);

Foo fooC = Foo('this is Foo C')
Foo fooD = Foo('this is Foo D')
List<Foo> secondFooList = <Foo>[fooC, fooD];
ObjectBox.fooBox.putMany(secondFooList);

Bar barA = Bar('this is bar A', foos=firstFooList)
ObjectBox.barBox.put(barA);  // ObjectBox assigns ID 1 to this Bar

Bar barB = Bar('this is bar B', id=barA.id, foos=secondFooList)  // Use the `id` from barA which we just persisted, but initialize `foos` with `secondFooList`

// Without this, the next line which persists `barB` would result in the
// instance of Bar which has ID 1 having a `barProp` value of 'this is bar B', 
// but a `foos` value equal to `firstFooList`, not the expected `secondFooList`.
ObjectBox.barBox.remove(barB.id);

ObjectBox.barBox.put(barB);

Time will tell if calling remove and removeAll like this are a bigger performance hit than not using Equatable, and for others reading this, that determination will depend on your specific application (i.e. does your app have more UI interactions that benefit from reduced builds because of the inclusion of Equatable, or does your app have more ObjectBox interactions where excessive calls to remove and removeAll cause a performance hit).

ubiquibacon
  • 10,451
  • 28
  • 109
  • 179