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);