Skip to main content

Spring Data Advanced Features

Finer Grained Control

The majority of Spring Data can be used without having to access low level classes other than creating a repository interface which extends AerospikeRepository. However, there are many classes released as part of Spring Data Aerospike's implementation which can be accessed in application code to allow finer grained control of object persistence. These are typically exposed as Spring Beans. One of the most common lower level classes to use is AerospikeTemplate.

Obtaining a reference to AerospikeTemplate is easy:

@Autowired
private AerospikeTemplate template;

Given Spring's Inversion Of Control (IoC) architecture, this is all the code that is needed in a class to obtain a reference to the AerospikeTemplate Bean, assuming the Autowired instance exists within a Spring Component (Service, Controller, etc)

The AerospikeTemplate provides access to the underlying IAerospikeClient, creating and deleting indexes, finding records, inserting and updating records and so on. One example of where you need to use the AerospikeTemplate is if you want to use a custom WritePolicy to save the record. In this case the persist method can be used:

Person person = new Person(101, "Bob", "Jones", new Date());
WritePolicy writePolicy = new WritePolicy(
aerospikeTemplate.getAerospikeClient().getWritePolicyDefault());
writePolicy.totalTimeout = 1000;
aerospikeTemplate.persist(person, writePolicy);

Versioning Data

Aerospike supports Check And Set (CAS) semantics, which allows Reading a record, modifiying it then writing it back to the database using optimistic concurrency to ensure the record has not changed between when it was read and when it is written. This optimistic concurrency control is off by default, however it can be enbaled by creating an @Version annotated field on the class.

@AllArgsConstructor
@NoArgsConstructor
@Data
@Document
public class Person {
@Id
private long id;
@Version
private int version;
private String firstName;
@Indexed(name = "lastName_idx", type = IndexType.STRING)
private String lastName;
private Date dateOfBirth;

public Person(long id, String firstName, String lastName, Date dateOfBirth) {
this(id, 0, firstName, lastName, dateOfBirth);
}
}

The @Version field must be a type which is able to be converted to an Integer, such as int, long or even String. To insert a new record the version must be 0, and once initialized in this way Spring Data Aerospike will take care of the version field automatically, updating it whenever the object is saved and performing version control of CAS semantics using the in-built Aerospike facilities.

Note that the underlying @Version field is a placeholder which is mapped to the Aerospike generation metadata field and as such is not actually persisted in the database. For example, with the class defined above, saving a Person object will persist other fields but not the version:

Person person = new Person(10001, 0, "Bob", "Jones", new Date());
personRepsitory.save(person);

yields a database view of:

aql> set record_print_metadata true
RECORD_PRINT_METADATA = true
aql> select * from test.Person where pk = "10001"
+---------+-----------+----------+-------------------------------------+---------------+---------+-------+
| PK | firstName | lastName | @_class | dateOfBirth | {ttl} | {gen} |
+---------+-----------+----------+-------------------------------------+---------------+---------+-------+
| "10001" | "Bob" | "Jones" | "com.aerospike.sample.model.Person" | 1678911088715 | 2591970 | 1 |
+---------+-----------+----------+-------------------------------------+---------------+---------+-------+
1 row in set (0.001 secs)

The version field will be mapped to the generation field of the record, shown here by turning on set record_print_metadata true.

Note that if using the AerospikeTemplate bean explicitly and Check-and-Set semantics are required, the save method must be used rather than the persist method.

Manual Reading Of Records

In some rare circumstances, it may be necessary to convert an Aerospike record into a business object. For example, there may be a need to do a complex filter expression to retrieve the correct record. (Something like an Account where creditLimit - exposureAmount < purchaseAmount).

The first step to achieve this is to get an instance of a MappingAerospikeConverter:

@Autowired
private MappingAerospikeConverter mappingConverter;

Next, the record needs to be read from Aerospike, and this requires an IAerospikeClient. This is exposed on the AerospikeTemplate, so this should be @Autowired as shown above.

IAerospikeClient client = aerospikeTemplate.getAerospikeClient();
Policy policy = new Policy(client.getReadPolicyDefault());
policy.filterExp = Exp.build(
Exp.gt(
Exp.sub(Exp.intBin("creditLimit"), Exp.intBin("exposure")),
Exp.val(purchaseAmount)
));
Key key = new Key(aerospikeTemplate.getNamespace(), aerospikeTemplate.getSetName(Account.class), "1");
Record record = aerospikeTemplate.getAerospikeClient().get(policy, key);

(Note that since Java 16 java.lang.Record typically comes in when a Java program refers to a Record. The Record in this example is referring to com.aerospike.client.Record)

Once the record has been read, the MappingAerospikeConverter can be used in conjunction with an AerospikeReadData instance:

AerospikeReadData readData = AerospikeReadData.forRead(key, record);
Account account = mappingConverter.read(Account.class, readData);