Skip to content

Commit a96193e

Browse files
authored
Merge pull request #292 from objectbox/rel-json-support
Relations - json_serializable/freezed support
2 parents 1ec264f + c9b4b9f commit a96193e

File tree

8 files changed

+217
-43
lines changed

8 files changed

+217
-43
lines changed

generator/integration-tests/part-partof/1.dart

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import 'dart:io';
22

33
import 'package:test/test.dart';
44

5-
import '../test_env.dart';
65
import '../common.dart';
7-
import 'lib/json.dart';
6+
import '../test_env.dart';
87
import 'lib/frozen.dart';
8+
import 'lib/json.dart';
99
import 'lib/objectbox.g.dart';
1010

1111
void main() {
@@ -16,12 +16,36 @@ void main() {
1616
expect(File('lib/objectbox-model.json').existsSync(), true);
1717
});
1818

19-
setupTestsFor(JsonEntity(id: 0, str: 'foo', date: DateTime.now()));
20-
setupTestsFor(FrozenEntity(id: 1, str: 'foo', date: DateTime.now()));
19+
group('package:JsonSerializable', () {
20+
setupTestsFor(JsonEntity(id: 0, str: 'foo', date: DateTime.now()));
21+
setupRelTestsFor(JsonBook.fromJson({
22+
'author': {'name': 'Charles'},
23+
'readers': [
24+
{'name': 'Emily'},
25+
{'name': 'Diana'}
26+
]
27+
}));
28+
});
29+
30+
group('package:Freezed', () {
31+
setupTestsFor(FrozenEntity(id: 1, str: 'foo', date: DateTime.now()));
32+
final author = FrozenPerson(id: 1, name: 'Charles');
33+
final readers = [
34+
FrozenPerson(id: 2, name: 'Emily'),
35+
FrozenPerson(id: 3, name: 'Diana')
36+
];
37+
setupRelTestsFor(
38+
FrozenBook(
39+
id: 1,
40+
author: ToOne(target: author),
41+
readers: ToMany(items: readers)),
42+
(Store store) =>
43+
store.box<FrozenPerson>().putMany([author, ...readers]));
44+
});
2145
}
2246

2347
void setupTestsFor<EntityT>(EntityT newObject) {
24-
group('${EntityT}', () {
48+
group(EntityT.toString(), () {
2549
late TestEnv<EntityT> env;
2650
setUp(() => env = TestEnv(getObjectBoxModel()));
2751
tearDown(() => env.close());
@@ -32,3 +56,24 @@ void setupTestsFor<EntityT>(EntityT newObject) {
3256
});
3357
});
3458
}
59+
60+
void setupRelTestsFor<BookEntityT>(BookEntityT book,
61+
[void Function(Store)? init]) {
62+
group(BookEntityT.toString(), () {
63+
late TestEnv<BookEntityT> env;
64+
setUp(() => env = TestEnv(getObjectBoxModel()));
65+
tearDown(() => env.close());
66+
67+
test('relations', () {
68+
if (init != null) init(env.store);
69+
env.box.put(book);
70+
71+
final bookRead = env.box.get(1)! as dynamic;
72+
expect(bookRead.author.targetId, 1);
73+
expect(bookRead.author.target!.name, 'Charles');
74+
75+
expect(bookRead.readers[0]!.name, 'Emily');
76+
expect(bookRead.readers[1]!.name, 'Diana');
77+
});
78+
});
79+
}

generator/integration-tests/part-partof/lib/frozen.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:objectbox/objectbox.dart';
21
import 'package:freezed_annotation/freezed_annotation.dart';
2+
import 'package:objectbox/objectbox.dart';
33

44
part 'frozen.freezed.dart';
55

@@ -11,3 +11,20 @@ class FrozenEntity with _$FrozenEntity {
1111
required String str,
1212
required DateTime date}) = _FrozenEntity;
1313
}
14+
15+
@freezed
16+
class FrozenPerson with _$FrozenPerson {
17+
@Entity(realClass: FrozenPerson)
18+
factory FrozenPerson(
19+
{@Id(assignable: true) required int id,
20+
required String name}) = _FrozenPerson;
21+
}
22+
23+
@freezed
24+
class FrozenBook with _$FrozenBook {
25+
@Entity(realClass: FrozenBook)
26+
factory FrozenBook(
27+
{@Id(assignable: true) required int id,
28+
required ToOne<FrozenPerson> author,
29+
required ToMany<FrozenPerson> readers}) = _FrozenBook;
30+
}

generator/integration-tests/part-partof/lib/json.dart

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:objectbox/objectbox.dart';
21
import 'package:json_annotation/json_annotation.dart';
2+
import 'package:objectbox/objectbox.dart';
33

44
part 'json.g.dart';
55

@@ -17,3 +17,64 @@ class JsonEntity {
1717

1818
Map<String, dynamic> toJson() => _$JsonEntityToJson(this);
1919
}
20+
21+
@Entity()
22+
@JsonSerializable()
23+
class JsonPerson {
24+
int? id;
25+
String name;
26+
27+
JsonPerson({required this.name});
28+
29+
factory JsonPerson.fromJson(Map<String, dynamic> json) =>
30+
_$JsonPersonFromJson(json);
31+
32+
Map<String, dynamic> toJson() => _$JsonPersonToJson(this);
33+
}
34+
35+
@Entity()
36+
@JsonSerializable()
37+
class JsonBook {
38+
int? id;
39+
40+
@_PersonRelToOneConverter()
41+
final ToOne<JsonPerson> author;
42+
43+
@_PersonRelToManyConverter()
44+
final ToMany<JsonPerson> readers;
45+
46+
JsonBook({required this.author, required this.readers});
47+
48+
factory JsonBook.fromJson(Map<String, dynamic> json) =>
49+
_$JsonBookFromJson(json);
50+
51+
Map<String, dynamic> toJson() => _$JsonBookToJson(this);
52+
}
53+
54+
class _PersonRelToOneConverter
55+
implements JsonConverter<ToOne<JsonPerson>, Map<String, dynamic>?> {
56+
const _PersonRelToOneConverter();
57+
58+
@override
59+
ToOne<JsonPerson> fromJson(Map<String, dynamic>? json) => ToOne<JsonPerson>(
60+
target: json == null ? null : JsonPerson.fromJson(json));
61+
62+
@override
63+
Map<String, dynamic>? toJson(ToOne<JsonPerson> rel) => rel.target?.toJson();
64+
}
65+
66+
class _PersonRelToManyConverter
67+
implements JsonConverter<ToMany<JsonPerson>, List<Map<String, dynamic>>?> {
68+
const _PersonRelToManyConverter();
69+
70+
@override
71+
ToMany<JsonPerson> fromJson(List<Map<String, dynamic>>? json) =>
72+
ToMany<JsonPerson>(
73+
items: json == null
74+
? null
75+
: json.map((e) => JsonPerson.fromJson(e)).toList());
76+
77+
@override
78+
List<Map<String, dynamic>>? toJson(ToMany<JsonPerson> rel) =>
79+
rel.map((JsonPerson obj) => obj.toJson()).toList();
80+
}

generator/integration-tests/test_env.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ export 'package:objectbox/internal.dart';
77
class TestEnv<Entity> {
88
static final dir = Directory('testdata');
99
late final Store store;
10-
late final Box<Entity> box;
10+
late final Box<Entity> box = store.box();
1111

1212
TestEnv(ModelDefinition defs) {
1313
if (dir.existsSync()) dir.deleteSync(recursive: true);
1414

1515
store = Store(defs, directory: dir.path);
16-
box = Box<Entity>(store);
1716
}
1817

1918
void close() {

generator/lib/src/code_chunks.dart

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -374,36 +374,36 @@ class CodeChunks {
374374
return readField();
375375
}).toList(growable: false);
376376

377-
// add initializers for relations
378-
entity.properties.forEachIndexed((int index, ModelProperty p) {
379-
if (p.isRelation) {
380-
postLines.add(
381-
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};'
382-
'\n object.${propertyFieldName(p)}.attach(store);');
383-
}
384-
});
385-
386-
postLines.addAll(entity.relations.map((ModelRelation rel) =>
387-
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));
388-
389-
postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
390-
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
391-
}));
392-
393377
// try to initialize as much as possible using the constructor
394378
entity.constructorParams.forEachWhile((String declaration) {
395379
// See [EntityResolver.constructorParams()] for the format.
396-
final paramName = declaration.split(' ')[0];
397-
final paramType = declaration.split(' ')[1];
380+
final declarationParts = declaration.split(' ');
381+
final paramName = declarationParts[0];
382+
final paramType = declarationParts[1];
383+
final paramDartType = declarationParts[2];
398384

399385
final index = fieldIndexes[paramName];
400-
if (index == null) {
386+
late String paramValueCode;
387+
if (index != null) {
388+
paramValueCode = fieldReaders[index];
389+
if (entity.properties[index].isRelation) {
390+
if (paramDartType.startsWith('ToOne<')) {
391+
paramValueCode = 'ToOne(targetId: $paramValueCode)';
392+
} else if (paramType == 'optional-named') {
393+
log.info('Skipping constructor parameter $paramName on '
394+
"'${entity.name}': the matching field is a relation but the type "
395+
"isn't - don't know how to initialize this parameter.");
396+
return true;
397+
}
398+
}
399+
} else if (paramDartType.startsWith('ToMany<')) {
400+
paramValueCode = 'ToMany()';
401+
} else {
401402
// If we can't find a positional param, we can't use the constructor at all.
402-
if (paramType == 'positional') {
403-
log.warning("Cannot use the default constructor of '${entity.name}': "
403+
if (paramType == 'positional' || paramType == 'required-named') {
404+
throw InvalidGenerationSourceError(
405+
"Cannot use the default constructor of '${entity.name}': "
404406
"don't know how to initialize param $paramName - no such property.");
405-
constructorLines.clear();
406-
return false;
407407
} else if (paramType == 'optional') {
408408
// OK, close the constructor, the rest will be initialized separately.
409409
return false;
@@ -414,18 +414,20 @@ class CodeChunks {
414414
switch (paramType) {
415415
case 'positional':
416416
case 'optional':
417-
constructorLines.add(fieldReaders[index]);
417+
constructorLines.add(paramValueCode);
418418
break;
419-
case 'named':
420-
constructorLines.add('$paramName: ${fieldReaders[index]}');
419+
case 'required-named':
420+
case 'optional-named':
421+
constructorLines.add('$paramName: $paramValueCode');
421422
break;
422423
default:
423424
throw InvalidGenerationSourceError(
424425
'Invalid constructor parameter type - internal error');
425426
}
426427

427-
// Good, we don't need to set this field anymore
428-
fieldReaders[index] = ''; // don't remove - that would mess up indexes
428+
// Good, we don't need to set this field anymore.
429+
// Don't remove - that would mess up indexes.
430+
if (index != null) fieldReaders[index] = '';
429431

430432
return true;
431433
});
@@ -438,6 +440,23 @@ class CodeChunks {
438440
}
439441
});
440442

443+
// add initializers for relations
444+
entity.properties.forEachIndexed((int index, ModelProperty p) {
445+
if (!p.isRelation) return;
446+
if (fieldReaders[index].isNotEmpty) {
447+
postLines.add(
448+
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};');
449+
}
450+
postLines.add('object.${propertyFieldName(p)}.attach(store);');
451+
});
452+
453+
postLines.addAll(entity.relations.map((ModelRelation rel) =>
454+
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));
455+
456+
postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
457+
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
458+
}));
459+
441460
return '''(Store store, ByteData fbData) {
442461
final buffer = fb.BufferContext(fbData);
443462
final rootOffset = buffer.derefObject(0);

generator/lib/src/entity_resolver.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,10 @@ class EntityResolver extends Builder {
420420
var info = param.name;
421421
if (param.isRequiredPositional) info += ' positional';
422422
if (param.isOptionalPositional) info += ' optional';
423-
if (param.isNamed) info += ' named';
423+
if (param.isRequiredNamed)
424+
info += ' required-named';
425+
else if (param.isNamed) info += ' optional-named';
426+
info += ' ${param.type}';
424427
return info;
425428
}).toList(growable: false);
426429
}

objectbox/lib/src/relations/to_many.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ class ToMany<EntityT> extends Object with ListMixin<EntityT> {
6161
final _counts = <EntityT, int>{};
6262
final _addedBeforeLoad = <EntityT>[];
6363

64+
/// Create a ToMany relationship.
65+
///
66+
/// Normally, you don't assign items in the constructor but rather use this
67+
/// class as a lazy-loaded/saved list. The option to assign in the constructor
68+
/// is useful to initialize objects from an external source, e.g. from JSON.
69+
/// Setting the items in the constructor bypasses the lazy loading, ignoring
70+
/// any relations that are currently stored in the DB for the source object.
71+
ToMany({List<EntityT>? items}) {
72+
if (items != null) {
73+
__items = items;
74+
items.forEach(_track);
75+
}
76+
}
77+
6478
@override
6579
int get length => _items.length;
6680

@@ -105,10 +119,7 @@ class ToMany<EntityT> extends Object with ListMixin<EntityT> {
105119

106120
@override
107121
void addAll(Iterable<EntityT> iterable) {
108-
iterable.forEach((element) {
109-
ArgumentError.checkNotNull(element, 'iterable element');
110-
_track(element, 1);
111-
});
122+
iterable.forEach(_track);
112123
if (__items == null) {
113124
// We don't need to load old data from DB to add new items.
114125
_addedBeforeLoad.addAll(iterable);
@@ -127,7 +138,7 @@ class ToMany<EntityT> extends Object with ListMixin<EntityT> {
127138

128139
/// "add": increment = 1
129140
/// "remove": increment = -1
130-
void _track(EntityT object, int increment) {
141+
void _track(EntityT object, [int increment = 1]) {
131142
if (_counts.containsKey(object)) {
132143
_counts[object] = _counts[object]! + increment;
133144
} else {

objectbox/lib/src/relations/to_one.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ class ToOne<EntityT> {
5656

5757
_ToOneValue<EntityT> _value = _ToOneValue<EntityT>.none();
5858

59+
/// Create a ToOne relationship.
60+
///
61+
/// Normally, you don't assign the target in the constructor but rather use
62+
/// the `.target` setter. The option to assign in the constructor is useful
63+
/// to initialize objects from an external source, e.g. from JSON.
64+
ToOne({EntityT? target, int? targetId}) {
65+
if (targetId != null) {
66+
if (target != null) {
67+
// May be a user error... and we can't check if (target.id == targetId).
68+
throw ArgumentError(
69+
'Provide at most one specification of a ToOne relation target: '
70+
'either [target] or [targetId] argument');
71+
}
72+
this.targetId = targetId;
73+
} else if (target != null) {
74+
this.target = target;
75+
}
76+
}
77+
5978
/// Get target object. If it's the first access, this reads from DB.
6079
EntityT? get target {
6180
if (_value._state == _ToOneState.lazy) {

0 commit comments

Comments
 (0)