Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions generator/integration-tests/part-partof/1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'dart:io';

import 'package:test/test.dart';

import '../test_env.dart';
import '../common.dart';
import 'lib/json.dart';
import '../test_env.dart';
import 'lib/frozen.dart';
import 'lib/json.dart';
import 'lib/objectbox.g.dart';

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

setupTestsFor(JsonEntity(id: 0, str: 'foo', date: DateTime.now()));
setupTestsFor(FrozenEntity(id: 1, str: 'foo', date: DateTime.now()));
group('package:JsonSerializable', () {
setupTestsFor(JsonEntity(id: 0, str: 'foo', date: DateTime.now()));
setupRelTestsFor(JsonBook.fromJson({
'author': {'name': 'Charles'},
'readers': [
{'name': 'Emily'},
{'name': 'Diana'}
]
}));
});

group('package:Freezed', () {
setupTestsFor(FrozenEntity(id: 1, str: 'foo', date: DateTime.now()));
final author = FrozenPerson(id: 1, name: 'Charles');
final readers = [
FrozenPerson(id: 2, name: 'Emily'),
FrozenPerson(id: 3, name: 'Diana')
];
setupRelTestsFor(
FrozenBook(
id: 1,
author: ToOne(target: author),
readers: ToMany(items: readers)),
(Store store) =>
store.box<FrozenPerson>().putMany([author, ...readers]));
});
}

void setupTestsFor<EntityT>(EntityT newObject) {
group('${EntityT}', () {
group(EntityT.toString(), () {
late TestEnv<EntityT> env;
setUp(() => env = TestEnv(getObjectBoxModel()));
tearDown(() => env.close());
Expand All @@ -32,3 +56,24 @@ void setupTestsFor<EntityT>(EntityT newObject) {
});
});
}

void setupRelTestsFor<BookEntityT>(BookEntityT book,
[void Function(Store)? init]) {
group(BookEntityT.toString(), () {
late TestEnv<BookEntityT> env;
setUp(() => env = TestEnv(getObjectBoxModel()));
tearDown(() => env.close());

test('relations', () {
if (init != null) init(env.store);
env.box.put(book);

final bookRead = env.box.get(1)! as dynamic;
expect(bookRead.author.targetId, 1);
expect(bookRead.author.target!.name, 'Charles');

expect(bookRead.readers[0]!.name, 'Emily');
expect(bookRead.readers[1]!.name, 'Diana');
});
});
}
19 changes: 18 additions & 1 deletion generator/integration-tests/part-partof/lib/frozen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:objectbox/objectbox.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

part 'frozen.freezed.dart';

Expand All @@ -11,3 +11,20 @@ class FrozenEntity with _$FrozenEntity {
required String str,
required DateTime date}) = _FrozenEntity;
}

@freezed
class FrozenPerson with _$FrozenPerson {
@Entity(realClass: FrozenPerson)
factory FrozenPerson(
{@Id(assignable: true) required int id,
required String name}) = _FrozenPerson;
}

@freezed
class FrozenBook with _$FrozenBook {
@Entity(realClass: FrozenBook)
factory FrozenBook(
{@Id(assignable: true) required int id,
required ToOne<FrozenPerson> author,
required ToMany<FrozenPerson> readers}) = _FrozenBook;
}
63 changes: 62 additions & 1 deletion generator/integration-tests/part-partof/lib/json.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:objectbox/objectbox.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:objectbox/objectbox.dart';

part 'json.g.dart';

Expand All @@ -17,3 +17,64 @@ class JsonEntity {

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

@Entity()
@JsonSerializable()
class JsonPerson {
int? id;
String name;

JsonPerson({required this.name});

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

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

@Entity()
@JsonSerializable()
class JsonBook {
int? id;

@_PersonRelToOneConverter()
final ToOne<JsonPerson> author;

@_PersonRelToManyConverter()
final ToMany<JsonPerson> readers;

JsonBook({required this.author, required this.readers});

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

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

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

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

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

class _PersonRelToManyConverter
implements JsonConverter<ToMany<JsonPerson>, List<Map<String, dynamic>>?> {
const _PersonRelToManyConverter();

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

@override
List<Map<String, dynamic>>? toJson(ToMany<JsonPerson> rel) =>
rel.map((JsonPerson obj) => obj.toJson()).toList();
}
3 changes: 1 addition & 2 deletions generator/integration-tests/test_env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ export 'package:objectbox/internal.dart';
class TestEnv<Entity> {
static final dir = Directory('testdata');
late final Store store;
late final Box<Entity> box;
late final Box<Entity> box = store.box();

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

store = Store(defs, directory: dir.path);
box = Box<Entity>(store);
}

void close() {
Expand Down
75 changes: 47 additions & 28 deletions generator/lib/src/code_chunks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -374,36 +374,36 @@ class CodeChunks {
return readField();
}).toList(growable: false);

// add initializers for relations
entity.properties.forEachIndexed((int index, ModelProperty p) {
if (p.isRelation) {
postLines.add(
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};'
'\n object.${propertyFieldName(p)}.attach(store);');
}
});

postLines.addAll(entity.relations.map((ModelRelation rel) =>
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));

postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
}));

// try to initialize as much as possible using the constructor
entity.constructorParams.forEachWhile((String declaration) {
// See [EntityResolver.constructorParams()] for the format.
final paramName = declaration.split(' ')[0];
final paramType = declaration.split(' ')[1];
final declarationParts = declaration.split(' ');
final paramName = declarationParts[0];
final paramType = declarationParts[1];
final paramDartType = declarationParts[2];

final index = fieldIndexes[paramName];
if (index == null) {
late String paramValueCode;
if (index != null) {
paramValueCode = fieldReaders[index];
if (entity.properties[index].isRelation) {
if (paramDartType.startsWith('ToOne<')) {
paramValueCode = 'ToOne(targetId: $paramValueCode)';
} else if (paramType == 'optional-named') {
log.info('Skipping constructor parameter $paramName on '
"'${entity.name}': the matching field is a relation but the type "
"isn't - don't know how to initialize this parameter.");
return true;
}
}
} else if (paramDartType.startsWith('ToMany<')) {
paramValueCode = 'ToMany()';
} else {
// If we can't find a positional param, we can't use the constructor at all.
if (paramType == 'positional') {
log.warning("Cannot use the default constructor of '${entity.name}': "
if (paramType == 'positional' || paramType == 'required-named') {
throw InvalidGenerationSourceError(
"Cannot use the default constructor of '${entity.name}': "
"don't know how to initialize param $paramName - no such property.");
constructorLines.clear();
return false;
} else if (paramType == 'optional') {
// OK, close the constructor, the rest will be initialized separately.
return false;
Expand All @@ -414,18 +414,20 @@ class CodeChunks {
switch (paramType) {
case 'positional':
case 'optional':
constructorLines.add(fieldReaders[index]);
constructorLines.add(paramValueCode);
break;
case 'named':
constructorLines.add('$paramName: ${fieldReaders[index]}');
case 'required-named':
case 'optional-named':
constructorLines.add('$paramName: $paramValueCode');
break;
default:
throw InvalidGenerationSourceError(
'Invalid constructor parameter type - internal error');
}

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

return true;
});
Expand All @@ -438,6 +440,23 @@ class CodeChunks {
}
});

// add initializers for relations
entity.properties.forEachIndexed((int index, ModelProperty p) {
if (!p.isRelation) return;
if (fieldReaders[index].isNotEmpty) {
postLines.add(
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};');
}
postLines.add('object.${propertyFieldName(p)}.attach(store);');
});

postLines.addAll(entity.relations.map((ModelRelation rel) =>
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));

postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
}));

return '''(Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
Expand Down
5 changes: 4 additions & 1 deletion generator/lib/src/entity_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,10 @@ class EntityResolver extends Builder {
var info = param.name;
if (param.isRequiredPositional) info += ' positional';
if (param.isOptionalPositional) info += ' optional';
if (param.isNamed) info += ' named';
if (param.isRequiredNamed)
info += ' required-named';
else if (param.isNamed) info += ' optional-named';
info += ' ${param.type}';
return info;
}).toList(growable: false);
}
Expand Down
21 changes: 16 additions & 5 deletions objectbox/lib/src/relations/to_many.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ class ToMany<EntityT> extends Object with ListMixin<EntityT> {
final _counts = <EntityT, int>{};
final _addedBeforeLoad = <EntityT>[];

/// Create a ToMany relationship.
///
/// Normally, you don't assign items in the constructor but rather use this
/// class as a lazy-loaded/saved list. The option to assign in the constructor
/// is useful to initialize objects from an external source, e.g. from JSON.
/// Setting the items in the constructor bypasses the lazy loading, ignoring
/// any relations that are currently stored in the DB for the source object.
ToMany({List<EntityT>? items}) {
if (items != null) {
__items = items;
items.forEach(_track);
}
}

@override
int get length => _items.length;

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

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

/// "add": increment = 1
/// "remove": increment = -1
void _track(EntityT object, int increment) {
void _track(EntityT object, [int increment = 1]) {
if (_counts.containsKey(object)) {
_counts[object] = _counts[object]! + increment;
} else {
Expand Down
19 changes: 19 additions & 0 deletions objectbox/lib/src/relations/to_one.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ class ToOne<EntityT> {

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

/// Create a ToOne relationship.
///
/// Normally, you don't assign the target in the constructor but rather use
/// the `.target` setter. The option to assign in the constructor is useful
/// to initialize objects from an external source, e.g. from JSON.
ToOne({EntityT? target, int? targetId}) {
if (targetId != null) {
if (target != null) {
// May be a user error... and we can't check if (target.id == targetId).
throw ArgumentError(
'Provide at most one specification of a ToOne relation target: '
'either [target] or [targetId] argument');
}
this.targetId = targetId;
} else if (target != null) {
this.target = target;
}
}

/// Get target object. If it's the first access, this reads from DB.
EntityT? get target {
if (_value._state == _ToOneState.lazy) {
Expand Down