In this section we’ll cover setup of the project from the start.
First, we’ll need to set up the basic project structure. You can do it either manually if you are familiar with the process or do it via Spring Initializr.
In Spring Initializr you’ll need to select:
-
Project: Maven project
-
Language: Java
-
Spring Boot: 3.3.0
-
Java: 17
Press Generate button in the bottom of the page, and you’ll get zip with initial project. Import project into your favorite IDE and run the test. It should be green and that means that you are ready to continue to the next section.
Note
|
Spring Boot compatibility can be
checked for the version of |
In our case we are going to use spring-data-aerospike
4.8.0 as we have Spring Boot 3.3.0:
<dependency>
<groupId>com.aerospike</groupId>
<artifactId>spring-data-aerospike</artifactId>
</dependency>
We will be using Lombok library to omit the boilerplate code in the entity class.
Add lombok
into your pom.xml
:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
We are going to use the movies domain in this project, so to be able to read and save our movies into Aerospike we will need to create:
-
document(entity) class that represents our domain model,
-
repository class that provides CRUD operations.
Movie document class will look the following way:
package com.example.demo.persistence.simplecrud;
import com.aerospike.client.query.IndexCollectionType;
import com.aerospike.client.query.IndexType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.experimental.NonFinal;
import org.springframework.data.aerospike.annotation.Indexed;
import org.springframework.data.aerospike.mapping.Document;
import org.springframework.data.aerospike.mapping.Field;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
@Value // (1)
@Document(collection = "demo-service-movies") // (2)
@Builder(toBuilder = true) // (3)
// Spring Data object creation can use all-args constructor instead of reflection which is much faster
@AllArgsConstructor // (4)
public class MovieDocument {
@Id // (5)
String id;
@Field // (6)
String name;
@Field("desc") // (7)
String description;
@Indexed(type = IndexType.NUMERIC, collectionType = IndexCollectionType.DEFAULT) // (8)
@Field
int likes;
@Field
double rating;
@Version // (9)
@NonFinal
long version;
}
Document explained:
-
@Value
makes class immutable, all fields are made private and final,toString()
,equals()
,hashCode()
, field getters and all args constructor are generated. -
@Document(collection = "demo-service-movies")
marks a class as an entity to be persisted to Aerospike. It also allows to specify set name, expiration and touch on read values. In current example custom set name is specified viacollection
. Please note that a set name cannot contain the ':' or ';' characters. (See more limitations) -
@Builder
provide Builder API for a class. -
@AllArgsConstructor
creates public all-args constructor for a class (which is hidden by@Builder
). Spring Data can use all-args constructor instead of reflection to gain performance boost for object creation. -
@Id
marks a field as the primary key. -
@Field
is optional, can be set just for the clarity purpose. -
@Field("desc")
configures the name of a field to be used when persisting the document. This value will be used as a bin name instead of the original field name. -
@Indexed
creates secondary index on application startup for the specific field. Note:@Indexed
annotation is not supported for the fields annotated with@Id
,@Expiration
or@Version
annotations. -
@Version
enables optimistic locking, so that concurrent updates are not lost when saving an entity. Note that if concurrent update fails —OptimisticLockingFailureException
is thrown.
Note
|
Aerospike has limitation on the bin name length.
If your document contains field with name that exceeds this limit, specify short name in @Field("shortName") String veryLoooongFieldName; |
Create Movie Repository interface:
package com.example.demo.persistence.simplecrud;
import org.springframework.data.repository.CrudRepository;
public interface MovieRepository extends CrudRepository<MovieDocument, String> { // (1)
}
Repository explained:
-
CrudRepository
provides sophisticated CRUD functionality for the entity class.
To configure connection to Aerospike you’ll need to create configuration class that extends AbstractAerospikeDataConfiguration
.
Basic setup requires getHosts()
and namespace()
methods to be implemented,
but you can also override e.g. getClientPolicy()
to specify custom configuration for the Aerospike client,
or customConverters()
to add custom converters.
We are going to use
@ConfigurationProperties
for binding Aerospike configuration properties to POJO. Since connection parameters are required, configuration needs to be validated.
To enable validation add hibernate-validator
dependency to pom.xml
or spring-boot-starter-validation
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Simple configuration will look the following way:
package com.example.demo.persistence.configuration;
import com.aerospike.client.Host;
import com.example.demo.persistence.compositeprimarykey.CommentsKey;
import com.example.demo.persistence.simplecrud.MovieRepository;
import com.example.demo.persistence.customconverters.ArticleDocument;
import com.example.demo.persistence.customconverters.ArticleDocumentConverters;
import com.example.demo.persistence.customconverters.UserDataConverters;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.aerospike.config.AbstractAerospikeDataConfiguration;
import org.springframework.data.aerospike.repository.config.EnableAerospikeRepositories;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import java.util.Collection;
import java.util.List;
@EnableAerospikeRepositories(basePackages = "com.example.demo.persistence")
@EnableConfigurationProperties(AerospikeConfiguration.AerospikeConfigurationProperties.class)
@Configuration
public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
@Autowired
AerospikeConfigurationProperties properties;
@Override
protected Collection<Host> getHosts() {
return Host.parseServiceHosts(properties.getHosts());
}
@Override
protected String nameSpace() {
return properties.getNamespace();
}
// Optional. Only needed when you need custom converters
@Override
protected List<?> customConverters() {
return List.of(
CommentsKey.CommentsKeyToStringConverter.INSTANCE,
CommentsKey.StringToCommentsKeyConverter.INSTANCE,
UserDataConverters.MapToUserDataToConverter.INSTANCE,
UserDataConverters.UserDataToMapConverter.INSTANCE,
ArticleDocumentConverters.AerospikeReadDataToArticleDocumentToConverter.INSTANCE,
new ArticleDocumentConverters.ArticleDocumentToAerospikeWriteDataConverter(properties.getNamespace(), ArticleDocument.SET_NAME)
);
}
@Data
@Validated // add this annotation if you want @ConfigurationProperties to be validated!
@ConfigurationProperties("aerospike")
public static class AerospikeConfigurationProperties {
@NotEmpty
String hosts;
@NotEmpty
String namespace;
}
}
To verify that the repository is actually working let’s add some basic tests:
package com.example.demo;
import com.example.demo.persistence.simplecrud.MovieDocument;
import com.example.demo.persistence.simplecrud.MovieRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
public class MovieRepositoryTests extends DemoApplicationTests {
String id;
MovieDocument movie;
@Autowired
MovieRepository repository;
@BeforeEach
void setUp() {
id = UUID.randomUUID().toString();
movie = MovieDocument.builder()
.id(id)
.name("Inception")
.description("Origin of an idea")
.rating(8.8)
.likes(555_555)
.build();
}
@Test
public void save_savesMovie() {
repository.save(movie);
assertThat(repository.findById(id)).hasValue(movie);
}
@Test
public void exists_returnsTrueIfMovieIsPresent() {
repository.save(movie);
assertThat(repository.existsById(id)).isTrue();
}
@Test
public void deleteById_deletesExistingMovie() {
repository.save(movie);
repository.deleteById(id);
assertThat(repository.findById(id)).isNotPresent();
}
@Test
void deleteById_doesNothingForNonexistingMovie() {
repository.deleteById(id);
}
}
For the test purposes you can use embedded-aerospike
library based on testcontainers
.
Note
|
spring-cloud-starter-bootstrap is required to be present on classpath. If you are not using Spring Cloud you can add spring-cloud-starter-bootstrap with test scope.
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.playtika.testcontainers</groupId>
<artifactId>embedded-aerospike</artifactId>
<scope>test</scope>
</dependency>
This will set up Aerospike container when the test starts.
Add Aerospike configuration to the test resources:
aerospike.hosts=${embedded.aerospike.host}:${embedded.aerospike.port} aerospike.namespace=${embedded.aerospike.namespace}
and run the test. It will be green now.