Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can not load ToMany data content #655

Open
ThatPham2000 opened this issue Jul 16, 2024 · 8 comments
Open

Can not load ToMany data content #655

ThatPham2000 opened this issue Jul 16, 2024 · 8 comments
Labels
bug Something isn't working

Comments

@ThatPham2000
Copy link

ThatPham2000 commented Jul 16, 2024

Is there an existing issue?

No

Build info

  • objectbox version: 4.0.1
  • Flutter/Dart version: 3.22.2
  • Build OS: macOS Ventura 13.6.1
  • Deployment OS or device: Android 12 emulator

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
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> b;

  factory A.fromJson(Map<String, dynamic> json) => _$AFromJson(json);

  Map<String, dynamic> 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> c;

  factory B.fromJson(Map<String, dynamic> json) => _$BFromJson(json);

  Map<String, dynamic> 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<String, dynamic> json) => _$CFromJson(json);

  Map<String, dynamic> toJson() => _$CToJson(this);
}

class _ToOneConverter
    implements JsonConverter<ToOne<B>, Map<String, dynamic>?> {
  const _ToOneConverter();

  @override
  ToOne<B> fromJson(Map<String, dynamic>? json) => ToOne<B>(
        target: json == null ? null : B.fromJson(json),
      );

  @override
  Map<String, dynamic>? toJson(ToOne<B> rel) => rel.target?.toJson();
}

class _ToManyConverter implements JsonConverter<ToMany<C>, List<dynamic>?> {
  const _ToManyConverter();

  @override
  ToMany<C> fromJson(List<dynamic>? json) => ToMany<C>(
        items: json?.map((e) => C.fromJson(e)).toList(),
      );

  @override
  List<Map<String, dynamic>>? toJson(ToMany<C> rel) =>
      rel.map((obj) => obj.toJson()).toList();
}

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<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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++;
    });
  }

  @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: <Widget>[
            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),
      ),
    );
  }
}
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
[Paste your logs here]
@ThatPham2000 ThatPham2000 added the bug Something isn't working label Jul 16, 2024
@greenrobot-team
Copy link
Member

greenrobot-team commented Jul 22, 2024

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.

@greenrobot-team greenrobot-team added the more info required Needs more info to become actionable. Auto-closed if no response. label Jul 22, 2024
@ThatPham2000
Copy link
Author

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

@github-actions github-actions bot removed the more info required Needs more info to become actionable. Auto-closed if no response. label Jul 23, 2024
@greenrobot-team
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Member

greenrobot-team commented Aug 6, 2024

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
Copy link
Author

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
Copy link
Member

greenrobot-team commented Aug 7, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants