Aerospike Vector opens new approaches to AI-driven recommendationsWebinar registration
Blog

Exploring new features in the Spring Data Aerospike 4.6.0 release

Untitled
Andrey Grosland
Software Engineer
February 20, 2024|7 min read

The latest release of Spring Data Aerospike, version 4.6.0, brings a host of exciting new features and enhancements. This blog will delve into three key highlights: support for native Aerospike keys, using paginated queries, and the ability to create custom queries. For further insights into the new capabilities, such as using CRUD with explicit set names and batch write operations, be sure to read our accompanying blog, Announcing the Spring Data Aerospike 4.6.0 release.

Supporting native Aerospike keys

Aerospike Database requires that you have an id field for all objects. This field can be of any primitive type as well as String or byte[].

By default, the id field type is turned into a String to be stored in the Aerospike Database. Spring Data Aerospike 4.6.0 introduces a new configuration parameter called keepOriginalKeyTypes with the value false by default:

// Define how @Id fields (primary keys) and Map keys are stored: false - always as String,
// true - preserve original type if supported
boolean keepOriginalKeyTypes = false;

When the parameter is set to true, id fields of type long (int will also be stored as long) and byte[] will be persisted as is.

If the original type cannot be persisted, it must be convertible to String to be stored in the database as such, then converted back to the original type when the object is read. This is transparent to the application but must be considered if external tools like AQL are used to view the data.

Paginated queries

When dealing with large amounts of data, we can break it down into smaller chunks to access a certain portion. We should also first sort the data based on certain criteria. Let’s see how we can do that with Spring Data Aerospike.

Let’s say we have a Person class:

@Document
public class Person {
    @Id
    private String id;
    private String name;
    private int favouriteNumber;
}

For accessing Persons we have a PersonRepository with a method for querying by name:

public interface ProductRepository<Person> extends AerospikeRepository<Person, String> {
    List<Person> findByName(double price, Pageable pageable);
}

Let’s assume we have a number of Person objects saved to the Aerospike Database, and we need to access the first page of Persons with a size of 10.

Using findAll()

The generic way to query all entities with pagination and/or sorting is to use the findAll() method inherited from PagingAndSortingRepository:

Page<T> findAll(Pageable pageable);

We can use it straight away, specifying the required parameters (the first page with the size of 10):

@Autowired PersonRepository repository;
@Test
public void readFirstPageCorrectly() {
     // The first argument 0 means offset number, 10 is the page size
    Page<Person> result = repository.findAll(PageRequest.of(0, 10));
    assertTrue(resultPage.isFirst());
}

Using PageRequest.of() allows us to specify sorting by one or more properties:

Page<Person> result = repository.findAll(PageRequest.of(0, 10, Sort.Direction.ASC, “name”);
Page<Person> result = repository.findAll(PageRequest.of(0, 10, Sort.Direction.ASC, “name”, “favouriteNumber”);

Defining a paginated query

Let’s narrow down the criteria and say that we need to find Persons with the name “John” and access the first page (with the same page size of 10). To achieve this, let’s define a new method in PersonRepository:

Page<Person> findByName(String name, Pageable pageable);

The main difference compared with findAll() is that we query by one or more specific criteria (in this case, it is the name).

After the method is defined, using it is simple:

Page<Person> result = repository.findByName(“John”, PageRequest.of(0, 10));
assertTrue(resultPage.isFirst());

In real-world scenarios, paginated queries can be more complex and include different criteria.

It is also worth noting that paginated queries in Spring Data can also return Slice.

Page<Person> findByFavouriteNumberGreaterThan(int number, Pageable pageable);

Slice<Person> findTop3ByNameStartingWith(String name, Pageable pageRequest);

Mandatory sorting for non-zero offset

It is mandatory to specify sorting if the given offset is bigger than zero:

// offset here is equal to zero: returning the first page with the size of 2
Page<Person> firstPage = repository.findByFavouriteNumber(10, PageRequest.of(0, 2));
// findAll(), offset here is equal to one: the second page with the size of 2
assertThatThrownBy(() -> repository.findAll(PageRequest.of(1, 2)))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("Unsorted query must not have offset value. For retrieving paged results use sorted query.");
// user-defined query, offset here is equal to two: the second page with the size of 2
assertThatThrownBy(() -> repository.findByFavouriteNumber(10, PageRequest.of(2, 2)))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("Unsorted query must not have offset value. For retrieving paged results use sorted query.");
// query with non-zero offset works when sorting is specified
Page<Person> firstPage = repository.findByFavouriteNumber(10, PageRequest.of(1, 2, Sort.Direction.ASC, “favouriteNumber”));

Pagination and sorting: A word of caution

While using paginated and sorted queries has its advantages, it also comes at a cost, especially with larger data sets. For example, consider a query to get the top 10 records sorted by name in a data set of millions of rows. This would require retrieving all the records from the client side to do the sorting and then returning the top 10. Ultimately, this would result in longer processing time and higher network traffic; hence, it should be used only when necessary.

Custom queries

By creating interfaces that extend AerospikeRepository, we can utilize Spring’s mechanism of defining queries as methods. Such query methods serve various use cases and can be created using different criteria. However, regular query methods have their limits regarding readability and maintainability.

Here are a couple of examples to illustrate:

Page<Person> findByAgeBetweenAndLastNameAndFirstNameStartingWithOrderByAgeAsc(int from, int to, String lastName, String firstnamePrefix, Pageable pageable);
List<Person> findByFriendBestFriendAddressZipCode(String zipCode);

Forming such cumbersome and hard-to-read methods is not optimal, to say the least. To solve the issue, we will see how to build custom queries in Spring Data Aerospike in a readable manner with the help of the “find using query” mechanism.

Let’s consider the following complex query: name must be equal to “John” or “Jack,” primary key must be “key1” or “key2”, and the record’s last update time must be after midnight (12 a.m.) on January the 10th, 2023.

We will build separate Qualifiers, which will be combined into a Query. The query will then be given to a findUsingQuery() method.

// create an expression "name is equal to John"
Qualifier nameEqJohn = Qualifier.builder()
        .setField("name")
        .setFilterOperation(FilterOperation.EQ)
        .setValue1(Value.get("John"))
    .build();

// create an expression "name is equal to Jack"
Qualifier nameEqJack = Qualifier.builder()
        .setField("name")
        .setFilterOperation(FilterOperation.EQ)
        .setValue1(Value.get("Jack"))
    .build();
// create an expression "primary key is equal to key1"
Qualifier keyEqKey1 = Qualifier.idEquals(“key1”);
// create an expression "primary key is equal to key2"
Qualifier keyEqKey2 = Qualifier.idEquals(“key2”);
// creating an expression "last_update_time metadata value is after 12 am January 10th 2023"
Qualifier lastUpdateTimeAfter2023Jan10 = Qualifier.metadataBuilder()
    .setMetadataField(LAST_UPDATE_TIME)
    .setFilterOperation(FilterOperation.GT)
    // Epoch timestamp in milliseconds: 10 Jan 2023 00:00:00 GMT
    .setValue1AsObj(1673308800000)
    .build();
// name expressions are combined using OR
Qualifier name = Qualifier.or(firstNameEqJohn, firstNameEqJack);
// primary key expressions are combined using OR
Qualifier key = Qualifier.or(keyEqKey1, keyEqKey2);
// expressions are combined using AND
List<Person> result = repository.findUsingQuery(
    new Query(Qualifier.and(name, key, lastUpdateTimeAfter2023Jan10))
);
Person john = new Person(“key1”, “John”);
assertThat(result).containsOnly(john);

As we can see, the created query contains different types of criteria (field, id, metadata) combined with logical conjunctions (OR, AND). Such a query is both scalable (as it consists of qualifiers that can be added or changed) and readable (as qualifiers’ names are fully defined by the user).

Learn more about Spring Data Aerospike 4.6.0

To delve deeper into the Spring Data Aerospike project, explore our GitHub page, where you can find comprehensive information. Remember to check out the release notes for Spring Data Aerospike 4.6.0 for additional insights.