objectbox / objectbox-dart

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

Can not load ToMany data content #655

Open ThatPham2000 opened 1 month ago

ThatPham2000 commented 1 month ago

Is there an existing issue?

No

Build info

Steps to reproduce

Just run this code:

  int _counter = 0;

  final boxA = objectbox.store.box<A>();
  final boxB = objectbox.store.box<B>();
  final boxC = objectbox.store.box<C>();

  void _incrementCounter() {
    final a = A.fromJson({
      'name': 'Name A',
      'b': {
        'c': [
          {
            'name3': 'Name 3A',
          },
          {
            'name3': 'Name 3B',
          },
        ],
      }
    });

    // save c
    boxC.putMany(a.b.target!.c);

    // save b
    boxB.put(a.b.target!);

    // save a
    boxA.put(a);

    final aFromDb = boxA.getAll();
    final bFromA = aFromDb.first.b.target;
    final cFromB = bFromA!.c;

    final bFromDb = boxB.getAll();
    final cFromDb = boxC.getAll();

    setState(() {
      _counter++;
    });
  }

Expected behavior

I want to get a list of "c" from "a".

Actual behavior

Get "c" from "a" is empty.

Code

Code ```dart import 'package:json_annotation/json_annotation.dart'; import 'package:objectbox/objectbox.dart'; part 'model.g.dart'; @Entity() @JsonSerializable(explicitToJson: true) class A { A({required this.name, required this.b}) { obId = name.hashCode; } @Id(assignable: true) @JsonKey(includeFromJson: false, includeToJson: false) int? obId; final String name; @_ToOneConverter() final ToOne b; factory A.fromJson(Map json) => _$AFromJson(json); Map toJson() => _$AToJson(this); } @Entity() @JsonSerializable(explicitToJson: true) class B { B({required this.c}) { obId = c.map((element) => element).toList().hashCode; } @Id(assignable: true) @JsonKey(includeFromJson: false, includeToJson: false) int? obId; @_ToManyConverter() final ToMany c; factory B.fromJson(Map json) => _$BFromJson(json); Map toJson() => _$BToJson(this); } @Entity() @JsonSerializable(explicitToJson: true) class C { C({required this.name3}) { obId = name3.hashCode; } @Id(assignable: true) @JsonKey(includeFromJson: false, includeToJson: false) int? obId; final String name3; factory C.fromJson(Map json) => _$CFromJson(json); Map toJson() => _$CToJson(this); } class _ToOneConverter implements JsonConverter, Map?> { const _ToOneConverter(); @override ToOne fromJson(Map? json) => ToOne( target: json == null ? null : B.fromJson(json), ); @override Map? toJson(ToOne rel) => rel.target?.toJson(); } class _ToManyConverter implements JsonConverter, List?> { const _ToManyConverter(); @override ToMany fromJson(List? json) => ToMany( items: json?.map((e) => C.fromJson(e)).toList(), ); @override List>? toJson(ToMany rel) => rel.map((obj) => obj.toJson()).toList(); } ``` ```dart import 'package:flutter/material.dart'; import 'package:object_box_to_many/model/object_box.dart'; import 'model/model.dart'; late ObjectBox objectbox; void main() async { WidgetsFlutterBinding.ensureInitialized(); objectbox = await ObjectBox.create(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; final boxA = objectbox.store.box(); final boxB = objectbox.store.box(); final boxC = objectbox.store.box(); void _incrementCounter() { final a = A.fromJson({ 'name': 'Name A', 'b': { 'c': [ { 'name3': 'Name 3A', }, { 'name3': 'Name 3B', }, ], } }); // save c boxC.putMany(a.b.target!.c); // save b boxB.put(a.b.target!); // save a boxA.put(a); final aFromDb = boxA.getAll(); final bFromA = aFromDb.first.b.target; final cFromB = bFromA!.c; final bFromDb = boxB.getAll(); final cFromDb = boxC.getAll(); setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } } ``` ```yaml name: object_box_to_many description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.1.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter objectbox: ^4.0.1 objectbox_flutter_libs: any path: any json_annotation: ^4.8.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.0.0 objectbox_generator: any json_serializable: ^6.2.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ```

Logs, stack traces

TODO Add relevant logs, a stack trace or crash report.

Logs ```console [Paste your logs here] ```
greenrobot-team commented 1 month ago

Is this maybe because the ToMany b.c is loaded on first access?

If not, then the relation of "b" is not updated. I would welcome a simple example project to reproduce it.

Anyhow, I strongly recommend to use a separate model for JSON and for ObjectBox and map between them. Otherwise, future changes may be difficult or impossible.

Also: please don't post screenshots, post actual code.

ThatPham2000 commented 1 month ago

Also: I provided all of my code. You can copy and run.

greenrobot-team commented 1 month ago

Oh, sorry I did not see the collapsed code block. I updated the description with the actual code that can be copied. I will have a look once I have time.

Anyhow, my question is still valid. Including the note about model separation.

ThatPham2000 commented 1 month ago

The problem is that my model (not mock ones here) is built based on API so I need to parse from JSON here.

greenrobot-team commented 1 month ago

The problem is that my model (not mock ones here) is built based on API so I need to parse from JSON here.

Sure. But your project should then still have a separate model for the database. So one for JSON parsing and one for the database. Then map between those.

greenrobot-team commented 1 month ago

The underlying issue is that the constructor for B accesses the ToMany:

  B({required this.c}) {
    obId = c.map((element) => element).toList().hashCode;
  }

This breaks the ToMany as it is initialized only after this. See the generated code:

          final cParam = obx.ToMany<C>();
          final object = B(c: cParam)
            ..obId =
                const fb.Int64Reader().vTableGetNullable(buffer, rootOffset, 4);
          obx_int.InternalToManyAccess.setRelInfo<B>(
              object.c, store, obx_int.RelInfo<B>.toMany(1, object.obId!));
          return object;

We might be able to change the generated code to call setRelInfo before passing the ToMany to the object.

ThatPham2000 commented 1 month ago

Thanks for your update. so what is the solution for duplicate objects in case I do not assign object box id by myself (assignable: true). Because when I call the API many times, we have a lot of duplicate objects with the same values.

greenrobot-team commented 1 month ago

The above issue only exists when reading from a Box. The data is correctly put. Maybe you can change the model to provide a default, no arguments constructor that ObjectBox can use.

Or as I said, do not use the same model for your network layer (JSON) and the database layer.