Skip to content

Commit abc73ec

Browse files
Merge pull request #338 from objectbox/unique-on-conflict-replace
Support Unique conflict resolution strategy replace.
2 parents 715afa3 + c9f5516 commit abc73ec

File tree

7 files changed

+116
-14
lines changed

7 files changed

+116
-14
lines changed

generator/lib/src/entity_resolver.dart

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ class EntityResolver extends Builder {
257257
}
258258
}
259259

260+
// Verify there is at most 1 unique property with REPLACE strategy.
261+
ensureSingleUniqueReplace(entity);
262+
// If sync enabled, verify all unique properties use REPLACE strategy.
263+
ifSyncEnsureAllUniqueAreReplace(entity);
264+
260265
entity.properties.forEach((p) => log.info(' $p'));
261266

262267
return entity;
@@ -301,9 +306,9 @@ class EntityResolver extends Builder {
301306
FieldElement f, int? fieldType, Element elementBare, ModelProperty prop) {
302307
IndexType? indexType;
303308

304-
final hasIndexAnnotation = _indexChecker.hasAnnotationOfExact(f);
305-
final hasUniqueAnnotation = _uniqueChecker.hasAnnotationOfExact(f);
306-
if (!hasIndexAnnotation && !hasUniqueAnnotation) return null;
309+
final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
310+
final uniqueAnnotation = _uniqueChecker.firstAnnotationOfExact(f);
311+
if (indexAnnotation == null && uniqueAnnotation == null) return null;
307312

308313
// Throw if property type does not support any index.
309314
if (fieldType == OBXPropertyType.Float ||
@@ -319,8 +324,6 @@ class EntityResolver extends Builder {
319324
}
320325

321326
// If available use index type from annotation.
322-
final indexAnnotation =
323-
hasIndexAnnotation ? _indexChecker.firstAnnotationOfExact(f) : null;
324327
if (indexAnnotation != null && !indexAnnotation.isNull) {
325328
final enumValItem = enumValueItem(indexAnnotation.getField('type')!);
326329
if (enumValItem != null) indexType = IndexType.values[enumValItem];
@@ -343,8 +346,15 @@ class EntityResolver extends Builder {
343346
"entity ${elementBare.name}: a hash index is not supported for type '${f.type}' of field '${f.name}'");
344347
}
345348

346-
if (hasUniqueAnnotation) {
349+
if (uniqueAnnotation != null && !uniqueAnnotation.isNull) {
347350
prop.flags |= OBXPropertyFlags.UNIQUE;
351+
// Determine unique conflict resolution.
352+
final onConflictVal =
353+
enumValueItem(uniqueAnnotation.getField('onConflict')!);
354+
if (onConflictVal != null &&
355+
ConflictStrategy.values[onConflictVal] == ConflictStrategy.replace) {
356+
prop.flags |= OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE;
357+
}
348358
}
349359

350360
switch (indexType) {
@@ -363,6 +373,27 @@ class EntityResolver extends Builder {
363373
}
364374
}
365375

376+
void ensureSingleUniqueReplace(ModelEntity entity) {
377+
final uniqueReplaceProps = entity.properties
378+
.where((p) => p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE));
379+
if (uniqueReplaceProps.length > 1) {
380+
throw InvalidGenerationSourceError(
381+
"ConflictStrategy.replace can only be used on a single property, but found multiple in '${entity.name}':\n ${uniqueReplaceProps.join('\n ')}");
382+
}
383+
}
384+
385+
void ifSyncEnsureAllUniqueAreReplace(ModelEntity entity) {
386+
if (!entity.hasFlag(OBXEntityFlags.SYNC_ENABLED)) return;
387+
final uniqueButNotReplaceProps = entity.properties.where((p) {
388+
return p.hasFlag(OBXPropertyFlags.UNIQUE) &&
389+
!p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE);
390+
});
391+
if (uniqueButNotReplaceProps.isNotEmpty) {
392+
throw InvalidGenerationSourceError(
393+
"Synced entities must use @Unique(onConflict: ConflictStrategy.replace) on all unique properties, but found others in '${entity.name}':\n ${uniqueButNotReplaceProps.join('\n ')}");
394+
}
395+
}
396+
366397
int? enumValueItem(DartObject typeField) {
367398
if (!typeField.isNull) {
368399
final enumValues = (typeField.type as InterfaceType)

objectbox/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## latest
22

3+
* Support annotating a single property with `@Unique(onConflict: ConflictStrategy.replace)` to
4+
replace an existing object if a conflict occurs when doing a put. #297
5+
36
## 1.2.1 (2021-11-09)
47

58
* Fix Flutter apps crashing on iOS 15 simulator. #313

objectbox/lib/src/annotations.dart

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:objectbox/objectbox.dart';
2+
13
/// Entity annotation is used on a class to let ObjectBox know it should store
24
/// it - making the class a "persistable Entity".
35
///
@@ -190,17 +192,31 @@ enum IndexType {
190192
hash64,
191193
}
192194

193-
/// Unique annotation forces that the value of a property is unique among all
194-
/// objects stored for the given entity.
195+
/// Enforces that the value of a property is unique among all objects in a box
196+
/// before an object can be put.
195197
///
196-
/// Trying to put an Object with offending values will result in an exception.
198+
/// Trying to put an object with offending values will result in a
199+
/// [UniqueViolationException] (see [ConflictStrategy.fail]).
200+
/// Set [onConflict] to change this strategy.
197201
///
198-
/// Unique properties are based on an [Index], so the same restrictions apply.
202+
/// Note: Unique properties are based on an [Index], so the same restrictions apply.
199203
/// It is supported to explicitly add the [Index] annotation to configure the
200-
/// index type.
204+
/// index.
201205
class Unique {
206+
/// The strategy to use when a conflict is detected when an object is put.
207+
final ConflictStrategy onConflict;
208+
202209
/// Create a Unique annotation.
203-
const Unique();
210+
const Unique({this.onConflict = ConflictStrategy.fail});
211+
}
212+
213+
/// Used with [Unique] to specify the conflict resolution strategy.
214+
enum ConflictStrategy {
215+
/// Throws [UniqueViolationException] if any property violates a [Unique] constraint.
216+
fail,
217+
218+
/// Any conflicting objects are deleted before the object is inserted.
219+
replace,
204220
}
205221

206222
/// Backlink annotation specifies a link in a reverse direction of another

objectbox/lib/src/modelinfo/enums.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ abstract class OBXPropertyFlags {
122122
/// ///
123123
/// /// For Time Series IDs, a companion property of type Date or DateNano represents the exact timestamp.
124124
static const int ID_COMPANION = 16384;
125+
126+
/// Unique on-conflict strategy: the object being put replaces any existing conflicting object (deletes it).
127+
static const int UNIQUE_ON_CONFLICT_REPLACE = 32768;
125128
}
126129

127130
abstract class OBXPropertyType {

objectbox/test/box_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,43 @@ void main() {
203203
e.toString().contains('same property value already exists'))));
204204
});
205205

206+
test('.put() replaces duplicate values on a unique replace field on insert', () {
207+
// insert without conflict
208+
box.putMany([
209+
TestEntity.uniqueReplace(replaceLong: 1, tString: 'original-1'),
210+
TestEntity.uniqueReplace(replaceLong: 2, tString: 'original-2')
211+
]);
212+
expect(box.count(), equals(2));
213+
214+
// insert with conflict, deletes ID 1 and inserts ID 3
215+
box.put(TestEntity.uniqueReplace(replaceLong: 1, tString: 'replacement-1'));
216+
expect(box.count(), equals(2));
217+
final replaced = box.get(3)!;
218+
expect(replaced.replaceLong, equals(1));
219+
expect(replaced.tString, equals('replacement-1'));
220+
});
221+
222+
test('.put() replaces duplicate values on a unique replace field on update', () {
223+
// update without conflict
224+
var first = TestEntity.uniqueReplace(replaceLong: 1, tString: 'first');
225+
box.put(first);
226+
first.replaceLong = 2;
227+
box.put(first);
228+
expect(box.count(), equals(1));
229+
final updated = box.get(1)!;
230+
expect(updated.replaceLong, equals(2));
231+
expect(updated.tString, 'first');
232+
233+
// update with conflict, deletes ID 2 and keeps ID 1
234+
box.put(TestEntity.uniqueReplace(replaceLong: 1, tString: 'second'));
235+
first.replaceLong = 1;
236+
box.put(first);
237+
expect(box.count(), equals(1));
238+
final updated2 = box.get(1)!;
239+
expect(updated2.replaceLong, equals(1));
240+
expect(updated2.tString, 'first');
241+
});
242+
206243
test('.getAll retrieves all items', () {
207244
final int id1 = box.put(TestEntity(tString: 'One'));
208245
final int id2 = box.put(TestEntity(tString: 'Two'));

objectbox/test/entity.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ class TestEntity {
137137
this.uChar,
138138
}) : tString = '';
139139

140+
@Unique(onConflict: ConflictStrategy.replace)
141+
int? replaceLong;
142+
143+
TestEntity.uniqueReplace({this.replaceLong, this.tString});
144+
140145
@Property(type: PropertyType.byte)
141146
@Index()
142147
int? iByte;

objectbox/test/objectbox-model.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"entities": [
66
{
77
"id": "1:4630700155272683157",
8-
"lastPropertyId": "34:3975438751767916074",
8+
"lastPropertyId": "35:1724663621433823504",
99
"name": "TestEntity",
1010
"properties": [
1111
{
@@ -189,6 +189,13 @@
189189
"id": "34:3975438751767916074",
190190
"name": "tDateNano",
191191
"type": 12
192+
},
193+
{
194+
"id": "35:1724663621433823504",
195+
"name": "replaceLong",
196+
"type": 6,
197+
"flags": 32808,
198+
"indexId": "20:4846837430056399798"
192199
}
193200
],
194201
"relations": [
@@ -523,7 +530,7 @@
523530
}
524531
],
525532
"lastEntityId": "10:8814538095619551454",
526-
"lastIndexId": "19:3009172190024929732",
533+
"lastIndexId": "20:4846837430056399798",
527534
"lastRelationId": "1:2155747579134420981",
528535
"lastSequenceId": "0:0",
529536
"modelVersion": 5,

0 commit comments

Comments
 (0)