objectbox-dart
objectbox-dart copied to clipboard
Can not load ToMany data content
Is there an existing issue?
No
- [ √ ] I have searched existing issues
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]
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.
Also: I provided all of my code. You can copy and run.
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.
The problem is that my model (not mock ones here) is built based on API so I need to parse from JSON here.
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.
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.
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.
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.