Spring Data Aerospike — Projections

Roi Menashe
Aerospike Developer Blog
3 min readOct 31, 2022
Photo by Vinicius “amnx” Amano on Unsplash

When using Spring Data Aerospike as a persistence layer — the operation responses usually contains 1 or more elements that each resembles an entire Aerospike record including all of its bins. In many cases we only need some particular fields and it doesn’t make sense to fetch all bins from Aerospike.

Projections allow you to fetch only relevant fields from Aerospike when using Spring Data Aerospike, which results in better performance, less network traffic and better understanding of what is required for the rest of the flow.

Spring Data Aerospike supports Projections stating at version 3.5.0.

Models

Person

@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Document
public class Person {

public enum Sex {
MALE, FEMALE
}

private @Id String id;
private String firstName;
private String lastName;
private int age;
private int waist;
private Sex sex;
private Map<String, String> map;
private Person friend;
private boolean active;
private Date dateOfBirth;
private List<String> strings;
private List<Integer> ints;
@Field("email")
private String emailAddress;

public Person(String id, String firstName) {
this.id = id;
this.firstName = firstName;
}

public Person(String id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}

public Person(String id, String firstName, int age) {
this.id = id;
this.firstName = firstName;
this.age = age;
}

public PersonSomeFields toPersonSomeFields() {
return PersonSomeFields.builder()
.firstName(getFirstName())
.lastName(getLastName())
.emailAddress(getEmailAddress())
.build();
}
}

PersonSomeFields

@Data
@Builder
public class PersonSomeFields {
private String firstName;
private String lastName;
@Field("email")
private String emailAddress;
}

AerospikeRepository Projections

You can use AerospikeRepository or ReactiveAerospikeRepository to define queries on the custom PersonRepository and get a PersonSomeFields object as a response.

Repository setup:

public interface PersonRepository extends AerospikeRepository<Person, String> {

List<Person> findByLastName(String lastName);

// DTO Projection
List<PersonSomeFields> findPersonSomeFieldsByLastName(String lastName);

// Dynamic Projection
<T> List<T> findByLastName(String lastName, Class<T> type);
}

DTO Projections

You can create a DTO class with some specific fields of Person class, for example PersonSomeFields contains firstName , lastName and emailAddress .

Notes:

  • If @Field annotation is used on a field in the class that represents an Aerospike record in our case Person — then we need to use @Field annotation with the same value (“email”) on the DTO class in order to recognize the field when reading from Aerospike.
  • If the DTO contains a field that doesn’t exists in the class that represents an Aerospike record — Person , queries will return null for that field.

Tests:

@Test
public void findPersonsSomeFieldsByLastnameProjection() {
List<PersonSomeFields> result = repository.findPersonSomeFieldsByLastName("Beauford");

assertThat(result)
.hasSize(1)
.containsOnly(carter.toPersonSomeFields());
}

Dynamic Projections

An alternative to DTO Projections is to define the query in the repository using generics. The class type of which to map the query results into will be determined by a given parameter.

Tests:

@Test
public void findDynamicTypeByLastnameDynamicProjection() {
List<PersonSomeFields> result = repository.findByLastName("Beauford", PersonSomeFields.class);

assertThat(result)
.hasSize(1)
.containsOnly(carter.toPersonSomeFields());
}

AerospikeOperations (AerospikeTemplate) Projections

For more flexibility you can use the AerospikeOperations interface that the AerospikeRepository is using behind the scenes.

You can pass the targetClass parameter in find supported methods, for example:

Tests:

findById() with Projection test

@Test
public void findByIdWithProjection() {
Person firstPerson = Person.builder()
.id(nextId())
.firstName("first")
.lastName("lastName1")
.emailAddress("gmail.com")
.age(40)
.build();
Person secondPerson = Person.builder()
.id(nextId())
.firstName("second")
.lastName("lastName2")
.emailAddress("gmail.com")
.age(50)
.build();
template.save(firstPerson);
template.save(secondPerson);

PersonSomeFields result = template.findById(firstPerson.getId(), Person.class, PersonSomeFields.class);

assertThat(result.getFirstName()).isEqualTo("first");
assertThat(result.getLastName()).isEqualTo("lastName1");
assertThat(result.getEmailAddress()).isEqualTo("gmail.com");
}

find() query with Projection test

@Test
public void findWithFilterEqualProjection() {
Qualifier.QualifierBuilder firstNameQualifier = new Qualifier.QualifierBuilder()
.setField("firstName")
.setValue1(Value.get("Dave"))
.setFilterOperation(FilterOperation.EQ);

Query query = new Query(new AerospikeCriteria(firstNameQualifier));

Stream<PersonSomeFields> result = template.find(query, Person.class, PersonSomeFields.class);

assertThat(result).containsOnly(PersonSomeFields.builder()
.firstName("Dave")
.lastName("Matthews")
.emailAddress("dave@gmail.com")
.build());
}

findAll() query with Projection test

@Test
public void findAll_findsAllExistingDocumentsProjection() {
Stream<PersonSomeFields> result = template.findAll(Person.class, PersonSomeFields.class);

assertThat(result).containsAll(all.stream().map(Person::toPersonSomeFields).collect(Collectors.toList()));
}

Summary

Projections are also supported for reactive repositories and reactive template — the concept and interfaces are similar.

For more information, tests and examples (for both reactive and non-reactive flows) check out the spring-data-aerospike repository on GitHub, you are also welcome to open a GitHub issue if you encounter any problem or if you want to suggest a new feature request:

--

--