Skip to content

Commit 954fac9

Browse files
tim-aeroroimenashereugn
authored
FMWK-468 Allow send key classes to use PK instead of Bin (#160)
* Allowed the use of the Aerospike Key instead of re-writing a key field. Added a new annotation field "storeInPkOnly" to use the PK field as storing the key, rather than explicitly setting a bin in the database. - Added unit tests for the same, both reactive and normal - Tested with annotations, YAML config, code config. * Rolled back accidental push of wrong connection port. * Format * Cleanup and format * Updated field name to storeAsBin Changed the config field name from `storeInPkOnly` to `storeAsBin`, added unit test to ensure config validation works. * Rolled back test port to port 3000 * Remove old "storeInPkOnly" terminology, add javadocs, cleanup * Apply suggestions from code review Co-authored-by: Eugene R. <[email protected]> * Revert change, causing NPE on runTestViaConfig() test --------- Co-authored-by: roimenashe <[email protected]> Co-authored-by: Roi Menashe <[email protected]> Co-authored-by: Eugene R. <[email protected]>
1 parent b21833f commit 954fac9

File tree

10 files changed

+368
-47
lines changed

10 files changed

+368
-47
lines changed

README.md

+53-2
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,57 @@ public String getKey() {
484484
485485
Note that it is not required to have a key on an object annotated with @AerospikeRecord. This is because an object can be embedded in another object (as a map or list) and hence not require a key to identify it to the database.
486486
487-
Also, the existence of @AerospikeKey on a field does not imply that the field will get stored in the database explicitly. Use @AerospikeBin or mapAll attribute to ensure that the key gets mapped to the database too.
487+
Also, the existence of `@AerospikeKey` on a field does not imply that the field will get stored in the database explicitly. Use `@AerospikeBin` or `mapAll` attribute to ensure that the key gets mapped to the database too.
488+
489+
By default, the key will always be stored in a separate column in the database. So for a class defined as
490+
491+
```java
492+
@AerospikeRecord(namespace = "test", set = "testSet")
493+
public static class A {
494+
@AerospikeKey
495+
private long key;
496+
private String value;
497+
}
498+
```
499+
500+
there will be a bin in the database called `key`, whose value will be the same as the value used in the primary key. This is because Aerospike does not implicitly store the value of the key in the database, but rather uses a hash of the primary key as a unique representation. So the value in the database might look like:
501+
502+
```
503+
aql> select * from test.testSet
504+
+-----+--------+
505+
| key | value |
506+
+-----+--------+
507+
| 1 | "test" |
508+
+-----+--------+
509+
```
510+
511+
If it is desired to force the primary key to be stored in the database and NOT have key added explicitly as a column then two things must be set:
512+
513+
1. The `@AerospikeRecord` annotation must have `sendKey = true`
514+
2. The `@AerospikeKey` annotation must have `storeAsBin = false`
515+
516+
So the object would look like:
517+
518+
```java
519+
@AerospikeRecord(namespace = "test", set = "testSet", sendKey = true)
520+
public static class A {
521+
@AerospikeKey(storeAsBin = false)
522+
private long key;
523+
private String value;
524+
}
525+
```
526+
527+
When data is inserted, the field `key` is not saved, but rather the key is saved as the primary key. When the value is read from the database, the stored primary key is put back into the `key` field. So the data in the database might be:
528+
529+
```
530+
aql> select * from test.testSet
531+
+----+--------+
532+
| PK | value |
533+
+----+--------+
534+
| 1 | "test" |
535+
+----+--------+
536+
```
537+
488538
489539
----
490540
@@ -679,7 +729,7 @@ Here are how standard Java types are mapped to Aerospike types:
679729
| Map<?,?> | Map |
680730
| Object Reference (@AerospikeRecord) | List or Map |
681731
682-
These types are built into the converter. However, if you wish to change them, you can use a [Custom Object Converter](#Custom-Object-Converters). For example, if you want Dates stored in the database as a string, you could do:
732+
These types are built into the converter. However, if you wish to change them, you can use a [Custom Object Converter](#custom-object-converters). For example, if you want Dates stored in the database as a string, you could do:
683733
684734
```java
685735
public static class DateConverter {
@@ -1975,6 +2025,7 @@ The key structure is used to specify the key to a record. Keys are optional in s
19752025

19762026
The key structure contains:
19772027
- **field**: The name of the field which to which this key is mapped. If this is provided, the getter and setter cannot be provided.
2028+
- **storeAsBin**: Store the primary key as a bin in the database, alternatively it is recommended to use the `sendKey` facility related to Aerospike to save the key in the record's metadata (and set this flag to false). When the record is read, the value will be pulled back and placed in the key field.
19782029
- **getter**: The getter method used to populate the key. This must be used in conjunction with a setter method, and excludes the use of the field attribute.
19792030
- **setter**: The setter method used to map data back to the Java key. This is used in conjunction with the getter method and precludes the use of the field attribute. Note that the return type of the getter must match the type of the first parameter of the setter, and the setter can have either 1 or 2 parameters, with the second (optional) parameter being either of type [com.aerospike.client.Key](https://www.aerospike.com/apidocs/java/com/aerospike/client/Key.html) or Object.
19802031

src/main/java/com/aerospike/mapper/annotations/AerospikeKey.java

+5
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@
1212
* The setter attribute is used only on Methods where the method is used to set the key on lazy object instantiation
1313
*/
1414
boolean setter() default false;
15+
16+
/**
17+
* Store the key as an Aerospike Bin, alternatively you can use @AerospikeRecord.sendKey to store the key in the record's metadata
18+
*/
19+
boolean storeAsBin() default true;
1520
}

src/main/java/com/aerospike/mapper/tools/AeroMapper.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ private <T> T read(Policy readPolicy, @NotNull Class<T> clazz, @NotNull Key key,
221221
try {
222222
ThreadLocalKeySaver.save(key);
223223
LoadedObjectResolver.begin();
224-
return mappingConverter.convertToObject(clazz, record, entry, resolveDependencies);
224+
return mappingConverter.convertToObject(clazz, key, record, entry, resolveDependencies);
225225
} catch (ReflectiveOperationException e) {
226226
throw new AerospikeException(e);
227227
} finally {
@@ -252,7 +252,7 @@ private <T> T[] readBatch(BatchPolicy batchPolicy, @NotNull Class<T> clazz, @Not
252252
} else {
253253
try {
254254
ThreadLocalKeySaver.save(keys[i]);
255-
T result = mappingConverter.convertToObject(clazz, records[i], entry, false);
255+
T result = mappingConverter.convertToObject(clazz, keys[i], records[i], entry, false);
256256
results[i] = result;
257257
} catch (ReflectiveOperationException e) {
258258
throw new AerospikeException(e);
@@ -372,7 +372,7 @@ public <T> void scan(ScanPolicy policy, @NotNull Class<T> clazz, @NotNull Proces
372372
AtomicBoolean userTerminated = new AtomicBoolean(false);
373373
try {
374374
mClient.scanAll(policy, namespace, setName, (key, record) -> {
375-
T object = this.getMappingConverter().convertToObject(clazz, record);
375+
T object = this.getMappingConverter().convertToObject(clazz, key, record);
376376
if (!processor.process(object)) {
377377
userTerminated.set(true);
378378
throw new AerospikeException.ScanTerminated();
@@ -420,7 +420,7 @@ public <T> void query(QueryPolicy policy, @NotNull Class<T> clazz, @NotNull Proc
420420
RecordSet recordSet = mClient.query(policy, statement);
421421
try {
422422
while (recordSet.next()) {
423-
T object = this.getMappingConverter().convertToObject(clazz, recordSet.getRecord());
423+
T object = this.getMappingConverter().convertToObject(clazz, recordSet.getKey(), recordSet.getRecord());
424424
if (!processor.process(object)) {
425425
break;
426426
}

src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java

+70-28
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public class ClassCacheEntry<T> {
6464
private final Class<T> clazz;
6565
private ValueType key;
6666
private String keyName = null;
67+
private boolean keyAsBin = true;
6768
private final TreeMap<String, ValueType> values = new TreeMap<>();
6869
private ClassCacheEntry<?> superClazz;
6970
private int binCount;
@@ -416,12 +417,12 @@ private Method findConstructorFactoryMethod() {
416417
if (!StringUtils.isBlank(this.factoryClass) || !StringUtils.isBlank(this.factoryMethod)) {
417418
// Both must be specified
418419
if (StringUtils.isBlank(this.factoryClass)) {
419-
throw new AerospikeException("Missing factoryClass definition when factoryMethod is specified on class " +
420-
clazz.getSimpleName());
420+
throw new AerospikeException(String.format("Missing factoryClass definition when factoryMethod is specified on class %s",
421+
clazz.getSimpleName()));
421422
}
422423
if (StringUtils.isBlank(this.factoryClass)) {
423-
throw new AerospikeException("Missing factoryMethod definition when factoryClass is specified on class " +
424-
clazz.getSimpleName());
424+
throw new AerospikeException(String.format("Missing factoryMethod definition when factoryClass is specified on class %s",
425+
clazz.getSimpleName()));
425426
}
426427
// Load the class and check for the method
427428
try {
@@ -479,8 +480,8 @@ private void setConstructorFactoryMethod(Method method) {
479480
private void findConstructor() {
480481
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
481482
if (constructors.length == 0) {
482-
throw new AerospikeException("Class " + clazz.getSimpleName() +
483-
" has no constructors and hence cannot be mapped to Aerospike");
483+
throw new AerospikeException(String.format("Class %s has no constructors and hence cannot be mapped to Aerospike",
484+
clazz.getSimpleName()));
484485
}
485486
Constructor<?> desiredConstructor = null;
486487
Constructor<?> noArgConstructor = null;
@@ -494,9 +495,9 @@ private void findConstructor() {
494495
AerospikeConstructor aerospikeConstructor = thisConstructor.getAnnotation(AerospikeConstructor.class);
495496
if (aerospikeConstructor != null) {
496497
if (desiredConstructor != null) {
497-
throw new AerospikeException("Class " + clazz.getSimpleName() +
498-
" has multiple constructors annotated with @AerospikeConstructor. " +
499-
"Only one constructor can be so annotated.");
498+
throw new AerospikeException(String.format("Class %s" +
499+
" has multiple constructors annotated with @AerospikeConstructor." +
500+
" Only one constructor can be so annotated.", clazz.getSimpleName()));
500501
} else {
501502
desiredConstructor = thisConstructor;
502503
}
@@ -509,8 +510,9 @@ private void findConstructor() {
509510
}
510511

511512
if (desiredConstructor == null) {
512-
throw new AerospikeException("Class " + clazz.getSimpleName() + " has neither a no-arg constructor, " +
513-
"nor a constructor annotated with @AerospikeConstructor so cannot be mapped to Aerospike.");
513+
throw new AerospikeException(String.format("Class %s has neither a no-arg constructor, " +
514+
"nor a constructor annotated with @AerospikeConstructor so cannot be mapped to Aerospike.",
515+
clazz.getSimpleName()));
514516
}
515517

516518
Parameter[] params = desiredConstructor.getParameters();
@@ -551,10 +553,11 @@ private void findConstructor() {
551553
}
552554
Class<?> type = thisParam.getType();
553555
if (!type.isAssignableFrom(allValues.get(binName).getType())) {
554-
throw new AerospikeException("Class " + clazz.getSimpleName() + " has a preferred constructor of " +
555-
desiredConstructor + ". However, parameter " + count +
556-
" is of type " + type + " but assigned from bin \"" + binName + "\" of type " +
557-
values.get(binName).getType() + ". These types are incompatible.");
556+
throw new AerospikeException(String.format("Class %s has a preferred constructor of" +
557+
" %s. However, parameter %s" +
558+
" is of type %s but assigned from bin \"%s\" of type %s." +
559+
" These types are incompatible.",
560+
clazz.getSimpleName(), desiredConstructor, count, type, binName, values.get(binName).getType()));
558561
}
559562
constructorParamBins[count - 1] = binName;
560563
constructorParamDefaults[count - 1] = PrimitiveDefaults.getDefaultValue(thisParam.getType());
@@ -627,20 +630,24 @@ private void loadPropertiesFromClass() {
627630

628631
if (keyProperty != null) {
629632
keyProperty.validate(clazz.getName(), config, true);
633+
630634
if (key != null) {
631-
throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key");
635+
throw new AerospikeException(String.format("Class %s cannot have more than one key", clazz.getName()));
632636
}
637+
633638
AnnotatedType annotatedType = new AnnotatedType(config, keyProperty.getGetter());
634639
TypeMapper typeMapper = TypeUtils.getMapper(keyProperty.getType(), annotatedType, this.mapper);
635640
this.key = new ValueType.MethodValue(keyProperty, typeMapper, annotatedType);
636641
}
637642
for (String thisPropertyName : properties.keySet()) {
638643
PropertyDefinition thisProperty = properties.get(thisPropertyName);
639644
thisProperty.validate(clazz.getName(), config, false);
645+
640646
if (this.values.get(thisPropertyName) != null) {
641-
throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " +
642-
thisPropertyName + " more than once");
647+
throw new AerospikeException(String.format("Class %s cannot define the mapped name %s more than once",
648+
clazz.getName(), thisPropertyName));
643649
}
650+
644651
AnnotatedType annotatedType = new AnnotatedType(config, thisProperty.getGetter());
645652
TypeMapper typeMapper = TypeUtils.getMapper(thisProperty.getType(), annotatedType, this.mapper);
646653
ValueType value = new ValueType.MethodValue(thisProperty, typeMapper, annotatedType);
@@ -654,20 +661,38 @@ private void loadFieldsFromClass() {
654661
for (Field thisField : this.clazz.getDeclaredFields()) {
655662
boolean isKey = false;
656663
BinConfig thisBin = getBinFromField(thisField);
664+
657665
if (Modifier.isFinal(thisField.getModifiers()) && Modifier.isStatic(thisField.getModifiers())) {
658666
// We cannot map static final fields
659667
continue;
660668
}
669+
661670
if (thisField.isAnnotationPresent(AerospikeKey.class) || (!StringUtils.isBlank(keyField) && keyField.equals(thisField.getName()))) {
662671
if (thisField.isAnnotationPresent(AerospikeExclude.class) || (thisBin != null && thisBin.isExclude() != null && thisBin.isExclude())) {
663-
throw new AerospikeException("Class " + clazz.getName() + " cannot have a field which is both a key and excluded.");
672+
throw new AerospikeException(String.format("Class %s cannot have a field which is both a key and excluded.",
673+
clazz.getName()));
664674
}
675+
665676
if (key != null) {
666-
throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key");
677+
throw new AerospikeException(String.format("Class %s cannot have more than one key",
678+
clazz.getName()));
679+
}
680+
AerospikeKey keyAnnotation = thisField.getAnnotation(AerospikeKey.class);
681+
boolean storeAsBin = keyAnnotation == null || keyAnnotation.storeAsBin();
682+
683+
if (keyConfig != null && keyConfig.getStoreAsBin() != null) {
684+
storeAsBin = keyConfig.getStoreAsBin();
667685
}
686+
687+
if (!storeAsBin && (this.sendKey == null || !this.sendKey)) {
688+
throw new AerospikeException(String.format("Class %s attempts to store primary key information" +
689+
" inside the aerospike key, but sendKey is not true at the record level", clazz.getName()));
690+
}
691+
668692
AnnotatedType annotatedType = new AnnotatedType(config, thisField);
669693
TypeMapper typeMapper = TypeUtils.getMapper(thisField.getType(), annotatedType, this.mapper);
670694
this.key = new ValueType.FieldValue(thisField, typeMapper, annotatedType);
695+
this.keyAsBin = storeAsBin;
671696
isKey = true;
672697
}
673698

@@ -694,7 +719,8 @@ private void loadFieldsFromClass() {
694719
}
695720

696721
if (this.values.get(name) != null) {
697-
throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " + name + " more than once");
722+
throw new AerospikeException(String.format("Class %s cannot define the mapped name %s more than once",
723+
clazz.getName(), name));
698724
}
699725
if ((bin != null && bin.useAccessors()) || (thisBin != null && thisBin.getUseAccessors() != null && thisBin.getUseAccessors())) {
700726
validateAccessorsForField(name, thisField);
@@ -778,8 +804,8 @@ public Object getKey(Object object) {
778804
try {
779805
Object key = this._getKey(object);
780806
if (key == null) {
781-
throw new AerospikeException("Null key from annotated object of class " + this.clazz.getSimpleName() +
782-
". Did you forget an @AerospikeKey annotation?");
807+
throw new AerospikeException(String.format("Null key from annotated object of class %s." +
808+
" Did you forget an @AerospikeKey annotation?", this.clazz.getSimpleName()));
783809
}
784810
return key;
785811
} catch (ReflectiveOperationException re) {
@@ -850,6 +876,10 @@ public Bin[] getBins(Object instance, boolean allowNullBins, String[] binNames)
850876
while (thisClass != null) {
851877
Set<String> keys = thisClass.values.keySet();
852878
for (String name : keys) {
879+
if (name.equals(thisClass.keyName) && !thisClass.keyAsBin) {
880+
// Do not explicitly write the key to the bin
881+
continue;
882+
}
853883
if (contains(binNames, name)) {
854884
ValueType value = (ValueType) thisClass.values.get(name);
855885
Object javaValue = value.get(instance);
@@ -948,15 +978,15 @@ public List<Object> getList(Object instance, boolean skipKey, boolean needsType)
948978
}
949979

950980
public T constructAndHydrate(Map<String, Object> map) {
951-
return constructAndHydrate(null, map);
981+
return constructAndHydrate(null, null, map);
952982
}
953983

954-
public T constructAndHydrate(Record record) {
955-
return constructAndHydrate(record, null);
984+
public T constructAndHydrate(Key key, Record record) {
985+
return constructAndHydrate(key, record, null);
956986
}
957987

958988
@SuppressWarnings("unchecked")
959-
private T constructAndHydrate(Record record, Map<String, Object> map) {
989+
private T constructAndHydrate(Key key, Record record, Map<String, Object> map) {
960990
Map<String, Object> valueMap = new HashMap<>();
961991
try {
962992
ClassCacheEntry<?> thisClass = this;
@@ -976,7 +1006,19 @@ private T constructAndHydrate(Record record, Map<String, Object> map) {
9761006
while (thisClass != null) {
9771007
for (String name : thisClass.values.keySet()) {
9781008
ValueType value = thisClass.values.get(name);
979-
Object aerospikeValue = record == null ? map.get(name) : record.getValue(name);
1009+
Object aerospikeValue;
1010+
if (record == null) {
1011+
aerospikeValue = map.get(name);
1012+
} else if (name.equals(thisClass.keyName) && !thisClass.keyAsBin) {
1013+
if (key.userKey != null) {
1014+
aerospikeValue = key.userKey.getObject();
1015+
} else {
1016+
throw new AerospikeException(String.format("Key field on class %s was <null> for key %s." +
1017+
" Was the record saved passing 'sendKey = true'? ", className, key));
1018+
}
1019+
} else {
1020+
aerospikeValue = record.getValue(name);
1021+
}
9801022
valueMap.put(name, value.getTypeMapper().fromAerospikeFormat(aerospikeValue));
9811023
}
9821024
if (result == null) {

0 commit comments

Comments
 (0)