diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..aaf12092 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +## Maven +target/ + +## Eclipse +.settings/ +.project +.classpath + +## IntelliJ +out/ +.idea/ +.idea_modules/ +*.iml +*.ipr +*.iws + +## OS X +.DS_Store \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..44298bdc --- /dev/null +++ b/index.html @@ -0,0 +1,4681 @@ + + + + + + + +Spring Data Aerospike - Documentation + + + + + +
+
+
+
+

© 2018-2024 The original authors.

+
+
+ + + + + +
+
Note
+
+Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. +
+
+ +
+
+
+

Preface

+
+
+

The Spring Data Aerospike project applies core Spring concepts and provides interface for using Aerospike key-value style data store. We provide a "repository" and a "template" as high-level abstractions for storing and querying data. You will notice similarities to the JDBC support in the Spring Framework.

+
+
+

This chapter provides some basic introduction to Spring and Aerospike, it explains Aerospike concepts and syntax. The rest of the documentation refers to Spring Data Aerospike features and assumes the user is familiar with Aerospike as well as Spring concepts.

+
+
+

Knowing Spring

+
+

Spring Data uses Spring framework’s core functionality, such as the IoC container, type conversion system, DAO exception hierarchy etc. While it is not important to know the Spring APIs, understanding the concepts behind them is. At a minimum, the idea behind IoC should be familiar regardless of IoC container you choose to use.

+
+
+

To learn more about Spring, you can refer to the comprehensive documentation that explains in detail the Spring Framework. There are a lot of articles, blog entries and books on the matter - take a look at the Spring framework documentation reference for more information.

+
+
+
+

Knowing NoSQL and Aerospike

+
+

NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms and patterns (to make things worthwhile even the term itself has multiple meanings). While some principles are common, it is crucial that the user is familiar to some degree with Aerospike key-value store operations that supply the mechanism for associating keys with a set of named values, similar to a row in standard RDBMS terminology. The data layer in Aerospike Database is optimized to store data in solid state drives, RAM, or traditional rotational media. The database indices are stored in RAM for quick availability, and data writes are optimized through large block writes to reduce latency. The software also employs two sub-programs that are codenamed Defragmenter and Evictor. Defragmenter removes data blocks that have been deleted, and Evictor frees RAM space by removing references to expired records.

+
+
+

The jumping off ground for learning about Aerospike is www.aerospike.com. Here is a list of other useful resources:

+
+
+ +
+
+
+

Requirements

+
+

Spring Data Aerospike binaries require JDK level 17.0 and above.

+
+
+

In terms of server, it is required to use at least Aerospike server version 6.1 (recommended to use the latest version when possible).

+
+
+
+

Additional Help Resources

+
+

Learning a new framework is not always straightforward. In this section, we try to provide what we think is an easy-to-follow guide for starting with Spring Data Aerospike module. However, if you encounter issues, or you are just looking for advice, feel free to use one of the links below:

+
+
+
+

Support

+
+

There are a few support options available:

+
+
+

Questions & Answers

+
+

Developers post questions and answers on Stack Overflow. The two key tags to search for related answers to this project are:

+
+ +
+
+

Following Development

+
+

If you encounter a bug or want to suggest an improvement, please create an issue on GitHub.

+
+
+
+
+
+

Reference documentation

+
+

Functionality

+
+
+

Spring Data Aerospike project aims to provide a familiar and consistent Spring-based programming model providing integration with the Aerospike database.

+
+
+

Spring Data Aerospike supports a wide range of features summarized below:

+
+
+
    +
  • +

    Supporting Repository interfaces (out-of-the-box CRUD operations and query implementations, for more information see Aerospike Repositories)

    +
  • +
  • +

    AerospikeTemplate for lower-level access to common Aerospike operations and fine-tuning (for more information see AerospikeTemplate)

    +
  • +
  • +

    Feature Rich Object Mapping integrated with Spring’s Conversion Service

    +
  • +
  • +

    Translating exceptions into Spring’s Data Access Exception hierarchy

    +
  • +
  • +

    Annotation-based metadata mapping

    +
  • +
  • +

    Ability to directly utilize Aerospike Java client functionality

    +
  • +
+
+
+
+
+

Installation & Usage

+
+
+

Getting Started

+
+

First, you need a running Aerospike server to connect to.

+
+
+

To use Spring Data Aerospike you can either set up Spring Boot or Spring application. Basic setup of Spring Boot application is described here: https://projects.spring.io/spring-boot.

+
+
+

In case you do not want to use Spring Boot, the best way to manage Spring dependencies is to declare spring-framework-bom of the needed version in the dependencyManagement section of your pom.xml:

+
+
+
+
<dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-framework-bom</artifactId>
+            <version>${spring-data-aerospike.version}</version>
+            <type>pom</type>
+            <scope>import</scope>
+        </dependency>
+    </dependencies>
+</dependencyManagement>
+
+
+
+ + + + + +
+
Note
+
+To create a Spring project in STS (Spring Tool Suite) go to File → New → Spring Template Project → Simple Spring Utility Project → press "Yes" when prompted. Then enter a project and a package name such as org.spring.aerospike.example. +
+
+
+
+

Adding Dependency

+
+

The first step is to add Spring Data Aerospike to your build process. It is recommended to use the latest version which can be found on the GitHub Releases page.

+
+
+

Adding Spring Data Aerospike dependency in Maven:

+
+
+
+
<dependency>
+    <groupId>com.aerospike</groupId>
+    <artifactId>spring-data-aerospike</artifactId>
+    <version>${spring-data-aerospike.version}</version>
+</dependency>
+
+
+
+

Adding Spring Data Aerospike dependency in Gradle:

+
+
+
+
implementation group: 'com.aerospike', name: 'spring-data-aerospike', version: '${spring-data-aerospike.version}'
+
+
+
+
+

Connecting to Aerospike DB

+
+

There are two ways of configuring a basic connection to Aerospike DB.

+
+
+
    +
  • +

    Overriding getHosts() and nameSpace() methods of the AbstractAerospikeDataConfiguration class:

    +
  • +
+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = { PersonRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("localhost", 3000));
+    }
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+}
+
+
+
+
    +
  • +

    Using application.properties:

    +
  • +
+
+
+

Basic configuration in this case requires enabling repositories and then setting hosts and namespace in the application.properties file.

+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = { PersonRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+
+}
+
+
+
+

In application.properties:

+
+
+
+
# application.properties
+spring-data-aerospike.hosts=localhost:3000
+spring-data-aerospike.namespace=test
+
+
+
+ + + + + +
+
Note
+
+Return values of getHosts() and nameSpace() methods of the AbstractAerospikeDataConfiguration class +have precedence over hosts and namespace parameters set via application.properties. +
+
+
+

For more detailed information see Configuration.

+
+
+
+

Creating Functionality

+
+

The base functionality is provided by AerospikeRepository interface.

+
+
+

It typically takes 2 parameters:

+
+
+
    +
  1. +

    The type managed by a class (it is typically entity class) to be stored in the database.

    +
  2. +
  3. +

    The type of ID.

    +
  4. +
+
+
+

Application code typically extends this interface for each of the types to be managed, and methods can be added to the interface to determine how the application can access the data. For example, consider a class Person with a simple structure:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    @Id
+    private long id;
+    private String firstName;
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+}
+
+
+
+

Note that this example uses the Project Lombok annotations to remove the need for explicit constructors and getters and setters. Normal POJOs which define these on their own can ignore the @AllArgsConstructor, @NoArgsConstructor and @Data annotations. The @Document annotation tells Spring Data Aerospike that this is a domain object to be persisted in the database, and @Id identifies the primary key of this class. The @Field annotation is used to create a shorter name for the bin in the Aerospike database (dateOfBirth will be stored in a bin called dob in this example).

+
+
+

For the Person object to be persisted to Aerospike, you must create an interface with the desired methods for retrieving data. For example:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    List<Person> findByLastName(String lastName);
+}
+
+
+
+

This defines a repository that can write Person entities and also query them by last name. The AerospikeRepository extends both PagingAndSortingRepository and CrudRepository, so methods like count(), findById(), save() and delete() are there by default. Those who need reactive flow can use ReactiveAerospikeRepository instead.

+
+
+ + + + + +
+
Note
+
+Repository is just an interface and not an actual class. In the background, when your context gets initialized, actual implementations for your repository descriptions get created, and you can access them through regular beans. This means you will omit lots of boilerplate code while still exposing full CRUD semantics to your service layer and application. +
+
+
+

Example repository is ready for use. A sample Spring Controller which uses this repository could be the following:

+
+
+
+
@RestController
+public class ApplicationController {
+    @Autowired
+    private PersonRepository personRepsitory;
+
+    @GetMapping("/seed")
+    public int seedData() {
+        Person person = new Person(1, "Bob", "Jones", new GregorianCalendar(1971, 12, 19).getTime());
+        personRepsitory.save(person);
+        return 1;
+    }
+
+    @GetMapping("/findByLastName/{lastName}")
+    public List<Person> findByLastName(@PathVariable(name = "lastName", required=true) String lastName) {
+        return personRepsitory.findByLastName(lastName);
+    }
+}
+
+
+
+

Invoking the seed method above gives you a record in the Aerospike database which looks like:

+
+
+
+
aql> select * from test.Person where pk = "1"
++-----+-----------+----------+-------------+-------------------------------------+
+| PK  | firstName | lastName | dob         | @_class                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "Bob"     | "Jones"  | 64652400000 | "com.aerospike.sample.model.Person" |
++-----+-----------+----------+-------------+-------------------------------------+
+1 row in set (0.001 secs)
+
+
+
+ + + + + +
+
Note
+
+The fully qualified path of the class is listed in each record. This is needed to instantiate the class correctly, especially in cases when the compile-time type and runtime type of the object differ. For example, where a field is declared as a super class but the instantiated class is a subclass. +
+
+
+ + + + + +
+
Note
+
+By default, the type of the field annotated with @id is turned into a String to be stored in Aerospike database. If the original type cannot be persisted (see keepOriginalKeyTypes for details), it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data. +
+
+
+
+
+
+

Aerospike repositories

+
+
+

Introduction

+
+

One of the main goals of the Spring Data is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores.

+
+
+

One of the core interfaces of Spring Data is Repository. +This interface acts primarily to capture the types to work with and to help user to discover interfaces that extend Repository.

+
+
+

In other words, it allows user to have basic and complicated queries without writing the implementation. This builds on the Core Spring Data Repository Support, so make sure you’ve got a sound understanding of this concept.

+
+
+
+

Usage

+
+

To access entities stored in Aerospike you can leverage repository support that eases implementing those quite significantly. To do so, simply create an interface for your repository:

+
+
+
Example 1. Sample Person entity
+
+
+
+
public class Person {
+
+	@Id
+	private String id;
+	private String name;
+	private int age;
+	public Person(String id, String name, int age) {
+		this.id = id;
+		this.name = name;
+		this.age = age;
+	}
+  // … getters and setters omitted
+}
+
+
+
+
+
+

We have a quite simple domain object here. The default serialization mechanism used in AerospikeTemplate (which is backing the repository support) regards properties named "id" as document id. Currently we support String and long as id-types.

+
+
+
Example 2. Basic repository interface to persist Person entities
+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, String> {
+
+	List<Person> findByName(String name);
+
+	List<Person> findByNameStartsWith(String prefix);
+
+}
+
+
+
+
+
+

Right now this interface simply serves typing purposes, but we will add additional methods to it later. In your Spring configuration simply add

+
+
+
Example 3. General Aerospike repository Spring configuration
+
+
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:aerospike="http://www.springframework.org/schema/data/aerospike"
+  xsi:schemaLocation="http://www.springframework.org/schema/beans
+    https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+    http://www.springframework.org/schema/data/aerospike
+    https://www.springframework.org/schema/data/aerospike/spring-aerospike-1.0.xsd">
+
+  <aerospike:aerospike id="aerospike" />
+
+  <bean id="aerospikeTemplate" class="rg.springframework.data.aerospike.core.AerospikeTemplate">
+ </bean>
+
+  <aerospike:repositories base-package="org.springframework.data.aerospike.example.data" />
+
+</beans>
+
+
+
+
+
+

This namespace element will cause the base packages to be scanned for interfaces extending AerospikeRepository and create Spring beans for each of them found. By default, the repositories will get an AerospikeTemplate Spring bean wired that is called aerospikeTemplate.

+
+
+

If you’d rather like to go with JavaConfig use the @EnableAerospikeRepositories annotation. The annotation carries the very same attributes like the namespace element. If no base package is configured the infrastructure will scan the package of the annotated configuration class.

+
+
+
Example 4. JavaConfig for repositories
+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackages = "org.springframework.data.aerospike.example")
+public class TestRepositoryConfig {
+	public @Bean(destroyMethod = "close") AerospikeClient aerospikeClient() {
+
+		ClientPolicy policy = new ClientPolicy();
+		policy.failIfNotConnected = true;
+
+		return new AerospikeClient(policy, "52.23.205.208", 3000);
+	}
+
+	public @Bean AerospikeTemplate aerospikeTemplate() {
+		return new AerospikeTemplate(aerospikeClient(), "test");
+	}
+}
+
+
+
+
+
+

As our domain repository extends PagingAndSortingRepository it provides you with CRUD operations as well as methods for paginated and sorted access to the entities. Working with the repository instance is just a matter of dependency injecting it into a client. So accessing the second page of `Person`s at a page size of 10 would simply look something like this:

+
+
+
Example 5. Paging access to Person entities
+
+
+
+
@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class PersonRepositoryTests {
+
+    @Autowired PersonRepository repository;
+
+    @Test
+    public void readsFirstPageCorrectly() {
+
+      Page<Person> persons = repository.findAll(new PageRequest(0, 10));
+      assertThat(persons.isFirstPage(), is(true));
+    }
+}
+
+
+
+
+
+

The sample creates an application context with Spring’s unit test support which will perform annotation-based dependency injection into test cases. Inside the test method, we simply use the repository to query the datastore. We hand the repository a PageRequest instance that requests the first page of persons at a page size of 10.

+
+
+
+

Query methods

+
+

Most of the data access operations you usually trigger on a repository result in a query being executed against the Aerospike databases. Defining such a query is just a matter of declaring a method on the repository interface

+
+
+
Example 6. PersonRepository with query methods
+
+
+
+
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {
+
+    List<Person> findByName(String name);                              (1)
+
+    Page<Person> findByName(String name, Pageable pageable);           (2)
+
+    List<Person> findByNameStartsWith(String prefix);                  (3)
+
+ }
+
+
+
+
    +
  1. +

    The method shows a query for all people with the given name. The query will be derived by parsing the method name for constraints that can be concatenated with And and Or.

    +
  2. +
  3. +

    Applies pagination to a query. Just equip your method signature with a Pageable parameter and let the method return a Page instance, and it will automatically page the query accordingly (i.e. return the required part of results).

    +
  4. +
  5. +

    Uses query-based partial name search.

    +
  6. +
+
+
+
+
+

Here’s a delete insert and query example

+
+
+
+
@ContextConfiguration(classes = TestRepositoryConfig.class)
+public class RepositoryExample {
+
+	@Autowired
+	protected PersonRepository repository;
+	@Autowired
+	AerospikeOperations aerospikeOperations;
+	@Autowired
+	AerospikeClient client;
+
+	public RepositoryExample(ApplicationContext ctx) {
+		aerospikeOperations = ctx.getBean(AerospikeTemplate.class);
+		repository = (PersonRepository) ctx.getBean("personRepository");
+		client = ctx.getBean(AerospikeClient.class);
+	}
+
+	protected void setUp() {
+		repository.deleteAll();
+		Person dave = new Person("Dave-01", "Matthews", 42);
+		Person donny = new Person("Dave-02", "Macintire", 39);
+		Person oliver = new Person("Oliver-01", "Matthews", 4);
+		Person carter = new Person("Carter-01", "Beauford", 49);
+		Person boyd = new Person("Boyd-01", "Tinsley", 45);
+		Person stefan = new Person("Stefan-01", "Lessard", 34);
+		Person leroi = new Person("Leroi-01", "Moore", 41);
+		Person leroi2 = new Person("Leroi-02", "Moore", 25);
+		Person alicia = new Person("Alicia-01", "Keys", 30);
+		repository.createIndex(Person.class, "person_name_index", "name",
+				IndexType.STRING);
+		List<Person> all = (List<Person>) repository.save(Arrays.asList(oliver,
+				dave, donny, carter, boyd, stefan, leroi, leroi2, alicia));
+	}
+
+	protected void cleanUp() {
+		repository.deleteAll();
+	}
+
+	protected void executeRepositoryCall() {
+		List<Person> result = repository.findByName("Beauford");
+		System.out.println("Results for exact match of 'Beauford'");
+		for (Person person : result) {
+			System.out.println(person.toString());
+		}
+		System.out.println("Results for name starting with letter 'M'");
+		List<Person> resultPartial = repository.findByNameStartsWith("M");
+		for (Person person : resultPartial) {
+			System.out.println(person.toString());
+		}
+	}
+
+	public static void main(String[] args) {
+		ApplicationContext ctx = new AnnotationConfigApplicationContext(
+				TestRepositoryConfig.class);
+		RepositoryExample repositoryExample = new RepositoryExample(ctx);
+		repositoryExample.setUp();
+		repositoryExample.executeRepositoryCall();
+		repositoryExample.cleanUp();
+	}
+}
+
+
+
+
+
+
+

Reactive Aerospike repositories

+
+
+

Introduction

+
+

This chapter will point out the specialties for reactive repository support for Aerospike. This builds on the core repository support explained in repositories. So make sure you’ve got a sound understanding of the basic concepts explained there.

+
+
+
+

Reactive Composition Libraries

+
+

The reactive space offers various reactive composition libraries. The most common library is Project Reactor.

+
+
+

Spring Data Aerospike is built on top of the Aerospike Reactor Java Client Library, to provide maximal interoperability by relying on the Reactive Streams initiative. Static APIs, such as ReactiveAerospikeOperations, are provided by using Project Reactor’s Flux and Mono types. Project Reactor offers various adapters to convert reactive wrapper types (Flux to Observable and vice versa).

+
+
+

Spring Data’s Repository abstraction is a dynamic API, mostly defined by you and your requirements as you declare query methods. Reactive Aerospike repositories can be implemented by using Project Reactor wrapper types by extending from the following library-specific repository interface:

+
+
+
    +
  • +

    ReactiveAerospikeRepository

    +
  • +
+
+
+
+

Usage

+
+

To access domain entities stored in an Aerospike you can use our sophisticated repository support that eases implementing those quite significantly. To do so, create an interface similar to your repository. Before you can do that, though, you need an entity, such as the entity defined in the following example:

+
+
+
Example 7. Sample Person entity
+
+
+
+
public class Person {
+
+  @Id
+  private String id;
+  private String firstname;
+  private String lastname;
+  private Address address;
+
+  // … getters and setters omitted
+}
+
+
+
+
+
+

We have a quite simple domain object here. The default serialization mechanism used in ReactiveAerospikeTemplate (which is backing the repository support) regards properties named id as document ID. Currently, we support String and long as id-types. The following example shows how to create an interface that defines queries against the Person object from the preceding example:

+
+
+
Example 8. Basic repository interface to persist Person entities
+
+
+
+
public interface ReactivePersonRepository extends ReactiveAerospikeRepository<Person, String> {
+
+  Flux<Person> findByFirstname(String firstname);
+
+}
+
+
+
+
+
+

Right now this interface simply serves typing purposes but we will add additional methods to it later.

+
+
+

For Java configuration, use the @EnableReactiveAerospikeRepositories annotation. The annotation carries the base +packages attribute. These base packages are to be scanned for interfaces extending ReactiveAerospikeRepository +and create Spring beans for each of them found. If no base package is configured, the infrastructure scans the package +of the annotated configuration class.

+
+
+

The following listing shows how to use Java configuration for a repository:

+
+
+
Example 9. Java configuration for repositories
+
+
+
+
@Configuration
+@EnableReactiveAerospikeRepositories(basePackages = "org.springframework.data.aerospike.example")
+public class TestRepositoryConfig extends AbstractReactiveAerospikeDataConfiguration {
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("52.23.205.208", 3000));
+    }
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+
+    @Override
+    protected EventLoops eventLoops() {
+        return new NioEventLoops();
+    }
+}
+
+
+
+
+
+

As our domain repository extends ReactiveAerospikeRepository it provides you with CRUD operations. Working with the repository instance is a matter of dependency injecting it into a client, as the following example shows:

+
+
+
Example 10. Access to Person entities
+
+
+
+
@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class PersonRepositoryTests {
+    @Autowired
+    ReactivePersonRepository repository;
+
+    @Test
+    public void findByFirstnameCorrectly() {
+      Flux<Person> persons = repository.findByFirstname("TestFirstName");
+    }
+}
+
+
+
+
+
+

The sample creates an application context with Spring’s unit test support which will perform annotation-based dependency injection into test cases. Inside the test method, we simply use the repository to query the datastore.

+
+
+
+

Query methods

+
+

Most of the data access operations you usually trigger on a repository result in a query being executed against the Aerospike databases. Defining such a query is just a matter of declaring a method on the repository interface

+
+
+
Example 11. PersonRepository with query methods
+
+
+
+
public interface ReactivePersonRepository extends ReactiveAerospikeRepository<Person, String> {
+
+  Flux<Person> findByFirstname(String firstname);                                   (1)
+
+  Flux<Person> findByFirstname(Publisher<String> firstname);                        (2)
+
+  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);       (3)
+
+  Mono<Person> findFirstByLastname(String lastname);                                (4)
+
+  Flux<Person> findByFirstnameStartsWith(String prefix);                            (5)
+
+}
+
+
+
+
    +
  1. +

    The method shows a query for all people with the given firstname. The query is derived by parsing the method name for constraints that can be concatenated with And and Or. Thus, the method name results in a query expression of {"firstname" : firstname}.

    +
  2. +
  3. +

    The method shows a query for all people with the given firstname once the firstname is emitted by the given Publisher.

    +
  4. +
  5. +

    Find a single entity for the given criteria. It completes with IncorrectResultSizeDataAccessException on non-unique results.

    +
  6. +
  7. +

    Unless <3>, the first entity is always emitted even if the query yields more result documents.

    +
  8. +
  9. +

    The method shows a query for all people with the firstname starts from prefix

    +
  10. +
+
+
+
+
+
+

Examples

+
+

Here’s a delete, insert and query example

+
+
+
+
@ContextConfiguration(classes = TestRepositoryConfig.class)
+public class ReactiveRepositoryExample {
+
+    @Autowired
+    protected ReactivePersonRepository repository;
+    @Autowired
+    ReactiveAerospikeOperations aerospikeOperations;
+    @Autowired
+    IAerospikeReactorClient client;
+
+    public RepositoryExample(ApplicationContext ctx) {
+        aerospikeOperations = ctx.getBean(ReactiveAerospikeTemplate.class);
+        repository = (ReactivePersonRepository) ctx.getBean("reactivePersonRepository");
+        client = ctx.getBean(IAerospikeReactorClient.class);
+    }
+
+    protected void setUp() {
+        // Insert new Person items into repository
+        Person dave = new Person("Dave-01", "Matthews", 42);
+        Person donny = new Person("Dave-02", "Macintire", 39);
+        Person oliver = new Person("Oliver-01", "Matthews", 4);
+        Person carter = new Person("Carter-01", "Beauford", 49);
+        List<Person> all = saveAll(Arrays.asList(dave, donny, oliver, carter))
+            .collectList().block();
+    }
+
+    protected void cleanUp() {
+        // Delete all Person items from repository
+        repository.findAll().flatMap(a -> repository.delete(a)).blockLast();
+    }
+
+    protected void executeRepositoryCall() {
+        System.out.println("Results for first name exact match of 'Dave-02'");
+        repository.findByFirstname("Dave-02")
+            .doOnNext(person -> System.out.println(person.toString())).blockLast();
+
+        System.out.println("Results for first name starting with letter 'D'");
+        repository.findByFirstnameStartsWith("D")
+            .doOnNext(person -> System.out.println(person.toString())).blockLast();
+    }
+
+    public static void main(String[] args) {
+        ApplicationContext ctx =
+            new AnnotationConfigApplicationContext(TestRepositoryConfig.class);
+        ReactiveRepositoryExample repositoryExample = new ReactiveRepositoryExample(ctx);
+        repositoryExample.setUp();
+        repositoryExample.executeRepositoryCall();
+        repositoryExample.cleanUp();
+    }
+}
+
+
+
+
+

Restrictions

+
+

ReactiveAerospikeRepository currently does not support the next operations:

+
+
+
    +
  • +

    all operations with indexes (create, delete, exists)

    +
  • +
  • +

    count()

    +
  • +
  • +

    deleteAll()

    +
  • +
+
+
+

This limitation is due to the lack of corresponding asynchronous methods in the Aerospike client.

+
+
+
+
+
+

Projections with Aerospike

+
+
+

Spring Data Aerospike supports Projections, a mechanism that allows you to fetch only relevant fields from Aerospike for a particular use case. This results in better performance, less network traffic, and a better understanding of what is required for the rest of the flow.

+
+
+

For more details, refer to Spring Data documentation: Projections.

+
+
+

For example, consider a Person class:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    public enum Gender {
+        MALE, FEMALE;
+    }
+    @Id
+    private long id;
+    private String firstName;
+    @Indexed(name = "lastName_idx", type = IndexType.STRING)
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+    private long heightInCm;
+    private boolean enabled;
+    private Gender gender;
+    private String hairColor;
+    private String eyeColor;
+    private String passportNo;
+    private String passptCnty;
+}
+
+
+
+

This is a moderately complex object, and a production object is likely to be more complex. The use case might call for a search box that shows the firstName, lastName and dateOfBirth fields, allowing the user to select a Person based on the criteria upon which the full object will be shown.

+
+
+

A simple projection of this object might be:

+
+
+
+
@Data
+@Builder
+public class SearchPerson {
+    private String firstName;
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+}
+
+
+
+

To tell Spring Data how to create a SearchPerson it is necessary to create a method on the Person class:

+
+
+
+
public SearchPerson toSearchPerson() {
+    return SearchPerson.builder()
+            .firstName(this.getFirstName())
+            .lastName(this.getLastName())
+            .dateOfBirth(this.getDateOfBirth())
+            .build();
+}
+
+
+
+

Now the repository interface can be extended to return this projection:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    public List<Person> findByLastName(String lastName);
+    public List<SearchPerson> findSearchPersonByLastName(String lastName);
+}
+
+
+
+

Notice that the method name now dictates the return type of SearchPerson as well as changing the return value. When this method is executed, Aerospike loads the full Person objects out of storage, invokes the toSearchPerson on each person and returns the resulting SearchPerson instances. This reduces the required network bandwidth to present these objects to the front end and simplifies logic.

+
+
+

A blog post with more details on projections can be found here.

+
+
+
+
+

Query Methods

+
+
+

Spring Data Aerospike supports defining queries by method name in the Repository interface so that the implementation is generated. +The format of method names is fairly flexible, comprising a verb and criteria.

+
+
+

Some of the verbs include find, query, read, get, count and delete. +For example, findByFirstName, countByLastName etc.

+
+
+

For more details, refer to basic Spring Data documentation: Defining Query Methods.

+
+
+

Repository Query Keywords

+
+

Here are the references to the examples of repository queries:

+
+ + +
+

Map

+
+
+

POJO

+
+
+

Id

+
+
+ + + + + +
+
Note
+
+Id repository read queries (like findById(), findByIds(), findByFirstNameAndId(), findAllById(), countById(), existsById() etc.) utilize get() operation of the underlying Java client. Repository read queries without id (like findByFirstName(), findByFirstNameAndLastName(), findAll() etc.) utilize query() operation of the underlying Java client. +
+
+
+
+

Repository Interface Example

+
+

Below is an example of an interface with several query methods:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    List<Person> findByLastName(String lastName);
+    List<Person> findByLastNameContaining(String lastName);
+    List<Person> findByLastNameStartingWith(String lastName);
+    List<Person> findByLastNameAndFirstNameContaining(String lastName, String firstName);
+    List<Person> findByAgeBetween(long startAge, long endAge);
+    Optional<Person> findById(Long id);
+}
+
+
+
+
+
+
+

Simple Property Repository Queries

+
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByLastName(String lastName)
+
+

…​where x.lastName = ?

Not, IsNot

+
+
findByLastNameNot(String lastName)
+
+

…​where x.lastName <> ?

True, isTrue

+
+
findByEnabledTrue()
+
+

…​where x.enabled = true

False, isFalse

+
+
findByEnabledFalse()
+
+

…​where x.enabled = false

In, IsIn

+
+
findByLastNameIn(Collection<String>)
+
+

…​where x.lastName in ?

NotIn, IsNotIn

+
+
findByLastNameNotIn(Collection<String>)
+
+

…​where x.lastName not in ?

Null, IsNull

+
+
findByEmailAddressIsNull()
+
+

…​where x.emailAddress = null or x.emailAddress does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByEmailAddressExists()
+
+
+
+
+
findByEmailAddressNotNull()
+
+

…​where x.emailAddress != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist in AerospikeDB when their value is not equal to null.

LessThan, IsLessThan

+
+
findByAgeLessThan(int age)
+
+findByFirstNameLessThan(String string)
+
+

…​where x.age < ?

+

…​where x.firstName < ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByAgeLessThanEqual(int age)
+
+findByFirstNameLessThanEqual(String string)
+
+

…​where x.age < = ?

+

…​where x.firstName < = ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByAgeGreaterThan(int age)
+
+findByFirstNameGreaterThan(String string)
+
+

…​where x.age > ?

+

…​where x.firstName > ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByAgeGreaterThanEqual(int age
+
+findByFirstNameGreaterThanEqual(String string)
+
+

…​where x.age >= ?

+

…​where x.firstName >= ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

Between, IsBetween

+
+
findByAgeBetween(int lowerLimit, int upperLimit)
+
+findByFirstNameBetween(String lowerLimit, String upperLimit)
+
+

…​where x.age between ? and ?

+

…​where x.firstName between ? and ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

Before, IsBefore

+
+
findByDateOfBirthBefore(Date date)
+
+

…​where x.dateOfBirth < ?

After, IsAfter

+
+
findByDateOfBirthAfter(Date date)
+
+

…​where x.dateOfBirth > ?

StartingWith, IsStartingWith, StartsWith

+
+
findByLastNameStartingWith(String string)
+
+

…​where x.lastName like 'abc%'

EndingWith, IsEndingWith, EndsWith

+
+
findByLastNameEndingWith(String string)
+
+

…​where x.lastName like '%abc'

Like, IsLike, MatchesRegex

+
+
findByLastNameLike(String lastNameRegex)
+
+

…​where x.lastName like ?

Containing, IsContaining, Contains

+
+
findByLastNameContaining(String substring)
+
+

…​where x.lastName like '%abc%'

NotContaining, IsNotContaining, NotContains

+
+
findByLastNameNotContaining(String substring)
+
+

…​where x.lastName not like '%abc%'

And

+
+
findByLastNameAndFirstName(String lastName, String firstName)
+
+

…​where x.lastName = ? and x.firstName = ?

Or

+
+
findByLastNameOrFirstName(String lastName, String firstName)
+
+

…​where x.lastName = ? or x.firstName = ?

+
+
+
+

Collection Repository Queries

+
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByStringList(Collection<String> stringList)
+
+

…​where x.stringList = ?

Not, IsNot

+
+
findByStringListNot(Collection<String> stringList)
+
+

…​where x.stringList <> ?

In

+
+
findByStringListIn(Collection<Collection<String>>)
+
+

…​where x.stringList in ?

Find records where stringList bin value equals one of the collections in the given argument.

Not In

+
+
findByStringListNotIn(Collection<Collection<String>>)
+
+

…​where x.stringList not in ?

Find records where stringList bin value is not equal to any of the collections in the given argument.

Null, IsNull

+
+
findByStringListIsNull()
+
+

…​where x.stringList = null or x.stringList does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByStringListExists()
+
+
+
+
+
findByStringListNotNull()
+
+

…​where x.stringList != null

("Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist in AerospikeDB when their value is not equal to null).

LessThan, IsLessThan

+
+
findByStringListLessThan(Collection<String> stringList)
+
+

…​where x.stringList < ?

Find records where stringList bin value has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByStringListLessThanEqual(Collection<String> stringList)
+
+

…​where x.stringList < = ?

Find records where stringList bin value has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByStringListGreaterThan(Collection<String> stringList)
+
+

…​where x.stringList > ?

Find records where stringList bin value has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByStringListGreaterThanEqual(Collection<String> stringList)
+
+

…​where x.stringList >= ?

Find records where stringList bin value has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByStringListBetween(Collection<String> lowerLimit, Collection<String> upperLimit)
+
+

…​where x.stringList between ? and ?

Find records where stringList bin value is in the range between the given arguments. See information about ordering.

Containing, IsContaining, Contains

+
+
findByStringListContaining(String string)
+
+

…​where x.stringList contains ?

NotContaining, IsNotContaining, NotContains

+
+
findByStringListNotContaining(String string)
+
+

…​where x.stringList not contains ?

And

+
+
findByStringListAndIntList(Collection<String> stringList, Collection<Integer> intList)
+
+

…​where x.stringList = ? and x.intList = ?

Or

+
+
findByStringListOrIntList(Collection<String> stringList, Collection<Integer> intList)
+
+

…​where x.stringList = ? or x.intList = ?

+
+
+
+

Map Repository Queries

+
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByStringMap(Map<String, String> stringMap)
+
+

…​where x.stringMap = ?

Not, IsNot

+
+
findByStringMapNot(Map<String, String> stringMap)
+
+

…​where x.stringMap <> ?

In

+
+
findByStringMapIn(Collection<Map<String, String>>)
+
+

…​where x.stringMap in ?

Find records where stringMap bin value equals one of the maps in the given argument.

Not In

+
+
findByStringMapNotIn(Collection<Map<String, String>>)
+
+

…​where x.stringMap not in ?

Find records where stringMap bin value is not equal to any of the maps in the given argument.

Null, IsNull

+
+
findByStringMapIsNull()
+
+

…​where x.stringMap = null or x.stringMap does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByStringMapExists()
+
+
+
+
+
findByStringMapNotNull()
+
+

…​where x.stringMap != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist when their value is not equal to null.

LessThan, IsLessThan

+
+
findByStringMapLessThan(Map<String, String> stringMap)
+
+

…​where x.stringMap < ?

Find records where stringMap bin value has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByStringMapLessThanEqual(Map<String, String> stringMap)
+
+

…​where x.stringMap < = ?

Find records where stringMap bin value has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByStringMapGreaterThan(Map<String, String> stringMap)
+
+

…​where x.stringMap > ?

Find records where stringMap bin value has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByStringMapGreaterThanEqual(Map<String, String> stringMap)
+
+

…​where x.stringMap >= ?

Find records where stringMap bin value has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByStringMapBetween(Map<String, String> lowerLimit, Map<String, String> upperLimit)
+
+

…​where x.stringMap between ? and ?

Find records where stringMap bin value is in the range between the given arguments. See information about ordering.

Containing, IsContaining, Contains

+
+
findByStringMapContaining(AerospikeQueryCriterion criterion, String string)
+
+findByStringMapContaining(AerospikeQueryCriterion criterionPair, String string, String value)
+
+

…​where x.stringMap contains ?

+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains key "key1":

    +
  • +
+
+
+

findByStringMapContaining(KEY, "key1")

+
+
+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains value "value1":

    +
  • +
+
+
+

findByStringMapContaining(VALUE, "value1")

+
+
+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains key "key1" with the value "value1":

    +
  • +
+
+
+

findByStringMapContaining(KEY_VALUE_PAIR, "key1", "value1")

+

NotContaining, IsNotContaining, NotContains

+
+
findByStringNameNotContaining(AerospikeQueryCriterion criterion, String string)
+
+

…​where x.stringMap not contains ?

findByStringMapNotContaining(KEY, "key1")

+

findByStringMapNotContaining(VALUE, "value1")

+

findByStringMapNotContaining(KEY_VALUE_PAIR, "key1", "value1")

And

+
+
findByStringMapAndIntMap(Map<String, String> stringMap, Map<Integer, Integer> intMap)
+
+

…​where x.stringMap = ? and x.intMap = ?

Or

+
+
findByStringMapOrIntMap(Map<String, String> stringMap, Map<Integer, Integer> intList)
+
+

…​where x.stringMap = ? or x.intMap = ?

+
+
+
+

POJO Repository Queries

+
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByAddress(Address address)
+
+

…​where x.address = ?

Not, IsNot

+
+
findByAddressNot(Address address)
+
+

…​where x.address <> ?

In

+
+
findByAddressIn(Collection<Address>)
+
+

…​where x.address in ?

Find records where address bin value equals one of the Address objects in the given argument.

Not In

+
+
findByAddressNotIn(Collection<Address>)
+
+

…​where x.address not in ?

Find records where address bin value is not equal to any of the Address objects in the given argument.

Null, IsNull

+
+
findByAddressIsNull()
+
+

…​where x.address = null or x.address does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByAddressExists()
+
+
+
+
+
findByAddressNotNull()
+
+

…​where x.address != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist when their value is not equal to null.

LessThan, IsLessThan

+
+
findByAddressLessThan(Address address)
+
+

…​where x.address < ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByAddressLessThanEqual(Address address)
+
+

…​where x.address < = ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByAddressGreaterThan(Address address)
+
+

…​where x.address > ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByAddressGreaterThanEqual(Address address)
+
+

…​where x.address >= ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByAddressBetween(Address lowerLimit, Address upperLimit)
+
+

…​where x.address between ? and ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) is in the range between the given arguments. See information about ordering.

And

+
+
findByAddressAndFriend(Address address, Person friend)
+
+

…​where x.address = ? and x.friend = ?

Or

+
+
findByAddressOrFriend(Address address, Person friend)
+
+

…​where x.address = ? or x.friend = ?

+
+
+
+

Id Repository Queries

+
+
+

Id repository reading queries (like findById(), findByIds(), findByFirstNameAndId(), findAllById(), countById(), existsById() etc.) utilize get operation of the underlying Java client (client.get()).

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

no keyword

+
+
findById(String id)
+
+

…​where x.PK = ?

And

+
+
findByIdAndFirstName(String id, String firstName)
+
+

…​where x.PK = ? and x.firstName = ?

+
+
+
+

Combined Query Methods

+
+
+

In Spring Data, complex query methods using And or Or conjunction allow developers to define custom database queries based on method names that combine multiple conditions. These methods leverage query derivation, enabling developers to create expressive and type-safe queries by simply defining method signatures.

+
+
+

For more details, see Defining Query Methods.

+
+
+

For instance, a method like findByFirstNameAndLastName will fetch records matching both conditions, while findByFirstNameOrLastName will return records that match either condition. These query methods simplify database interaction by reducing boilerplate code and relying on convention over configuration for readability and maintainability.

+
+
+

In Spring Data Aerospike you define such queries by adding query methods signatures to a Repository, as you would typically, wrapping each query parameter with QueryParam.of() method. This method is required to pass arguments to each part of a combined query, it can receive one or more objects of the same type.

+
+
+

This way QueryParam stores arguments passed to each part of a combined repository query, e.g., repository.findByNameAndEmail(QueryParam.of("John"), QueryParam.of("email")).

+
+
+

Here are some examples:

+
+
+
+
public interface CustomerRepository extends AerospikeRepository<Customer, String> {
+
+     // simple query
+    List<Customer> findByLastName(String lastName);
+
+     // simple query
+    List<Customer> findByFirstName(String firstName);
+
+    // combined query with AND conjunction
+    List<Customer> findByEmailAndFirstName(QueryParam email, QueryParam firstName);
+
+    // combined query with AND conjunctions
+    List<Customer> findByIdAndFirstNameAndAge(QueryParam id, QueryParam firstName, QueryParam age);
+
+    // combined query with OR conjunction
+    List<Consumer> findByFirstNameOrAge(QueryParam firstName, QueryParam age);
+
+    // combined query with AND and OR conjunctions
+    List<Consumer> findByEmailAndFirstNameOrAge(QueryParam email, QueryParam firstName, QueryParam age);
+}
+
+    @Test
+    void findByCombinedQuery() {
+        QueryParam email = QueryParam.of(dave.getEmail());
+        QueryParam name = QueryParam.of(carter.getFirstName());
+        List<Customer> customers = repository.findByEmailAndFirstName(email, name);
+        assertThat(customers).isEmpty();
+
+        QueryParam ids = QueryParam.of(List.of(leroi.getId(), dave.getId(), carter.getId()));
+        QueryParam firstName = QueryParam.of(leroi.getFirstName());
+        QueryParam age = QueryParam.of(leroi.getAge());
+        List<Customer> customers2 = repository.findByIdAndFirstNameAndAge(ids, firstName, age);
+        assertThat(customers).containsOnly(leroi);
+    }
+
+
+
+
+
+

Query Modification

+
+
+

Query Modifiers

+ +++++ + + + + + + + + + + + + + + + + + + + +
KeywordSampleSnippet

IgnoreCase

findByLastNameIgnoreCase

…​where UPPER(x.lastName) = UPPER(?)

OrderBy

findByLastNameOrderByFirstNameDesc

…​where x.lastName = ? order by x.firstName desc

+
+
+

Limiting Query Results

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordSampleSnippet

First

findFirstByAge

select top 1 where x.age = ?

First N

findFirst3ByAge

select top 3 where x.age = ?

Top

findTopByLastNameStartingWith

select top 1 where x.lastName like 'abc%' = ?

Top N

findTop4ByLastNameStartingWith

select top 4 where x.lastName like 'abc%'

Distinct

findDistinctByFirstNameContaining

select distinct …​ where x.firstName like 'abc%'

+
+
+

Find Using Query

+
+

User can perform a custom Query for finding matching entities in the Aerospike database. +A Query can be created using a Qualifier which represents an expression. +It may contain other qualifiers and combine them using either AND or OR.

+
+
+

Qualifier can be created for regular bins, metadata and ids (primary keys). +Below is an example of different variations:

+
+
+
+
    // creating an expression "firsName is equal to John"
+    Qualifier firstNameEqJohn = Qualifier.builder()
+        .setField("firstName")
+        .setFilterOperation(FilterOperation.EQ)
+        .setValue("John")
+        .build();
+    result = repository.findUsingQuery(new Query(firstNameEqJohn));
+    assertThat(result).containsOnly(john);
+
+    // creating an expression "primary key is equal to person's id"
+    Qualifier keyEqJohnsId = Qualifier.idEquals(john.getId());
+    result = repository.findUsingQuery(new Query(keyEqJohnsId));
+    assertThat(result).containsOnly(john);
+
+    // creating an expression "since_update_time metadata value is less than 50 seconds"
+    Qualifier sinceUpdateTimeLt50Seconds = Qualifier.metadataBuilder()
+        .setMetadataField(SINCE_UPDATE_TIME)
+        .setFilterOperation(FilterOperation.LT)
+        .setValue(50000L)
+        .build();
+    result = repository.findUsingQuery(new Query(sinceUpdateTimeLt50Seconds));
+    assertThat(result).contains(john);
+
+    // expressions are combined using AND
+    result = repository.findUsingQuery(new Query(Qualifier.and(firstNameEqJohn, keyEqJohnsId, sinceUpdateTimeLt50Seconds)));
+    assertThat(result).containsOnly(john);
+
+
+
+
+
+
+

Aerospike Object Mapping

+
+
+

Rich mapping support is provided by the AerospikeMappingConverter which has a rich metadata model that provides a full feature set of functionality to map domain objects to Aerospike objects. The mapping metadata model is populated using annotations on your domain objects. +However, the infrastructure is not limited to using annotations as the only source of metadata information. +The AerospikeMappingConverter also allows you to map objects without providing any additional metadata, by following a set of conventions.

+
+
+

In this section, we will describe the features of the AerospikeMappingConverter, how to use conventions for mapping objects to documents and how to override those conventions with annotation-based mapping metadata.

+
+
+

For more details, refer to Spring Data documentation: +Object Mapping.

+
+
+

Convention Based Mapping

+
+

AerospikeMappingConverter has a few conventions for mapping objects to documents when no additional mapping metadata is provided. +The conventions are:

+
+
+

How the 'id' Field Is Handled in the Mapping Layer

+
+

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

+
+
+

The following table outlines the requirements for the id field:

+
+ + ++++ + + + + + + + + + + + + + + + + +
Table 1. Examples for the translation of '_id'-field definitions
Field definitionDescription

String id

A field named 'id' without an annotation

@Id String myId

A field annotated with @Id (org.springframework.data.annotation.Id)

+
+

The following description outlines what type of conversion, if any, will be done on the property mapped to the id document field:

+
+
+
    +
  • +

    By default, the type of the field annotated with @id is turned into a String to be stored in Aerospike database. +If the original type cannot be persisted (see keepOriginalKeyTypes +for details), it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data.

    +
  • +
  • +

    If no field named "id" is present in the Java class then an implicit '_id' file will be generated by the driver but not mapped to a property or field of the Java class.

    +
  • +
+
+
+

When querying and updating AerospikeTemplate will use the converter to handle conversions of the Query and Update objects that correspond to the above rules for saving documents so field names and types used in your queries will be able to match what is in your domain classes.

+
+
+
+
+

Mapping Configuration

+
+

Unless explicitly configured, an instance of AerospikeMappingConverter is created by default when creating a AerospikeTemplate. +You can create your own instance of the MappingAerospikeConverter so as to tell it where to scan the classpath at the startup of your domain classes in order to extract metadata and construct indexes. +Also, to have more control over the conversion process (if needed), you can register converters to use for mapping specific classes to and from the database.

+
+
+ + + + + +
+
Note
+
+AbstractAerospikeConfiguration will create an AerospikeTemplate instance and register with the container under the name 'AerospikeTemplate'. +
+
+
+

Mapping Annotation Overview

+
+

The MappingAerospikeConverter can use metadata to drive the mapping of objects to documents using annotations. +An overview of the annotations is provided below

+
+
+
    +
  • +

    @Id - applied at the field level to mark the field used for identity purposes.

    +
  • +
  • +

    @Field - applied at the field level, describes the name of the field as it will be represented in the AerospikeDB BSON document thus allowing the name to be different from the field name of the class.

    +
  • +
  • +

    @Version - applied at the field level to mark record modification count. +The value must be effectively integer. +In Spring Data Aerospike, documents come in two forms – non-versioned and versioned. +Documents with an @Version annotation have a version field populated by the corresponding record’s generation count. +Version can be passed to a constructor or not (in that case it stays equal to zero).

    +
  • +
  • +

    @Expiration - applied at the field level to mark a property to be used as expiration field. +Expiration can be specified in two flavors: as an offset in seconds from the current time (then field value must be effectively integer) or as an absolute Unix timestamp. +Client system time must be synchronized with Aerospike server system time, otherwise expiration behaviour will be unpredictable.

    +
  • +
+
+
+

The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology-agnostic. +Specific subclasses are used in the AerospikeDB support to support annotation-based metadata. +Other strategies are also possible to put in place if there is demand.

+
+
+

Here is an example of a more complex mapping.

+
+
+
+
public class Person<T extends Address> {
+
+  @Id
+  private String id;
+
+  private Integer ssn;
+
+  @Field("fName")
+  private String firstName;
+
+  private String lastName;
+
+  private Integer age;
+
+  private Integer accountTotal;
+
+  private List<Account> accounts;
+
+  private T address;
+
+  @Version
+  private int id; // must be integer
+
+  public Person(Integer ssn) {
+    this.ssn = ssn;
+  }
+
+  public Person(Integer ssn, String firstName, String lastName, Integer age, T address, int version) {
+    this.ssn = ssn;
+    this.firstName = firstName;
+    this.lastName = lastName;
+    this.age = age;
+    this.address = address;
+    this.version = version;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  // no setter for Id.  (getter is only exposed for some unit testing)
+
+  public Integer getSsn() {
+    return ssn;
+  }
+
+// other getters/setters omitted
+}
+
+
+
+
+
+
+
+

Aerospike Custom Converters

+
+
+

Spring type converters are components used to convert data between different types, particularly when interacting with databases or binding data from external sources. They facilitate seamless transformation of data, such as converting between String and database-specific types (e.g., LocalDate to DATE or String to enumerations).

+
+
+

For more details, see Spring Type Conversion.

+
+
+

Spring provides a set of default type converters for common conversions. Spring Data Aerospike has its own built-in converters in DateConverters and AerospikeConverters classes.

+
+
+

However, in certain cases, custom converters are necessary to handle specific logic or custom serialization requirements. Custom converters allow developers to define precise conversion rules, ensuring data integrity and compatibility between application types and database representations.

+
+
+

In order to add a custom converter you can leverage Spring’s Converter SPI to implement type conversion logic and override customConverters() method available in AerospikeDataConfigurationSupport. Here is an example:

+
+
+
+
public class BlockingTestConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected List<Object> customConverters() {
+        return List.of(
+            CompositeKey.CompositeKeyToStringConverter.INSTANCE,
+            CompositeKey.StringToCompositeKeyConverter.INSTANCE
+        );
+    }
+
+    @Value
+    public static class CompositeKey {
+
+        String firstPart;
+        long secondPart;
+
+        @WritingConverter
+        public enum CompositeKeyToStringConverter implements Converter<CompositeKey, String> {
+            INSTANCE;
+
+            @Override
+            public String convert(CompositeKey source) {
+                return source.firstPart + "::" + source.secondPart;
+            }
+        }
+
+        @ReadingConverter
+        public enum StringToCompositeKeyConverter implements Converter<String, CompositeKey> {
+            INSTANCE;
+
+            @Override
+            public CompositeKey convert(String source) {
+                String[] split = source.split("::");
+                return new CompositeKey(split[0], Long.parseLong(split[1]));
+            }
+        }
+    }
+}
+
+
+
+
+
+

Aerospike Template

+
+
+

Aerospike Template provides a set of features for interacting with the database. +It allows lower-level access than a Repository and also serves as the foundation for repositories.

+
+
+

Template is the central support class for Aerospike database operations. +It provides the following functionality:

+
+
+
    +
  • +

    Methods to interact with the database

    +
  • +
  • +

    Mapping between Java objects and Aerospike Bins (see Object Mapping)

    +
  • +
  • +

    Providing connection callback

    +
  • +
  • +

    Translating exceptions into Spring’s technology-agnostic DAO exceptions hierarchy

    +
  • +
+
+
+

Instantiating AerospikeTemplate

+
+

If you are subclassing AbstractAerospikeDataConfiguration then the aerospikeTemplate bean is already present in your context, and you can use it.

+
+
+
+
@Autowired
+protected AerospikeTemplate template;
+
+
+
+

An alternative is to instantiate it yourself, you can see the bean in AbstractAerospikeDataConfiguration.

+
+
+

In case if you need to use custom WritePolicy, the persist operation can be used.

+
+
+

For CAS updates save operation must be used.

+
+
+
+

Methods for interacting with database

+
+

AerospikeOperations interface provides operations for interacting with the database (exists, find, insert, update etc.) as well as basic operations with indexes: createIndex, deleteIndex, indexExists.

+
+
+

The names of operations are typically self-descriptive. To read from Aerospike you can use findById, findByIds and find methods, to delete - delete methods, and so on.

+
+
+
+
template.findById(id, Person.class)
+
+
+
+

For indexed documents use find with provided Query object.

+
+
+
+
Stream<Person> result = template.find(query, Person.class);
+assertThat(result).hasSize(6);
+
+
+
+
+

Example

+
+

The simple case of using the save operation is to save a POJO.

+
+
+ + + + + +
+
Note
+
+For more information about Id property when inserting or saving see Mapping Conventions: Id Field for more information. +
+
+
+
+
public class Person {
+
+    @Id
+    private String id;
+    private String firstName;
+    private String lastName;
+    private int age;
+}
+
+
+
+
+
template.insert(new Person(id, "John", 50));
+
+long count = template.count
+            (new Query
+                (new QualifierBuilder()
+                    .setFilterOperation(FilterOperation.EQ)
+                    .setField("firstName")
+                    .setValue("John")
+                    .build()
+                ),
+            Person.class
+            );
+
+        assertThat(count).isEqualTo(3);
+
+
+
+
+
+
+

Secondary indexes

+
+
+

A secondary index (SI) is a data structure that locates all the records in a namespace, or a set within it, based on a bin value in the record. +When a value is updated in the indexed record, the secondary index automatically updates.

+
+
+

You can read more about secondary index implementation and usage in Aerospike on the official documentation page.

+
+
+

Why Secondary Index

+
+

Let’s consider a simple query for finding by equality:

+
+
+
+
public List<Person> personRepository.findByLastName(lastName);
+
+
+
+

Notice that findByLastName is not a simple lookup by key, but rather finding all records in a set. +Aerospike has 2 ways of achieving this:

+
+
+
    +
  1. +

    Scanning all the records in the set and extracting the appropriate records.

    +
  2. +
  3. +

    Defining a secondary index on the field lastName and using this secondary index to satisfy the query.

    +
  4. +
+
+
+

The second approach is far more efficient. +Aerospike stores the secondary indexes in a memory structure, allowing exceptionally fast identification of the records that match.

+
+
+

It relies on a secondary index having been created.

+
+
+
+

Ways to Create Secondary Indexes

+
+

In SpringData Aerospike secondary indexes can either be created by systems administrators using the asadm tool, or by developers telling SpringData that such an index is necessary.

+
+
+

There are two ways to accomplish this task with the help of SpringData Aerospike:

+
+
+
    +
  1. +

    Using AerospikeTemplate createIndex method.

    +
  2. +
  3. +

    Using @Indexed annotation on the necessary field of an entity.

    +
  4. +
+
+
+
+

Creating Secondary Index via AerospikeTemplate

+
+

For more information about AerospikeTemplate see the documentation page.

+
+
+

Setting a secondary index via AerospikeTemplate can be helpful, for example, in cases when an index creation does not change a lot.

+
+
+

Here is an example of a numeric secondary index for the rating field in the MovieDocument entity:

+
+
+
+
@Slf4j
+@Configuration
+public class AerospikeIndexConfiguration {
+
+    private static final String INDEX_NAME = "movie-rating-index";
+
+    @Bean
+    @ConditionalOnProperty(
+            value = "aerospike." + INDEX_NAME + ".create-on-startup",
+            havingValue = "true",
+            matchIfMissing = true)
+    public boolean createAerospikeIndex(AerospikeTemplate aerospikeTemplate) {
+        try {
+            aerospikeTemplate.createIndex(MovieDocument.class, INDEX_NAME, "rating", IndexType.NUMERIC);
+            log.info("Index {} was successfully created", INDEX_NAME);
+        } catch (Exception e) {
+            log.info("Index {} creation failed: {}", INDEX_NAME, e.getMessage());
+        }
+        return true;
+    }
+}
+
+
+
+
+

Creating Secondary Index using @Indexed annotation

+
+

You can use @Indexed annotation on the field where the index is required. +Here is an example of the Person object getting indexed by lastName:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    @Id
+    private long id;
+    private String firstName;
+    @Indexed(name = "lastName_idx", type = IndexType.STRING)
+    private String lastName;
+    private Date dateOfBirth;
+}
+
+
+
+

The annotation allows to specify also bin name, collectionType and ctx (context) if needed. +For the details on using @Indexed annotation see Indexed Annotation.

+
+
+
+

Matching the Secondary Index

+
+ + + + + +
+
Note
+
+In Aerospike, secondary indexes are case-sensitive, they match the exact queries. +
+
+
+

Following the query from the example above, assume there was a new requirement to be able to find by lastName +containing a String (rather than having an equality match):

+
+
+
+
public List<Person> findByLastNameContaining(String lastName);
+
+
+
+

In this case findByLastNameContaining query is not satisfied by the created secondary index. +Aerospike would need to scan the data which can be an expensive operation as all records in the set must be read +by the Aerospike server, and then the condition is applied to see if they match.

+
+
+

Due to the cost of performing this operation, scans from Spring Data Aerospike are disabled by default.

+
+
+

For the details on how to enable scans see Scan Operation.

+
+
+

Following the query from the example above, assume there was a new requirement to be able to find by firstName with an exact match:

+
+
+
+
public List<Person> findByLastName(String lastName);
+public List<Person> findByFirstName(String firstName);
+
+
+
+

In this case firstName is not marked as @Indexed, so SpringData Aerospike is not instructed to create an index on it. +Hence, it will scan the repository (a costly operation that could be avoided by using an index).

+
+
+ + + + + +
+
Note
+
+There are relevant configuration parameters: +create indexes on startup and +indexes cache refresh frequency. +
+
+
+
+
+
+

Indexed Annotation

+
+
+

The @Indexed annotation allows to create secondary index based on a specific field of a Java object. +For the details on secondary indexes in Aerospike see Secondary Indexes.

+
+
+

The annotation allows to specify the following parameters:

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parameterindex typemandatoryexample

name

index name

yes

"friend_address_keys_idx"

type

index type

yes

IndexType.STRING

bin

indexed bin type

no

"friend"

collectionType

index type

no

IndexCollectionType.MAPKEYS

ctx

context (path to the indexed elements)

no

"address"

+
+

Here is an example of creating a complex secondary index for fields of a person’s friend address.

+
+
+
+
@Data
+@AllArgsConstructor
+public class Address {
+
+    private String street;
+    private Integer apartment;
+    private String zipCode;
+    private String city;
+}
+
+@Data
+@NoArgsConstructor
+@Setter
+public class Friend {
+
+    String name;
+    Address address;
+}
+
+@Data
+@Document
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+public class Person {
+
+    @Id
+    String id;
+    @Indexed(type = IndexType.STRING, name = "friend_address_keys_idx",
+    collectionType = IndexCollectionType.MAPKEYS, ctx = "address")
+    Friend friend;
+}
+
+@Test
+void test() {
+    Friend carter = new Friend();
+    carter.setAddress(new Address("Street", 14, "1234567890", "City"));
+
+    Person dave = new Person();
+    dave.setFriend(carter);
+    repository.save(dave);
+}
+
+
+
+

A Person object in this example has a field called "friend" (Friend object). +A Friend object has a field called "address" (Address object). +So when "friend" field is set to a Friend with existing Address, we have a person (dave in the example above) with a friend (carter) who has +a particular address.

+
+
+

Address object on its own has certain fields: street, apartment, zipCode, city.

+
+
+ + + + + +
+
Note
+
+In Aerospike DB a POJO (such as Address) is represented by a Map, so the fields of POJO become map keys. +
+
+
+

Thus, if we want to index by Address object fields, we set collectionType to IndexCollectionType.MAPKEYS.

+
+
+

Ctx parameter represents context, or path to the necessary element in the specified bin ("friend") - which is "address", because we want to index by fields of friend’s address.

+
+
+

Secondary Index Context DSL

+
+

Secondary index context (ctx parameter in @Indexed annotation) represents path to a necessary element in hierarchy. It uses infix notation.

+
+
+

The document path is described as dot-separated context elements (e.g., "a.b.[2].c") written as a string. A path is made of singular path elements and ends with one (a leaf element) or more elements (leaves) - for example, "a.b.[2].c.[0:3]".

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Path ElementMatchesNotes

"a"

Map key “a”

Single element by key

"1" or '1'

Map key (numeric string) “1”

1

Map key (integer) 1

{1}

Map index 1

{=1}

Map value (integer) 1

{=bb}

Map value “bb”

Also {="bb"}

{="1"} or {='1'}

Map value (string) “1”

{#1}

Map rank 1

[1]

List index 1

[=1]

List value 1

[#1]

List rank 1

+
+

Example

+
+

Let’s consider a Map bin example:

+
+
+
+
{
+  1: a,
+  2: b,
+  4: d,
+  "5": e,
+  a: {
+    55: ee,
+    "66": ff,
+    aa: {
+      aaa: 111,
+      bbb: 222,
+      ccc: 333,
+    },
+    bb: {
+      bba: 221,
+      bbc: 223
+    },
+    cc: [ 22, 33, 44, 55, 43, 32, 44 ],
+    dd: [ {e: 5, f:6}, {z:26, y:25}, {8: h, "9": j} ]
+  }
+}
+
+
+
+

So the following will be true:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathCTXMatched Value

a.aa.aaa

[mapKey("a"), mapKey("aa"), mapKey("aaa")]

111

a.55

[mapKey("a"), mapKey(55)]

ee

a."66"

[mapKey("a"), mapKey("66")]

ff

a.aa.{2}

[mapKey("a"), mapKey("aa"),mapIndex(2)]

333

a.aa.{=222}

[mapKey("a"), mapKey("aa"),mapValue(222)]

222

a.bb.{#-1}

[mapKey("a"), mapKey("bb"),mapRank(-1)]

223

a.cc.[0]

[mapKey("a"), mapKey("cc"),listIndex(0)]

22

a.cc.[#1]

[mapKey("a"), mapKey("cc"),listRank(1)]

32

a.cc.[=44]

[mapKey("a"), mapKey("cc"),listValue(44)]

[44, 44]

a.dd.[0].e

[mapKey("a"), mapKey("dd"),listIndex(0), mapKey("e")]

5

a.dd.[2].8

[mapKey("a"), mapKey("dd"),listIndex(2), mapKey(8)]

h

a.dd.[-1]."9"

[mapKey("a"), mapKey("dd"),listIndex(-1), mapKey("9")]

j

a.dd.[1].{#0}

[mapKey("a"), mapKey("dd"),listIndex(1), mapRank(0)]

y

+
+ + + + + +
+
Note
+
+There are relevant configuration parameters: +create indexes on startup and +indexes cache refresh frequency. +
+
+
+
+
+
+
+

Caching

+
+
+

Caching is the process of storing data in a cache or temporary storage location, usually to improve application performance and make data access faster.

+
+
+

The caching process also provides an efficient way to reuse previously retrieved or computed data. +The cache is used to reduce the need for accessing the underlying storage layer which is slower.

+
+
+

Spring Cache with Aerospike database allows you to use annotations such as @Cacheable, @CachePut and @CacheEvict that provide a fully managed cache store using Aerospike database.

+
+
+

Introduction

+
+

In this example, we are going to use the annotations on UserRepository class methods to create/read/update and delete user’s data from the cache.

+
+
+

If a User is stored in the cache, calling a method with @Cacheable annotation will fetch the user from the cache instead of executing the method’s body responsible for the actual user fetch from the database.

+
+
+

If the User does not exist in the cache, the user’s data will be fetched from the database and put in the cache for later usage (a “cache miss”).

+
+
+

With Spring Cache and Aerospike database, we can achieve that with only a few lines of code.

+
+
+
+

Motivation

+
+

Let’s say that we are using another database as our main data store. +We don’t want to fetch the results from it every time we request the data, instead, we want to get the data from a cache layer.

+
+
+

There is a number of benefits of using a cache layer, here are some of them:

+
+
+
    +
  1. +

    Performance: Aerospike can work purely in RAM but reading a record from Aerospike in Hybrid Memory (primary index in memory, data stored on Flash drives) is extremely fast as well (~1ms).

    +
  2. +
  3. +

    Reduce database load: Moving a significant part of the read load from the main database to Aerospike can help balance the resources on heavy loads.

    +
  4. +
  5. +

    Scalability: Aerospike scales horizontally by adding more nodes to the cluster, scaling a relational database might be tricky and expensive, so if you are facing a read heavy load you can easily scale up the cache layer.

    +
  6. +
+
+
+
+

Example

+
+

We will not use an actual database as our main data store for this example, instead, we will simulate database access by printing a simulation message and replace a database read by just returning a specific User.

+
+
+

Configuration

+ +
+
+

AerospikeConfigurationProperties

+
+
+
@Data
+@Component
+@ConfigurationProperties(prefix = "aerospike")
+public class AerospikeConfigurationProperties {
+    private String host;
+    private int port;
+}
+
+
+
+
+

AerospikeConfiguration

+
+
+
@Configuration
+@EnableConfigurationProperties(AerospikeConfigurationProperties.class)
+@Import(value = {MappingAerospikeConverter.class, AerospikeMappingContext.class,
+        AerospikeTypeAliasAccessor.class,
+        AerospikeCustomConversions.class, SimpleTypeHolder.class})
+public class AerospikeConfiguration {
+
+    @Autowired
+    private MappingAerospikeConverter mappingAerospikeConverter;
+    @Autowired
+    private AerospikeConfigurationProperties aerospikeConfigurationProperties;
+
+    @Bean(destroyMethod = "close")
+    public AerospikeClient aerospikeClient() {
+        ClientPolicy clientPolicy = new ClientPolicy();
+        clientPolicy.failIfNotConnected = true;
+        return new AerospikeClient(clientPolicy, aerospikeConfigurationProperties.getHost(),
+            aerospikeConfigurationProperties.getPort());
+    }
+
+    @Bean
+    public CacheManager cacheManager(IAerospikeClient aerospikeClient,
+                                            MappingAerospikeConverter aerospikeConverter,
+                                     AerospikeCacheKeyProcessor cacheKeyProcessor) {
+        AerospikeCacheConfiguration defaultConfiguration = new AerospikeCacheConfiguration("test");
+        return new AerospikeCacheManager(aerospikeClient, mappingAerospikeConverter, defaultConfiguration,
+            cacheKeyProcessor);
+    }
+}
+
+
+
+

In the AerospikeConfiguration we will create two types of Beans:

+
+
+
+

AerospikeClient

+
+

Responsible for accessing an Aerospike database and performing database operations.

+
+
+
+

AerospikeCacheManager

+
+

The heart of the cache layer, to define an AerospikeCacheManager you need:

+
+
+
    +
  1. +

    aerospikeClient (AerospikeClient).

    +
  2. +
  3. +

    aerospikeConverter (MappingAerospikeConverter).

    +
  4. +
  5. +

    defaultCacheConfiguration (AerospikeCacheConfiguration), a default cache configuration that applies when creating new caches. +Cache configuration contains a namespace, a set (null by default meaning write directly to the namespace w/o specifying a set) and an expirationInSeconds (AKA TTL, default is 0 meaning use Aerospike server’s default).

    +
  6. +
  7. +

    Optional: initialPerCacheConfiguration (Map<String, AerospikeCacheConfiguration>), You can also specify a map of cache names and matching configuration, it will create the caches with the given matching configuration at the application startup.

    +
  8. +
+
+
+ + + + + +
+
Note
+
+A cache name is only a link to the cache configuration. +
+
+
+
+

Objects

+
+
User
+
+
+
@Data
+@Document
+@AllArgsConstructor
+public class User {
+    @Id
+    private int id;
+    private String name;
+    private String email;
+    private int age;
+}
+
+
+
+
+
+

Repositories

+
+
UserRepository
+
+
+
@Repository
+public class UserRepository {
+
+    @Cacheable(value = "test", key = "#id")
+    public Optional<User> getUserById(int id) {
+        System.out.println("Simulating a read from the main data store.");
+        // In case the id doesn't exist in the cache it will "fetch" jimmy page with the requested id and add it to the cache (cache miss).
+        return Optional.of(new User(id, "jimmy page", "jimmy@gmail.com", 77));
+    }
+
+    @CachePut(value = "test", key = "#user.id")
+    public User addUser(User user) {
+        System.out.println("Simulating addition of " + user + " to the main data store.");
+        return user;
+    }
+
+    @CacheEvict(value = "test", key = "#id")
+    public void removeUserById(int id) {
+        System.out.println("Simulating removal of " + id + " from the main data store.");
+    }
+}
+
+
+
+

The cache annotations require a “value” field, which is the cache name, if the cache name doesn’t exist — by passing initialPerCacheConfiguration param when creating a Bean of AerospikeCacheManager in a configuration class, it will configure the cache with the properties of the given defaultCacheConfiguration (Configuration > AerospikeCacheManager).

+
+
+
+
+

Services

+
+
UserService
+
+
+
@Service
+@AllArgsConstructor
+public class UserService {
+
+    UserRepository userRepository;
+
+    public Optional<User> readUserById(int id) {
+        return userRepository.getUserById(id);
+    }
+
+    public User addUser(User user) {
+        return userRepository.addUser(user);
+    }
+
+    public void removeUserById(int id) {
+        userRepository.removeUserById(id);
+    }
+}
+
+
+
+
+
+

Controllers

+
+
UserController
+
+
+
@RestController
+@AllArgsConstructor
+public class UserController {
+
+    UserService userService;
+
+    @GetMapping("/users/{id}")
+    public Optional<User> readUserById(@PathVariable("id") Integer id) {
+        return userService.readUserById(id);
+    }
+
+    @PostMapping("/users")
+    public User addUser(@RequestBody User user) {
+        return userService.addUser(user);
+    }
+
+    @DeleteMapping("/users/{id}")
+    public void deleteUserById(@PathVariable("id") Integer id) {
+        userService.removeUserById(id);
+    }
+}
+
+
+
+
+
+

Add @EnableCaching

+
+
SimpleSpringBootAerospikeCacheApplication
+
+

Add @EnableCaching to the class that contains the main method.

+
+
+
+
@EnableCaching
+@SpringBootApplication
+public class SimpleSpringBootAerospikeCacheApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SimpleSpringBootAerospikeCacheApplication.class, args);
+    }
+}
+
+
+
+
+
+
+

Test

+
+

We will use Postman to simulate client requests.

+
+
+

Add User (@CachePut)

+
+
    +
  1. +

    Create a new POST request with the following url: http://localhost:8080/users

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Add a Body in a valid JSON format:

    +
    +
    +
    {
    +   "id":1,
    +   "name":"guthrie",
    +   "email":"guthriegovan@gmail.com",
    +   "age":35
    +}
    +
    +
    +
  6. +
  7. +

    Press Send.

    +
  8. +
+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+| @user_key  | name | @_class | email         | age                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "guthrie" | "com.aerospike.cache.simpleSpringBootAerospikeCache.objects.User"  | "guthriegovan@gmail.com" | 35 |
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+

We can now see that this user was added to the cache.

+
+
+
+

Read User (@Cacheable)

+
+
    +
  1. +

    Create a new GET request with the following url: http://localhost:8080/users/1

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Press Send.

    +
  6. +
+
+
+
+

Remove User (@CacheEvict)

+
+
    +
  1. +

    Create a new DELETE request with the following url: http://localhost:8080/users/1

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Press Send.

    +
  6. +
+
+
+

We can now see that this user was deleted from the cache (thanks to the @CacheEvict annotation in the UserRepository).

+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+0 rows in set
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+
+

Cache miss (@Cacheable)

+
+

For reading User that is not in the cache we can use the GET request configured before with an id that we know for sure is not there.

+
+
+

If we try calling the GET request with the id 5, we get the following user data:

+
+
+
+
{
+    "id": 5,
+    "name": "jimmy page",
+    "email": "jimmy@gmail.com",
+    "age": 77
+}
+
+
+
+

We wrote it hard-coded in UserRepository to simulate an actual database fetch of a user id that doesn’t exist in the cache.

+
+
+

We can now also see that the user was added to the cache.

+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+| @user_key  | name | @_class | email         | age                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "jimmy page" | "com.aerospike.cache.simpleSpringBootAerospikeCache.objects.User"  | "jimmy@gmail.com" | 77 |
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+
+
+
+
+

Configuration

+
+
+

Configuration parameters can be set in a standard application.properties file using spring.data.aerospike* prefix +or by overriding configuration from AbstractAerospikeDataConfiguration class.

+
+
+

Application.properties

+
+

Here is an example:

+
+
+
+
# application.properties
+spring.aerospike.hosts=localhost:3000
+spring.data.aerospike.namespace=test
+spring.data.aerospike.scans-enabled=false
+spring.data.aerospike.send-key=true
+spring-data-aerospike.create-indexes-on-startup=true
+spring.data.aerospike.index-cache-refresh-seconds=3600
+spring.data.aerospike.server-version-refresh-seconds=3600
+spring.data.aerospike.query-max-records=10000
+spring.data.aerospike.batch-write-size=100
+spring.data.aerospike.keep-original-key-types=false
+
+
+
+

Configuration class:

+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = {TestRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+
+}
+
+
+
+

In this case extending AbstractAerospikeDataConfiguration class is required to enable repositories.

+
+
+
+

Overriding configuration

+
+

Configuration can also be set by overriding getHosts(), nameSpace() and configureDataSettings() methods of the AbstractAerospikeDataConfiguration class.

+
+
+

Here is an example:

+
+
+
+
@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("localhost", 3000));
+    }
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setScansEnabled(false);
+        aerospikeDataSettings.setCreateIndexesOnStartup(true);
+        aerospikeDataSettings.setIndexCacheRefreshSeconds(3600);
+        aerospikeDataSettings.setServerVersionRefreshSeconds(3600);
+        aerospikeDataSettings.setQueryMaxRecords(10000L);
+        aerospikeDataSettings.setBatchWriteSize(100);
+        aerospikeDataSettings.setKeepOriginalKeyTypes(false);
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+Return values of getHosts(), nameSpace() and configureDataSettings() methods +of the AbstractAerospikeDataConfiguration class have precedence over the parameters +set via application.properties. +
+
+
+
+

Configuration Parameters

+
+

hosts

+
+
+
# application.properties
+spring.aerospike.hosts=hostname1:3001, hostname2:tlsName2:3002
+
+
+
+

A String of hosts separated by , in form of hostname1[:tlsName1][:port1],…​

+
+
+

IP addresses must be given in one of the following formats:

+
+
+
+
IPv4: xxx.xxx.xxx.xxx
+IPv6: [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]
+IPv6: [xxxx::xxxx]
+
+
+
+

IPv6 addresses must be enclosed by brackets. tlsName is optional.

+
+
+ + + + + +
+
Note
+
+Another way of defining hosts is overriding the getHosts() method. +It has precedence over hosts parameter from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("hostname1", 3001));
+    }
+}
+
+
+
+

Default: null.

+
+
+
+

namespace

+
+
+
# application.properties
+spring.data.aerospike.namespace=test
+
+
+
+

Aerospike DB namespace.

+
+
+ + + + + +
+
Note
+
+Another way of defining hosts is overriding the nameSpace() method. +It has precedence over namespace parameter from application.properties. +Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+To use multiple namespaces it is required to override nameSpace() and AerospikeTemplate for each +configuration class per namespace. +See multiple namespaces example +for implementation details. +
+
+
+

Default: null.

+
+
+
+

scansEnabled

+
+
+
# application.properties
+spring.data.aerospike.scans-enabled=false
+
+
+
+

A scan can be an expensive operation as all records in the set must be read by the Aerospike server, +and then the condition is applied to see if they match.

+
+
+

Due to the cost of performing this operation, scans from Spring Data Aerospike are disabled by default.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setScansEnabled(false);
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+Once this flag is enabled, scans run whenever needed with no warnings. This may or may not be optimal +in a particular use case. +
+
+
+

Default: false.

+
+
+
+

createIndexesOnStartup

+
+
+
# application.properties
+spring.data.aerospike.create-indexes-on-startup=true
+
+
+
+

Create secondary indexes specified using @Indexed annotation on startup.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setCreateIndexesOnStartup(true);
+    }
+}
+
+
+
+

Default: true.

+
+
+
+

indexCacheRefreshSeconds

+
+
+
# application.properties
+spring.data.aerospike.index-cache-refresh-seconds=3600
+
+
+
+

Automatically refresh indexes cache every <N> seconds.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setIndexCacheRefreshSeconds(3600);
+    }
+}
+
+
+
+

Default: 3600.

+
+
+
+

serverVersionRefreshSeconds

+
+
+
# application.properties
+spring.data.aerospike.server-version-refresh-seconds=3600
+
+
+
+

Automatically refresh cached server version every <N> seconds.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setServerVersionRefreshSeconds(3600);
+    }
+}
+
+
+
+

Default: 3600.

+
+
+
+

queryMaxRecords

+
+
+
# application.properties
+spring.data.aerospike.query-max-records=10000
+
+
+
+

Limit amount of results returned by server. Non-positive value means no limit.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setQueryMaxRecords(10000L);
+    }
+}
+
+
+
+

Default: 10 000.

+
+
+
+

batchWriteSize

+
+
+
# application.properties
+spring.data.aerospike.batch-write-size=100
+
+
+
+

Maximum batch size for batch write operations. Non-positive value means no limit.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setBatchWriteSize(100);
+    }
+}
+
+
+
+

Default: 100.

+
+
+
+

keepOriginalKeyTypes

+
+
+
# application.properties
+spring.data.aerospike.keep-original-key-types=false
+
+
+
+

Define how @Id fields (primary keys) and Map keys are stored in the Aerospike database: +false - always as String, true - preserve original type if supported.

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@Id field typekeepOriginalKeyTypes = falsekeepOriginalKeyTypes = true

long

String

long

int

String

long

String

String

String

byte[]

String

byte[]

other types

String

String

+
+ + + + + +
+
Note
+
+If @Id field’s type cannot be persisted as is, it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data. +
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Map key typekeepOriginalKeyTypes = falsekeepOriginalKeyTypes = true

long

String

long

int

String

long

double

String

double

String

String

String

byte[]

String

byte[]

other types

String

String

+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setKeepOriginalKeyTypes(false);
+    }
+}
+
+
+
+

Default: false (store keys only as String).

+
+
+
+

writeSortedMaps

+
+
+
# application.properties
+spring.data.aerospike.writeSortedMaps=true
+
+
+
+

Define how Maps and POJOs are written: true - as sorted maps (TreeMap, default), false - as unsorted (HashMap).

+
+
+

Writing as unsorted maps (false) degrades performance of Map-related operations and does not allow comparing Maps, +so it is strongly recommended to change the default value only if required during upgrade from older versions +of Spring Data Aerospike.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setWriteSortedMaps(true);
+    }
+}
+
+
+
+

Default: true (write Maps and POJOs as sorted maps).

+
+
+
+
+
+

Appendix

+
+ +
+
+ + \ No newline at end of file diff --git a/preface.html b/preface.html new file mode 100644 index 00000000..237722ff --- /dev/null +++ b/preface.html @@ -0,0 +1,538 @@ + + + + + + + +Preface + + + + + +
+
+
+
+

The Spring Data Aerospike project applies core Spring concepts and provides interface for using Aerospike key-value style data store. We provide a "repository" and a "template" as high-level abstractions for storing and querying data. You will notice similarities to the JDBC support in the Spring Framework.

+
+
+

This chapter provides some basic introduction to Spring and Aerospike, it explains Aerospike concepts and syntax. The rest of the documentation refers to Spring Data Aerospike features and assumes the user is familiar with Aerospike as well as Spring concepts.

+
+
+
+
+

Knowing Spring

+
+
+

Spring Data uses Spring framework’s core functionality, such as the IoC container, type conversion system, DAO exception hierarchy etc. While it is not important to know the Spring APIs, understanding the concepts behind them is. At a minimum, the idea behind IoC should be familiar regardless of IoC container you choose to use.

+
+
+

To learn more about Spring, you can refer to the comprehensive documentation that explains in detail the Spring Framework. There are a lot of articles, blog entries and books on the matter - take a look at the Spring framework documentation reference for more information.

+
+
+
+
+

Knowing NoSQL and Aerospike

+
+
+

NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms and patterns (to make things worthwhile even the term itself has multiple meanings). While some principles are common, it is crucial that the user is familiar to some degree with Aerospike key-value store operations that supply the mechanism for associating keys with a set of named values, similar to a row in standard RDBMS terminology. The data layer in Aerospike Database is optimized to store data in solid state drives, RAM, or traditional rotational media. The database indices are stored in RAM for quick availability, and data writes are optimized through large block writes to reduce latency. The software also employs two sub-programs that are codenamed Defragmenter and Evictor. Defragmenter removes data blocks that have been deleted, and Evictor frees RAM space by removing references to expired records.

+
+
+

The jumping off ground for learning about Aerospike is www.aerospike.com. Here is a list of other useful resources:

+
+
+ +
+
+
+
+

Requirements

+
+
+

Spring Data Aerospike binaries require JDK level 17.0 and above.

+
+
+

In terms of server, it is required to use at least Aerospike server version 6.1 (recommended to use the latest version when possible).

+
+
+
+
+

Additional Help Resources

+
+
+

Learning a new framework is not always straightforward. In this section, we try to provide what we think is an easy-to-follow guide for starting with Spring Data Aerospike module. However, if you encounter issues, or you are just looking for advice, feel free to use one of the links below:

+
+
+
+
+

Support

+
+
+

There are a few support options available:

+
+
+

Questions & Answers

+
+

Developers post questions and answers on Stack Overflow. The two key tags to search for related answers to this project are:

+
+ +
+
+

Following Development

+
+

If you encounter a bug or want to suggest an improvement, please create an issue on GitHub.

+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/aerospike-custom-converters.html b/reference/aerospike-custom-converters.html new file mode 100644 index 00000000..71ebe485 --- /dev/null +++ b/reference/aerospike-custom-converters.html @@ -0,0 +1,501 @@ + + + + + + + +Aerospike Custom Converters + + + + + +
+
+

Spring type converters are components used to convert data between different types, particularly when interacting with databases or binding data from external sources. They facilitate seamless transformation of data, such as converting between String and database-specific types (e.g., LocalDate to DATE or String to enumerations).

+
+
+

For more details, see Spring Type Conversion.

+
+
+

Spring provides a set of default type converters for common conversions. Spring Data Aerospike has its own built-in converters in DateConverters and AerospikeConverters classes.

+
+
+

However, in certain cases, custom converters are necessary to handle specific logic or custom serialization requirements. Custom converters allow developers to define precise conversion rules, ensuring data integrity and compatibility between application types and database representations.

+
+
+

In order to add a custom converter you can leverage Spring’s Converter SPI to implement type conversion logic and override customConverters() method available in AerospikeDataConfigurationSupport. Here is an example:

+
+
+
+
public class BlockingTestConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected List<Object> customConverters() {
+        return List.of(
+            CompositeKey.CompositeKeyToStringConverter.INSTANCE,
+            CompositeKey.StringToCompositeKeyConverter.INSTANCE
+        );
+    }
+
+    @Value
+    public static class CompositeKey {
+
+        String firstPart;
+        long secondPart;
+
+        @WritingConverter
+        public enum CompositeKeyToStringConverter implements Converter<CompositeKey, String> {
+            INSTANCE;
+
+            @Override
+            public String convert(CompositeKey source) {
+                return source.firstPart + "::" + source.secondPart;
+            }
+        }
+
+        @ReadingConverter
+        public enum StringToCompositeKeyConverter implements Converter<String, CompositeKey> {
+            INSTANCE;
+
+            @Override
+            public CompositeKey convert(String source) {
+                String[] split = source.split("::");
+                return new CompositeKey(split[0], Long.parseLong(split[1]));
+            }
+        }
+    }
+}
+
+
+
+ + \ No newline at end of file diff --git a/reference/aerospike-object-mapping.html b/reference/aerospike-object-mapping.html new file mode 100644 index 00000000..c7b3d659 --- /dev/null +++ b/reference/aerospike-object-mapping.html @@ -0,0 +1,632 @@ + + + + + + + +Aerospike Object Mapping + + + + + +
+
+
+
+

Rich mapping support is provided by the AerospikeMappingConverter which has a rich metadata model that provides a full feature set of functionality to map domain objects to Aerospike objects. The mapping metadata model is populated using annotations on your domain objects. +However, the infrastructure is not limited to using annotations as the only source of metadata information. +The AerospikeMappingConverter also allows you to map objects without providing any additional metadata, by following a set of conventions.

+
+
+

In this section, we will describe the features of the AerospikeMappingConverter, how to use conventions for mapping objects to documents and how to override those conventions with annotation-based mapping metadata.

+
+
+

For more details, refer to Spring Data documentation: +Object Mapping.

+
+
+
+
+

Convention Based Mapping

+
+
+

AerospikeMappingConverter has a few conventions for mapping objects to documents when no additional mapping metadata is provided. +The conventions are:

+
+
+

How the 'id' Field Is Handled in the Mapping Layer

+
+

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

+
+
+

The following table outlines the requirements for the id field:

+
+ + ++++ + + + + + + + + + + + + + + + + +
Table 1. Examples for the translation of '_id'-field definitions
Field definitionDescription

String id

A field named 'id' without an annotation

@Id String myId

A field annotated with @Id (org.springframework.data.annotation.Id)

+
+

The following description outlines what type of conversion, if any, will be done on the property mapped to the id document field:

+
+
+
    +
  • +

    By default, the type of the field annotated with @id is turned into a String to be stored in Aerospike database. +If the original type cannot be persisted (see keepOriginalKeyTypes +for details), it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data.

    +
  • +
  • +

    If no field named "id" is present in the Java class then an implicit '_id' file will be generated by the driver but not mapped to a property or field of the Java class.

    +
  • +
+
+
+

When querying and updating AerospikeTemplate will use the converter to handle conversions of the Query and Update objects that correspond to the above rules for saving documents so field names and types used in your queries will be able to match what is in your domain classes.

+
+
+
+
+
+

Mapping Configuration

+
+
+

Unless explicitly configured, an instance of AerospikeMappingConverter is created by default when creating a AerospikeTemplate. +You can create your own instance of the MappingAerospikeConverter so as to tell it where to scan the classpath at the startup of your domain classes in order to extract metadata and construct indexes. +Also, to have more control over the conversion process (if needed), you can register converters to use for mapping specific classes to and from the database.

+
+
+ + + + + +
+
Note
+
+AbstractAerospikeConfiguration will create an AerospikeTemplate instance and register with the container under the name 'AerospikeTemplate'. +
+
+
+

Mapping Annotation Overview

+
+

The MappingAerospikeConverter can use metadata to drive the mapping of objects to documents using annotations. +An overview of the annotations is provided below

+
+
+
    +
  • +

    @Id - applied at the field level to mark the field used for identity purposes.

    +
  • +
  • +

    @Field - applied at the field level, describes the name of the field as it will be represented in the AerospikeDB BSON document thus allowing the name to be different from the field name of the class.

    +
  • +
  • +

    @Version - applied at the field level to mark record modification count. +The value must be effectively integer. +In Spring Data Aerospike, documents come in two forms – non-versioned and versioned. +Documents with an @Version annotation have a version field populated by the corresponding record’s generation count. +Version can be passed to a constructor or not (in that case it stays equal to zero).

    +
  • +
  • +

    @Expiration - applied at the field level to mark a property to be used as expiration field. +Expiration can be specified in two flavors: as an offset in seconds from the current time (then field value must be effectively integer) or as an absolute Unix timestamp. +Client system time must be synchronized with Aerospike server system time, otherwise expiration behaviour will be unpredictable.

    +
  • +
+
+
+

The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology-agnostic. +Specific subclasses are used in the AerospikeDB support to support annotation-based metadata. +Other strategies are also possible to put in place if there is demand.

+
+
+

Here is an example of a more complex mapping.

+
+
+
+
public class Person<T extends Address> {
+
+  @Id
+  private String id;
+
+  private Integer ssn;
+
+  @Field("fName")
+  private String firstName;
+
+  private String lastName;
+
+  private Integer age;
+
+  private Integer accountTotal;
+
+  private List<Account> accounts;
+
+  private T address;
+
+  @Version
+  private int id; // must be integer
+
+  public Person(Integer ssn) {
+    this.ssn = ssn;
+  }
+
+  public Person(Integer ssn, String firstName, String lastName, Integer age, T address, int version) {
+    this.ssn = ssn;
+    this.firstName = firstName;
+    this.lastName = lastName;
+    this.age = age;
+    this.address = address;
+    this.version = version;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  // no setter for Id.  (getter is only exposed for some unit testing)
+
+  public Integer getSsn() {
+    return ssn;
+  }
+
+// other getters/setters omitted
+}
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/aerospike-projections.html b/reference/aerospike-projections.html new file mode 100644 index 00000000..f74a4680 --- /dev/null +++ b/reference/aerospike-projections.html @@ -0,0 +1,529 @@ + + + + + + + +Projections with Aerospike + + + + + +
+
+

Spring Data Aerospike supports Projections, a mechanism that allows you to fetch only relevant fields from Aerospike for a particular use case. This results in better performance, less network traffic, and a better understanding of what is required for the rest of the flow.

+
+
+

For more details, refer to Spring Data documentation: Projections.

+
+
+

For example, consider a Person class:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    public enum Gender {
+        MALE, FEMALE;
+    }
+    @Id
+    private long id;
+    private String firstName;
+    @Indexed(name = "lastName_idx", type = IndexType.STRING)
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+    private long heightInCm;
+    private boolean enabled;
+    private Gender gender;
+    private String hairColor;
+    private String eyeColor;
+    private String passportNo;
+    private String passptCnty;
+}
+
+
+
+

This is a moderately complex object, and a production object is likely to be more complex. The use case might call for a search box that shows the firstName, lastName and dateOfBirth fields, allowing the user to select a Person based on the criteria upon which the full object will be shown.

+
+
+

A simple projection of this object might be:

+
+
+
+
@Data
+@Builder
+public class SearchPerson {
+    private String firstName;
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+}
+
+
+
+

To tell Spring Data how to create a SearchPerson it is necessary to create a method on the Person class:

+
+
+
+
public SearchPerson toSearchPerson() {
+    return SearchPerson.builder()
+            .firstName(this.getFirstName())
+            .lastName(this.getLastName())
+            .dateOfBirth(this.getDateOfBirth())
+            .build();
+}
+
+
+
+

Now the repository interface can be extended to return this projection:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    public List<Person> findByLastName(String lastName);
+    public List<SearchPerson> findSearchPersonByLastName(String lastName);
+}
+
+
+
+

Notice that the method name now dictates the return type of SearchPerson as well as changing the return value. When this method is executed, Aerospike loads the full Person objects out of storage, invokes the toSearchPerson on each person and returns the resulting SearchPerson instances. This reduces the required network bandwidth to present these objects to the front end and simplifies logic.

+
+
+

A blog post with more details on projections can be found here.

+
+
+ + \ No newline at end of file diff --git a/reference/aerospike-reactive-repositories.html b/reference/aerospike-reactive-repositories.html new file mode 100644 index 00000000..2c0e7639 --- /dev/null +++ b/reference/aerospike-reactive-repositories.html @@ -0,0 +1,717 @@ + + + + + + + +Reactive Aerospike repositories + + + + + +
+
+

Introduction

+
+
+

This chapter will point out the specialties for reactive repository support for Aerospike. This builds on the core repository support explained in repositories. So make sure you’ve got a sound understanding of the basic concepts explained there.

+
+
+
+
+

Reactive Composition Libraries

+
+
+

The reactive space offers various reactive composition libraries. The most common library is Project Reactor.

+
+
+

Spring Data Aerospike is built on top of the Aerospike Reactor Java Client Library, to provide maximal interoperability by relying on the Reactive Streams initiative. Static APIs, such as ReactiveAerospikeOperations, are provided by using Project Reactor’s Flux and Mono types. Project Reactor offers various adapters to convert reactive wrapper types (Flux to Observable and vice versa).

+
+
+

Spring Data’s Repository abstraction is a dynamic API, mostly defined by you and your requirements as you declare query methods. Reactive Aerospike repositories can be implemented by using Project Reactor wrapper types by extending from the following library-specific repository interface:

+
+
+
    +
  • +

    ReactiveAerospikeRepository

    +
  • +
+
+
+
+
+

Usage

+
+
+

To access domain entities stored in an Aerospike you can use our sophisticated repository support that eases implementing those quite significantly. To do so, create an interface similar to your repository. Before you can do that, though, you need an entity, such as the entity defined in the following example:

+
+
+
Example 1. Sample Person entity
+
+
+
+
public class Person {
+
+  @Id
+  private String id;
+  private String firstname;
+  private String lastname;
+  private Address address;
+
+  // … getters and setters omitted
+}
+
+
+
+
+
+

We have a quite simple domain object here. The default serialization mechanism used in ReactiveAerospikeTemplate (which is backing the repository support) regards properties named id as document ID. Currently, we support String and long as id-types. The following example shows how to create an interface that defines queries against the Person object from the preceding example:

+
+
+
Example 2. Basic repository interface to persist Person entities
+
+
+
+
public interface ReactivePersonRepository extends ReactiveAerospikeRepository<Person, String> {
+
+  Flux<Person> findByFirstname(String firstname);
+
+}
+
+
+
+
+
+

Right now this interface simply serves typing purposes but we will add additional methods to it later.

+
+
+

For Java configuration, use the @EnableReactiveAerospikeRepositories annotation. The annotation carries the base +packages attribute. These base packages are to be scanned for interfaces extending ReactiveAerospikeRepository +and create Spring beans for each of them found. If no base package is configured, the infrastructure scans the package +of the annotated configuration class.

+
+
+

The following listing shows how to use Java configuration for a repository:

+
+
+
Example 3. Java configuration for repositories
+
+
+
+
@Configuration
+@EnableReactiveAerospikeRepositories(basePackages = "org.springframework.data.aerospike.example")
+public class TestRepositoryConfig extends AbstractReactiveAerospikeDataConfiguration {
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("52.23.205.208", 3000));
+    }
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+
+    @Override
+    protected EventLoops eventLoops() {
+        return new NioEventLoops();
+    }
+}
+
+
+
+
+
+

As our domain repository extends ReactiveAerospikeRepository it provides you with CRUD operations. Working with the repository instance is a matter of dependency injecting it into a client, as the following example shows:

+
+
+
Example 4. Access to Person entities
+
+
+
+
@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class PersonRepositoryTests {
+    @Autowired
+    ReactivePersonRepository repository;
+
+    @Test
+    public void findByFirstnameCorrectly() {
+      Flux<Person> persons = repository.findByFirstname("TestFirstName");
+    }
+}
+
+
+
+
+
+

The sample creates an application context with Spring’s unit test support which will perform annotation-based dependency injection into test cases. Inside the test method, we simply use the repository to query the datastore.

+
+
+
+
+

Query methods

+
+
+

Most of the data access operations you usually trigger on a repository result in a query being executed against the Aerospike databases. Defining such a query is just a matter of declaring a method on the repository interface

+
+
+
Example 5. PersonRepository with query methods
+
+
+
+
public interface ReactivePersonRepository extends ReactiveAerospikeRepository<Person, String> {
+
+  Flux<Person> findByFirstname(String firstname);                                   (1)
+
+  Flux<Person> findByFirstname(Publisher<String> firstname);                        (2)
+
+  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);       (3)
+
+  Mono<Person> findFirstByLastname(String lastname);                                (4)
+
+  Flux<Person> findByFirstnameStartsWith(String prefix);                            (5)
+
+}
+
+
+
+
    +
  1. +

    The method shows a query for all people with the given firstname. The query is derived by parsing the method name for constraints that can be concatenated with And and Or. Thus, the method name results in a query expression of {"firstname" : firstname}.

    +
  2. +
  3. +

    The method shows a query for all people with the given firstname once the firstname is emitted by the given Publisher.

    +
  4. +
  5. +

    Find a single entity for the given criteria. It completes with IncorrectResultSizeDataAccessException on non-unique results.

    +
  6. +
  7. +

    Unless <3>, the first entity is always emitted even if the query yields more result documents.

    +
  8. +
  9. +

    The method shows a query for all people with the firstname starts from prefix

    +
  10. +
+
+
+
+
+
+
+

Examples

+
+
+

Here’s a delete, insert and query example

+
+
+
+
@ContextConfiguration(classes = TestRepositoryConfig.class)
+public class ReactiveRepositoryExample {
+
+    @Autowired
+    protected ReactivePersonRepository repository;
+    @Autowired
+    ReactiveAerospikeOperations aerospikeOperations;
+    @Autowired
+    IAerospikeReactorClient client;
+
+    public RepositoryExample(ApplicationContext ctx) {
+        aerospikeOperations = ctx.getBean(ReactiveAerospikeTemplate.class);
+        repository = (ReactivePersonRepository) ctx.getBean("reactivePersonRepository");
+        client = ctx.getBean(IAerospikeReactorClient.class);
+    }
+
+    protected void setUp() {
+        // Insert new Person items into repository
+        Person dave = new Person("Dave-01", "Matthews", 42);
+        Person donny = new Person("Dave-02", "Macintire", 39);
+        Person oliver = new Person("Oliver-01", "Matthews", 4);
+        Person carter = new Person("Carter-01", "Beauford", 49);
+        List<Person> all = saveAll(Arrays.asList(dave, donny, oliver, carter))
+            .collectList().block();
+    }
+
+    protected void cleanUp() {
+        // Delete all Person items from repository
+        repository.findAll().flatMap(a -> repository.delete(a)).blockLast();
+    }
+
+    protected void executeRepositoryCall() {
+        System.out.println("Results for first name exact match of 'Dave-02'");
+        repository.findByFirstname("Dave-02")
+            .doOnNext(person -> System.out.println(person.toString())).blockLast();
+
+        System.out.println("Results for first name starting with letter 'D'");
+        repository.findByFirstnameStartsWith("D")
+            .doOnNext(person -> System.out.println(person.toString())).blockLast();
+    }
+
+    public static void main(String[] args) {
+        ApplicationContext ctx =
+            new AnnotationConfigApplicationContext(TestRepositoryConfig.class);
+        ReactiveRepositoryExample repositoryExample = new ReactiveRepositoryExample(ctx);
+        repositoryExample.setUp();
+        repositoryExample.executeRepositoryCall();
+        repositoryExample.cleanUp();
+    }
+}
+
+
+
+
+
+

Restrictions

+
+
+

ReactiveAerospikeRepository currently does not support the next operations:

+
+
+
    +
  • +

    all operations with indexes (create, delete, exists)

    +
  • +
  • +

    count()

    +
  • +
  • +

    deleteAll()

    +
  • +
+
+
+

This limitation is due to the lack of corresponding asynchronous methods in the Aerospike client.

+
+
+
+
+ + \ No newline at end of file diff --git a/reference/aerospike-repositories.html b/reference/aerospike-repositories.html new file mode 100644 index 00000000..a02cd5c3 --- /dev/null +++ b/reference/aerospike-repositories.html @@ -0,0 +1,700 @@ + + + + + + + +Aerospike repositories + + + + + +
+
+

Introduction

+
+
+

One of the main goals of the Spring Data is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores.

+
+
+

One of the core interfaces of Spring Data is Repository. +This interface acts primarily to capture the types to work with and to help user to discover interfaces that extend Repository.

+
+
+

In other words, it allows user to have basic and complicated queries without writing the implementation. This builds on the Core Spring Data Repository Support, so make sure you’ve got a sound understanding of this concept.

+
+
+
+
+

Usage

+
+
+

To access entities stored in Aerospike you can leverage repository support that eases implementing those quite significantly. To do so, simply create an interface for your repository:

+
+
+
Example 1. Sample Person entity
+
+
+
+
public class Person {
+
+	@Id
+	private String id;
+	private String name;
+	private int age;
+	public Person(String id, String name, int age) {
+		this.id = id;
+		this.name = name;
+		this.age = age;
+	}
+  // … getters and setters omitted
+}
+
+
+
+
+
+

We have a quite simple domain object here. The default serialization mechanism used in AerospikeTemplate (which is backing the repository support) regards properties named "id" as document id. Currently we support String and long as id-types.

+
+
+
Example 2. Basic repository interface to persist Person entities
+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, String> {
+
+	List<Person> findByName(String name);
+
+	List<Person> findByNameStartsWith(String prefix);
+
+}
+
+
+
+
+
+

Right now this interface simply serves typing purposes, but we will add additional methods to it later. In your Spring configuration simply add

+
+
+
Example 3. General Aerospike repository Spring configuration
+
+
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:aerospike="http://www.springframework.org/schema/data/aerospike"
+  xsi:schemaLocation="http://www.springframework.org/schema/beans
+    https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+    http://www.springframework.org/schema/data/aerospike
+    https://www.springframework.org/schema/data/aerospike/spring-aerospike-1.0.xsd">
+
+  <aerospike:aerospike id="aerospike" />
+
+  <bean id="aerospikeTemplate" class="rg.springframework.data.aerospike.core.AerospikeTemplate">
+ </bean>
+
+  <aerospike:repositories base-package="org.springframework.data.aerospike.example.data" />
+
+</beans>
+
+
+
+
+
+

This namespace element will cause the base packages to be scanned for interfaces extending AerospikeRepository and create Spring beans for each of them found. By default, the repositories will get an AerospikeTemplate Spring bean wired that is called aerospikeTemplate.

+
+
+

If you’d rather like to go with JavaConfig use the @EnableAerospikeRepositories annotation. The annotation carries the very same attributes like the namespace element. If no base package is configured the infrastructure will scan the package of the annotated configuration class.

+
+
+
Example 4. JavaConfig for repositories
+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackages = "org.springframework.data.aerospike.example")
+public class TestRepositoryConfig {
+	public @Bean(destroyMethod = "close") AerospikeClient aerospikeClient() {
+
+		ClientPolicy policy = new ClientPolicy();
+		policy.failIfNotConnected = true;
+
+		return new AerospikeClient(policy, "52.23.205.208", 3000);
+	}
+
+	public @Bean AerospikeTemplate aerospikeTemplate() {
+		return new AerospikeTemplate(aerospikeClient(), "test");
+	}
+}
+
+
+
+
+
+

As our domain repository extends PagingAndSortingRepository it provides you with CRUD operations as well as methods for paginated and sorted access to the entities. Working with the repository instance is just a matter of dependency injecting it into a client. So accessing the second page of `Person`s at a page size of 10 would simply look something like this:

+
+
+
Example 5. Paging access to Person entities
+
+
+
+
@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class PersonRepositoryTests {
+
+    @Autowired PersonRepository repository;
+
+    @Test
+    public void readsFirstPageCorrectly() {
+
+      Page<Person> persons = repository.findAll(new PageRequest(0, 10));
+      assertThat(persons.isFirstPage(), is(true));
+    }
+}
+
+
+
+
+
+

The sample creates an application context with Spring’s unit test support which will perform annotation-based dependency injection into test cases. Inside the test method, we simply use the repository to query the datastore. We hand the repository a PageRequest instance that requests the first page of persons at a page size of 10.

+
+
+
+
+

Query methods

+
+
+

Most of the data access operations you usually trigger on a repository result in a query being executed against the Aerospike databases. Defining such a query is just a matter of declaring a method on the repository interface

+
+
+
Example 6. PersonRepository with query methods
+
+
+
+
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {
+
+    List<Person> findByName(String name);                              (1)
+
+    Page<Person> findByName(String name, Pageable pageable);           (2)
+
+    List<Person> findByNameStartsWith(String prefix);                  (3)
+
+ }
+
+
+
+
    +
  1. +

    The method shows a query for all people with the given name. The query will be derived by parsing the method name for constraints that can be concatenated with And and Or.

    +
  2. +
  3. +

    Applies pagination to a query. Just equip your method signature with a Pageable parameter and let the method return a Page instance, and it will automatically page the query accordingly (i.e. return the required part of results).

    +
  4. +
  5. +

    Uses query-based partial name search.

    +
  6. +
+
+
+
+
+

Here’s a delete insert and query example

+
+
+
+
@ContextConfiguration(classes = TestRepositoryConfig.class)
+public class RepositoryExample {
+
+	@Autowired
+	protected PersonRepository repository;
+	@Autowired
+	AerospikeOperations aerospikeOperations;
+	@Autowired
+	AerospikeClient client;
+
+	public RepositoryExample(ApplicationContext ctx) {
+		aerospikeOperations = ctx.getBean(AerospikeTemplate.class);
+		repository = (PersonRepository) ctx.getBean("personRepository");
+		client = ctx.getBean(AerospikeClient.class);
+	}
+
+	protected void setUp() {
+		repository.deleteAll();
+		Person dave = new Person("Dave-01", "Matthews", 42);
+		Person donny = new Person("Dave-02", "Macintire", 39);
+		Person oliver = new Person("Oliver-01", "Matthews", 4);
+		Person carter = new Person("Carter-01", "Beauford", 49);
+		Person boyd = new Person("Boyd-01", "Tinsley", 45);
+		Person stefan = new Person("Stefan-01", "Lessard", 34);
+		Person leroi = new Person("Leroi-01", "Moore", 41);
+		Person leroi2 = new Person("Leroi-02", "Moore", 25);
+		Person alicia = new Person("Alicia-01", "Keys", 30);
+		repository.createIndex(Person.class, "person_name_index", "name",
+				IndexType.STRING);
+		List<Person> all = (List<Person>) repository.save(Arrays.asList(oliver,
+				dave, donny, carter, boyd, stefan, leroi, leroi2, alicia));
+	}
+
+	protected void cleanUp() {
+		repository.deleteAll();
+	}
+
+	protected void executeRepositoryCall() {
+		List<Person> result = repository.findByName("Beauford");
+		System.out.println("Results for exact match of 'Beauford'");
+		for (Person person : result) {
+			System.out.println(person.toString());
+		}
+		System.out.println("Results for name starting with letter 'M'");
+		List<Person> resultPartial = repository.findByNameStartsWith("M");
+		for (Person person : resultPartial) {
+			System.out.println(person.toString());
+		}
+	}
+
+	public static void main(String[] args) {
+		ApplicationContext ctx = new AnnotationConfigApplicationContext(
+				TestRepositoryConfig.class);
+		RepositoryExample repositoryExample = new RepositoryExample(ctx);
+		repositoryExample.setUp();
+		repositoryExample.executeRepositoryCall();
+		repositoryExample.cleanUp();
+	}
+}
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/caching.html b/reference/caching.html new file mode 100644 index 00000000..68edb875 --- /dev/null +++ b/reference/caching.html @@ -0,0 +1,897 @@ + + + + + + + +Caching + + + + + +
+
+
+
+

Caching is the process of storing data in a cache or temporary storage location, usually to improve application performance and make data access faster.

+
+
+

The caching process also provides an efficient way to reuse previously retrieved or computed data. +The cache is used to reduce the need for accessing the underlying storage layer which is slower.

+
+
+

Spring Cache with Aerospike database allows you to use annotations such as @Cacheable, @CachePut and @CacheEvict that provide a fully managed cache store using Aerospike database.

+
+
+
+
+

Introduction

+
+
+

In this example, we are going to use the annotations on UserRepository class methods to create/read/update and delete user’s data from the cache.

+
+
+

If a User is stored in the cache, calling a method with @Cacheable annotation will fetch the user from the cache instead of executing the method’s body responsible for the actual user fetch from the database.

+
+
+

If the User does not exist in the cache, the user’s data will be fetched from the database and put in the cache for later usage (a “cache miss”).

+
+
+

With Spring Cache and Aerospike database, we can achieve that with only a few lines of code.

+
+
+
+
+

Motivation

+
+
+

Let’s say that we are using another database as our main data store. +We don’t want to fetch the results from it every time we request the data, instead, we want to get the data from a cache layer.

+
+
+

There is a number of benefits of using a cache layer, here are some of them:

+
+
+
    +
  1. +

    Performance: Aerospike can work purely in RAM but reading a record from Aerospike in Hybrid Memory (primary index in memory, data stored on Flash drives) is extremely fast as well (~1ms).

    +
  2. +
  3. +

    Reduce database load: Moving a significant part of the read load from the main database to Aerospike can help balance the resources on heavy loads.

    +
  4. +
  5. +

    Scalability: Aerospike scales horizontally by adding more nodes to the cluster, scaling a relational database might be tricky and expensive, so if you are facing a read heavy load you can easily scale up the cache layer.

    +
  6. +
+
+
+
+
+

Example

+
+
+

We will not use an actual database as our main data store for this example, instead, we will simulate database access by printing a simulation message and replace a database read by just returning a specific User.

+
+
+

Configuration

+ +
+
+

AerospikeConfigurationProperties

+
+
+
@Data
+@Component
+@ConfigurationProperties(prefix = "aerospike")
+public class AerospikeConfigurationProperties {
+    private String host;
+    private int port;
+}
+
+
+
+
+

AerospikeConfiguration

+
+
+
@Configuration
+@EnableConfigurationProperties(AerospikeConfigurationProperties.class)
+@Import(value = {MappingAerospikeConverter.class, AerospikeMappingContext.class,
+        AerospikeTypeAliasAccessor.class,
+        AerospikeCustomConversions.class, SimpleTypeHolder.class})
+public class AerospikeConfiguration {
+
+    @Autowired
+    private MappingAerospikeConverter mappingAerospikeConverter;
+    @Autowired
+    private AerospikeConfigurationProperties aerospikeConfigurationProperties;
+
+    @Bean(destroyMethod = "close")
+    public AerospikeClient aerospikeClient() {
+        ClientPolicy clientPolicy = new ClientPolicy();
+        clientPolicy.failIfNotConnected = true;
+        return new AerospikeClient(clientPolicy, aerospikeConfigurationProperties.getHost(),
+            aerospikeConfigurationProperties.getPort());
+    }
+
+    @Bean
+    public CacheManager cacheManager(IAerospikeClient aerospikeClient,
+                                            MappingAerospikeConverter aerospikeConverter,
+                                     AerospikeCacheKeyProcessor cacheKeyProcessor) {
+        AerospikeCacheConfiguration defaultConfiguration = new AerospikeCacheConfiguration("test");
+        return new AerospikeCacheManager(aerospikeClient, mappingAerospikeConverter, defaultConfiguration,
+            cacheKeyProcessor);
+    }
+}
+
+
+
+

In the AerospikeConfiguration we will create two types of Beans:

+
+
+
+

AerospikeClient

+
+

Responsible for accessing an Aerospike database and performing database operations.

+
+
+
+

AerospikeCacheManager

+
+

The heart of the cache layer, to define an AerospikeCacheManager you need:

+
+
+
    +
  1. +

    aerospikeClient (AerospikeClient).

    +
  2. +
  3. +

    aerospikeConverter (MappingAerospikeConverter).

    +
  4. +
  5. +

    defaultCacheConfiguration (AerospikeCacheConfiguration), a default cache configuration that applies when creating new caches. +Cache configuration contains a namespace, a set (null by default meaning write directly to the namespace w/o specifying a set) and an expirationInSeconds (AKA TTL, default is 0 meaning use Aerospike server’s default).

    +
  6. +
  7. +

    Optional: initialPerCacheConfiguration (Map<String, AerospikeCacheConfiguration>), You can also specify a map of cache names and matching configuration, it will create the caches with the given matching configuration at the application startup.

    +
  8. +
+
+
+ + + + + +
+
Note
+
+A cache name is only a link to the cache configuration. +
+
+
+
+

Objects

+
+

User

+
+
+
@Data
+@Document
+@AllArgsConstructor
+public class User {
+    @Id
+    private int id;
+    private String name;
+    private String email;
+    private int age;
+}
+
+
+
+
+
+

Repositories

+
+

UserRepository

+
+
+
@Repository
+public class UserRepository {
+
+    @Cacheable(value = "test", key = "#id")
+    public Optional<User> getUserById(int id) {
+        System.out.println("Simulating a read from the main data store.");
+        // In case the id doesn't exist in the cache it will "fetch" jimmy page with the requested id and add it to the cache (cache miss).
+        return Optional.of(new User(id, "jimmy page", "jimmy@gmail.com", 77));
+    }
+
+    @CachePut(value = "test", key = "#user.id")
+    public User addUser(User user) {
+        System.out.println("Simulating addition of " + user + " to the main data store.");
+        return user;
+    }
+
+    @CacheEvict(value = "test", key = "#id")
+    public void removeUserById(int id) {
+        System.out.println("Simulating removal of " + id + " from the main data store.");
+    }
+}
+
+
+
+

The cache annotations require a “value” field, which is the cache name, if the cache name doesn’t exist — by passing initialPerCacheConfiguration param when creating a Bean of AerospikeCacheManager in a configuration class, it will configure the cache with the properties of the given defaultCacheConfiguration (Configuration > AerospikeCacheManager).

+
+
+
+
+

Services

+
+

UserService

+
+
+
@Service
+@AllArgsConstructor
+public class UserService {
+
+    UserRepository userRepository;
+
+    public Optional<User> readUserById(int id) {
+        return userRepository.getUserById(id);
+    }
+
+    public User addUser(User user) {
+        return userRepository.addUser(user);
+    }
+
+    public void removeUserById(int id) {
+        userRepository.removeUserById(id);
+    }
+}
+
+
+
+
+
+

Controllers

+
+

UserController

+
+
+
@RestController
+@AllArgsConstructor
+public class UserController {
+
+    UserService userService;
+
+    @GetMapping("/users/{id}")
+    public Optional<User> readUserById(@PathVariable("id") Integer id) {
+        return userService.readUserById(id);
+    }
+
+    @PostMapping("/users")
+    public User addUser(@RequestBody User user) {
+        return userService.addUser(user);
+    }
+
+    @DeleteMapping("/users/{id}")
+    public void deleteUserById(@PathVariable("id") Integer id) {
+        userService.removeUserById(id);
+    }
+}
+
+
+
+
+
+

Add @EnableCaching

+
+

SimpleSpringBootAerospikeCacheApplication

+
+

Add @EnableCaching to the class that contains the main method.

+
+
+
+
@EnableCaching
+@SpringBootApplication
+public class SimpleSpringBootAerospikeCacheApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SimpleSpringBootAerospikeCacheApplication.class, args);
+    }
+}
+
+
+
+
+
+
+
+

Test

+
+
+

We will use Postman to simulate client requests.

+
+
+

Add User (@CachePut)

+
+
    +
  1. +

    Create a new POST request with the following url: http://localhost:8080/users

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Add a Body in a valid JSON format:

    +
    +
    +
    {
    +   "id":1,
    +   "name":"guthrie",
    +   "email":"guthriegovan@gmail.com",
    +   "age":35
    +}
    +
    +
    +
  6. +
  7. +

    Press Send.

    +
  8. +
+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+| @user_key  | name | @_class | email         | age                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "guthrie" | "com.aerospike.cache.simpleSpringBootAerospikeCache.objects.User"  | "guthriegovan@gmail.com" | 35 |
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+

We can now see that this user was added to the cache.

+
+
+
+

Read User (@Cacheable)

+
+
    +
  1. +

    Create a new GET request with the following url: http://localhost:8080/users/1

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Press Send.

    +
  6. +
+
+
+
+

Remove User (@CacheEvict)

+
+
    +
  1. +

    Create a new DELETE request with the following url: http://localhost:8080/users/1

    +
  2. +
  3. +

    Add a new key-value header in the Headers section:

    +
    +
    +
    Key: Content-Type
    +
    +
    +
    +
    +
    Value: application/json
    +
    +
    +
  4. +
  5. +

    Press Send.

    +
  6. +
+
+
+

We can now see that this user was deleted from the cache (thanks to the @CacheEvict annotation in the UserRepository).

+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+0 rows in set
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+
+

Cache miss (@Cacheable)

+
+

For reading User that is not in the cache we can use the GET request configured before with an id that we know for sure is not there.

+
+
+

If we try calling the GET request with the id 5, we get the following user data:

+
+
+
+
{
+    "id": 5,
+    "name": "jimmy page",
+    "email": "jimmy@gmail.com",
+    "age": 77
+}
+
+
+
+

We wrote it hard-coded in UserRepository to simulate an actual database fetch of a user id that doesn’t exist in the cache.

+
+
+

We can now also see that the user was added to the cache.

+
+
+
+
aql> select * from test
++-----+-----------+----------+-------------+-------------------------------------+
+| @user_key  | name | @_class | email         | age                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "jimmy page" | "com.aerospike.cache.simpleSpringBootAerospikeCache.objects.User"  | "jimmy@gmail.com" | 77 |
++-----+-----------+----------+-------------+-------------------------------------+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/configuration.html b/reference/configuration.html new file mode 100644 index 00000000..3860f959 --- /dev/null +++ b/reference/configuration.html @@ -0,0 +1,1111 @@ + + + + + + + +Configuration + + + + + +
+
+
+
+

Configuration parameters can be set in a standard application.properties file using spring.data.aerospike* prefix +or by overriding configuration from AbstractAerospikeDataConfiguration class.

+
+
+
+
+

Application.properties

+
+
+

Here is an example:

+
+
+
+
# application.properties
+spring.aerospike.hosts=localhost:3000
+spring.data.aerospike.namespace=test
+spring.data.aerospike.scans-enabled=false
+spring.data.aerospike.send-key=true
+spring-data-aerospike.create-indexes-on-startup=true
+spring.data.aerospike.index-cache-refresh-seconds=3600
+spring.data.aerospike.server-version-refresh-seconds=3600
+spring.data.aerospike.query-max-records=10000
+spring.data.aerospike.batch-write-size=100
+spring.data.aerospike.keep-original-key-types=false
+
+
+
+

Configuration class:

+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = {TestRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+
+}
+
+
+
+

In this case extending AbstractAerospikeDataConfiguration class is required to enable repositories.

+
+
+
+
+

Overriding configuration

+
+
+

Configuration can also be set by overriding getHosts(), nameSpace() and configureDataSettings() methods of the AbstractAerospikeDataConfiguration class.

+
+
+

Here is an example:

+
+
+
+
@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("localhost", 3000));
+    }
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setScansEnabled(false);
+        aerospikeDataSettings.setCreateIndexesOnStartup(true);
+        aerospikeDataSettings.setIndexCacheRefreshSeconds(3600);
+        aerospikeDataSettings.setServerVersionRefreshSeconds(3600);
+        aerospikeDataSettings.setQueryMaxRecords(10000L);
+        aerospikeDataSettings.setBatchWriteSize(100);
+        aerospikeDataSettings.setKeepOriginalKeyTypes(false);
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+Return values of getHosts(), nameSpace() and configureDataSettings() methods +of the AbstractAerospikeDataConfiguration class have precedence over the parameters +set via application.properties. +
+
+
+
+
+

Configuration Parameters

+
+
+

hosts

+
+
+
# application.properties
+spring.aerospike.hosts=hostname1:3001, hostname2:tlsName2:3002
+
+
+
+

A String of hosts separated by , in form of hostname1[:tlsName1][:port1],…​

+
+
+

IP addresses must be given in one of the following formats:

+
+
+
+
IPv4: xxx.xxx.xxx.xxx
+IPv6: [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]
+IPv6: [xxxx::xxxx]
+
+
+
+

IPv6 addresses must be enclosed by brackets. tlsName is optional.

+
+
+ + + + + +
+
Note
+
+Another way of defining hosts is overriding the getHosts() method. +It has precedence over hosts parameter from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("hostname1", 3001));
+    }
+}
+
+
+
+

Default: null.

+
+
+
+

namespace

+
+
+
# application.properties
+spring.data.aerospike.namespace=test
+
+
+
+

Aerospike DB namespace.

+
+
+ + + + + +
+
Note
+
+Another way of defining hosts is overriding the nameSpace() method. +It has precedence over namespace parameter from application.properties. +Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+To use multiple namespaces it is required to override nameSpace() and AerospikeTemplate for each +configuration class per namespace. +See multiple namespaces example +for implementation details. +
+
+
+

Default: null.

+
+
+
+

scansEnabled

+
+
+
# application.properties
+spring.data.aerospike.scans-enabled=false
+
+
+
+

A scan can be an expensive operation as all records in the set must be read by the Aerospike server, +and then the condition is applied to see if they match.

+
+
+

Due to the cost of performing this operation, scans from Spring Data Aerospike are disabled by default.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setScansEnabled(false);
+    }
+}
+
+
+
+ + + + + +
+
Note
+
+Once this flag is enabled, scans run whenever needed with no warnings. This may or may not be optimal +in a particular use case. +
+
+
+

Default: false.

+
+
+
+

createIndexesOnStartup

+
+
+
# application.properties
+spring.data.aerospike.create-indexes-on-startup=true
+
+
+
+

Create secondary indexes specified using @Indexed annotation on startup.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setCreateIndexesOnStartup(true);
+    }
+}
+
+
+
+

Default: true.

+
+
+
+

indexCacheRefreshSeconds

+
+
+
# application.properties
+spring.data.aerospike.index-cache-refresh-seconds=3600
+
+
+
+

Automatically refresh indexes cache every <N> seconds.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setIndexCacheRefreshSeconds(3600);
+    }
+}
+
+
+
+

Default: 3600.

+
+
+
+

serverVersionRefreshSeconds

+
+
+
# application.properties
+spring.data.aerospike.server-version-refresh-seconds=3600
+
+
+
+

Automatically refresh cached server version every <N> seconds.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setServerVersionRefreshSeconds(3600);
+    }
+}
+
+
+
+

Default: 3600.

+
+
+
+

queryMaxRecords

+
+
+
# application.properties
+spring.data.aerospike.query-max-records=10000
+
+
+
+

Limit amount of results returned by server. Non-positive value means no limit.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setQueryMaxRecords(10000L);
+    }
+}
+
+
+
+

Default: 10 000.

+
+
+
+

batchWriteSize

+
+
+
# application.properties
+spring.data.aerospike.batch-write-size=100
+
+
+
+

Maximum batch size for batch write operations. Non-positive value means no limit.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setBatchWriteSize(100);
+    }
+}
+
+
+
+

Default: 100.

+
+
+
+

keepOriginalKeyTypes

+
+
+
# application.properties
+spring.data.aerospike.keep-original-key-types=false
+
+
+
+

Define how @Id fields (primary keys) and Map keys are stored in the Aerospike database: +false - always as String, true - preserve original type if supported.

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@Id field typekeepOriginalKeyTypes = falsekeepOriginalKeyTypes = true

long

String

long

int

String

long

String

String

String

byte[]

String

byte[]

other types

String

String

+
+ + + + + +
+
Note
+
+If @Id field’s type cannot be persisted as is, it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data. +
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Map key typekeepOriginalKeyTypes = falsekeepOriginalKeyTypes = true

long

String

long

int

String

long

double

String

double

String

String

String

byte[]

String

byte[]

other types

String

String

+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setKeepOriginalKeyTypes(false);
+    }
+}
+
+
+
+

Default: false (store keys only as String).

+
+
+
+

writeSortedMaps

+
+
+
# application.properties
+spring.data.aerospike.writeSortedMaps=true
+
+
+
+

Define how Maps and POJOs are written: true - as sorted maps (TreeMap, default), false - as unsorted (HashMap).

+
+
+

Writing as unsorted maps (false) degrades performance of Map-related operations and does not allow comparing Maps, +so it is strongly recommended to change the default value only if required during upgrade from older versions +of Spring Data Aerospike.

+
+
+ + + + + +
+
Note
+
+Another way of defining the parameter is overriding the configureDataSettings() method. +It has precedence over reading from application.properties. Here is an example: +
+
+
+
+
// overriding method
+@EnableAerospikeRepositories(basePackageClasses = TestRepository.class)
+class ApplicationConfig extends AbstractAerospikeDataConfiguration {
+
+    @Override
+    protected void configureDataSettings(AerospikeDataSettings aerospikeDataSettings) {
+        aerospikeDataSettings.setWriteSortedMaps(true);
+    }
+}
+
+
+
+

Default: true (write Maps and POJOs as sorted maps).

+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/functionality.html b/reference/functionality.html new file mode 100644 index 00000000..17e907c2 --- /dev/null +++ b/reference/functionality.html @@ -0,0 +1,472 @@ + + + + + + + +Functionality + + + + + +
+
+

Spring Data Aerospike project aims to provide a familiar and consistent Spring-based programming model providing integration with the Aerospike database.

+
+
+

Spring Data Aerospike supports a wide range of features summarized below:

+
+
+ +
+
+ + \ No newline at end of file diff --git a/reference/indexed-annotation.html b/reference/indexed-annotation.html new file mode 100644 index 00000000..2c3fbf53 --- /dev/null +++ b/reference/indexed-annotation.html @@ -0,0 +1,790 @@ + + + + + + + +Indexed Annotation + + + + + +
+
+
+
+

The @Indexed annotation allows to create secondary index based on a specific field of a Java object. +For the details on secondary indexes in Aerospike see Secondary Indexes.

+
+
+

The annotation allows to specify the following parameters:

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parameterindex typemandatoryexample

name

index name

yes

"friend_address_keys_idx"

type

index type

yes

IndexType.STRING

bin

indexed bin type

no

"friend"

collectionType

index type

no

IndexCollectionType.MAPKEYS

ctx

context (path to the indexed elements)

no

"address"

+
+

Here is an example of creating a complex secondary index for fields of a person’s friend address.

+
+
+
+
@Data
+@AllArgsConstructor
+public class Address {
+
+    private String street;
+    private Integer apartment;
+    private String zipCode;
+    private String city;
+}
+
+@Data
+@NoArgsConstructor
+@Setter
+public class Friend {
+
+    String name;
+    Address address;
+}
+
+@Data
+@Document
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+public class Person {
+
+    @Id
+    String id;
+    @Indexed(type = IndexType.STRING, name = "friend_address_keys_idx",
+    collectionType = IndexCollectionType.MAPKEYS, ctx = "address")
+    Friend friend;
+}
+
+@Test
+void test() {
+    Friend carter = new Friend();
+    carter.setAddress(new Address("Street", 14, "1234567890", "City"));
+
+    Person dave = new Person();
+    dave.setFriend(carter);
+    repository.save(dave);
+}
+
+
+
+

A Person object in this example has a field called "friend" (Friend object). +A Friend object has a field called "address" (Address object). +So when "friend" field is set to a Friend with existing Address, we have a person (dave in the example above) with a friend (carter) who has +a particular address.

+
+
+

Address object on its own has certain fields: street, apartment, zipCode, city.

+
+
+ + + + + +
+
Note
+
+In Aerospike DB a POJO (such as Address) is represented by a Map, so the fields of POJO become map keys. +
+
+
+

Thus, if we want to index by Address object fields, we set collectionType to IndexCollectionType.MAPKEYS.

+
+
+

Ctx parameter represents context, or path to the necessary element in the specified bin ("friend") - which is "address", because we want to index by fields of friend’s address.

+
+
+
+
+

Secondary Index Context DSL

+
+
+

Secondary index context (ctx parameter in @Indexed annotation) represents path to a necessary element in hierarchy. It uses infix notation.

+
+
+

The document path is described as dot-separated context elements (e.g., "a.b.[2].c") written as a string. A path is made of singular path elements and ends with one (a leaf element) or more elements (leaves) - for example, "a.b.[2].c.[0:3]".

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Path ElementMatchesNotes

"a"

Map key “a”

Single element by key

"1" or '1'

Map key (numeric string) “1”

1

Map key (integer) 1

{1}

Map index 1

{=1}

Map value (integer) 1

{=bb}

Map value “bb”

Also {="bb"}

{="1"} or {='1'}

Map value (string) “1”

{#1}

Map rank 1

[1]

List index 1

[=1]

List value 1

[#1]

List rank 1

+
+

Example

+
+

Let’s consider a Map bin example:

+
+
+
+
{
+  1: a,
+  2: b,
+  4: d,
+  "5": e,
+  a: {
+    55: ee,
+    "66": ff,
+    aa: {
+      aaa: 111,
+      bbb: 222,
+      ccc: 333,
+    },
+    bb: {
+      bba: 221,
+      bbc: 223
+    },
+    cc: [ 22, 33, 44, 55, 43, 32, 44 ],
+    dd: [ {e: 5, f:6}, {z:26, y:25}, {8: h, "9": j} ]
+  }
+}
+
+
+
+

So the following will be true:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathCTXMatched Value

a.aa.aaa

[mapKey("a"), mapKey("aa"), mapKey("aaa")]

111

a.55

[mapKey("a"), mapKey(55)]

ee

a."66"

[mapKey("a"), mapKey("66")]

ff

a.aa.{2}

[mapKey("a"), mapKey("aa"),mapIndex(2)]

333

a.aa.{=222}

[mapKey("a"), mapKey("aa"),mapValue(222)]

222

a.bb.{#-1}

[mapKey("a"), mapKey("bb"),mapRank(-1)]

223

a.cc.[0]

[mapKey("a"), mapKey("cc"),listIndex(0)]

22

a.cc.[#1]

[mapKey("a"), mapKey("cc"),listRank(1)]

32

a.cc.[=44]

[mapKey("a"), mapKey("cc"),listValue(44)]

[44, 44]

a.dd.[0].e

[mapKey("a"), mapKey("dd"),listIndex(0), mapKey("e")]

5

a.dd.[2].8

[mapKey("a"), mapKey("dd"),listIndex(2), mapKey(8)]

h

a.dd.[-1]."9"

[mapKey("a"), mapKey("dd"),listIndex(-1), mapKey("9")]

j

a.dd.[1].{#0}

[mapKey("a"), mapKey("dd"),listIndex(1), mapRank(0)]

y

+
+ + + + + +
+
Note
+
+There are relevant configuration parameters: +create indexes on startup and +indexes cache refresh frequency. +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/installation-and-usage.html b/reference/installation-and-usage.html new file mode 100644 index 00000000..85aeafaa --- /dev/null +++ b/reference/installation-and-usage.html @@ -0,0 +1,719 @@ + + + + + + + +Installation & Usage + + + + + +
+
+

Getting Started

+
+
+

First, you need a running Aerospike server to connect to.

+
+
+

To use Spring Data Aerospike you can either set up Spring Boot or Spring application. Basic setup of Spring Boot application is described here: https://projects.spring.io/spring-boot.

+
+
+

In case you do not want to use Spring Boot, the best way to manage Spring dependencies is to declare spring-framework-bom of the needed version in the dependencyManagement section of your pom.xml:

+
+
+
+
<dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-framework-bom</artifactId>
+            <version>${spring-data-aerospike.version}</version>
+            <type>pom</type>
+            <scope>import</scope>
+        </dependency>
+    </dependencies>
+</dependencyManagement>
+
+
+
+ + + + + +
+
Note
+
+To create a Spring project in STS (Spring Tool Suite) go to File → New → Spring Template Project → Simple Spring Utility Project → press "Yes" when prompted. Then enter a project and a package name such as org.spring.aerospike.example. +
+
+
+
+
+

Adding Dependency

+
+
+

The first step is to add Spring Data Aerospike to your build process. It is recommended to use the latest version which can be found on the GitHub Releases page.

+
+
+

Adding Spring Data Aerospike dependency in Maven:

+
+
+
+
<dependency>
+    <groupId>com.aerospike</groupId>
+    <artifactId>spring-data-aerospike</artifactId>
+    <version>${spring-data-aerospike.version}</version>
+</dependency>
+
+
+
+

Adding Spring Data Aerospike dependency in Gradle:

+
+
+
+
implementation group: 'com.aerospike', name: 'spring-data-aerospike', version: '${spring-data-aerospike.version}'
+
+
+
+
+
+

Connecting to Aerospike DB

+
+
+

There are two ways of configuring a basic connection to Aerospike DB.

+
+
+
    +
  • +

    Overriding getHosts() and nameSpace() methods of the AbstractAerospikeDataConfiguration class:

    +
  • +
+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = { PersonRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+    @Override
+    protected Collection<Host> getHosts() {
+        return Collections.singleton(new Host("localhost", 3000));
+    }
+    @Override
+    protected String nameSpace() {
+        return "test";
+    }
+}
+
+
+
+
    +
  • +

    Using application.properties:

    +
  • +
+
+
+

Basic configuration in this case requires enabling repositories and then setting hosts and namespace in the application.properties file.

+
+
+
+
@Configuration
+@EnableAerospikeRepositories(basePackageClasses = { PersonRepository.class})
+public class AerospikeConfiguration extends AbstractAerospikeDataConfiguration {
+
+}
+
+
+
+

In application.properties:

+
+
+
+
# application.properties
+spring-data-aerospike.hosts=localhost:3000
+spring-data-aerospike.namespace=test
+
+
+
+ + + + + +
+
Note
+
+Return values of getHosts() and nameSpace() methods of the AbstractAerospikeDataConfiguration class +have precedence over hosts and namespace parameters set via application.properties. +
+
+
+

For more detailed information see Configuration.

+
+
+
+
+

Creating Functionality

+
+
+

The base functionality is provided by AerospikeRepository interface.

+
+
+

It typically takes 2 parameters:

+
+
+
    +
  1. +

    The type managed by a class (it is typically entity class) to be stored in the database.

    +
  2. +
  3. +

    The type of ID.

    +
  4. +
+
+
+

Application code typically extends this interface for each of the types to be managed, and methods can be added to the interface to determine how the application can access the data. For example, consider a class Person with a simple structure:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    @Id
+    private long id;
+    private String firstName;
+    private String lastName;
+    @Field("dob")
+    private Date dateOfBirth;
+}
+
+
+
+

Note that this example uses the Project Lombok annotations to remove the need for explicit constructors and getters and setters. Normal POJOs which define these on their own can ignore the @AllArgsConstructor, @NoArgsConstructor and @Data annotations. The @Document annotation tells Spring Data Aerospike that this is a domain object to be persisted in the database, and @Id identifies the primary key of this class. The @Field annotation is used to create a shorter name for the bin in the Aerospike database (dateOfBirth will be stored in a bin called dob in this example).

+
+
+

For the Person object to be persisted to Aerospike, you must create an interface with the desired methods for retrieving data. For example:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    List<Person> findByLastName(String lastName);
+}
+
+
+
+

This defines a repository that can write Person entities and also query them by last name. The AerospikeRepository extends both PagingAndSortingRepository and CrudRepository, so methods like count(), findById(), save() and delete() are there by default. Those who need reactive flow can use ReactiveAerospikeRepository instead.

+
+
+ + + + + +
+
Note
+
+Repository is just an interface and not an actual class. In the background, when your context gets initialized, actual implementations for your repository descriptions get created, and you can access them through regular beans. This means you will omit lots of boilerplate code while still exposing full CRUD semantics to your service layer and application. +
+
+
+

Example repository is ready for use. A sample Spring Controller which uses this repository could be the following:

+
+
+
+
@RestController
+public class ApplicationController {
+    @Autowired
+    private PersonRepository personRepsitory;
+
+    @GetMapping("/seed")
+    public int seedData() {
+        Person person = new Person(1, "Bob", "Jones", new GregorianCalendar(1971, 12, 19).getTime());
+        personRepsitory.save(person);
+        return 1;
+    }
+
+    @GetMapping("/findByLastName/{lastName}")
+    public List<Person> findByLastName(@PathVariable(name = "lastName", required=true) String lastName) {
+        return personRepsitory.findByLastName(lastName);
+    }
+}
+
+
+
+

Invoking the seed method above gives you a record in the Aerospike database which looks like:

+
+
+
+
aql> select * from test.Person where pk = "1"
++-----+-----------+----------+-------------+-------------------------------------+
+| PK  | firstName | lastName | dob         | @_class                             |
++-----+-----------+----------+-------------+-------------------------------------+
+| "1" | "Bob"     | "Jones"  | 64652400000 | "com.aerospike.sample.model.Person" |
++-----+-----------+----------+-------------+-------------------------------------+
+1 row in set (0.001 secs)
+
+
+
+ + + + + +
+
Note
+
+The fully qualified path of the class is listed in each record. This is needed to instantiate the class correctly, especially in cases when the compile-time type and runtime type of the object differ. For example, where a field is declared as a super class but the instantiated class is a subclass. +
+
+
+ + + + + +
+
Note
+
+By default, the type of the field annotated with @id is turned into a String to be stored in Aerospike database. If the original type cannot be persisted (see keepOriginalKeyTypes for details), it must be convertible to String and will 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 needs to be considered if using external tools like AQL to view the data. +
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/query-methods-collection.html b/reference/query-methods-collection.html new file mode 100644 index 00000000..e4d335df --- /dev/null +++ b/reference/query-methods-collection.html @@ -0,0 +1,631 @@ + + + + + + + +Collection Repository Queries + + + + + +
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByStringList(Collection<String> stringList)
+
+

…​where x.stringList = ?

Not, IsNot

+
+
findByStringListNot(Collection<String> stringList)
+
+

…​where x.stringList <> ?

In

+
+
findByStringListIn(Collection<Collection<String>>)
+
+

…​where x.stringList in ?

Find records where stringList bin value equals one of the collections in the given argument.

Not In

+
+
findByStringListNotIn(Collection<Collection<String>>)
+
+

…​where x.stringList not in ?

Find records where stringList bin value is not equal to any of the collections in the given argument.

Null, IsNull

+
+
findByStringListIsNull()
+
+

…​where x.stringList = null or x.stringList does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByStringListExists()
+
+
+
+
+
findByStringListNotNull()
+
+

…​where x.stringList != null

("Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist in AerospikeDB when their value is not equal to null).

LessThan, IsLessThan

+
+
findByStringListLessThan(Collection<String> stringList)
+
+

…​where x.stringList < ?

Find records where stringList bin value has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByStringListLessThanEqual(Collection<String> stringList)
+
+

…​where x.stringList < = ?

Find records where stringList bin value has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByStringListGreaterThan(Collection<String> stringList)
+
+

…​where x.stringList > ?

Find records where stringList bin value has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByStringListGreaterThanEqual(Collection<String> stringList)
+
+

…​where x.stringList >= ?

Find records where stringList bin value has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByStringListBetween(Collection<String> lowerLimit, Collection<String> upperLimit)
+
+

…​where x.stringList between ? and ?

Find records where stringList bin value is in the range between the given arguments. See information about ordering.

Containing, IsContaining, Contains

+
+
findByStringListContaining(String string)
+
+

…​where x.stringList contains ?

NotContaining, IsNotContaining, NotContains

+
+
findByStringListNotContaining(String string)
+
+

…​where x.stringList not contains ?

And

+
+
findByStringListAndIntList(Collection<String> stringList, Collection<Integer> intList)
+
+

…​where x.stringList = ? and x.intList = ?

Or

+
+
findByStringListOrIntList(Collection<String> stringList, Collection<Integer> intList)
+
+

…​where x.stringList = ? or x.intList = ?

+
+ + \ No newline at end of file diff --git a/reference/query-methods-combined.html b/reference/query-methods-combined.html new file mode 100644 index 00000000..dbcfedd1 --- /dev/null +++ b/reference/query-methods-combined.html @@ -0,0 +1,500 @@ + + + + + + + +Combined Query Methods + + + + + +
+
+

In Spring Data, complex query methods using And or Or conjunction allow developers to define custom database queries based on method names that combine multiple conditions. These methods leverage query derivation, enabling developers to create expressive and type-safe queries by simply defining method signatures.

+
+
+

For more details, see Defining Query Methods.

+
+
+

For instance, a method like findByFirstNameAndLastName will fetch records matching both conditions, while findByFirstNameOrLastName will return records that match either condition. These query methods simplify database interaction by reducing boilerplate code and relying on convention over configuration for readability and maintainability.

+
+
+

In Spring Data Aerospike you define such queries by adding query methods signatures to a Repository, as you would typically, wrapping each query parameter with QueryParam.of() method. This method is required to pass arguments to each part of a combined query, it can receive one or more objects of the same type.

+
+
+

This way QueryParam stores arguments passed to each part of a combined repository query, e.g., repository.findByNameAndEmail(QueryParam.of("John"), QueryParam.of("email")).

+
+
+

Here are some examples:

+
+
+
+
public interface CustomerRepository extends AerospikeRepository<Customer, String> {
+
+     // simple query
+    List<Customer> findByLastName(String lastName);
+
+     // simple query
+    List<Customer> findByFirstName(String firstName);
+
+    // combined query with AND conjunction
+    List<Customer> findByEmailAndFirstName(QueryParam email, QueryParam firstName);
+
+    // combined query with AND conjunctions
+    List<Customer> findByIdAndFirstNameAndAge(QueryParam id, QueryParam firstName, QueryParam age);
+
+    // combined query with OR conjunction
+    List<Consumer> findByFirstNameOrAge(QueryParam firstName, QueryParam age);
+
+    // combined query with AND and OR conjunctions
+    List<Consumer> findByEmailAndFirstNameOrAge(QueryParam email, QueryParam firstName, QueryParam age);
+}
+
+    @Test
+    void findByCombinedQuery() {
+        QueryParam email = QueryParam.of(dave.getEmail());
+        QueryParam name = QueryParam.of(carter.getFirstName());
+        List<Customer> customers = repository.findByEmailAndFirstName(email, name);
+        assertThat(customers).isEmpty();
+
+        QueryParam ids = QueryParam.of(List.of(leroi.getId(), dave.getId(), carter.getId()));
+        QueryParam firstName = QueryParam.of(leroi.getFirstName());
+        QueryParam age = QueryParam.of(leroi.getAge());
+        List<Customer> customers2 = repository.findByIdAndFirstNameAndAge(ids, firstName, age);
+        assertThat(customers).containsOnly(leroi);
+    }
+
+
+
+ + \ No newline at end of file diff --git a/reference/query-methods-id.html b/reference/query-methods-id.html new file mode 100644 index 00000000..18328d27 --- /dev/null +++ b/reference/query-methods-id.html @@ -0,0 +1,485 @@ + + + + + + + +Id Repository Queries + + + + + +
+
+

Id repository reading queries (like findById(), findByIds(), findByFirstNameAndId(), findAllById(), countById(), existsById() etc.) utilize get operation of the underlying Java client (client.get()).

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

no keyword

+
+
findById(String id)
+
+

…​where x.PK = ?

And

+
+
findByIdAndFirstName(String id, String firstName)
+
+

…​where x.PK = ? and x.firstName = ?

+
+ + \ No newline at end of file diff --git a/reference/query-methods-map.html b/reference/query-methods-map.html new file mode 100644 index 00000000..4b408562 --- /dev/null +++ b/reference/query-methods-map.html @@ -0,0 +1,664 @@ + + + + + + + +Map Repository Queries + + + + + +
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByStringMap(Map<String, String> stringMap)
+
+

…​where x.stringMap = ?

Not, IsNot

+
+
findByStringMapNot(Map<String, String> stringMap)
+
+

…​where x.stringMap <> ?

In

+
+
findByStringMapIn(Collection<Map<String, String>>)
+
+

…​where x.stringMap in ?

Find records where stringMap bin value equals one of the maps in the given argument.

Not In

+
+
findByStringMapNotIn(Collection<Map<String, String>>)
+
+

…​where x.stringMap not in ?

Find records where stringMap bin value is not equal to any of the maps in the given argument.

Null, IsNull

+
+
findByStringMapIsNull()
+
+

…​where x.stringMap = null or x.stringMap does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByStringMapExists()
+
+
+
+
+
findByStringMapNotNull()
+
+

…​where x.stringMap != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist when their value is not equal to null.

LessThan, IsLessThan

+
+
findByStringMapLessThan(Map<String, String> stringMap)
+
+

…​where x.stringMap < ?

Find records where stringMap bin value has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByStringMapLessThanEqual(Map<String, String> stringMap)
+
+

…​where x.stringMap < = ?

Find records where stringMap bin value has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByStringMapGreaterThan(Map<String, String> stringMap)
+
+

…​where x.stringMap > ?

Find records where stringMap bin value has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByStringMapGreaterThanEqual(Map<String, String> stringMap)
+
+

…​where x.stringMap >= ?

Find records where stringMap bin value has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByStringMapBetween(Map<String, String> lowerLimit, Map<String, String> upperLimit)
+
+

…​where x.stringMap between ? and ?

Find records where stringMap bin value is in the range between the given arguments. See information about ordering.

Containing, IsContaining, Contains

+
+
findByStringMapContaining(AerospikeQueryCriterion criterion, String string)
+
+findByStringMapContaining(AerospikeQueryCriterion criterionPair, String string, String value)
+
+

…​where x.stringMap contains ?

+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains key "key1":

    +
  • +
+
+
+

findByStringMapContaining(KEY, "key1")

+
+
+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains value "value1":

    +
  • +
+
+
+

findByStringMapContaining(VALUE, "value1")

+
+
+
    +
  • +

    Find records where stringMap bin value (which is a Map) contains key "key1" with the value "value1":

    +
  • +
+
+
+

findByStringMapContaining(KEY_VALUE_PAIR, "key1", "value1")

+

NotContaining, IsNotContaining, NotContains

+
+
findByStringNameNotContaining(AerospikeQueryCriterion criterion, String string)
+
+

…​where x.stringMap not contains ?

findByStringMapNotContaining(KEY, "key1")

+

findByStringMapNotContaining(VALUE, "value1")

+

findByStringMapNotContaining(KEY_VALUE_PAIR, "key1", "value1")

And

+
+
findByStringMapAndIntMap(Map<String, String> stringMap, Map<Integer, Integer> intMap)
+
+

…​where x.stringMap = ? and x.intMap = ?

Or

+
+
findByStringMapOrIntMap(Map<String, String> stringMap, Map<Integer, Integer> intList)
+
+

…​where x.stringMap = ? or x.intMap = ?

+
+ + \ No newline at end of file diff --git a/reference/query-methods-modification.html b/reference/query-methods-modification.html new file mode 100644 index 00000000..1e8fff46 --- /dev/null +++ b/reference/query-methods-modification.html @@ -0,0 +1,565 @@ + + + + + + + +Query Modification + + + + + +
+
+

Query Modifiers

+
+ +++++ + + + + + + + + + + + + + + + + + + + +
KeywordSampleSnippet

IgnoreCase

findByLastNameIgnoreCase

…​where UPPER(x.lastName) = UPPER(?)

OrderBy

findByLastNameOrderByFirstNameDesc

…​where x.lastName = ? order by x.firstName desc

+
+
+
+

Limiting Query Results

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordSampleSnippet

First

findFirstByAge

select top 1 where x.age = ?

First N

findFirst3ByAge

select top 3 where x.age = ?

Top

findTopByLastNameStartingWith

select top 1 where x.lastName like 'abc%' = ?

Top N

findTop4ByLastNameStartingWith

select top 4 where x.lastName like 'abc%'

Distinct

findDistinctByFirstNameContaining

select distinct …​ where x.firstName like 'abc%'

+
+
+
+

Find Using Query

+
+
+

User can perform a custom Query for finding matching entities in the Aerospike database. +A Query can be created using a Qualifier which represents an expression. +It may contain other qualifiers and combine them using either AND or OR.

+
+
+

Qualifier can be created for regular bins, metadata and ids (primary keys). +Below is an example of different variations:

+
+
+
+
    // creating an expression "firsName is equal to John"
+    Qualifier firstNameEqJohn = Qualifier.builder()
+        .setField("firstName")
+        .setFilterOperation(FilterOperation.EQ)
+        .setValue("John")
+        .build();
+    result = repository.findUsingQuery(new Query(firstNameEqJohn));
+    assertThat(result).containsOnly(john);
+
+    // creating an expression "primary key is equal to person's id"
+    Qualifier keyEqJohnsId = Qualifier.idEquals(john.getId());
+    result = repository.findUsingQuery(new Query(keyEqJohnsId));
+    assertThat(result).containsOnly(john);
+
+    // creating an expression "since_update_time metadata value is less than 50 seconds"
+    Qualifier sinceUpdateTimeLt50Seconds = Qualifier.metadataBuilder()
+        .setMetadataField(SINCE_UPDATE_TIME)
+        .setFilterOperation(FilterOperation.LT)
+        .setValue(50000L)
+        .build();
+    result = repository.findUsingQuery(new Query(sinceUpdateTimeLt50Seconds));
+    assertThat(result).contains(john);
+
+    // expressions are combined using AND
+    result = repository.findUsingQuery(new Query(Qualifier.and(firstNameEqJohn, keyEqJohnsId, sinceUpdateTimeLt50Seconds)));
+    assertThat(result).containsOnly(john);
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/query-methods-pojo.html b/reference/query-methods-pojo.html new file mode 100644 index 00000000..1fd116aa --- /dev/null +++ b/reference/query-methods-pojo.html @@ -0,0 +1,611 @@ + + + + + + + +POJO Repository Queries + + + + + +
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByAddress(Address address)
+
+

…​where x.address = ?

Not, IsNot

+
+
findByAddressNot(Address address)
+
+

…​where x.address <> ?

In

+
+
findByAddressIn(Collection<Address>)
+
+

…​where x.address in ?

Find records where address bin value equals one of the Address objects in the given argument.

Not In

+
+
findByAddressNotIn(Collection<Address>)
+
+

…​where x.address not in ?

Find records where address bin value is not equal to any of the Address objects in the given argument.

Null, IsNull

+
+
findByAddressIsNull()
+
+

…​where x.address = null or x.address does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByAddressExists()
+
+
+
+
+
findByAddressNotNull()
+
+

…​where x.address != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist when their value is not equal to null.

LessThan, IsLessThan

+
+
findByAddressLessThan(Address address)
+
+

…​where x.address < ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has fewer elements or has a corresponding element lower in ordering than in the given argument. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByAddressLessThanEqual(Address address)
+
+

…​where x.address < = ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has smaller or the same amount of elements or has each corresponding element lower in ordering or the same as in the given argument. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByAddressGreaterThan(Address address)
+
+

…​where x.address > ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has more elements or has a corresponding element higher in ordering than in the given argument. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByAddressGreaterThanEqual(Address address)
+
+

…​where x.address >= ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) has larger or the same amount of elements or has each corresponding element higher in ordering or the same as in the given argument. See information about ordering.

Between, IsBetween

+
+
findByAddressBetween(Address lowerLimit, Address upperLimit)
+
+

…​where x.address between ? and ?

Find records where address bin value (POJOs are stored in AerospikeDB as maps) is in the range between the given arguments. See information about ordering.

And

+
+
findByAddressAndFriend(Address address, Person friend)
+
+

…​where x.address = ? and x.friend = ?

Or

+
+
findByAddressOrFriend(Address address, Person friend)
+
+

…​where x.address = ? or x.friend = ?

+
+ + \ No newline at end of file diff --git a/reference/query-methods-preface.html b/reference/query-methods-preface.html new file mode 100644 index 00000000..3defe808 --- /dev/null +++ b/reference/query-methods-preface.html @@ -0,0 +1,514 @@ + + + + + + + +Query Methods + + + + + +
+
+
+
+

Spring Data Aerospike supports defining queries by method name in the Repository interface so that the implementation is generated. +The format of method names is fairly flexible, comprising a verb and criteria.

+
+
+

Some of the verbs include find, query, read, get, count and delete. +For example, findByFirstName, countByLastName etc.

+
+
+

For more details, refer to basic Spring Data documentation: Defining Query Methods.

+
+
+
+
+

Repository Query Keywords

+
+
+

Here are the references to the examples of repository queries:

+
+ + +
+

Map

+
+
+

POJO

+
+
+

Id

+
+
+ + + + + +
+
Note
+
+Id repository read queries (like findById(), findByIds(), findByFirstNameAndId(), findAllById(), countById(), existsById() etc.) utilize get() operation of the underlying Java client. Repository read queries without id (like findByFirstName(), findByFirstNameAndLastName(), findAll() etc.) utilize query() operation of the underlying Java client. +
+
+
+
+
+

Repository Interface Example

+
+
+

Below is an example of an interface with several query methods:

+
+
+
+
public interface PersonRepository extends AerospikeRepository<Person, Long> {
+    List<Person> findByLastName(String lastName);
+    List<Person> findByLastNameContaining(String lastName);
+    List<Person> findByLastNameStartingWith(String lastName);
+    List<Person> findByLastNameAndFirstNameContaining(String lastName, String firstName);
+    List<Person> findByAgeBetween(long startAge, long endAge);
+    Optional<Person> findById(Long id);
+}
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/query-methods-simple-property.html b/reference/query-methods-simple-property.html new file mode 100644 index 00000000..08da4f57 --- /dev/null +++ b/reference/query-methods-simple-property.html @@ -0,0 +1,716 @@ + + + + + + + +Simple Property Repository Queries + + + + + +
+
+ + + + + +
+
Note
+
+Repository read queries without id utilize query() operation of the underlying Java client. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeywordRepository query sampleSnippetNotes

Is, Equals

+

or no keyword

+
+
findByLastName(String lastName)
+
+

…​where x.lastName = ?

Not, IsNot

+
+
findByLastNameNot(String lastName)
+
+

…​where x.lastName <> ?

True, isTrue

+
+
findByEnabledTrue()
+
+

…​where x.enabled = true

False, isFalse

+
+
findByEnabledFalse()
+
+

…​where x.enabled = false

In, IsIn

+
+
findByLastNameIn(Collection<String>)
+
+

…​where x.lastName in ?

NotIn, IsNotIn

+
+
findByLastNameNotIn(Collection<String>)
+
+

…​where x.lastName not in ?

Null, IsNull

+
+
findByEmailAddressIsNull()
+
+

…​where x.emailAddress = null or x.emailAddress does not exist

The same as "does not exist", objects and fields exist in AerospikeDB when their value is not equal to null.

Exists

+

NotNull, IsNotNull

+
+
findByEmailAddressExists()
+
+
+
+
+
findByEmailAddressNotNull()
+
+

…​where x.emailAddress != null

"Exists" and "IsNotNull" represent the same functionality and can be used interchangeably, objects and fields exist in AerospikeDB when their value is not equal to null.

LessThan, IsLessThan

+
+
findByAgeLessThan(int age)
+
+findByFirstNameLessThan(String string)
+
+

…​where x.age < ?

+

…​where x.firstName < ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

LessThanEqual, IsLessThanEqual

+
+
findByAgeLessThanEqual(int age)
+
+findByFirstNameLessThanEqual(String string)
+
+

…​where x.age < = ?

+

…​where x.firstName < = ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

GreaterThan, IsGreaterThan

+
+
findByAgeGreaterThan(int age)
+
+findByFirstNameGreaterThan(String string)
+
+

…​where x.age > ?

+

…​where x.firstName > ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

GreaterThanEqual, IsGreaterThanEqual

+
+
findByAgeGreaterThanEqual(int age
+
+findByFirstNameGreaterThanEqual(String string)
+
+

…​where x.age >= ?

+

…​where x.firstName >= ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

Between, IsBetween

+
+
findByAgeBetween(int lowerLimit, int upperLimit)
+
+findByFirstNameBetween(String lowerLimit, String upperLimit)
+
+

…​where x.age between ? and ?

+

…​where x.firstName between ? and ?

Strings are compared by order of each byte, assuming they have UTF-8 encoding. See information about ordering.

Before, IsBefore

+
+
findByDateOfBirthBefore(Date date)
+
+

…​where x.dateOfBirth < ?

After, IsAfter

+
+
findByDateOfBirthAfter(Date date)
+
+

…​where x.dateOfBirth > ?

StartingWith, IsStartingWith, StartsWith

+
+
findByLastNameStartingWith(String string)
+
+

…​where x.lastName like 'abc%'

EndingWith, IsEndingWith, EndsWith

+
+
findByLastNameEndingWith(String string)
+
+

…​where x.lastName like '%abc'

Like, IsLike, MatchesRegex

+
+
findByLastNameLike(String lastNameRegex)
+
+

…​where x.lastName like ?

Containing, IsContaining, Contains

+
+
findByLastNameContaining(String substring)
+
+

…​where x.lastName like '%abc%'

NotContaining, IsNotContaining, NotContains

+
+
findByLastNameNotContaining(String substring)
+
+

…​where x.lastName not like '%abc%'

And

+
+
findByLastNameAndFirstName(String lastName, String firstName)
+
+

…​where x.lastName = ? and x.firstName = ?

Or

+
+
findByLastNameOrFirstName(String lastName, String firstName)
+
+

…​where x.lastName = ? or x.firstName = ?

+
+ + \ No newline at end of file diff --git a/reference/scan-operation.html b/reference/scan-operation.html new file mode 100644 index 00000000..0b25254f --- /dev/null +++ b/reference/scan-operation.html @@ -0,0 +1,482 @@ + + + + + + + +Scan Operation + + + + + +
+
+
+
+

A scan can be an expensive operation as all records in the set must be read by the Aerospike server, +and then the condition is applied to see if they match.

+
+
+

Due to the cost of performing this operation, scans from Spring Data Aerospike are disabled by default.

+
+
+
+
+

Enabling Scan

+
+
+

If the cost of the scans is acceptable to an organization, they can be enabled by setting +scansEnabled parameter to true.

+
+
+
+
spring.data.aerospike.scans-enabled=true
+
+
+
+ + + + + +
+
Note
+
+Once this flag is enabled, scans run whenever needed with no warnings. +This may or may not be optimal in a particular use case. +
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/secondary-indexes.html b/reference/secondary-indexes.html new file mode 100644 index 00000000..2e2e2b79 --- /dev/null +++ b/reference/secondary-indexes.html @@ -0,0 +1,642 @@ + + + + + + + +Secondary indexes + + + + + +
+
+
+
+

A secondary index (SI) is a data structure that locates all the records in a namespace, or a set within it, based on a bin value in the record. +When a value is updated in the indexed record, the secondary index automatically updates.

+
+
+

You can read more about secondary index implementation and usage in Aerospike on the official documentation page.

+
+
+
+
+

Why Secondary Index

+
+
+

Let’s consider a simple query for finding by equality:

+
+
+
+
public List<Person> personRepository.findByLastName(lastName);
+
+
+
+

Notice that findByLastName is not a simple lookup by key, but rather finding all records in a set. +Aerospike has 2 ways of achieving this:

+
+
+
    +
  1. +

    Scanning all the records in the set and extracting the appropriate records.

    +
  2. +
  3. +

    Defining a secondary index on the field lastName and using this secondary index to satisfy the query.

    +
  4. +
+
+
+

The second approach is far more efficient. +Aerospike stores the secondary indexes in a memory structure, allowing exceptionally fast identification of the records that match.

+
+
+

It relies on a secondary index having been created.

+
+
+
+
+

Ways to Create Secondary Indexes

+
+
+

In SpringData Aerospike secondary indexes can either be created by systems administrators using the asadm tool, or by developers telling SpringData that such an index is necessary.

+
+
+

There are two ways to accomplish this task with the help of SpringData Aerospike:

+
+
+
    +
  1. +

    Using AerospikeTemplate createIndex method.

    +
  2. +
  3. +

    Using @Indexed annotation on the necessary field of an entity.

    +
  4. +
+
+
+
+
+

Creating Secondary Index via AerospikeTemplate

+
+
+

For more information about AerospikeTemplate see the documentation page.

+
+
+

Setting a secondary index via AerospikeTemplate can be helpful, for example, in cases when an index creation does not change a lot.

+
+
+

Here is an example of a numeric secondary index for the rating field in the MovieDocument entity:

+
+
+
+
@Slf4j
+@Configuration
+public class AerospikeIndexConfiguration {
+
+    private static final String INDEX_NAME = "movie-rating-index";
+
+    @Bean
+    @ConditionalOnProperty(
+            value = "aerospike." + INDEX_NAME + ".create-on-startup",
+            havingValue = "true",
+            matchIfMissing = true)
+    public boolean createAerospikeIndex(AerospikeTemplate aerospikeTemplate) {
+        try {
+            aerospikeTemplate.createIndex(MovieDocument.class, INDEX_NAME, "rating", IndexType.NUMERIC);
+            log.info("Index {} was successfully created", INDEX_NAME);
+        } catch (Exception e) {
+            log.info("Index {} creation failed: {}", INDEX_NAME, e.getMessage());
+        }
+        return true;
+    }
+}
+
+
+
+
+
+

Creating Secondary Index using @Indexed annotation

+
+
+

You can use @Indexed annotation on the field where the index is required. +Here is an example of the Person object getting indexed by lastName:

+
+
+
+
@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Document
+public class Person {
+    @Id
+    private long id;
+    private String firstName;
+    @Indexed(name = "lastName_idx", type = IndexType.STRING)
+    private String lastName;
+    private Date dateOfBirth;
+}
+
+
+
+

The annotation allows to specify also bin name, collectionType and ctx (context) if needed. +For the details on using @Indexed annotation see Indexed Annotation.

+
+
+
+
+

Matching the Secondary Index

+
+
+ + + + + +
+
Note
+
+In Aerospike, secondary indexes are case-sensitive, they match the exact queries. +
+
+
+

Following the query from the example above, assume there was a new requirement to be able to find by lastName +containing a String (rather than having an equality match):

+
+
+
+
public List<Person> findByLastNameContaining(String lastName);
+
+
+
+

In this case findByLastNameContaining query is not satisfied by the created secondary index. +Aerospike would need to scan the data which can be an expensive operation as all records in the set must be read +by the Aerospike server, and then the condition is applied to see if they match.

+
+
+

Due to the cost of performing this operation, scans from Spring Data Aerospike are disabled by default.

+
+
+

For the details on how to enable scans see Scan Operation.

+
+
+

Following the query from the example above, assume there was a new requirement to be able to find by firstName with an exact match:

+
+
+
+
public List<Person> findByLastName(String lastName);
+public List<Person> findByFirstName(String firstName);
+
+
+
+

In this case firstName is not marked as @Indexed, so SpringData Aerospike is not instructed to create an index on it. +Hence, it will scan the repository (a costly operation that could be avoided by using an index).

+
+
+ + + + + +
+
Note
+
+There are relevant configuration parameters: +create indexes on startup and +indexes cache refresh frequency. +
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/template.html b/reference/template.html new file mode 100644 index 00000000..3800050a --- /dev/null +++ b/reference/template.html @@ -0,0 +1,570 @@ + + + + + + + +Aerospike Template + + + + + +
+
+
+
+

Aerospike Template provides a set of features for interacting with the database. +It allows lower-level access than a Repository and also serves as the foundation for repositories.

+
+
+

Template is the central support class for Aerospike database operations. +It provides the following functionality:

+
+
+
    +
  • +

    Methods to interact with the database

    +
  • +
  • +

    Mapping between Java objects and Aerospike Bins (see Object Mapping)

    +
  • +
  • +

    Providing connection callback

    +
  • +
  • +

    Translating exceptions into Spring’s technology-agnostic DAO exceptions hierarchy

    +
  • +
+
+
+
+
+

Instantiating AerospikeTemplate

+
+
+

If you are subclassing AbstractAerospikeDataConfiguration then the aerospikeTemplate bean is already present in your context, and you can use it.

+
+
+
+
@Autowired
+protected AerospikeTemplate template;
+
+
+
+

An alternative is to instantiate it yourself, you can see the bean in AbstractAerospikeDataConfiguration.

+
+
+

In case if you need to use custom WritePolicy, the persist operation can be used.

+
+
+

For CAS updates save operation must be used.

+
+
+
+
+

Methods for interacting with database

+
+
+

AerospikeOperations interface provides operations for interacting with the database (exists, find, insert, update etc.) as well as basic operations with indexes: createIndex, deleteIndex, indexExists.

+
+
+

The names of operations are typically self-descriptive. To read from Aerospike you can use findById, findByIds and find methods, to delete - delete methods, and so on.

+
+
+
+
template.findById(id, Person.class)
+
+
+
+

For indexed documents use find with provided Query object.

+
+
+
+
Stream<Person> result = template.find(query, Person.class);
+assertThat(result).hasSize(6);
+
+
+
+
+
+

Example

+
+
+

The simple case of using the save operation is to save a POJO.

+
+
+ + + + + +
+
Note
+
+For more information about Id property when inserting or saving see Mapping Conventions: Id Field for more information. +
+
+
+
+
public class Person {
+
+    @Id
+    private String id;
+    private String firstName;
+    private String lastName;
+    private int age;
+}
+
+
+
+
+
template.insert(new Person(id, "John", 50));
+
+long count = template.count
+            (new Query
+                (new QualifierBuilder()
+                    .setFilterOperation(FilterOperation.EQ)
+                    .setField("firstName")
+                    .setValue("John")
+                    .build()
+                ),
+            Person.class
+            );
+
+        assertThat(count).isEqualTo(3);
+
+
+
+
+
+ + \ No newline at end of file diff --git a/reference/transactions.html b/reference/transactions.html new file mode 100644 index 00000000..28a504c1 --- /dev/null +++ b/reference/transactions.html @@ -0,0 +1,679 @@ + + + + + + + +Transactions + + + + + +
+
+
+
+

In the context of database operations, a transaction is a sequence of statements that are executed as a single unit of work. Transactions typically follow the A.C.I.D. principle:

+
+
+
    +
  1. +

    Atomicity ensures that a transaction is treated as a single, indivisible unit; either all operations within the +transaction are completed successfully, or none of them are applied.

    +
  2. +
  3. +

    Consistency ensures that a transaction brings the database from one valid state to another, maintaining all +predefined rules and constraints.

    +
  4. +
  5. +

    Isolation ensures that transactions operate independently of one another, so that intermediate states of a +transaction are not visible to others.

    +
  6. +
  7. +

    Durability guarantees that once a transaction has been committed, its changes are permanent.

    +
  8. +
+
+
+

For more details, see Spring Transaction Management.

+
+
+
+
+

Choosing Transaction Management Model

+
+
+

Spring offers two models of transaction management: declarative and programmatic. When choosing between them, +consider the complexity and requirements of your application.

+
+
+

Declarative transaction management is typically preferred for its simplicity and ease of maintenance, as it allows +to define transaction boundaries using annotations without altering the business logic code. +This model suits for most applications where transaction boundaries are straightforward and the business logic +does not require intricate transaction control.

+
+
+

Programmatic transaction management is chosen when you need more fine-grained control over transactions, +such as handling complex transaction scenarios. +This approach is useful in situations where specific transaction behavior needs to be dynamically adjusted +or when integrating with legacy code that requires explicit transaction management. When using this approach, +it is possible to explicitly start, commit, and rollback transactions within the code if needed.

+
+
+

In general, declarative management is more straightforward and reduces boilerplate code, +while programmatic management offers more control but at the cost of increased complexity.

+
+
+
+
+

Declarative Transaction Management

+
+
+

Declarative transaction management uses annotations to define transaction boundaries and behavior without changing +the business logic code. It’s usually more common in Spring applications due to its simplicity and ease of use.

+
+
+

You can annotate methods and/or classes with @Transactional to automatically handle transactions, including +committing or rolling back based on execution.

+
+
+

Couple other things needed to start working with transactions using declarative approach:

+
+
+
    +
  1. +

    A transaction manager must be specified in your Spring Configuration.

    +
  2. +
  3. +

    Spring Configuration must be annotated with the @EnableTransactionManagement annotation.

    +
  4. +
+
+
+

Example

+
+

Here is an example that shows applying @Transactional to a method. +It ensures that the entire method runs within a transaction context, and Spring manages the transaction lifecycle +(automatically committing the transaction if the method succeeds or rolling back if it encounters an exception).

+
+
+
+
@Configuration
+@EnableTransactionManagement
+public class Config {
+
+    @Bean
+    public AerospikeTransactionManager aerospikeTransactionManager(IAerospikeClient client) {
+        return new AerospikeTransactionManager(client);
+    }
+
+    // Other configuration
+}
+
+@Service
+public class MyService {
+
+    @Transactional
+    public void performDatabaseOperations() {
+        // Perform database operations
+    }
+}
+
+
+
+
+
+
+

Programmatic Transaction Management

+
+
+

Programmatic transaction management gives developers fine-grained control over transactions through code. +This approach involves manually managing transactions using Spring’s API.

+
+
+

The Spring Framework offers two ways for programmatic transaction management:

+
+
+
    +
  1. +

    Using TransactionTemplate or TransactionalOperator which use callback approach +(for programmatic transaction management in imperative code it is typically recommended to use TransactionTemplate; +for reactive code, TransactionalOperator is preferred).

    +
  2. +
  3. +

    Directly using a TransactionManager implementation.

    +
  4. +
+
+
+

Example

+
+

Here is an example that shows using a programmatic transaction in a method. +You would use TransactionTemplate to wrap your database operations in a transaction block, +ensuring the transaction is automatically committed if successful or rolled back if an exception occurs.

+
+
+
+
@Configuration
+public class Config {
+
+    @Bean
+    public AerospikeTransactionManager aerospikeTransactionManager(IAerospikeClient client) {
+        return new AerospikeTransactionManager(client);
+    }
+
+    @Bean
+    public TransactionTemplate transactionTemplate(AerospikeTransactionManager transactionManager) {
+        return new TransactionTemplate(transactionManager);
+    }
+
+    // Other configuration
+}
+
+@Service
+public class MyService {
+
+    @Autowired
+    TransactionTemplate transactionTemplate;
+
+    public void performDatabaseOperations() {
+        transactionTemplate.executeWithoutResult(status -> {
+            // Perform database operations
+        });
+    }
+}
+
+
+
+
+
+
+

Aerospike Operations Support

+
+
+

Behind the curtains Aerospike transaction manager uses MRTs (multi-record transactions) +which is an Aerospike feature allowing to group together multiple Aerospike operation requests +into a single transaction.

+
+
+ + + + + +
+
Note
+
+Not all of the Aerospike operations can participate in transactions. +
+
+
+

Here is a list of Aerospike operations that participate in transactions:

+
+
+
    +
  1. +

    all single record operations (insert, save, update, add, append, persist, findById, exists, delete)

    +
  2. +
  3. +

    all batch operations without query (insertAll, saveAll, findByIds, deleteAll)

    +
  4. +
  5. +

    queries that include id (e.g., repository queries like findByIdAndName)

    +
  6. +
+
+
+

The following operations do not participate in transactions +(will not become part of a transaction if included into it):

+
+
+
    +
  1. +

    truncate

    +
  2. +
  3. +

    queries that do not include id (e.g., repository queries like findByName)

    +
  4. +
  5. +

    operations that perform info commands (e.g., indexExists)

    +
  6. +
  7. +

    operations that perform scans (using ScanPolicy)

    +
  8. +
+
+
+
+
+ + \ No newline at end of file