Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3a032fe

Browse files
Support @TargetIdProperty to rename ToOne relation property #160
1 parent 265166d commit 3a032fe

File tree

10 files changed

+166
-16
lines changed

10 files changed

+166
-16
lines changed

‎generator/lib/src/code_builder.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ class CodeBuilder extends Builder {
247247
propInModel.type = prop.type;
248248
propInModel.flags = prop.flags;
249249
propInModel.dartFieldType = prop.dartFieldType;
250+
propInModel.relationField = prop.relationField;
250251
propInModel.relationTarget = prop.relationTarget;
251252
propInModel.hnswParams = prop.hnswParams;
252253
propInModel.externalName = prop.externalName;

‎generator/lib/src/code_chunks.dart‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ class CodeChunks {
152152
if (property.indexId != null && !property.indexId!.isEmpty) {
153153
additionalArgs += ', indexId: ${createIdUid(property.indexId!)}';
154154
}
155+
if (property.relationField != null && property.relationField!.isNotEmpty) {
156+
additionalArgs += ", relationField: '${property.relationField!}'";
157+
}
155158
if (property.relationTarget != null &&
156159
property.relationTarget!.isNotEmpty) {
157160
additionalArgs += ", relationTarget: '${property.relationTarget!}'";
@@ -220,19 +223,14 @@ class CodeChunks {
220223
''';
221224
}
222225

226+
/// Returns the name of the Dart field associated with the [property].
227+
///
228+
/// For ToOne relations, there is no field named like the property,
229+
/// so this returns the name of the field that defines the ToOne instead.
223230
static String propertyFieldName(ModelProperty property) {
224231
if (property.isRelation) {
225-
if (!property.name.endsWith('Id')) {
226-
throw ArgumentError.value(
227-
property.name,
228-
'property.name',
229-
'Relation property name must end with "Id"',
230-
);
231-
}
232-
233-
return property.name.substring(0, property.name.length - 2);
232+
return property.relationField!;
234233
}
235-
236234
return property.name;
237235
}
238236

‎generator/lib/src/entity_resolver.dart‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class EntityResolver extends Builder {
5252
Backlink,
5353
inPackage: _annotationsPackage,
5454
);
55+
final _targetIdPropertyChecker = const TypeChecker.typeNamed(
56+
TargetIdProperty,
57+
inPackage: _annotationsPackage,
58+
);
5559
final _hnswChecker = const TypeChecker.typeNamed(
5660
HnswIndex,
5761
inPackage: _annotationsPackage,
@@ -278,23 +282,44 @@ class EntityResolver extends Builder {
278282

279283
log.info(' $rel');
280284
} else {
281-
// Handles regular properties
285+
// Handles regular property including ToOne relation
286+
287+
// By default, name properties like the field. For ToOne relations,
288+
// default to naming the property like the ToOne field + Id suffix.
289+
// If the ToOne field is annotated with @TargetIdProperty use its name
290+
// value.
291+
final String propName;
292+
if (fieldType == OBXPropertyType.Relation) {
293+
var customName =
294+
_targetIdPropertyChecker
295+
.firstAnnotationOfExact(annotated)
296+
?.getField('name')
297+
?.toStringValue();
298+
if (customName != null && customName.isNotEmpty) {
299+
propName = customName;
300+
} else {
301+
propName = '${f.displayName}Id';
302+
}
303+
} else {
304+
propName = f.displayName;
305+
}
306+
282307
// create property (do not use readEntity.createProperty in order to avoid generating new ids)
283308
final prop = ModelProperty.create(
284309
IdUid(0, propUid ?? 0),
285-
f.displayName,
310+
propName,
286311
fieldType,
287312
flags: flags,
288313
entity: entity,
289314
uidRequest: propUid != null && propUid == 0,
290315
);
291316

317+
// ToOne relation
292318
if (fieldType == OBXPropertyType.Relation) {
293-
prop.name +='Id';
319+
prop.relationField = f.displayName;
294320
prop.relationTarget = relTargetName;
295321
prop.flags |= OBXPropertyFlags.INDEXED;
296322
prop.flags |= OBXPropertyFlags.INDEX_PARTIAL_SKIP_ZERO;
297-
298323
// IDs must not be tagged unsigned for compatibility reasons
299324
prop.flags &= ~OBXPropertyFlags.UNSIGNED;
300325
}

‎generator/test/code_builder_test.dart‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,33 @@ void main() {
271271
''');
272272
});
273273

274+
test('@TargetIdProperty ToOne annotation', () async {
275+
final source = r'''
276+
library example;
277+
import 'package:objectbox/objectbox.dart';
278+
279+
@Entity()
280+
class Example {
281+
@Id()
282+
int id = 0;
283+
284+
@TargetIdProperty('customerRef')
285+
final customer = ToOne<Example>();
286+
}
287+
''';
288+
289+
final testEnv = GeneratorTestEnv();
290+
await testEnv.run(source);
291+
292+
final exampleEntity = testEnv.model.entities[0];
293+
expect(exampleEntity.findPropertyByName('customerId'), isNull);
294+
final renamedRelationProperty = exampleEntity.findPropertyByName(
295+
'customerRef',
296+
);
297+
expect(renamedRelationProperty, isNotNull);
298+
expect(renamedRelationProperty!.type, OBXPropertyType.Relation);
299+
});
300+
274301
test('HNSW annotation on unsupported type errors', () async {
275302
final source = r'''
276303
library example;

‎objectbox/CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
* Generator: migrate to `analyzer` 8 APIs. Require at least `analyzer` 8.1.1 and `source_gen` 4.0.1.
44
* Generator: require at least `build` 4.0.0. This will also allow using `build_runner` versions
55
`2.7.2` or newer. [#759](https://github.com/objectbox/objectbox-dart/issues/759)
6+
* For `ToOne` support renaming the implicitly created target ID (or "relation") property:
7+
8+
```dart
9+
// Change target ID property name from default "customerId" to "customerRef"
10+
@TargetIdProperty('customerRef')
11+
final customer = ToOne<Customer>();
12+
```
13+
14+
This can be useful if the default name needs to be used for another property. Or when syncing with
15+
MongoDB to match the name used in the MongoDB database. [#713](https://github.com/objectbox/objectbox-dart/issues/713)
616

717
## 5.0.0 (2025年10月01日)
818

‎objectbox/lib/src/annotations.dart‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,30 @@ class ExternalName {
674674
/// The field may be of type [ToMany].
675675
const ExternalName({required this.name});
676676
}
677+
678+
/// An annotation for a [ToOne] to change the name of its target ID property.
679+
class TargetIdProperty {
680+
/// Name used in the database.
681+
final String name;
682+
683+
/// For a `ToOne`, changes the name of its associated target ID (or
684+
/// "relation") property.
685+
///
686+
/// ```dart
687+
/// @Entity
688+
/// public class Order {
689+
/// // Change from default "customerId" to "customerRef"
690+
/// @TargetIdProperty("customerRef")
691+
/// final customer = ToOne<Customer>();
692+
/// }
693+
/// ```
694+
///
695+
/// A target ID property is implicitly created (so without defining it in
696+
/// the `@Entity` class) for each `ToOne` and stores the ID of the referenced
697+
/// target object. By default, it's named like the `ToOne` field plus the
698+
/// suffix `Id` (for example `customerId`).
699+
///
700+
/// See the [relations documentation](https://docs.objectbox.io/relations) for
701+
/// details.
702+
const TargetIdProperty(this.name);
703+
}

‎objectbox/lib/src/modelinfo/modelproperty.dart‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import 'modelentity.dart';
99
class ModelProperty {
1010
IdUid id;
1111

12+
/// See [name].
1213
late String _name;
1314

1415
late int _type, _flags;
1516
IdUid? _indexId;
1617
ModelEntity? entity;
18+
19+
/// If this [isRelation], the name of the field of the ToOne.
20+
String? relationField;
21+
22+
/// If this [isRelation], the name of the entity class the ToOne targets.
1723
String? relationTarget;
1824

1925
/// The optional [HnswIndex] parameters of this property.
@@ -34,6 +40,9 @@ class ModelProperty {
3440
// whether the user requested UID information (started a rename process)
3541
final bool uidRequest;
3642

43+
/// The name of the property. Except if [isRelation], this is also the name of
44+
/// the associated Dart field. If [isRelation] use [relationField] to get the
45+
/// name of the ToOne field.
3746
String get name => _name;
3847

3948
set name(String? value) {
@@ -121,6 +130,7 @@ class ModelProperty {
121130
required int type,
122131
required int flags,
123132
IdUid? indexId,
133+
this.relationField,
124134
this.relationTarget,
125135
this.hnswParams,
126136
this.externalName,
@@ -133,6 +143,7 @@ class ModelProperty {
133143

134144
ModelProperty.fromMap(Map<String, dynamic> data, this.entity)
135145
: id = IdUid.fromString(data[ModelPropertyKey.id] as String?),
146+
relationField = data[ModelPropertyKey.relationField] as String?,
136147
relationTarget = data[ModelPropertyKey.relationTarget] as String?,
137148
_dartFieldType = data[ModelPropertyKey.dartFieldType] as String?,
138149
uidRequest = data[ModelPropertyKey.uidRequest] as bool? ?? false,
@@ -164,6 +175,9 @@ class ModelProperty {
164175
ret[ModelPropertyKey.relationTarget] = relationTarget;
165176
}
166177
if (!forModelJson) {
178+
if (relationField != null) {
179+
ret[ModelPropertyKey.relationField] = relationField;
180+
}
167181
if (_dartFieldType != null) {
168182
ret[ModelPropertyKey.dartFieldType] = _dartFieldType;
169183
}
@@ -216,6 +230,9 @@ class ModelProperty {
216230
result += ' index:$type';
217231
}
218232

233+
if (relationField != null) {
234+
result += ' relField:$relationField';
235+
}
219236
if (relationTarget != null) {
220237
result += ' relTarget:$relationTarget';
221238
}
@@ -231,6 +248,7 @@ class ModelPropertyKey {
231248
static const String indexId = 'indexId';
232249
static const String type = 'type';
233250
static const String flags = 'flags';
251+
static const String relationField = 'relationField';
234252
static const String relationTarget = 'relationTarget';
235253
static const String dartFieldType = 'dartFieldType';
236254
static const String uidRequest = 'uidRequest';

‎objectbox_test/test/entity.dart‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ class RelatedEntityB {
219219
RelatedEntityB({this.id, this.tString, this.tDouble});
220220
}
221221

222+
@Entity()
223+
class RenamedTargetIdProperty {
224+
@Id()
225+
int id = 0;
226+
227+
// Instead of the default "testRelId", property should be named "testRef"
228+
@TargetIdProperty('testRef')
229+
final testRel = ToOne<TestEntity>();
230+
}
231+
222232
@Entity()
223233
class TestEntityNonRel {
224234
int? id;

‎objectbox_test/test/objectbox-model.json‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,10 +803,32 @@
803803
}
804804
],
805805
"relations": []
806+
},
807+
{
808+
"id": "19:908685173741818689",
809+
"lastPropertyId": "2:2489121038957388096",
810+
"name": "RenamedTargetIdProperty",
811+
"properties": [
812+
{
813+
"id": "1:565634008862466228",
814+
"name": "id",
815+
"type": 6,
816+
"flags": 1
817+
},
818+
{
819+
"id": "2:2489121038957388096",
820+
"name": "testRef",
821+
"indexId": "25:5667866372157411357",
822+
"type": 11,
823+
"flags": 520,
824+
"relationTarget": "TestEntity"
825+
}
826+
],
827+
"relations": []
806828
}
807829
],
808-
"lastEntityId": "18:2680158095061865736",
809-
"lastIndexId": "24:4321803250180166911",
830+
"lastEntityId": "19:908685173741818689",
831+
"lastIndexId": "25:5667866372157411357",
810832
"lastRelationId": "2:313640065593441165",
811833
"lastSequenceId": "0:0",
812834
"modelVersion": 5,

‎objectbox_test/test/relations_test.dart‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,18 @@ void main() {
193193
query.close();
194194
}
195195
});
196+
197+
test('renamed target ID property', () {
198+
// Check put and get works even though the target ID property isn't using
199+
// the default name.
200+
final Box<RenamedTargetIdProperty> box = env.store.box();
201+
final targetName = 'target object';
202+
final id = box.put(RenamedTargetIdProperty()
203+
..testRel.target = TestEntity(tString: targetName));
204+
var storedTarget = box.get(id)!.testRel.target;
205+
expect(storedTarget, isNotNull);
206+
expect(storedTarget!.tString, targetName);
207+
});
196208
});
197209

198210
group('ToMany list management', () {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /