diff --git a/README.md b/README.md index bac15d9..7d3cc7b 100644 --- a/README.md +++ b/README.md @@ -484,7 +484,57 @@ public String getKey() { 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. -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. +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. + +By default, the key will always be stored in a separate column in the database. So for a class defined as + +```java +@AerospikeRecord(namespace = "test", set = "testSet") +public static class A { + @AerospikeKey + private long key; + private String value; +} +``` + +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: + +``` +aql> select * from test.testSet ++-----+--------+ +| key | value | ++-----+--------+ +| 1 | "test" | ++-----+--------+ +``` + +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: + +1. The `@AerospikeRecord` annotation must have `sendKey = true` +2. The `@AerospikeKey` annotation must have `storeAsBin = false` + +So the object would look like: + +```java +@AerospikeRecord(namespace = "test", set = "testSet", sendKey = true) +public static class A { + @AerospikeKey(storeAsBin = false) + private long key; + private String value; +} +``` + +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: + +``` +aql> select * from test.testSet ++----+--------+ +| PK | value | ++----+--------+ +| 1 | "test" | ++----+--------+ +``` + ---- @@ -679,7 +729,7 @@ Here are how standard Java types are mapped to Aerospike types: | Map | Map | | Object Reference (@AerospikeRecord) | List or Map | -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: +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: ```java 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 The key structure contains: - **field**: The name of the field which to which this key is mapped. If this is provided, the getter and setter cannot be provided. +- **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. - **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. - **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. diff --git a/src/main/java/com/aerospike/mapper/annotations/AerospikeKey.java b/src/main/java/com/aerospike/mapper/annotations/AerospikeKey.java index ec5bd67..d171b25 100644 --- a/src/main/java/com/aerospike/mapper/annotations/AerospikeKey.java +++ b/src/main/java/com/aerospike/mapper/annotations/AerospikeKey.java @@ -12,4 +12,9 @@ * The setter attribute is used only on Methods where the method is used to set the key on lazy object instantiation */ boolean setter() default false; + + /** + * Store the key as an Aerospike Bin, alternatively you can use @AerospikeRecord.sendKey to store the key in the record's metadata + */ + boolean storeAsBin() default true; } diff --git a/src/main/java/com/aerospike/mapper/tools/AeroMapper.java b/src/main/java/com/aerospike/mapper/tools/AeroMapper.java index 6479af3..272c51c 100644 --- a/src/main/java/com/aerospike/mapper/tools/AeroMapper.java +++ b/src/main/java/com/aerospike/mapper/tools/AeroMapper.java @@ -221,7 +221,7 @@ private T read(Policy readPolicy, @NotNull Class clazz, @NotNull Key key, try { ThreadLocalKeySaver.save(key); LoadedObjectResolver.begin(); - return mappingConverter.convertToObject(clazz, record, entry, resolveDependencies); + return mappingConverter.convertToObject(clazz, key, record, entry, resolveDependencies); } catch (ReflectiveOperationException e) { throw new AerospikeException(e); } finally { @@ -252,7 +252,7 @@ private T[] readBatch(BatchPolicy batchPolicy, @NotNull Class clazz, @Not } else { try { ThreadLocalKeySaver.save(keys[i]); - T result = mappingConverter.convertToObject(clazz, records[i], entry, false); + T result = mappingConverter.convertToObject(clazz, keys[i], records[i], entry, false); results[i] = result; } catch (ReflectiveOperationException e) { throw new AerospikeException(e); @@ -372,7 +372,7 @@ public void scan(ScanPolicy policy, @NotNull Class clazz, @NotNull Proces AtomicBoolean userTerminated = new AtomicBoolean(false); try { mClient.scanAll(policy, namespace, setName, (key, record) -> { - T object = this.getMappingConverter().convertToObject(clazz, record); + T object = this.getMappingConverter().convertToObject(clazz, key, record); if (!processor.process(object)) { userTerminated.set(true); throw new AerospikeException.ScanTerminated(); @@ -420,7 +420,7 @@ public void query(QueryPolicy policy, @NotNull Class clazz, @NotNull Proc RecordSet recordSet = mClient.query(policy, statement); try { while (recordSet.next()) { - T object = this.getMappingConverter().convertToObject(clazz, recordSet.getRecord()); + T object = this.getMappingConverter().convertToObject(clazz, recordSet.getKey(), recordSet.getRecord()); if (!processor.process(object)) { break; } diff --git a/src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java b/src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java index 6e7f32c..ee5b894 100644 --- a/src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java +++ b/src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java @@ -64,6 +64,7 @@ public class ClassCacheEntry { private final Class clazz; private ValueType key; private String keyName = null; + private boolean keyAsBin = true; private final TreeMap values = new TreeMap<>(); private ClassCacheEntry superClazz; private int binCount; @@ -416,12 +417,12 @@ private Method findConstructorFactoryMethod() { if (!StringUtils.isBlank(this.factoryClass) || !StringUtils.isBlank(this.factoryMethod)) { // Both must be specified if (StringUtils.isBlank(this.factoryClass)) { - throw new AerospikeException("Missing factoryClass definition when factoryMethod is specified on class " + - clazz.getSimpleName()); + throw new AerospikeException(String.format("Missing factoryClass definition when factoryMethod is specified on class %s", + clazz.getSimpleName())); } if (StringUtils.isBlank(this.factoryClass)) { - throw new AerospikeException("Missing factoryMethod definition when factoryClass is specified on class " + - clazz.getSimpleName()); + throw new AerospikeException(String.format("Missing factoryMethod definition when factoryClass is specified on class %s", + clazz.getSimpleName())); } // Load the class and check for the method try { @@ -479,8 +480,8 @@ private void setConstructorFactoryMethod(Method method) { private void findConstructor() { Constructor[] constructors = clazz.getDeclaredConstructors(); if (constructors.length == 0) { - throw new AerospikeException("Class " + clazz.getSimpleName() + - " has no constructors and hence cannot be mapped to Aerospike"); + throw new AerospikeException(String.format("Class %s has no constructors and hence cannot be mapped to Aerospike", + clazz.getSimpleName())); } Constructor desiredConstructor = null; Constructor noArgConstructor = null; @@ -494,9 +495,9 @@ private void findConstructor() { AerospikeConstructor aerospikeConstructor = thisConstructor.getAnnotation(AerospikeConstructor.class); if (aerospikeConstructor != null) { if (desiredConstructor != null) { - throw new AerospikeException("Class " + clazz.getSimpleName() + - " has multiple constructors annotated with @AerospikeConstructor. " + - "Only one constructor can be so annotated."); + throw new AerospikeException(String.format("Class %s" + + " has multiple constructors annotated with @AerospikeConstructor." + + " Only one constructor can be so annotated.", clazz.getSimpleName())); } else { desiredConstructor = thisConstructor; } @@ -509,8 +510,9 @@ private void findConstructor() { } if (desiredConstructor == null) { - throw new AerospikeException("Class " + clazz.getSimpleName() + " has neither a no-arg constructor, " + - "nor a constructor annotated with @AerospikeConstructor so cannot be mapped to Aerospike."); + throw new AerospikeException(String.format("Class %s has neither a no-arg constructor, " + + "nor a constructor annotated with @AerospikeConstructor so cannot be mapped to Aerospike.", + clazz.getSimpleName())); } Parameter[] params = desiredConstructor.getParameters(); @@ -551,10 +553,11 @@ private void findConstructor() { } Class type = thisParam.getType(); if (!type.isAssignableFrom(allValues.get(binName).getType())) { - throw new AerospikeException("Class " + clazz.getSimpleName() + " has a preferred constructor of " + - desiredConstructor + ". However, parameter " + count + - " is of type " + type + " but assigned from bin \"" + binName + "\" of type " + - values.get(binName).getType() + ". These types are incompatible."); + throw new AerospikeException(String.format("Class %s has a preferred constructor of" + + " %s. However, parameter %s" + + " is of type %s but assigned from bin \"%s\" of type %s." + + " These types are incompatible.", + clazz.getSimpleName(), desiredConstructor, count, type, binName, values.get(binName).getType())); } constructorParamBins[count - 1] = binName; constructorParamDefaults[count - 1] = PrimitiveDefaults.getDefaultValue(thisParam.getType()); @@ -627,9 +630,11 @@ private void loadPropertiesFromClass() { if (keyProperty != null) { keyProperty.validate(clazz.getName(), config, true); + if (key != null) { - throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key"); + throw new AerospikeException(String.format("Class %s cannot have more than one key", clazz.getName())); } + AnnotatedType annotatedType = new AnnotatedType(config, keyProperty.getGetter()); TypeMapper typeMapper = TypeUtils.getMapper(keyProperty.getType(), annotatedType, this.mapper); this.key = new ValueType.MethodValue(keyProperty, typeMapper, annotatedType); @@ -637,10 +642,12 @@ private void loadPropertiesFromClass() { for (String thisPropertyName : properties.keySet()) { PropertyDefinition thisProperty = properties.get(thisPropertyName); thisProperty.validate(clazz.getName(), config, false); + if (this.values.get(thisPropertyName) != null) { - throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " + - thisPropertyName + " more than once"); + throw new AerospikeException(String.format("Class %s cannot define the mapped name %s more than once", + clazz.getName(), thisPropertyName)); } + AnnotatedType annotatedType = new AnnotatedType(config, thisProperty.getGetter()); TypeMapper typeMapper = TypeUtils.getMapper(thisProperty.getType(), annotatedType, this.mapper); ValueType value = new ValueType.MethodValue(thisProperty, typeMapper, annotatedType); @@ -654,20 +661,38 @@ private void loadFieldsFromClass() { for (Field thisField : this.clazz.getDeclaredFields()) { boolean isKey = false; BinConfig thisBin = getBinFromField(thisField); + if (Modifier.isFinal(thisField.getModifiers()) && Modifier.isStatic(thisField.getModifiers())) { // We cannot map static final fields continue; } + if (thisField.isAnnotationPresent(AerospikeKey.class) || (!StringUtils.isBlank(keyField) && keyField.equals(thisField.getName()))) { if (thisField.isAnnotationPresent(AerospikeExclude.class) || (thisBin != null && thisBin.isExclude() != null && thisBin.isExclude())) { - throw new AerospikeException("Class " + clazz.getName() + " cannot have a field which is both a key and excluded."); + throw new AerospikeException(String.format("Class %s cannot have a field which is both a key and excluded.", + clazz.getName())); } + if (key != null) { - throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key"); + throw new AerospikeException(String.format("Class %s cannot have more than one key", + clazz.getName())); + } + AerospikeKey keyAnnotation = thisField.getAnnotation(AerospikeKey.class); + boolean storeAsBin = keyAnnotation == null || keyAnnotation.storeAsBin(); + + if (keyConfig != null && keyConfig.getStoreAsBin() != null) { + storeAsBin = keyConfig.getStoreAsBin(); } + + if (!storeAsBin && (this.sendKey == null || !this.sendKey)) { + throw new AerospikeException(String.format("Class %s attempts to store primary key information" + + " inside the aerospike key, but sendKey is not true at the record level", clazz.getName())); + } + AnnotatedType annotatedType = new AnnotatedType(config, thisField); TypeMapper typeMapper = TypeUtils.getMapper(thisField.getType(), annotatedType, this.mapper); this.key = new ValueType.FieldValue(thisField, typeMapper, annotatedType); + this.keyAsBin = storeAsBin; isKey = true; } @@ -694,7 +719,8 @@ private void loadFieldsFromClass() { } if (this.values.get(name) != null) { - throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " + name + " more than once"); + throw new AerospikeException(String.format("Class %s cannot define the mapped name %s more than once", + clazz.getName(), name)); } if ((bin != null && bin.useAccessors()) || (thisBin != null && thisBin.getUseAccessors() != null && thisBin.getUseAccessors())) { validateAccessorsForField(name, thisField); @@ -778,8 +804,8 @@ public Object getKey(Object object) { try { Object key = this._getKey(object); if (key == null) { - throw new AerospikeException("Null key from annotated object of class " + this.clazz.getSimpleName() + - ". Did you forget an @AerospikeKey annotation?"); + throw new AerospikeException(String.format("Null key from annotated object of class %s." + + " Did you forget an @AerospikeKey annotation?", this.clazz.getSimpleName())); } return key; } catch (ReflectiveOperationException re) { @@ -850,6 +876,10 @@ public Bin[] getBins(Object instance, boolean allowNullBins, String[] binNames) while (thisClass != null) { Set keys = thisClass.values.keySet(); for (String name : keys) { + if (name.equals(thisClass.keyName) && !thisClass.keyAsBin) { + // Do not explicitly write the key to the bin + continue; + } if (contains(binNames, name)) { ValueType value = (ValueType) thisClass.values.get(name); Object javaValue = value.get(instance); @@ -948,15 +978,15 @@ public List getList(Object instance, boolean skipKey, boolean needsType) } public T constructAndHydrate(Map map) { - return constructAndHydrate(null, map); + return constructAndHydrate(null, null, map); } - public T constructAndHydrate(Record record) { - return constructAndHydrate(record, null); + public T constructAndHydrate(Key key, Record record) { + return constructAndHydrate(key, record, null); } @SuppressWarnings("unchecked") - private T constructAndHydrate(Record record, Map map) { + private T constructAndHydrate(Key key, Record record, Map map) { Map valueMap = new HashMap<>(); try { ClassCacheEntry thisClass = this; @@ -976,7 +1006,19 @@ private T constructAndHydrate(Record record, Map map) { while (thisClass != null) { for (String name : thisClass.values.keySet()) { ValueType value = thisClass.values.get(name); - Object aerospikeValue = record == null ? map.get(name) : record.getValue(name); + Object aerospikeValue; + if (record == null) { + aerospikeValue = map.get(name); + } else if (name.equals(thisClass.keyName) && !thisClass.keyAsBin) { + if (key.userKey != null) { + aerospikeValue = key.userKey.getObject(); + } else { + throw new AerospikeException(String.format("Key field on class %s was for key %s." + + " Was the record saved passing 'sendKey = true'? ", className, key)); + } + } else { + aerospikeValue = record.getValue(name); + } valueMap.put(name, value.getTypeMapper().fromAerospikeFormat(aerospikeValue)); } if (result == null) { diff --git a/src/main/java/com/aerospike/mapper/tools/ReactiveAeroMapper.java b/src/main/java/com/aerospike/mapper/tools/ReactiveAeroMapper.java index 18b5bb3..9754111 100644 --- a/src/main/java/com/aerospike/mapper/tools/ReactiveAeroMapper.java +++ b/src/main/java/com/aerospike/mapper/tools/ReactiveAeroMapper.java @@ -201,7 +201,7 @@ private Mono read(Policy readPolicy, @NotNull Class clazz, @NotNull Ke .map(keyRecord -> { try { ThreadLocalKeySaver.save(key); - return mappingConverter.convertToObject(clazz, keyRecord.record, entry, resolveDependencies); + return mappingConverter.convertToObject(clazz, key, keyRecord.record, entry, resolveDependencies); } catch (ReflectiveOperationException e) { throw new AerospikeException(e); } finally { @@ -230,7 +230,7 @@ private Flux readBatch(BatchPolicy batchPolicy, @NotNull Class clazz, .map(keyRecord -> { try { ThreadLocalKeySaver.save(keyRecord.key); - return mappingConverter.convertToObject(clazz, keyRecord.record, entry, true); + return mappingConverter.convertToObject(clazz, keyRecord.key, keyRecord.record, entry, true); } catch (ReflectiveOperationException e) { throw new AerospikeException(e); } finally { @@ -252,7 +252,7 @@ public Mono delete(WritePolicy writePolicy, @NotNull Class clazz if (writePolicy == null) { writePolicy = entry.getWritePolicy(); if (entry.getDurableDelete() != null) { - // Clone the write policy so we're not changing the original one + // Clone the write policy, so we're not changing the original one writePolicy = new WritePolicy(writePolicy); writePolicy.durableDelete = entry.getDurableDelete(); } @@ -324,7 +324,7 @@ public Flux scan(ScanPolicy policy, @NotNull Class clazz, int recordsP String setName = entry.getSetName(); return reactorClient.scanAll(policy, namespace, setName) - .map(keyRecord -> getMappingConverter().convertToObject(clazz, keyRecord.record)); + .map(keyRecord -> getMappingConverter().convertToObject(clazz, keyRecord.key, keyRecord.record)); } @Override @@ -344,7 +344,7 @@ public Flux query(QueryPolicy policy, @NotNull Class clazz, Filter fil statement.setSetName(entry.getSetName()); return reactorClient.query(policy, statement) - .map(keyRecord -> getMappingConverter().convertToObject(clazz, keyRecord.record)); + .map(keyRecord -> getMappingConverter().convertToObject(clazz, keyRecord.key, keyRecord.record)); } @Override diff --git a/src/main/java/com/aerospike/mapper/tools/configuration/ClassConfig.java b/src/main/java/com/aerospike/mapper/tools/configuration/ClassConfig.java index d5da40e..91f9638 100644 --- a/src/main/java/com/aerospike/mapper/tools/configuration/ClassConfig.java +++ b/src/main/java/com/aerospike/mapper/tools/configuration/ClassConfig.java @@ -231,6 +231,16 @@ public Builder withKeyField(String fieldName) { return this; } + public Builder withKeyFieldAndStoreAsBin(String fieldName, boolean storeAsBin) { + if (this.classConfig.getKey() == null) { + this.classConfig.setKey(new KeyConfig()); + } + this.validateFieldExists(fieldName); + this.classConfig.getKey().setField(fieldName); + this.classConfig.getKey().setStoreAsBin(storeAsBin); + return this; + } + public Builder withKeyGetterAndSetterOf(String getterName, String setterName) { if (this.classConfig.getKey() == null) { this.classConfig.setKey(new KeyConfig()); diff --git a/src/main/java/com/aerospike/mapper/tools/configuration/KeyConfig.java b/src/main/java/com/aerospike/mapper/tools/configuration/KeyConfig.java index 04f1f82..7dbe3ff 100644 --- a/src/main/java/com/aerospike/mapper/tools/configuration/KeyConfig.java +++ b/src/main/java/com/aerospike/mapper/tools/configuration/KeyConfig.java @@ -6,6 +6,7 @@ public class KeyConfig { private String field; private String getter; private String setter; + private Boolean storeAsBin; public String getField() { return field; @@ -18,7 +19,14 @@ public String getGetter() { public String getSetter() { return setter; } + + public Boolean getStoreAsBin() { + return storeAsBin; + } + public void setStoreAsBin(boolean value) { + this.storeAsBin = value; + } public void setField(String field) { this.field = field; diff --git a/src/main/java/com/aerospike/mapper/tools/converters/MappingConverter.java b/src/main/java/com/aerospike/mapper/tools/converters/MappingConverter.java index b0b68e2..8c55a5c 100644 --- a/src/main/java/com/aerospike/mapper/tools/converters/MappingConverter.java +++ b/src/main/java/com/aerospike/mapper/tools/converters/MappingConverter.java @@ -78,9 +78,9 @@ public T translateFromAerospike(@NotNull Object obj, @NotNull Class expec * @return A virtual list. * @throws AerospikeException an AerospikeException will be thrown in case of an encountering a ReflectiveOperationException. */ - public T convertToObject(Class clazz, Record record) { + public T convertToObject(Class clazz, Key key, Record record) { try { - return convertToObject(clazz, record, null); + return convertToObject(clazz, key, record, null); } catch (ReflectiveOperationException e) { throw new AerospikeException(e); } @@ -96,18 +96,18 @@ public T convertToObject(Class clazz, Record record) { * @return A virtual list. * @throws AerospikeException an AerospikeException will be thrown in case of an encountering a ReflectiveOperationException. */ - public T convertToObject(Class clazz, Record record, ClassCacheEntry entry) throws ReflectiveOperationException { - return this.convertToObject(clazz, record, entry, true); + public T convertToObject(Class clazz, Key key, Record record, ClassCacheEntry entry) throws ReflectiveOperationException { + return this.convertToObject(clazz, key, record, entry, true); } /** * This method should not be used, it is public only to allow mappers to see it. */ - public T convertToObject(Class clazz, Record record, ClassCacheEntry entry, boolean resolveDependencies) throws ReflectiveOperationException { + public T convertToObject(Class clazz, Key key, Record record, ClassCacheEntry entry, boolean resolveDependencies) throws ReflectiveOperationException { if (entry == null) { entry = ClassCache.getInstance().loadClass(clazz, mapper); } - T result = entry.constructAndHydrate(record); + T result = entry.constructAndHydrate(key, record); if (resolveDependencies) { resolveDependencies(entry); } @@ -206,7 +206,7 @@ private Key createKey(ClassCacheEntry entry, DeferredObjectLoader.DeferredObj public void resolveDependencies(ClassCacheEntry parentEntity) { List deferredObjects = DeferredObjectLoader.getAndClear(); - if (deferredObjects.size() == 0) { + if (deferredObjects.isEmpty()) { return; } @@ -252,7 +252,7 @@ public void resolveDependencies(ClassCacheEntry parentEntity) { DeferredObjectLoader.DeferredObjectSetter thisObjectSetter = deferredObjects.get(i); try { ThreadLocalKeySaver.save(keys[i]); - Object result = records[i] == null ? null : convertToObject((Class) thisObjectSetter.getObject().getType(), records[i], classCacheEntryList.get(i), false); + Object result = records[i] == null ? null : convertToObject((Class) thisObjectSetter.getObject().getType(), keys[i], records[i], classCacheEntryList.get(i), false); thisObjectSetter.getSetter().setValue(result); } catch (ReflectiveOperationException e) { throw new AerospikeException(e); diff --git a/src/test/java/com/aerospike/mapper/UsePKColumnInsteadOfBinForKeyTest.java b/src/test/java/com/aerospike/mapper/UsePKColumnInsteadOfBinForKeyTest.java new file mode 100644 index 0000000..f0db6d0 --- /dev/null +++ b/src/test/java/com/aerospike/mapper/UsePKColumnInsteadOfBinForKeyTest.java @@ -0,0 +1,111 @@ +package com.aerospike.mapper; + +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Record; +import com.aerospike.mapper.annotations.AerospikeKey; +import com.aerospike.mapper.annotations.AerospikeRecord; +import com.aerospike.mapper.tools.AeroMapper; +import com.aerospike.mapper.tools.configuration.ClassConfig; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class UsePKColumnInsteadOfBinForKeyTest extends AeroMapperBaseTest { + + @Data + @AllArgsConstructor + @NoArgsConstructor + @AerospikeRecord(namespace = "test", set = "testSet", sendKey = true) + public static class A { + @AerospikeKey(storeAsBin = false) + private long key1; + private String value; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class B { + private String keyStr; + private String value; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @AerospikeRecord(namespace = "test", set = "testSet", sendKey = false) + public static class C { + @AerospikeKey(storeAsBin = false) + private long key1; + private String value; + } + + @Test + public void runTest() { + AeroMapper mapper = new AeroMapper.Builder(client).build(); + A a = new A(1, "test"); + mapper.save(a); + + A readA = mapper.read(A.class, 1); + assertEquals(1, readA.key1); + + Record rawObject = client.get(null, mapper.getRecordKey(a)); + assertFalse(rawObject.bins.containsKey("key1")); + } + + @Test + public void runTestViaYaml() throws Exception { + String yaml = "---\n" + + "classes:\n" + + " - class: com.aerospike.mapper.UsePKColumnInsteadOfBinForKeyTest$B\n" + + " namespace: test\n" + + " set: testSet\n" + + " sendKey: true\n" + + " key:\n" + + " field: keyStr\n" + + " storeAsBin: false\n"; + AeroMapper mapper = new AeroMapper.Builder(client).withConfiguration(yaml).build(); + B b = new B("key1", "test1"); + mapper.save(b); + + B readB = mapper.read(B.class, "key1"); + assertEquals("key1", readB.keyStr); + + Record rawObject = client.get(null, mapper.getRecordKey(b)); + assertFalse(rawObject.bins.containsKey("keyStr")); + } + + @Test + public void runTestViaConfig() { + ClassConfig classBConfig = new ClassConfig.Builder(B.class) + .withNamespace("test") + .withSet("testSet") + .withSendKey(true) + .withKeyFieldAndStoreAsBin("keyStr", false).build(); + + client.truncate(null, NAMESPACE, "testSet", null); + AeroMapper mapper = new AeroMapper.Builder(client).withClassConfigurations(classBConfig).build(); + B b = new B("key2", "test2"); + mapper.save(b); + + B readB = mapper.read(B.class, "key2"); + assertEquals("key2", readB.keyStr); + + Record rawObject = client.get(null, mapper.getRecordKey(b)); + assertFalse(rawObject.bins.containsKey("keyStr")); + } + + @Test + public void runTestWithInvalidConfig() { + AeroMapper mapper = new AeroMapper.Builder(client).build(); + C c = new C(1, "test"); + assertThrows(AerospikeException.class, () -> { + mapper.save(c); + }); + } +} diff --git a/src/test/java/com/aerospike/mapper/reactive/ReactiveUsePKColumnInsteadOfBinForKeyTest.java b/src/test/java/com/aerospike/mapper/reactive/ReactiveUsePKColumnInsteadOfBinForKeyTest.java new file mode 100644 index 0000000..6a623e5 --- /dev/null +++ b/src/test/java/com/aerospike/mapper/reactive/ReactiveUsePKColumnInsteadOfBinForKeyTest.java @@ -0,0 +1,94 @@ +package com.aerospike.mapper.reactive; + +import com.aerospike.client.query.KeyRecord; +import com.aerospike.mapper.annotations.AerospikeKey; +import com.aerospike.mapper.annotations.AerospikeRecord; +import com.aerospike.mapper.tools.ReactiveAeroMapper; +import com.aerospike.mapper.tools.configuration.ClassConfig; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; +import reactor.core.scheduler.Schedulers; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReactiveUsePKColumnInsteadOfBinForKeyTest extends ReactiveAeroMapperBaseTest { + + @Data + @AllArgsConstructor + @NoArgsConstructor + @AerospikeRecord(namespace = "test", set = "testSet", sendKey = true) + public static class A { + @AerospikeKey(storeAsBin = false) + private long key1; + private String value; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class B { + private String keyStr; + private String value; + } + + @Test + public void runTest() { + ReactiveAeroMapper reactiveMapper = new ReactiveAeroMapper.Builder(reactorClient).build(); + A a = new A(1, "test"); + reactiveMapper.save(a).subscribeOn(Schedulers.parallel()).block(); + + A readA = reactiveMapper.read(A.class, 1).subscribeOn(Schedulers.parallel()).block(); + assertNotNull(readA); + assertEquals(1, readA.key1); + + KeyRecord rawObject = reactorClient.get(null, reactiveMapper.getRecordKey(a).block()).block(); + assertNotNull(rawObject); + assertFalse(rawObject.record.bins.containsKey("key1")); + } + + @Test + public void runTestViaYaml() throws Exception { + String yaml = "---\n" + + "classes:\n" + + " - class: com.aerospike.mapper.reactive.ReactiveUsePKColumnInsteadOfBinForKeyTest$B\n" + + " namespace: test\n" + + " set: testSet\n" + + " sendKey: true\n" + + " key:\n" + + " field: keyStr\n" + + " storeAsBin: false\n"; + ReactiveAeroMapper reactiveMapper = new ReactiveAeroMapper.Builder(reactorClient).withConfiguration(yaml).build(); + B b = new B("key1", "test1"); + reactiveMapper.save(b).subscribeOn(Schedulers.parallel()).block(); + + B readB = reactiveMapper.read(B.class, "key1").subscribeOn(Schedulers.parallel()).block(); + assertNotNull(readB); + assertEquals("key1", readB.keyStr); + + KeyRecord rawObject = reactorClient.get(null, reactiveMapper.getRecordKey(b).block()).block(); + assertNotNull(rawObject); + assertFalse(rawObject.record.bins.containsKey("keyStr")); + } + + @Test + public void runTestViaConfig() { + ClassConfig classBConfig = new ClassConfig.Builder(B.class) + .withNamespace("test") + .withSet("testSet") + .withSendKey(true) + .withKeyFieldAndStoreAsBin("keyStr", false).build(); + ReactiveAeroMapper reactiveMapper = new ReactiveAeroMapper.Builder(reactorClient).withClassConfigurations(classBConfig).build(); + B b = new B("key2", "test2"); + reactiveMapper.save(b).subscribeOn(Schedulers.parallel()).block(); + + B readB = reactiveMapper.read(B.class, "key2").subscribeOn(Schedulers.parallel()).block(); + assertNotNull(readB); + assertEquals("key2", readB.keyStr); + + KeyRecord rawObject = reactorClient.get(null, reactiveMapper.getRecordKey(b).block()).block(); + assertNotNull(rawObject); + assertFalse(rawObject.record.bins.containsKey("keyStr")); + } +}