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

Announcing the Spring Data Aerospike 4.6.0 release

Untitled
Andrey Grosland
Software Engineer
January 29, 2024|10 min read

The new Spring Data Aerospike release 4.6.0 introduces several new features and enhancements. In this article, we will talk about customizing set names and using batch write operations. Other major new features are described in the second part.

Supporting explicit set name parameter

Spring Data Aerospike has a mechanism for determining the default set name based on a document’s class. This way, the user does not need to provide a set name explicitly:

public class Person {
    @Id
    private String id;
    private String name;
}
// Create new document
Person jack = new Person("id2", "Jack");
// Save a document using the default set name “Person”
template.save(jack);

However, there are certain use cases when it is required to specify customized set names explicitly (e.g., having meaningful set names treated as criteria for sharding entities by different sets, etc.).

In previous versions of Spring Data Aerospike, the only way of set name customization was to use the "collection" attribute in the @Document annotation:

@Document(collection = “customSetName”)
public class Person {
    @Id
    private String id;
    private String name;
}

With the 4.6.0 release, users now have the ability to provide set name per operation. Developers can easily specify which set they want to use and avoid potential conflicts with data from other sets.

Here is an example:

// Create a new document
Person george = new Person("id1", "George");
// Save the document using the specified set name instead of default set “Person”
template.save(george, "test_set");
// Find the created record in the specified set
assertThat(template.findById("id1", Person.class, "test_set")).isEqualTo(george);

Supporting batch-write operations

A batch is a series of requests that are sent together to the database server. A batch groups multiple operations into one unit and passes it in a single network trip to each database node. There are certain advantages to using batch operations; see the documentation for more details.

Release 4.6.0 of Spring Data Aerospike contains the following methods that make use of batch-write operations:

AerospikeRepository: deleteAllById(), saveAll()
AerospikeTemplate: deleteAllById(), deleteByIds(), saveAll(), insertAll(), updateAll().

Aerospike server version >= 6.0 is required to use the methods listed above. Each of them throws AerospikeException.BatchRecordArray if results contain errors and org.springframework.dao.DataAccessException if the batch operation has failed.

The following code snippets below provide examples of utilizing batch-write operations in Spring Data Aerospike.

Versioned and non-versioned documents

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.

public class VersionedClass {
   @Version
   private int version;
   @Id
   private String id;
   private String field;
  }

Before a document is saved for the first time, its version is zero. Once a document is saved, the generation number of its corresponding record is retrieved from the database and set as the document’s version.

This means a generation check will be applied if a versioned document is retrieved, modified, and then saved. If another thread modifies the record in the meantime, an exception will be thrown.

Save multiple new documents in one batch request

Non-versioned documents

You can save multiple non-versioned documents in one request. If a corresponding database record does not exist, it will be created; otherwise, it will be updated (an "upsert").

Person john = new Person("id1", "John");
Person jack = new Person("id2", "Jack");
// Saving multiple non-versioned documents in one request to create database records
template.saveAll(List.of(john, jack)); 
 // Database record has been created 
assertThat(template.findById("id1", Person.class)).isEqualTo(john);
// Database record has been updated 
assertThat(template.findById("id2", Person.class)).isEqualTo(jack); 

Versioned documents

Multiple versioned documents can also be saved in one request. The version property determines whether to create a new database record or update an existing one.

// This is a versioned document (with a class field annotated with @Version) 
// It is being created, so its version is equal to zero
VersionedClass first = new VersionedClass("newId1", "foo");
// In this case, zero version gets explicitly passed to the constructor
VersionedClass second = new VersionedClass("newId2", "bar", 0); 
// The documents’ versions are equal to zero meaning the documents have not been saved to DB yet
assertThat(first.getVersion() == 0).isTrue(); 
assertThat(second.getVersion() == 0).isTrue();
// Save multiple versioned documents in one request. If the document's version is equal to zero, the document is saved as a new Aerospike database record
template.saveAll(List.of(first, second)); 
// Database records are created
assertThat(template.findById("newId1", VersionedClass.class).version).isEqualTo(1); 
assertThat(template.findById("newId2", VersionedClass.class).version).isEqualTo(1);
// Generation counts of the database records have been retrieved to update the version of the documents, which are now equal to one
assertThat(first.version).isEqualTo(1); 
assertThat(second.version).isEqualTo(1);

Saving the same versioned documents within one batch is not allowed

// Versioned documents are created
VersionedClass first = new VersionedClass("newId1", "foo");  
VersionedClass second = new VersionedClass("newId2", "bar");
// The documents’ versions are equal to zero, meaning the documents have not been saved to the database yet
assertThat(first.getVersion() == 0).isTrue(); 
assertThat(second.getVersion() == 0).isTrue();
// An attempt to save the same versioned documents in one batch results in getting an exception
assertThatThrownBy(() -> {
template.saveAll(List.of(first, first, second, second))}) 
    .isInstanceOf(AerospikeException.BatchRecordArray.class)
    .hasMessageContaining("Errors during batch save");
// The document's version gets updated after it is read from the corresponding database record
assertThat(first.getVersion() == 1).isTrue(); 
assertThat(second.getVersion() == 1).isTrue();

The same versioned documents can be saved if they are not in the same batch. This way, the generation counts of the corresponding database records can be used to update the documents’ versions each time.

VersionedClass first = new VersionedClass("newId1", "foo");
VersionedClass second = new VersionedClass("newId2", "bar");
  
assertThat(first.getVersion() == 0).isTrue();
assertThat(second.getVersion() == 0).isTrue();
template.saveAll(List.of(first, second));
assertThat(first.getVersion() == 1).isTrue();
assertThat(second.getVersion() == 1).isTrue();
template.saveAll(List.of(first, second));
assertThat(first.getVersion() == 2).isTrue();
assertThat(second.getVersion() == 2).isTrue();

Insert multiple new records in one batch request

You can insert multiple documents in one request. It is a “create only” operation.

Non-versioned documents

Person john = new Person("id1", "John");
Person jack = new Person("id2", "Jack");
// Insert multiple non-versioned documents in one request. If a corresponding record does not exist it will be created, otherwise rejected
template.insertAll(List.of(john, jack)); 
// Database record has been created 
assertThat(template.findById("id1", Person.class)).isEqualTo(john); 
// Database record has been updated 
assertThat(template.findById("id2", Person.class)).isEqualTo(jack); 

Versioned documents

// This document has a version (class field annotated with @Version). The constructor does not receive the version parameter, so it stays equal to zero
VersionedClass first = new VersionedClass("id1", "foo");  
// In this case, the non-zero version gets explicitly passed to the constructor
VersionedClass second = new VersionedClass("id2", "foo", 1); 
// In this case, the non-zero version gets explicitly passed to the constructor
VersionedClass third = new VersionedClass("id3", "foo", 2); 
assertThat(first.getVersion() == 0).isTrue();
assertThat(second.getVersion() == 1).isTrue(); assertThat(third.getVersion() == 2).isTrue(); 
// Insert multiple versioned documents to create new database records in one request
template.insertAll(List.of(first, second, third)); 
// Initial document versions are overridden by the actual generation counts retrieved from the created database records
assertThat(first.getVersion() == 1).isTrue();
assertThat(second.getVersion() == 1).isTrue(); assertThat(third.getVersion() == 1).isTrue(); 

Inserting documents if the corresponding records already exist is not allowed

// This class has a version (class field annotated with @Version). The constructor does not receive the version parameter, so it stays equal to zero
VersionedClass first = new VersionedClass("newId1", "foo"); 
VersionedClass second = new VersionedClass("newId2", "bar");
// The documents' versions are equal to zero meaning the documents have not been saved to the database yet
assertThat(first.getVersion() == 0).isTrue(); 
assertThat(second.getVersion() == 0).isTrue();
// Inserting the documents
template.insertAll(List.of(first, second)));
// An attempt to insert the same versioned documents repeatedly results in getting an exception
assertThatThrownBy(() -> template.insertAll(List.of(first, second))) 
    .isInstanceOf(AerospikeException.BatchRecordArray.class)
    .hasMessageContaining("Errors during batch insert");
// The document versions are updated after they have been read from the corresponding database records
assertThat(first.getVersion() == 1).isTrue(); 
assertThat(second.getVersion() == 1).isTrue();

Update multiple new records in one batch request

You can update multiple documents in one request. It is an “update only” operation.

Non-versioned documents

int age1 = 40;
int age2 = 50;
// Create new non-versioned documents
Person person1 = new Person(“id1”, "Jack", age1); 
Person person2 = new Person(“id2”, "John", age2); 
// Insert multiple documents in one request
template.insertAll(List.of(person1, person2)); 
// Update the “first name” field in the documents
person1.setFirstName("Jack M");
person2.setFirstName("John B");
// Update multiple records in one request
template.updateAll(List.of(person1, person2)); 
Person result1 = template.findById(person1.getId(), Person.class);
Person result2 = template.findById(person2.getId(), Person.class);
// Check that the “age” field is not changed
assertThat(result1.getAge()).isEqualTo(age1);
assertThat(result2.getAge()).isEqualTo(age2); 
// Check that the update is written to the database and the “first name” field is changed
assertThat(result1.getFirstName()).isEqualTo("Jack M"); 
assertThat(result2.getFirstName()).isEqualTo("John B"); 

Versioned documents

// This document has a version (class field annotated with @Version). The constructor does not receive the version parameter, so it stays equal to zero
VersionedClass first = new VersionedClass("id1", "foo"); 
// In this case non-zero version gets explicitly passed to the constructor
VersionedClass second = new VersionedClass("id2", "foo", 1);
VersionedClass third = new VersionedClass("id3", "foo", 2); 
// Insert multiple versioned documents to create new database records
template.insertAll(List.of(first, second, third)); 
// Initial document's version is overridden by the version from the created database record
assertThat(first.getVersion() == 1).isTrue();
assertThat(second.getVersion() == 1).isTrue(); 
// Initial document's version is overridden by the version from the created database record
assertThat(third.getVersion() == 1).isTrue(); 
// Update the field in the documents
first = new VersionedClass(first.getId(), "foobar1", first.getVersion());
second = new VersionedClass(second.getId(), "foobar2", second.getVersion());
third = new VersionedClass(third.getId(), "foobar3", third.getVersion());
template.updateAll(List.of(first, second, third));
// Check that the values of the field in the database records have changed
assertThat(template.findById(first.getId(), VersionedClass.class)).satisfies(doc -> {
   assertThat(doc.getField()).isEqualTo("foobar1");
   assertThat(doc.getVersion()).isEqualTo(2);
});
assertThat(template.findById(second.getId(), VersionedClass.class)).satisfies(doc -> {
   assertThat(doc.getField()).isEqualTo("foobar2");
   assertThat(doc.getVersion()).isEqualTo(2);
});
assertThat(template.findById(third.getId(), VersionedClass.class)).satisfies(doc -> {
   assertThat(doc.getField()).isEqualTo("foobar3");
   assertThat(doc.getVersion()).isEqualTo(2);
});

Updating documents if the corresponding records do not exist is not allowed

// This class has a version (class field annotated with @Version). The constructor does not receive the version parameter, so it stays equal to zero
VersionedClass first = new VersionedClass("newId1", "foo"); 
VersionedClass second = new VersionedClass("newId2", "bar");
// The document's version is zero meaning the documents have not been saved to database yet
assertThat(first.getVersion() == 0).isTrue(); 
assertThat(second.getVersion() == 0).isTrue();
// Insert the first document
template.insert(first); 
// The document's version is one meaning there is a corresponding database record
assertThat(first.getVersion() == 1).isTrue(); 
// An attempt to update without preexisting database records results in getting a BatchRecordArray exception
assertThatThrownBy(() -> template.updateAll(List.of(first, second))) 
    .isInstanceOf(AerospikeException.BatchRecordArray.class)
    .hasMessageContaining("Errors during batch update");
// This document's version gets updated after it is read from the corresponding database record
assertThat(first.getVersion() == 2).isTrue(); 
// This document's version stays equal to zero as there is no corresponding database record
assertThat(second.getVersion() == 0).isTrue(); 

Delete multiple records in one batch request

You can delete multiple documents in one request.

In the following example, the documents of type ‘Person,’ defined as a repository type, provide information about the used Aerospike set.

personRepository.deleteAllById(List.of(dave.getId(), carter.getId()));
 // The corresponding records are deleted
assertThat(personRepository.findAllById(List.of(dave.getId(), carter.getId()))).isEmpty();

Learn more about Spring Data Aerospike 4.6.0

For more details about the Spring Data Aerospike project, look at our GitHub page. See Spring Data Aerospike 4.6.0 release notes.