Skip to content

Commit 18d67ee

Browse files
committed
ToMany - support initializing the list in the constructor
1 parent 8a5e050 commit 18d67ee

File tree

4 files changed

+82
-88
lines changed

4 files changed

+82
-88
lines changed

generator/lib/src/code_chunks.dart

Lines changed: 59 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:pubspec_parse/pubspec_parse.dart';
66
import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
77

88
class CodeChunks {
9-
static String objectboxDart(ModelInfo model, List<String> imports,
10-
Pubspec? pubspec) =>
9+
static String objectboxDart(
10+
ModelInfo model, List<String> imports, Pubspec? pubspec) =>
1111
"""
1212
// GENERATED CODE - DO NOT MODIFY BY HAND
1313
@@ -35,8 +35,7 @@ class CodeChunks {
3535
${defineModel(model)}
3636
3737
final bindings = <Type, EntityDefinition>{
38-
${model.entities.mapIndexed((i, entity) => "${entity
39-
.name}: ${entityBinding(i, entity)}").join(",\n")}
38+
${model.entities.mapIndexed((i, entity) => "${entity.name}: ${entityBinding(i, entity)}").join(",\n")}
4039
};
4140
4241
return ModelDefinition(model, bindings);
@@ -54,13 +53,9 @@ class CodeChunks {
5453
int$nullableOperator fileMode,
5554
int$nullableOperator maxReaders,
5655
bool queriesCaseSensitiveDefault = true,
57-
String$nullableOperator macosApplicationGroup})${obxFlutter
58-
? ' async'
59-
: ''} =>
56+
String$nullableOperator macosApplicationGroup})${obxFlutter ? ' async' : ''} =>
6057
Store(getObjectBoxModel(),
61-
directory: directory${obxFlutter
62-
? ' ?? (await defaultStoreDirectory()).path'
63-
: ''},
58+
directory: directory${obxFlutter ? ' ?? (await defaultStoreDirectory()).path' : ''},
6459
maxDBSizeInKB: maxDBSizeInKB,
6560
fileMode: fileMode,
6661
maxReaders: maxReaders,
@@ -192,11 +187,9 @@ class CodeChunks {
192187
// Such ID must already be set, i.e. it could not have been assigned.
193188
return '''{
194189
if (object.${propertyFieldName(entity.idProperty)} != id) {
195-
throw ArgumentError('Field ${entity.name}.${propertyFieldName(
196-
entity.idProperty)} is read-only '
190+
throw ArgumentError('Field ${entity.name}.${propertyFieldName(entity.idProperty)} is read-only '
197191
'(final or getter-only) and it was declared to be self-assigned. '
198-
'However, the currently inserted object (.${propertyFieldName(
199-
entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) '
192+
'However, the currently inserted object (.${propertyFieldName(entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) '
200193
"doesn't match the inserted ID (ID \$id). "
201194
'You must assign an ID before calling [box.put()].');
202195
}
@@ -216,8 +209,7 @@ class CodeChunks {
216209
return '[]';
217210
default:
218211
throw InvalidGenerationSourceError(
219-
'Cannot figure out default value for field: ${p.fieldType} ${p
220-
.name}');
212+
'Cannot figure out default value for field: ${p.fieldType} ${p.name}');
221213
}
222214
}
223215

@@ -294,14 +286,13 @@ class CodeChunks {
294286
} else if (p.type == OBXPropertyType.DateNano) {
295287
if (p.fieldIsNullable) {
296288
accessorSuffix =
297-
' == null ? null : object.${propertyFieldName(p)}';
289+
' == null ? null : object.${propertyFieldName(p)}';
298290
if (p.entity!.nullSafetyEnabled) accessorSuffix += '!';
299291
}
300292
accessorSuffix += '.microsecondsSinceEpoch * 1000';
301293
}
302294
}
303-
return 'fbb.add${_propertyFlatBuffersType[p
304-
.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);';
295+
return 'fbb.add${_propertyFlatBuffersType[p.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);';
305296
}
306297
});
307298

@@ -325,16 +316,14 @@ class CodeChunks {
325316
// property to its index in entity.properties.
326317
final fieldIndexes = <String, int>{};
327318
final fieldReaders =
328-
entity.properties.mapIndexed((int index, ModelProperty p) {
319+
entity.properties.mapIndexed((int index, ModelProperty p) {
329320
fieldIndexes[propertyFieldName(p)] = index;
330321

331322
String? fbReader;
332323
var readFieldOrNull = () =>
333-
'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(
334-
p)})';
324+
'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)})';
335325
var readFieldNonNull = ([String? defaultValue]) =>
336-
'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(
337-
p)}, ${defaultValue ?? fieldDefaultValue(p)})';
326+
'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)}, ${defaultValue ?? fieldDefaultValue(p)})';
338327
var readField =
339328
() => p.fieldIsNullable ? readFieldOrNull() : readFieldNonNull();
340329
final valueVar = '${propertyFieldName(p)}Value';
@@ -347,8 +336,7 @@ class CodeChunks {
347336
fbReader = 'fb.ListReader<int>(fb.Int8Reader())';
348337
if (p.fieldIsNullable) {
349338
preLines.add('final $valueVar = ${readFieldOrNull()};');
350-
return '$valueVar == null ? null : ${p
351-
.fieldType}.fromList($valueVar)';
339+
return '$valueVar == null ? null : ${p.fieldType}.fromList($valueVar)';
352340
} else {
353341
return '${p.fieldType}.fromList(${readFieldNonNull('[]')})';
354342
}
@@ -375,16 +363,13 @@ class CodeChunks {
375363
}
376364
} else {
377365
if (p.type == OBXPropertyType.Date) {
378-
return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull(
379-
'0')})";
366+
return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull('0')})";
380367
} else if (p.type == OBXPropertyType.DateNano) {
381-
return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull(
382-
'0')} / 1000).round())";
368+
return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull('0')} / 1000).round())";
383369
}
384370
}
385371
throw InvalidGenerationSourceError(
386-
'Invalid property data type ${p.type} for a DateTime field ${entity
387-
.name}.${p.name}');
372+
'Invalid property data type ${p.type} for a DateTime field ${entity.name}.${p.name}');
388373
}
389374
return readField();
390375
}).toList(growable: false);
@@ -398,48 +383,51 @@ class CodeChunks {
398383
final paramDartType = declarationParts[2];
399384

400385
final index = fieldIndexes[paramName];
401-
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 {
402402
// If we can't find a positional param, we can't use the constructor at all.
403403
if (paramType == 'positional' || paramType == 'required-named') {
404-
log.warning("Cannot use the default constructor of '${entity.name}': "
404+
throw InvalidGenerationSourceError(
405+
"Cannot use the default constructor of '${entity.name}': "
405406
"don't know how to initialize param $paramName - no such property.");
406-
constructorLines.clear();
407-
return false;
408407
} else if (paramType == 'optional') {
409408
// OK, close the constructor, the rest will be initialized separately.
410409
return false;
411410
}
412411
return true; // continue to the next param
413412
}
414413

415-
var paramValueCode = fieldReaders[index];
416-
if (entity.properties[index].isRelation) {
417-
if (paramDartType.startsWith('ToOne<')) {
418-
paramValueCode = 'ToOne(targetId: ${paramValueCode})';
419-
} else if (paramType == 'optional-named') {
420-
log.info("Skipping constructor parameter $paramName on "
421-
"'${entity.name}': the matching field is a relation but the type "
422-
"isn't - don't know how to initialize this parameter.");
423-
return true;
424-
}
425-
}
426-
427414
switch (paramType) {
428415
case 'positional':
429416
case 'optional':
430417
constructorLines.add(paramValueCode);
431418
break;
432419
case 'required-named':
433420
case 'optional-named':
434-
constructorLines.add('$paramName: ${paramValueCode}');
421+
constructorLines.add('$paramName: $paramValueCode');
435422
break;
436423
default:
437424
throw InvalidGenerationSourceError(
438425
'Invalid constructor parameter type - internal error');
439426
}
440427

441-
// Good, we don't need to set this field anymore
442-
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] = '';
443431

444432
return true;
445433
});
@@ -457,51 +445,45 @@ class CodeChunks {
457445
if (!p.isRelation) return;
458446
if (fieldReaders[index].isNotEmpty) {
459447
postLines.add(
460-
'object.${propertyFieldName(
461-
p)}.targetId = ${fieldReaders[index]};');
448+
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};');
462449
}
463450
postLines.add('object.${propertyFieldName(p)}.attach(store);');
464451
});
465452

466453
postLines.addAll(entity.relations.map((ModelRelation rel) =>
467-
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(
468-
entity, rel)}, store.box<${entity.name}>());'));
454+
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));
469455

470456
postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
471-
return 'InternalToManyAccess.setRelInfo(object.${bl
472-
.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity
473-
.name}>());';
457+
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
474458
}));
475459

476460
return '''(Store store, ByteData fbData) {
477461
final buffer = fb.BufferContext(fbData);
478462
final rootOffset = buffer.derefObject(0);
479463
${preLines.join('\n')}
480-
final object = ${entity.name}(${constructorLines.join(
481-
', \n')})${cascadeLines.join('\n')};
464+
final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')};
482465
${postLines.join('\n')}
483466
return object;
484467
}''';
485468
}
486469

487470
static String toOneRelations(ModelEntity entity) =>
488471
'[' +
489-
entity.properties
490-
.where((ModelProperty prop) => prop.isRelation)
491-
.map((ModelProperty prop) => 'object.${propertyFieldName(prop)}')
492-
.join(',') +
493-
']';
472+
entity.properties
473+
.where((ModelProperty prop) => prop.isRelation)
474+
.map((ModelProperty prop) => 'object.${propertyFieldName(prop)}')
475+
.join(',') +
476+
']';
494477

495478
static String relInfo(ModelEntity entity, ModelRelation rel) =>
496-
'RelInfo<${entity.name}>.toMany(${rel.id
497-
.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
479+
'RelInfo<${entity.name}>.toMany(${rel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
498480

499481
static String backlinkRelInfo(ModelEntity entity, ModelBacklink bl) {
500482
final srcEntity = entity.model.findEntityByName(bl.srcEntity);
501483
if (srcEntity == null) {
502484
throw InvalidGenerationSourceError(
503485
'Invalid relation backlink ${entity.name}.${bl.name} '
504-
'- source entity ${bl.srcEntity} not found.');
486+
'- source entity ${bl.srcEntity} not found.');
505487
}
506488

507489
// either of these will be set, based on the source field that matches
@@ -512,13 +494,13 @@ class CodeChunks {
512494
final matchingProps = srcEntity.properties
513495
.where((p) => p.isRelation && p.relationTarget == entity.name);
514496
final matchingRels =
515-
srcEntity.relations.where((r) => r.targetId == entity.id);
497+
srcEntity.relations.where((r) => r.targetId == entity.id);
516498
final candidatesCount = matchingProps.length + matchingRels.length;
517499
if (candidatesCount > 1) {
518500
throw InvalidGenerationSourceError(
519501
'Ambiguous relation backlink source for ${entity.name}.${bl.name}.'
520-
' Matching property: $matchingProps.'
521-
' Matching standalone relations: $matchingRels.');
502+
' Matching property: $matchingProps.'
503+
' Matching standalone relations: $matchingRels.');
522504
} else if (matchingProps.isNotEmpty) {
523505
srcProp = matchingProps.first;
524506
} else if (matchingRels.isNotEmpty) {
@@ -534,14 +516,11 @@ class CodeChunks {
534516

535517
if (srcRel != null) {
536518
return 'RelInfo<${srcEntity.name}>.toManyBacklink('
537-
'${srcRel.id.id}, object.${propertyFieldAccess(
538-
entity.idProperty, '!')})';
519+
'${srcRel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
539520
} else if (srcProp != null) {
540521
return 'RelInfo<${srcEntity.name}>.toOneBacklink('
541-
'${srcProp.id.id}, object.${propertyFieldAccess(
542-
entity.idProperty, '!')}, '
543-
'(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(
544-
srcProp)})';
522+
'${srcProp.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')}, '
523+
'(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(srcProp)})';
545524
} else {
546525
throw InvalidGenerationSourceError(
547526
'Unknown relation backlink source for ${entity.name}.${bl.name}');
@@ -551,10 +530,9 @@ class CodeChunks {
551530
static String toManyRelations(ModelEntity entity) {
552531
final definitions = <String>[];
553532
definitions.addAll(entity.relations.map(
554-
(ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel
555-
.name}'));
533+
(ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel.name}'));
556534
definitions.addAll(entity.backlinks.map((ModelBacklink bl) =>
557-
'${backlinkRelInfo(entity, bl)}: object.${bl.name}'));
535+
'${backlinkRelInfo(entity, bl)}: object.${bl.name}'));
558536
return '{${definitions.join(',')}}';
559537
}
560538

@@ -605,7 +583,7 @@ class CodeChunks {
605583
static final ${propertyFieldName(prop)} = ''';
606584
if (prop.isRelation) {
607585
propCode +=
608-
'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>';
586+
'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>';
609587
} else {
610588
propCode += 'Query${fieldType}Property<${entity.name}>';
611589
}

generator/lib/src/entity_resolver.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,8 @@ class EntityResolver extends Builder {
420420
var info = param.name;
421421
if (param.isRequiredPositional) info += ' positional';
422422
if (param.isOptionalPositional) info += ' optional';
423-
if (param.isRequiredNamed) info += ' required-named';
423+
if (param.isRequiredNamed)
424+
info += ' required-named';
424425
else if (param.isNamed) info += ' optional-named';
425426
info += ' ${param.type}';
426427
return info;

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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ class ToOne<EntityT> {
5656

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

59-
/// Create a ToOne relationship. Normally, you don't assign the target yet.
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.
6064
ToOne({EntityT? target, int? targetId}) {
6165
if (targetId != null) {
6266
if (target != null) {

0 commit comments

Comments
 (0)