Spring Data Aerospike — Projections
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 casePerson
— 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: