From 3a3e5ce31ea448772d3d05942147dd0d635f053b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Aug 2021 20:01:32 +0200 Subject: [PATCH 1/4] ToOne - support initializing the target in the constructor --- generator/lib/src/code_chunks.dart | 153 +++++++++++++++--------- generator/lib/src/entity_resolver.dart | 4 +- objectbox/lib/src/relations/to_one.dart | 15 +++ 3 files changed, 115 insertions(+), 57 deletions(-) diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index c9d6836f3..818591ff5 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -6,8 +6,8 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError; class CodeChunks { - static String objectboxDart( - ModelInfo model, List imports, Pubspec? pubspec) => + static String objectboxDart(ModelInfo model, List imports, + Pubspec? pubspec) => """ // GENERATED CODE - DO NOT MODIFY BY HAND @@ -35,7 +35,8 @@ class CodeChunks { ${defineModel(model)} final bindings = { - ${model.entities.mapIndexed((i, entity) => "${entity.name}: ${entityBinding(i, entity)}").join(",\n")} + ${model.entities.mapIndexed((i, entity) => "${entity + .name}: ${entityBinding(i, entity)}").join(",\n")} }; return ModelDefinition(model, bindings); @@ -53,9 +54,13 @@ class CodeChunks { int$nullableOperator fileMode, int$nullableOperator maxReaders, bool queriesCaseSensitiveDefault = true, - String$nullableOperator macosApplicationGroup})${obxFlutter ? ' async' : ''} => + String$nullableOperator macosApplicationGroup})${obxFlutter + ? ' async' + : ''} => Store(getObjectBoxModel(), - directory: directory${obxFlutter ? ' ?? (await defaultStoreDirectory()).path' : ''}, + directory: directory${obxFlutter + ? ' ?? (await defaultStoreDirectory()).path' + : ''}, maxDBSizeInKB: maxDBSizeInKB, fileMode: fileMode, maxReaders: maxReaders, @@ -187,9 +192,11 @@ class CodeChunks { // Such ID must already be set, i.e. it could not have been assigned. return '''{ if (object.${propertyFieldName(entity.idProperty)} != id) { - throw ArgumentError('Field ${entity.name}.${propertyFieldName(entity.idProperty)} is read-only ' + throw ArgumentError('Field ${entity.name}.${propertyFieldName( + entity.idProperty)} is read-only ' '(final or getter-only) and it was declared to be self-assigned. ' - 'However, the currently inserted object (.${propertyFieldName(entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) ' + 'However, the currently inserted object (.${propertyFieldName( + entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) ' "doesn't match the inserted ID (ID \$id). " 'You must assign an ID before calling [box.put()].'); } @@ -209,7 +216,8 @@ class CodeChunks { return '[]'; default: throw InvalidGenerationSourceError( - 'Cannot figure out default value for field: ${p.fieldType} ${p.name}'); + 'Cannot figure out default value for field: ${p.fieldType} ${p + .name}'); } } @@ -286,13 +294,14 @@ class CodeChunks { } else if (p.type == OBXPropertyType.DateNano) { if (p.fieldIsNullable) { accessorSuffix = - ' == null ? null : object.${propertyFieldName(p)}'; + ' == null ? null : object.${propertyFieldName(p)}'; if (p.entity!.nullSafetyEnabled) accessorSuffix += '!'; } accessorSuffix += '.microsecondsSinceEpoch * 1000'; } } - return 'fbb.add${_propertyFlatBuffersType[p.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);'; + return 'fbb.add${_propertyFlatBuffersType[p + .type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);'; } }); @@ -316,14 +325,16 @@ class CodeChunks { // property to its index in entity.properties. final fieldIndexes = {}; final fieldReaders = - entity.properties.mapIndexed((int index, ModelProperty p) { + entity.properties.mapIndexed((int index, ModelProperty p) { fieldIndexes[propertyFieldName(p)] = index; String? fbReader; var readFieldOrNull = () => - 'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)})'; + 'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset( + p)})'; var readFieldNonNull = ([String? defaultValue]) => - 'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)}, ${defaultValue ?? fieldDefaultValue(p)})'; + 'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset( + p)}, ${defaultValue ?? fieldDefaultValue(p)})'; var readField = () => p.fieldIsNullable ? readFieldOrNull() : readFieldNonNull(); final valueVar = '${propertyFieldName(p)}Value'; @@ -336,7 +347,8 @@ class CodeChunks { fbReader = 'fb.ListReader(fb.Int8Reader())'; if (p.fieldIsNullable) { preLines.add('final $valueVar = ${readFieldOrNull()};'); - return '$valueVar == null ? null : ${p.fieldType}.fromList($valueVar)'; + return '$valueVar == null ? null : ${p + .fieldType}.fromList($valueVar)'; } else { return '${p.fieldType}.fromList(${readFieldNonNull('[]')})'; } @@ -363,43 +375,32 @@ class CodeChunks { } } else { if (p.type == OBXPropertyType.Date) { - return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull('0')})"; + return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull( + '0')})"; } else if (p.type == OBXPropertyType.DateNano) { - return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull('0')} / 1000).round())"; + return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull( + '0')} / 1000).round())"; } } throw InvalidGenerationSourceError( - 'Invalid property data type ${p.type} for a DateTime field ${entity.name}.${p.name}'); + 'Invalid property data type ${p.type} for a DateTime field ${entity + .name}.${p.name}'); } 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) { // If we can't find a positional param, we can't use the constructor at all. - if (paramType == 'positional') { + if (paramType == 'positional' || paramType == 'required-named') { log.warning("Cannot use the default constructor of '${entity.name}': " "don't know how to initialize param $paramName - no such property."); constructorLines.clear(); @@ -411,13 +412,26 @@ class CodeChunks { return true; // continue to the next param } + var 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; + } + } + 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( @@ -438,11 +452,33 @@ 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); ${preLines.join('\n')} - final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')}; + final object = ${entity.name}(${constructorLines.join( + ', \n')})${cascadeLines.join('\n')}; ${postLines.join('\n')} return object; }'''; @@ -450,21 +486,22 @@ class CodeChunks { static String toOneRelations(ModelEntity entity) => '[' + - entity.properties - .where((ModelProperty prop) => prop.isRelation) - .map((ModelProperty prop) => 'object.${propertyFieldName(prop)}') - .join(',') + - ']'; + entity.properties + .where((ModelProperty prop) => prop.isRelation) + .map((ModelProperty prop) => 'object.${propertyFieldName(prop)}') + .join(',') + + ']'; static String relInfo(ModelEntity entity, ModelRelation rel) => - 'RelInfo<${entity.name}>.toMany(${rel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; + 'RelInfo<${entity.name}>.toMany(${rel.id + .id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; static String backlinkRelInfo(ModelEntity entity, ModelBacklink bl) { final srcEntity = entity.model.findEntityByName(bl.srcEntity); if (srcEntity == null) { throw InvalidGenerationSourceError( 'Invalid relation backlink ${entity.name}.${bl.name} ' - '- source entity ${bl.srcEntity} not found.'); + '- source entity ${bl.srcEntity} not found.'); } // either of these will be set, based on the source field that matches @@ -475,13 +512,13 @@ class CodeChunks { final matchingProps = srcEntity.properties .where((p) => p.isRelation && p.relationTarget == entity.name); final matchingRels = - srcEntity.relations.where((r) => r.targetId == entity.id); + srcEntity.relations.where((r) => r.targetId == entity.id); final candidatesCount = matchingProps.length + matchingRels.length; if (candidatesCount > 1) { throw InvalidGenerationSourceError( 'Ambiguous relation backlink source for ${entity.name}.${bl.name}.' - ' Matching property: $matchingProps.' - ' Matching standalone relations: $matchingRels.'); + ' Matching property: $matchingProps.' + ' Matching standalone relations: $matchingRels.'); } else if (matchingProps.isNotEmpty) { srcProp = matchingProps.first; } else if (matchingRels.isNotEmpty) { @@ -497,11 +534,14 @@ class CodeChunks { if (srcRel != null) { return 'RelInfo<${srcEntity.name}>.toManyBacklink(' - '${srcRel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; + '${srcRel.id.id}, object.${propertyFieldAccess( + entity.idProperty, '!')})'; } else if (srcProp != null) { return 'RelInfo<${srcEntity.name}>.toOneBacklink(' - '${srcProp.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')}, ' - '(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(srcProp)})'; + '${srcProp.id.id}, object.${propertyFieldAccess( + entity.idProperty, '!')}, ' + '(${srcEntity.name} srcObject) => srcObject.${propertyFieldName( + srcProp)})'; } else { throw InvalidGenerationSourceError( 'Unknown relation backlink source for ${entity.name}.${bl.name}'); @@ -511,9 +551,10 @@ class CodeChunks { static String toManyRelations(ModelEntity entity) { final definitions = []; definitions.addAll(entity.relations.map( - (ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel.name}')); + (ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel + .name}')); definitions.addAll(entity.backlinks.map((ModelBacklink bl) => - '${backlinkRelInfo(entity, bl)}: object.${bl.name}')); + '${backlinkRelInfo(entity, bl)}: object.${bl.name}')); return '{${definitions.join(',')}}'; } @@ -564,7 +605,7 @@ class CodeChunks { static final ${propertyFieldName(prop)} = '''; if (prop.isRelation) { propCode += - 'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>'; + 'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>'; } else { propCode += 'Query${fieldType}Property<${entity.name}>'; } diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart index d32238941..ea4543b56 100644 --- a/generator/lib/src/entity_resolver.dart +++ b/generator/lib/src/entity_resolver.dart @@ -420,7 +420,9 @@ 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); } diff --git a/objectbox/lib/src/relations/to_one.dart b/objectbox/lib/src/relations/to_one.dart index 5aed8c872..685573a23 100644 --- a/objectbox/lib/src/relations/to_one.dart +++ b/objectbox/lib/src/relations/to_one.dart @@ -56,6 +56,21 @@ class ToOne { _ToOneValue _value = _ToOneValue.none(); + /// Create a ToOne relationship. Normally, you don't assign the target yet. + 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) { From 8a5e0507a02d90a137e621082d9df1f8dd0ffbc7 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Aug 2021 20:06:06 +0200 Subject: [PATCH 2/4] ToOne with json_serializable & freezed - generator integration tests --- .../integration-tests/part-partof/1.dart | 42 +++++++++++++++--- .../part-partof/lib/frozen.dart | 18 +++++++- .../part-partof/lib/json.dart | 43 ++++++++++++++++++- generator/integration-tests/test_env.dart | 3 +- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/generator/integration-tests/part-partof/1.dart b/generator/integration-tests/part-partof/1.dart index 34eceb608..bf7104dfb 100644 --- a/generator/integration-tests/part-partof/1.dart +++ b/generator/integration-tests/part-partof/1.dart @@ -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() { @@ -16,12 +16,23 @@ 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'} + })); + }); + + group('package:Freezed', () { + setupTestsFor(FrozenEntity(id: 1, str: 'foo', date: DateTime.now())); + final author = FrozenPerson(id: 1, name: 'Charles'); + setupRelTestsFor( + FrozenBook(id: 1, author: ToOne(target: author)), author); + }); } void setupTestsFor(EntityT newObject) { - group('${EntityT}', () { + group(EntityT.toString(), () { late TestEnv env; setUp(() => env = TestEnv(getObjectBoxModel())); tearDown(() => env.close()); @@ -32,3 +43,24 @@ void setupTestsFor(EntityT newObject) { }); }); } + +void setupRelTestsFor(BookEntityT book, + [PersonEntityT? author]) { + group(BookEntityT.toString(), () { + late TestEnv env; + setUp(() => env = TestEnv(getObjectBoxModel())); + tearDown(() => env.close()); + + test('relations', () { + if (author != null) { + env.store.box().put(author); + (book as dynamic).author.target = author; + } + env.box.put(book); + + final bookRead = env.box.get(1)! as dynamic; + expect(bookRead.author.targetId, 1); + expect(bookRead.author.target!.name, 'Charles'); + }); + }); +} diff --git a/generator/integration-tests/part-partof/lib/frozen.dart b/generator/integration-tests/part-partof/lib/frozen.dart index 5523f3b5a..c3b9a9f68 100644 --- a/generator/integration-tests/part-partof/lib/frozen.dart +++ b/generator/integration-tests/part-partof/lib/frozen.dart @@ -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'; @@ -11,3 +11,19 @@ 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 author}) = _FrozenBook; +} diff --git a/generator/integration-tests/part-partof/lib/json.dart b/generator/integration-tests/part-partof/lib/json.dart index df4cd6ffa..0892f0f84 100644 --- a/generator/integration-tests/part-partof/lib/json.dart +++ b/generator/integration-tests/part-partof/lib/json.dart @@ -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'; @@ -17,3 +17,44 @@ class JsonEntity { Map toJson() => _$JsonEntityToJson(this); } + +@Entity() +@JsonSerializable() +class JsonPerson { + int? id; + String name; + + JsonPerson({required this.name}); + + factory JsonPerson.fromJson(Map json) => + _$JsonPersonFromJson(json); + + Map toJson() => _$JsonPersonToJson(this); +} + +@Entity() +@JsonSerializable() +class JsonBook { + int? id; + + @_PersonRelToOneConverter() + final ToOne author; + + JsonBook({required this.author}); + + factory JsonBook.fromJson(Map json) => _$JsonBookFromJson(json); + + Map toJson() => _$JsonBookToJson(this); +} + +class _PersonRelToOneConverter + implements JsonConverter, Map?> { + const _PersonRelToOneConverter(); + + @override + ToOne fromJson(Map? json) => ToOne( + target: json == null ? null : JsonPerson.fromJson(json)); + + @override + Map? toJson(ToOne rel) => rel.target?.toJson(); +} diff --git a/generator/integration-tests/test_env.dart b/generator/integration-tests/test_env.dart index 86fd4a709..276b00607 100644 --- a/generator/integration-tests/test_env.dart +++ b/generator/integration-tests/test_env.dart @@ -7,13 +7,12 @@ export 'package:objectbox/internal.dart'; class TestEnv { static final dir = Directory('testdata'); late final Store store; - late final Box box; + late final Box box = store.box(); TestEnv(ModelDefinition defs) { if (dir.existsSync()) dir.deleteSync(recursive: true); store = Store(defs, directory: dir.path); - box = Box(store); } void close() { From 18d67ee89ef704052244108403895d9981536f75 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Aug 2021 17:43:49 +0200 Subject: [PATCH 3/4] ToMany - support initializing the list in the constructor --- generator/lib/src/code_chunks.dart | 140 ++++++++++------------- generator/lib/src/entity_resolver.dart | 3 +- objectbox/lib/src/relations/to_many.dart | 21 +++- objectbox/lib/src/relations/to_one.dart | 6 +- 4 files changed, 82 insertions(+), 88 deletions(-) diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index 818591ff5..6a0465b7c 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -6,8 +6,8 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError; class CodeChunks { - static String objectboxDart(ModelInfo model, List imports, - Pubspec? pubspec) => + static String objectboxDart( + ModelInfo model, List imports, Pubspec? pubspec) => """ // GENERATED CODE - DO NOT MODIFY BY HAND @@ -35,8 +35,7 @@ class CodeChunks { ${defineModel(model)} final bindings = { - ${model.entities.mapIndexed((i, entity) => "${entity - .name}: ${entityBinding(i, entity)}").join(",\n")} + ${model.entities.mapIndexed((i, entity) => "${entity.name}: ${entityBinding(i, entity)}").join(",\n")} }; return ModelDefinition(model, bindings); @@ -54,13 +53,9 @@ class CodeChunks { int$nullableOperator fileMode, int$nullableOperator maxReaders, bool queriesCaseSensitiveDefault = true, - String$nullableOperator macosApplicationGroup})${obxFlutter - ? ' async' - : ''} => + String$nullableOperator macosApplicationGroup})${obxFlutter ? ' async' : ''} => Store(getObjectBoxModel(), - directory: directory${obxFlutter - ? ' ?? (await defaultStoreDirectory()).path' - : ''}, + directory: directory${obxFlutter ? ' ?? (await defaultStoreDirectory()).path' : ''}, maxDBSizeInKB: maxDBSizeInKB, fileMode: fileMode, maxReaders: maxReaders, @@ -192,11 +187,9 @@ class CodeChunks { // Such ID must already be set, i.e. it could not have been assigned. return '''{ if (object.${propertyFieldName(entity.idProperty)} != id) { - throw ArgumentError('Field ${entity.name}.${propertyFieldName( - entity.idProperty)} is read-only ' + throw ArgumentError('Field ${entity.name}.${propertyFieldName(entity.idProperty)} is read-only ' '(final or getter-only) and it was declared to be self-assigned. ' - 'However, the currently inserted object (.${propertyFieldName( - entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) ' + 'However, the currently inserted object (.${propertyFieldName(entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) ' "doesn't match the inserted ID (ID \$id). " 'You must assign an ID before calling [box.put()].'); } @@ -216,8 +209,7 @@ class CodeChunks { return '[]'; default: throw InvalidGenerationSourceError( - 'Cannot figure out default value for field: ${p.fieldType} ${p - .name}'); + 'Cannot figure out default value for field: ${p.fieldType} ${p.name}'); } } @@ -294,14 +286,13 @@ class CodeChunks { } else if (p.type == OBXPropertyType.DateNano) { if (p.fieldIsNullable) { accessorSuffix = - ' == null ? null : object.${propertyFieldName(p)}'; + ' == null ? null : object.${propertyFieldName(p)}'; if (p.entity!.nullSafetyEnabled) accessorSuffix += '!'; } accessorSuffix += '.microsecondsSinceEpoch * 1000'; } } - return 'fbb.add${_propertyFlatBuffersType[p - .type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);'; + return 'fbb.add${_propertyFlatBuffersType[p.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);'; } }); @@ -325,16 +316,14 @@ class CodeChunks { // property to its index in entity.properties. final fieldIndexes = {}; final fieldReaders = - entity.properties.mapIndexed((int index, ModelProperty p) { + entity.properties.mapIndexed((int index, ModelProperty p) { fieldIndexes[propertyFieldName(p)] = index; String? fbReader; var readFieldOrNull = () => - 'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset( - p)})'; + 'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)})'; var readFieldNonNull = ([String? defaultValue]) => - 'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset( - p)}, ${defaultValue ?? fieldDefaultValue(p)})'; + 'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)}, ${defaultValue ?? fieldDefaultValue(p)})'; var readField = () => p.fieldIsNullable ? readFieldOrNull() : readFieldNonNull(); final valueVar = '${propertyFieldName(p)}Value'; @@ -347,8 +336,7 @@ class CodeChunks { fbReader = 'fb.ListReader(fb.Int8Reader())'; if (p.fieldIsNullable) { preLines.add('final $valueVar = ${readFieldOrNull()};'); - return '$valueVar == null ? null : ${p - .fieldType}.fromList($valueVar)'; + return '$valueVar == null ? null : ${p.fieldType}.fromList($valueVar)'; } else { return '${p.fieldType}.fromList(${readFieldNonNull('[]')})'; } @@ -375,16 +363,13 @@ class CodeChunks { } } else { if (p.type == OBXPropertyType.Date) { - return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull( - '0')})"; + return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull('0')})"; } else if (p.type == OBXPropertyType.DateNano) { - return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull( - '0')} / 1000).round())"; + return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull('0')} / 1000).round())"; } } throw InvalidGenerationSourceError( - 'Invalid property data type ${p.type} for a DateTime field ${entity - .name}.${p.name}'); + 'Invalid property data type ${p.type} for a DateTime field ${entity.name}.${p.name}'); } return readField(); }).toList(growable: false); @@ -398,13 +383,27 @@ class CodeChunks { 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' || paramType == 'required-named') { - log.warning("Cannot use the default constructor of '${entity.name}': " + 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; @@ -412,18 +411,6 @@ class CodeChunks { return true; // continue to the next param } - var 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; - } - } - switch (paramType) { case 'positional': case 'optional': @@ -431,15 +418,16 @@ class CodeChunks { break; case 'required-named': case 'optional-named': - constructorLines.add('$paramName: ${paramValueCode}'); + 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; }); @@ -457,28 +445,23 @@ class CodeChunks { if (!p.isRelation) return; if (fieldReaders[index].isNotEmpty) { postLines.add( - 'object.${propertyFieldName( - p)}.targetId = ${fieldReaders[index]};'); + '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}>());')); + '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 '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); ${preLines.join('\n')} - final object = ${entity.name}(${constructorLines.join( - ', \n')})${cascadeLines.join('\n')}; + final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')}; ${postLines.join('\n')} return object; }'''; @@ -486,22 +469,21 @@ class CodeChunks { static String toOneRelations(ModelEntity entity) => '[' + - entity.properties - .where((ModelProperty prop) => prop.isRelation) - .map((ModelProperty prop) => 'object.${propertyFieldName(prop)}') - .join(',') + - ']'; + entity.properties + .where((ModelProperty prop) => prop.isRelation) + .map((ModelProperty prop) => 'object.${propertyFieldName(prop)}') + .join(',') + + ']'; static String relInfo(ModelEntity entity, ModelRelation rel) => - 'RelInfo<${entity.name}>.toMany(${rel.id - .id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; + 'RelInfo<${entity.name}>.toMany(${rel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; static String backlinkRelInfo(ModelEntity entity, ModelBacklink bl) { final srcEntity = entity.model.findEntityByName(bl.srcEntity); if (srcEntity == null) { throw InvalidGenerationSourceError( 'Invalid relation backlink ${entity.name}.${bl.name} ' - '- source entity ${bl.srcEntity} not found.'); + '- source entity ${bl.srcEntity} not found.'); } // either of these will be set, based on the source field that matches @@ -512,13 +494,13 @@ class CodeChunks { final matchingProps = srcEntity.properties .where((p) => p.isRelation && p.relationTarget == entity.name); final matchingRels = - srcEntity.relations.where((r) => r.targetId == entity.id); + srcEntity.relations.where((r) => r.targetId == entity.id); final candidatesCount = matchingProps.length + matchingRels.length; if (candidatesCount > 1) { throw InvalidGenerationSourceError( 'Ambiguous relation backlink source for ${entity.name}.${bl.name}.' - ' Matching property: $matchingProps.' - ' Matching standalone relations: $matchingRels.'); + ' Matching property: $matchingProps.' + ' Matching standalone relations: $matchingRels.'); } else if (matchingProps.isNotEmpty) { srcProp = matchingProps.first; } else if (matchingRels.isNotEmpty) { @@ -534,14 +516,11 @@ class CodeChunks { if (srcRel != null) { return 'RelInfo<${srcEntity.name}>.toManyBacklink(' - '${srcRel.id.id}, object.${propertyFieldAccess( - entity.idProperty, '!')})'; + '${srcRel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})'; } else if (srcProp != null) { return 'RelInfo<${srcEntity.name}>.toOneBacklink(' - '${srcProp.id.id}, object.${propertyFieldAccess( - entity.idProperty, '!')}, ' - '(${srcEntity.name} srcObject) => srcObject.${propertyFieldName( - srcProp)})'; + '${srcProp.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')}, ' + '(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(srcProp)})'; } else { throw InvalidGenerationSourceError( 'Unknown relation backlink source for ${entity.name}.${bl.name}'); @@ -551,10 +530,9 @@ class CodeChunks { static String toManyRelations(ModelEntity entity) { final definitions = []; definitions.addAll(entity.relations.map( - (ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel - .name}')); + (ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel.name}')); definitions.addAll(entity.backlinks.map((ModelBacklink bl) => - '${backlinkRelInfo(entity, bl)}: object.${bl.name}')); + '${backlinkRelInfo(entity, bl)}: object.${bl.name}')); return '{${definitions.join(',')}}'; } @@ -605,7 +583,7 @@ class CodeChunks { static final ${propertyFieldName(prop)} = '''; if (prop.isRelation) { propCode += - 'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>'; + 'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>'; } else { propCode += 'Query${fieldType}Property<${entity.name}>'; } diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart index ea4543b56..0eeccc8c5 100644 --- a/generator/lib/src/entity_resolver.dart +++ b/generator/lib/src/entity_resolver.dart @@ -420,7 +420,8 @@ class EntityResolver extends Builder { var info = param.name; if (param.isRequiredPositional) info += ' positional'; if (param.isOptionalPositional) info += ' optional'; - if (param.isRequiredNamed) info += ' required-named'; + if (param.isRequiredNamed) + info += ' required-named'; else if (param.isNamed) info += ' optional-named'; info += ' ${param.type}'; return info; diff --git a/objectbox/lib/src/relations/to_many.dart b/objectbox/lib/src/relations/to_many.dart index 4fc2e4027..28a2d19e9 100644 --- a/objectbox/lib/src/relations/to_many.dart +++ b/objectbox/lib/src/relations/to_many.dart @@ -61,6 +61,20 @@ class ToMany extends Object with ListMixin { final _counts = {}; final _addedBeforeLoad = []; + /// 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? items}) { + if (items != null) { + __items = items; + items.forEach(_track); + } + } + @override int get length => _items.length; @@ -105,10 +119,7 @@ class ToMany extends Object with ListMixin { @override void addAll(Iterable 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); @@ -127,7 +138,7 @@ class ToMany extends Object with ListMixin { /// "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 { diff --git a/objectbox/lib/src/relations/to_one.dart b/objectbox/lib/src/relations/to_one.dart index 685573a23..d3755e72a 100644 --- a/objectbox/lib/src/relations/to_one.dart +++ b/objectbox/lib/src/relations/to_one.dart @@ -56,7 +56,11 @@ class ToOne { _ToOneValue _value = _ToOneValue.none(); - /// Create a ToOne relationship. Normally, you don't assign the target yet. + /// 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) { From c9b4b9f802c3d3f23c779c3d39fd39d6fdd661aa Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Aug 2021 17:44:33 +0200 Subject: [PATCH 4/4] ToMany with json_serializable & freezed - generator integration tests --- .../integration-tests/part-partof/1.dart | 29 ++++++++++++++----- .../part-partof/lib/frozen.dart | 3 +- .../part-partof/lib/json.dart | 24 +++++++++++++-- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/generator/integration-tests/part-partof/1.dart b/generator/integration-tests/part-partof/1.dart index bf7104dfb..6ac321b98 100644 --- a/generator/integration-tests/part-partof/1.dart +++ b/generator/integration-tests/part-partof/1.dart @@ -19,15 +19,28 @@ void main() { group('package:JsonSerializable', () { setupTestsFor(JsonEntity(id: 0, str: 'foo', date: DateTime.now())); setupRelTestsFor(JsonBook.fromJson({ - 'author': {'name': 'Charles'} + '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)), author); + FrozenBook( + id: 1, + author: ToOne(target: author), + readers: ToMany(items: readers)), + (Store store) => + store.box().putMany([author, ...readers])); }); } @@ -44,23 +57,23 @@ void setupTestsFor(EntityT newObject) { }); } -void setupRelTestsFor(BookEntityT book, - [PersonEntityT? author]) { +void setupRelTestsFor(BookEntityT book, + [void Function(Store)? init]) { group(BookEntityT.toString(), () { late TestEnv env; setUp(() => env = TestEnv(getObjectBoxModel())); tearDown(() => env.close()); test('relations', () { - if (author != null) { - env.store.box().put(author); - (book as dynamic).author.target = author; - } + 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'); }); }); } diff --git a/generator/integration-tests/part-partof/lib/frozen.dart b/generator/integration-tests/part-partof/lib/frozen.dart index c3b9a9f68..237cd5f6c 100644 --- a/generator/integration-tests/part-partof/lib/frozen.dart +++ b/generator/integration-tests/part-partof/lib/frozen.dart @@ -25,5 +25,6 @@ class FrozenBook with _$FrozenBook { @Entity(realClass: FrozenBook) factory FrozenBook( {@Id(assignable: true) required int id, - required ToOne author}) = _FrozenBook; + required ToOne author, + required ToMany readers}) = _FrozenBook; } diff --git a/generator/integration-tests/part-partof/lib/json.dart b/generator/integration-tests/part-partof/lib/json.dart index 0892f0f84..ce3c430bc 100644 --- a/generator/integration-tests/part-partof/lib/json.dart +++ b/generator/integration-tests/part-partof/lib/json.dart @@ -40,9 +40,13 @@ class JsonBook { @_PersonRelToOneConverter() final ToOne author; - JsonBook({required this.author}); + @_PersonRelToManyConverter() + final ToMany readers; - factory JsonBook.fromJson(Map json) => _$JsonBookFromJson(json); + JsonBook({required this.author, required this.readers}); + + factory JsonBook.fromJson(Map json) => + _$JsonBookFromJson(json); Map toJson() => _$JsonBookToJson(this); } @@ -58,3 +62,19 @@ class _PersonRelToOneConverter @override Map? toJson(ToOne rel) => rel.target?.toJson(); } + +class _PersonRelToManyConverter + implements JsonConverter, List>?> { + const _PersonRelToManyConverter(); + + @override + ToMany fromJson(List>? json) => + ToMany( + items: json == null + ? null + : json.map((e) => JsonPerson.fromJson(e)).toList()); + + @override + List>? toJson(ToMany rel) => + rel.map((JsonPerson obj) => obj.toJson()).toList(); +}