Skip to main content

Java Object Mapper

Launch in Binder

This notebook demonstrates the use of the Java Object Mapper, which provides a convenient way of saving objects and their relationships in an Aerospike database, and also retrieving them.

This notebook requires Aerospike database running on localhost. Visit Aerospike notebooks repo for additional details and the docker container.

Introduction

The Java Object Mapper code and documentation is available in this repo.

The goal of this tutorial is to describe how to serialize (save) Java objects into Aerospike database and deserialize (load) Aerospike records into Java objects using the object mapper. The tutorial focuses on the core functionality of the object mapper, and points out more advanced topics for the reader to explore.

The object mapper uses Java annotations to define the Aerospoke semantics for the saving and loading behavior. Since the respective annontations are next to the definitions of a class, methods, and fields, the object mapper makes persistence using Aerospike:

  • easier to implement,
  • easier to understand, and
  • less error prone.

The main topics in this notebook include:

  • Basic operations
  • Mapping between Java and Aerospike types
  • Specifying fields to persist
  • List and Map object representation
  • Embedding an object vs storing a reference

Prerequisites

This tutorial assumes familiarity with the following topics:

Setup

Ensure database is running

This notebook requires that Aerospike datbase is running.

import io.github.spencerpark.ijava.IJava;
import io.github.spencerpark.jupyter.kernel.magic.common.Shell;
IJava.getKernelInstance().getMagics().registerMagics(Shell.class);
%sh asd

Download and Install Additional Components

Aerospike Java client 5.1.3 and the object mapper library 2.0.0.

%%loadFromPOM
<dependencies>
<dependency>
<groupId>com.aerospike</groupId>
<artifactId>aerospike-client</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>com.aerospike</groupId>
<artifactId>java-object-mapper</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>

Initialize Client and Define Convenience Functions

Initialize the client, and define a convenience functions truncateTestData to delete test data, and toJsonString to convert and object to JSON notation for printing.

import com.aerospike.client.AerospikeClient;
import com.aerospike.client.AerospikeException;

final String NAMESPACE = "test";

AerospikeClient client = new AerospikeClient("localhost", 3000);
System.out.println("Initialized Aerospike client and connected to the cluster.");

// convenience function to truncate test data
void truncateTestData() {
try {
client.truncate(null, NAMESPACE, null, null);
}
catch (AerospikeException e) {
// ignore
}
}

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
String toJsonString(Object object) {
return gson.toJson(object).toString();
}

Output:

Initialized Aerospike client and connected to the cluster.

Access Shell Commands

You may execute shell commands including Aerospike tools like aql and asadm in the terminal tab throughout this tutorial. Open a terminal tab by selecting File->Open from the notebook menu, and then New->Terminal.

Basic Operations

The basic annotations to save and object are @AerospikeRecord and @AerospikeKey.

  • @AerospikeRecord: Used with a class definition. It defines how the objects of the class should be stored in Aerospike.
  • @AerospikeKey: Used with an attribute within the class to define the object id or “primary key”.

Consider a simple class Person consisting of two fields: ssn and name. The class definiition is annotated with @AerospikeRecord annotation that takes two parameters for the namespace and set where the instances of this class will be stored. The field ssn is the key field and is annotated with @AerospikeKey.

NOTE:

  1. All class attributes or fields are made public for convenience of access in this tutorial. In practice, one would define getter and setter methods for each.
  2. Some class definitions need to change through this tutorial. Becuse a class cannot be redefined in a notebook, multiple versions of a class ClassName are defined with a numeric added to its name like ClassName2, ClassName3, and so on.
  3. If you make changes to a class definition in a code cell and rerun it, you will get kernel errors. Restart the kernel after such code changes.
import com.aerospike.mapper.annotations.AerospikeRecord;
import com.aerospike.mapper.annotations.AerospikeKey;

@AerospikeRecord(namespace="test", set="om-persons")
public class Person {
@AerospikeKey
public String ssn;
public String name;
}

Saving Object

Let's instantiate an object of class Person.

Person p = new Person();
p.ssn = "111-11-1111";
p.name = "John Doe";;

In order to map the object, we first create an AeroMapper object by passing in the AerospikeClient object.

import com.aerospike.mapper.tools.AeroMapper;

AeroMapper mapper = new AeroMapper.Builder(client).build();

Then to save the object, we pass it in the save operation on the Aeromapper instance.

mapper.save(p);

You may view the state of the database by running the following command in the terminal tab:

aql -c "set output raw; select * from test.om-persons"

The output should look like:

*************************** 1. row ***************************
name: "John Doe"
ssn: "111-11-1111"

Reading Object

The object is instantiated in memory through the readoperation on the AeroMapper instance, which takes two parameters:

  • object class
  • object id (i.e., the value of the field annotated by @AerospikeKey annotation)

In our example, these parameters are class Person and string "111-11-1111" respectively.

Person person = mapper.read(Person.class, "111-11-1111");
System.out.format("Instantiated Person object: %s\n", toJsonString(person));;

Output:

Instantiated Person object: {
"ssn": "111-11-1111",
"name": "John Doe"
}

Processing Objects

Stored objects can be retrieved and processed with the find operation that takes the class and a callback. The callback is defined as a mapping function that takes an object and returns a boolean. The class records will continue to be processed with the callback until the function returns false.

import java.util.function.Function;

// let's add another Person object
Person p2 = new Person();
p2.ssn = "222-22-2222";
p2.name = "Jane Doe";
mapper.save(p2);

// the function simply prints the name of a retrieved person record
Function<Person,Boolean> function = person -> {
System.out.println(person.name);
return true;
};

// scan records and process with the function
mapper.find(Person.class, function);

Output:

Jane Doe
John Doe

Deleting Object

A stored object may be deleted from the database with mapper's delete operation.

// delete the person object
mapper.delete(person);

//now try to read back
Person gone = mapper.read(Person.class, "111-11-1111");
System.out.format("Instantiated Person object: %s\n", toJsonString(gone));;

Output:

Instantiated Person object: null

Mapping Java and Aerospike Types

The following table summarizes how Java types are mapped into Aerospike types during save. During read, the Java class definition is used to map Aerospike types into specific Java types.

Java TypeAerospike Type
byte, char, short, int, longInteger
booleanBoolean
float, doubleDouble
java.util.date, java.util.instantInteger
stringString
byte[]Blob
enumString
arrays, List<?>List
MapMap

Specifying Fields for Mapping

By default, all fields are mapped to the bins named after the respective fields. The mapper allows you to select specific fields to save, and also change bin names.

Selecting Specific Fields

In order to specify specific fields to persist, use@AerospikeBin and AerospikeExclude annotations, and the mapAll parameter in @AerospikeRecord.

If it is desired to save only specific bins, annotate those bins with @AerospikeBin and usemapAll = false on @AerospikeRecord. In this case, make sure to annotate the key field also with @AerospikeBin to ensure that the key gets mapped to the database.

In the example below, only the fields explicitly annotated with @AerospikeBin will be stored in the database.

import com.aerospike.mapper.annotations.AerospikeBin;

// drop old data
truncateTestData();

@AerospikeRecord(namespace="test", set="om-persons", mapAll=false) // note mapAll is false
public class Person2 {
@AerospikeBin // explicit @AerospikeBin -> persisted
@AerospikeKey
public String ssn;
@AerospikeBin
public String name; // explicit @AerospikeBin -> persisted
public String notStored; // no @AerospikeBin -> not persisted
}
Person2 p = new Person2();
p.ssn = "222-22-2222";
p.name = "Jane Doe";
p.notStored = "Does Not Persist";

mapper.save(p);

Person2 person = mapper.read(Person2.class, "222-22-2222");
System.out.format("Instantiated object: %s\n", toJsonString(person));;

Output:

Instantiated object: {
"ssn": "222-22-2222",
"name": "Jane Doe",
"notStored": null
}

You can also exclude a specific attribute with an @AerospikeExclude annotation. It would be used with the default setting of mapAll = true.

The above example can be implemented with @AerospikeExclude as follows.

import com.aerospike.mapper.annotations.AerospikeExclude;

// drop old data
truncateTestData();

@AerospikeRecord(namespace="test", set="om-persons") // note, default mapAll is true
public class Person3 {
@AerospikeKey
public String ssn;
public String name;
@AerospikeExclude
public String notStored; // explicitly excluded -> not persisted
}

Person3 p = new Person3();
p.ssn = "333-33-3333";
p.name = "Jack Doe";
p.notStored = "Does Not Persist";

mapper.save(p);

Person3 person = mapper.read(Person3.class, "333-33-3333");
System.out.format("Instantiated object: %s\n", toJsonString(person));;

Output:

Instantiated object: {
"ssn": "333-33-3333",
"name": "Jack Doe",
"notStored": null
}

Specifying Bin Names

By default, a bin name is the same as the respective field name. A bin name can be named differently using @AerospikeBin annotation's name parameter.

In the following example, we change the bin name for the name field to full_name.

// drop the old data
truncateTestData();

@AerospikeRecord(namespace="test", set="om-persons")
public class Person4 {
@AerospikeKey
public String ssn;
@AerospikeBin(name="full_name")
public String name; // stored in bin full_name
}

Person4 p = new Person4();
p.ssn = "444-44-4444";
p.name = "Jill Doe";

mapper.save(p);

Confirm the field name is saved in bin full_name by running the following command in the terminal tab:

aql -c "set output raw; select * from test.om-persons"

The output should be like:

*************************** 1. row ***************************
full_name: "Jill Doe"
ssn: "444-44-4444"

Specifying Object Representation

An object can be embedded in another object. We use @AerospikeEmbed to annotate the field representing the embedded object. An embedded object is implicitly saved and loaded with the embedding object.

An object can be stored in Aerospike as a List or a Map.

For example, consider the Address object:

Address {
street = "100 Main St"
city = "Smartville"
state = "CA"
zipcode = "911001"
}

The object can be stored as a Map using the EmbedType parameter set to MAP in @AerospikeEmbed annotation:

{"street":"100 Main St", "city":"Smartville", "state":"CA", "zipcode":"911001"}

A Map representation is the mapping of field names to their value. While less space efficient than a List (described below), it doesn’t need additional information to be stored for schema versioning.

Alternatively, the above object can be stored as a List of its field values using the EmbedType parameter set to LIST in @AerospikeEmbed annotation::

["100 Main St", "Smartville", "CA", "911001"]

A List representation of an object is space efficient and stores its fields in alphabetical order. An explicit order of fields can be specified using the @AerospikeOrdinal annotation to ensure a specific sort order of a List of such objects.

In the following code, two Address objects are embedded in a Person object. The object home_addr is stored as a Map whereas office_addr is stored as a List.

Note that in the database, the default order of List representation is the alphabetical order of field names. So in the above case, the List order will be: city, state, street, zipcode.

import com.aerospike.mapper.annotations.AerospikeEmbed;
import com.aerospike.mapper.annotations.AerospikeEmbed.EmbedType;

// drop the old data
truncateTestData();

@AerospikeRecord(namespace="test", set="object-mapper")
public class Address {
public String street;
public String city;
public String state;
public String zipcode;
}

@AerospikeRecord(namespace="test", set="om-persons")
public class Person5 {
@AerospikeKey
public String ssn;
public String name;
@AerospikeEmbed(type = EmbedType.MAP)
public Address home_addr; // embedded object in Map representation
@AerospikeEmbed(type = EmbedType.LIST)
public Address office_addr; // embedded object in List representation
}

// home address object
Address home = new Address();
home.street = "555 Burb St";
home.city = "Smartville";
home.state = "CA";
home. zipcode = "911011";

// office address object
Address office = new Address();
office.street = "100 Main St";
office.city = "Smartville";
office.state = "CA";
office. zipcode = "911001";

Person5 p = new Person5();
p.ssn = "555-55-5555";
p.name = "Joey Doe";
p.home_addr = home;
p.office_addr = office;

mapper.save(p);

Run the following commands in the terminal tab:

aql -c "set output raw; select * from test.om-persons"

It should show an output like:

*************************** 1. row ***************************
home_addr: MAP('{"street":"555 Burb St", "city":"Smartville", "zipcode":"911011", "state":"CA"}')
name: "Joey Doe"
office_addr: LIST('["Smartville", "CA", "100 Main St", "911001"]')
ssn: "555-55-5555"

Embedding Object Vs Storing Reference

As noted above, @AerospikeEmbed is used to embed a field representing an object. An embedded object is implicitly saved and loaded with the embedding object.

Note below, when we read the parent object that was saved above, the two embedded objects are retrieved with it.

Person5 person = mapper.read(Person5.class, "555-55-5555");
System.out.format("Instantiated object: %s\n", toJsonString(person));;

Output:

Instantiated object: {
"ssn": "555-55-5555",
"name": "Joey Doe",
"home_addr": {
"street": "555 Burb St",
"city": "Smartville",
"state": "CA",
"zipcode": "911011"
},
"office_addr": {
"street": "100 Main St",
"city": "Smartville",
"state": "CA",
"zipcode": "911001"
}
}

On the other hand, a reference to an object is annotated with @AerospikeReference. A reference is stored within the referring object as the id or key of the annotated object. A referenced object is loaded automatically with the referring object. However, it must be saved explicitly.

Below, we save the course that a person is enrolled for by reference.

Courses: Number: 100, Title: English Number: 200, Title: Math Number: 300, Title: Science Number: 400, Title: History

John's courses: English, Math, History Jill's courses: Math, Science

The following cell shows the code.

Some points to highlight:

  • While embedded objects need not have a key or id attribute, it is a good practice to use an id for flexibility of switching between embed and reference.
  • Use generics (as in List<Course> below) to describe the type as fully as possible.
import com.aerospike.mapper.annotations.AerospikeReference;

// drop old data
truncateTestData();

// course object saved in set "om-courses"
@AerospikeRecord(namespace="test", set="om-courses")
public class Course {
@AerospikeKey
public Integer course_num;
public String title;

public Course(Integer course_num, String title) {
this.course_num = course_num;
this.title = title;
}
}

// define and save courses
Course english = new Course(100, "English");
//mapper.save(english); // reference object need not already exist in database

Course math = new Course(200, "Math");
//mapper.save(math); // reference object need not already exist in database

Course science = new Course(300, "Science");
//mapper.save(science); // reference object need not already exist in database

Course history = new Course(400, "History");
//mapper.save(history); // reference object need not already exist in database

// person object
@AerospikeRecord(namespace="test", set="om-persons")
public class Person6 {
@AerospikeKey
public String ssn;
public String name;
@AerospikeReference
public List<Course> courses; // list of courses by reference; note fully defined generics

public Person6(String ssn, String name) {
this.ssn = ssn;
this.name = name;
}
}

Person6 john = new Person6("111-11-1111", "John Doe");
john.courses = new ArrayList<>(Arrays.asList(english, science, history));
mapper.save(john);

Person6 jane = new Person6("222-22-2222", "Jane Doe");
jane.courses = new ArrayList<>(Arrays.asList(math, science));
mapper.save(jane);

Run the following commands in the terminal tab to see how a list by reference is stored:

aql -c "set output raw; select * from test.om-persons"

It should show an output like:

*************************** 1. row ***************************
courses: LIST('[200, 300]')
name: "Jane Doe"
ssn: "222-22-2222"
*************************** 2. row ***************************
courses: LIST('[100, 300, 400]')
name: "John Doe"
ssn: "111-11-1111"

And to view the courses (if stored separately):

aql> select * from test.om-courses
*************************** 1. row ***************************
course_num: 300
title: "Science"
*************************** 2. row ***************************
course_num: 200
title: "Math"
*************************** 3. row ***************************
course_num: 400
title: "History"
*************************** 4. row ***************************
course_num: 100
title: "English"

Mapping Circular References

The object mapper handles circular references in the object graph. Self-referncing classes are a special case of circular reference. A class that has references to other instances of the same class is often needed. For example, a Person object with a spouse field that is a reference to another Person, and similarly with a children field.

// drop old data
truncateTestData();

// person object
@AerospikeRecord(namespace="test", set="om-persons")
public class Person7 {
@AerospikeKey
public String ssn;
public String name;
@AerospikeReference
public Person7 spouse; // reference to Person7
@AerospikeReference
public List<Person7> children; // list of Person7 references

public Person7(String ssn, String name) {
this.ssn = ssn;
this.name = name;
}
}

Person7 john = new Person7("111-11-1111", "John Doe");
Person7 jane = new Person7("222-22-2222", "Jane Doe");
Person7 jack = new Person7("333-33-3333", "Jack Doe");
Person7 jill = new Person7("444-44-4444", "Jill Doe");

john.spouse = jane;
john.children = new ArrayList<>(Arrays.asList(jack, jill));
jane.spouse = john;
jane.children = new ArrayList<>(Arrays.asList(jack, jill));
mapper.save(john);
mapper.save(jane);
// chidren objects saved with a null spouse and children
mapper.save(jack);
mapper.save(jill);

Run the following commands in the terminal tab to see how the references are stored:

aql -c "set output raw; select * from test.om-persons"

It should show an output like:

*************************** 1. row ***************************
children: LIST('["333-33-3333", "444-44-4444"]')
name: "Jane Doe"
spouse: "111-11-1111"
ssn: "222-22-2222"
*************************** 2. row ***************************
name: "Jack Doe"
ssn: "333-33-3333"
*************************** 3. row ***************************
name: "Jill Doe"
ssn: "444-44-4444"
*************************** 4. row ***************************
children: LIST('["333-33-3333", "444-44-4444"]')
name: "John Doe"
spouse: "222-22-2222"
ssn: "111-11-1111"

Nested Object Graphs

In an arbitrarily deep nested graph, all dependent objects which are @AerospikeRecord will be loaded. If it is desired for the objects not to load dependent data, the reference can be marked with lazy = true.

We will extend the object model with the following relationships:

Person

AccountHolder is-a Person

AccountHolder has-a Account (1:N)

Student is-a Person

Student registersFor Course (M:N)

Course taughtBy Person (N:1)

Some relationshps are stored in both entities in this object model. For example, a course stores a list of its students and each student stores a list of their courses.

A few other things to note:

  • an object may be stored without other objects it references already existing in the database
  • a subclass can be stored in its own namespace and set through its @AerospikeRecord annotation; by default it is stored in its closest ancestor's namespace and set.
  • In the database, the actual class name is stored along with an object reference if the field definition uses its parent class.
// drop old data
truncateTestData();

// person object definition
@AerospikeRecord(namespace="test", set="om-persons")
public class Person8 {
@AerospikeKey
public String ssn;
public String name;

public Person8(String ssn, String name) {
this.ssn = ssn;
this.name = name;
}
}

// account object defiition
public static enum AccountType {
SAVING, CHECKING
}

@AerospikeRecord(namespace="test", set="om-accounts")
public class Account {
@AerospikeKey
public Integer account_num;
public AccountType type;
public Integer balance;
@AerospikeReference
public Person8 owner;

public Account(Integer account_num, AccountType type, Integer balance) {
this.account_num = account_num;
this.type = type;
this.balance = balance;
}
}

// create account objects
Account johnsChecking = new Account(1, AccountType.CHECKING, 1000);
Account johnsSaving = new Account(11, AccountType.SAVING, 100);
Account janesSaving = new Account(12, AccountType.SAVING, 200);
Account petesChecking = new Account(2, AccountType.CHECKING, 2000);
Account willsChecking = new Account(3, AccountType.CHECKING, 3000);
Account willsSaving = new Account(13, AccountType.SAVING, 300);

// account-holder definition
@AerospikeRecord(namespace="test", set="om-acct-holders")
public class AccountHolder extends Person8 {
@AerospikeReference
public List<Account> accounts;
AccountHolder(String ssn, String name) {
super(ssn, name);
}
}

// create account holder objects
AccountHolder john = new AccountHolder("111-11-1111", "John Doe");
AccountHolder jane = new AccountHolder("222-22-2222", "Jane Doe");
AccountHolder pete = new AccountHolder("555-55-5555", "Pete Poe");
AccountHolder will = new AccountHolder("666-66-6666", "Will Woe");

// define account and account-holder relationships
johnsChecking.owner = john;
johnsSaving.owner = john;
john.accounts = new ArrayList<>(Arrays.asList(johnsChecking, johnsSaving));
janesSaving.owner = jane;
jane.accounts = new ArrayList<>(Arrays.asList(janesSaving));
petesChecking.owner = pete;
pete.accounts = new ArrayList<>(Arrays.asList(petesChecking));
willsChecking.owner = will;
willsSaving.owner = will;
will.accounts = new ArrayList<>(Arrays.asList(willsChecking, willsSaving));

// save account objects
mapper.save(johnsChecking); // reference object need not already exist in database
mapper.save(johnsSaving);
mapper.save(janesSaving);
mapper.save(petesChecking);
mapper.save(willsChecking);
mapper.save(willsSaving);

// save account holder objects
mapper.save(john);
mapper.save(jane);
mapper.save(pete);
mapper.save(will);

// course object definition
// course objects are saved in set "om-courses"
@AerospikeRecord(namespace="test", set="om-courses")
public class Course2 {
@AerospikeKey
public Integer course_num;
public String title;
@AerospikeReference
public Person8 teacher;
@AerospikeReference
public List<Person8> students;

public Course2(Integer course_num, String title) {
this.course_num = course_num;
this.title = title;
}
}

// create course objects
Course2 english = new Course2(100, "English");
Course2 math = new Course2(200, "Math");
Course2 science = new Course2(300, "Science");
Course2 history = new Course2(400, "History");

// student object definition
// student objects are saved in set "om-students"
@AerospikeRecord(namespace="test", set="om-students")
public class Student extends Person8 {
@AerospikeReference
public List<Course2> courses;
Student(String ssn, String name) {
super(ssn, name);
}
}

// create student objects
Student jack = new Student("333-33-3333", "Jack Doe");
Student jill = new Student("444-44-4444", "Jill Doe");

// define course and student relationships
english.teacher = pete;
english.students = new ArrayList<>(Arrays.asList(jack));
math.teacher = pete;
math.students = new ArrayList<>(Arrays.asList(jill));
science.teacher = will;
science.students = new ArrayList<>(Arrays.asList(jack, jill));
history.teacher = will;
history.students = new ArrayList<>(Arrays.asList(jack));
jack.courses = new ArrayList<>(Arrays.asList(english, science, history));
jill.courses = new ArrayList<>(Arrays.asList(math, science));

// save course objects
mapper.save(english);
mapper.save(math);
mapper.save(science);
mapper.save(history);

// save student objects
mapper.save(jack);
mapper.save(jill);

Run the following commands in the terminal tab to see how the object graphs is stored:

aql -c "set output raw; select * from test.om-accounts"

Output:

*************************** 1. row ***************************
account_num: 12
balance: 200
owner: LIST('["222-22-2222", "AccountHolder"]')
type: "SAVING"

...

*************************** 6. row ***************************
account_num: 2
balance: 2000
owner: LIST('["555-55-5555", "AccountHolder"]')
type: "CHECKING"

Run:

aql -c "set output raw; select * from test.om-acct-holders"

Output:

*************************** 1. row ***************************
accounts: LIST('[1, 11]')
name: "John Doe"
ssn: "111-11-1111"

...

*************************** 4. row ***************************
accounts: LIST('[3, 13]')
name: "Will Woe"
ssn: "666-66-6666"

Run:

aql -c "set output raw; select * from test.om-courses"

Output:

*************************** 1. row ***************************
course_num: 300
students: LIST('[["333-33-3333", "Student"], ["444-44-4444", "Student"]]')
teacher: LIST('["666-66-6666", "AccountHolder"]')
title: "Science"

...

*************************** 4. row ***************************
course_num: 100
students: LIST('[["333-33-3333", "Student"]]')
teacher: LIST('["555-55-5555", "AccountHolder"]')
title: "English"

aql -c "set output raw; select * from test.om-students"

Output:

*************************** 1. row ***************************
courses: LIST('[200, 300]')
name: "Jill Doe"
ssn: "444-44-4444"
*************************** 2. row ***************************
courses: LIST('[100, 300, 400]')
name: "Jack Doe"
ssn: "333-33-3333"

Let's now try to read back some of the objects from the stored graph topology. Note, we cannot use the JSON print function toJsonString as earlier because it does not handle circular references. Also for each list object, we need to iterate over elements and output an identiying attribute.

// read back the account record with key 1
Account object = mapper.read(Account.class, 1);
System.out.format("Account object: account_num: %d, type: %s, owner: %s\n", object.account_num, object.type, object.owner.name);;

// read back the account holder record with key "111-11-1111"
AccountHolder object = mapper.read(AccountHolder.class, "111-11-1111");
System.out.format("AccountHolder object: ssn: %s, name: %s, ", object.ssn, object.name);
ListIterator<Account> iter = object.accounts.listIterator();
ArrayList<Integer> list = new ArrayList<Integer>();
while (iter.hasNext()) {
list.add(iter.next().account_num);
}
System.out.format("accounts: %s\n", list);

// read back the course record with key 100
Course2 object = mapper.read(Course2.class, 100);
System.out.format("Course object: course_num: %s, title: %s, ", object.course_num, object.title);
ListIterator<Person8> iter = object.students.listIterator();
ArrayList<String> list = new ArrayList<String>();
while (iter.hasNext()) {
list.add(iter.next().name);
}
System.out.format("students: %s\n", list);

// read back the student record with key "333-33-3333"
Student object = mapper.read(Student.class, "333-33-3333");
System.out.format("Student object: ssn: %s, name: %s, ", object.ssn, object.name);
ListIterator<Course2> iter = object.courses.listIterator();
ArrayList<String> list = new ArrayList<String>();
while (iter.hasNext()) {
list.add(iter.next().title);
}
System.out.format("courses: %s\n", list);;

Output:

Account object: account_num: 1, type: CHECKING, owner: John Doe
AccountHolder object: ssn: 111-11-1111, name: John Doe, accounts: [1, 11]
Course object: course_num: 100, title: English, students: [Jack Doe]
Student object: ssn: 333-33-3333, name: Jack Doe, courses: [English, Science, History]

More Advanced Topics

The object mapper provides many sophisticated mechanisms, many of which are listed below, to design and implementat real life use cases. Please visit the object mapper repo for their description and examples.

  • Config via YAML string, and config file with precedence rules.
  • Policy specification and precedence rules
  • Versioning
  • Class hierarchies
  • Object deserialization using constructor factories
  • Custom data converters, custom getter/setter methods

Annotations Summary

The table below provides a summary of various annotations available in the object mapper.

AnnotationApplied ToParameters (default)Description
@AerospikeRecordclassnamespace, set, mapAll (true), version (1), factoryMethod (null), factoryClass (null), shortName(null), ttl, durableDelete, sendKeyDetails of record location and metadata
@AerospikeKeyfield or methodObject id
@AerospikeBinfieldbin name (field name)Persisted field and bin name
@AerospikeExcludefieldExcluded field
@AerospikeEmbedfield (object)type (Map or List)Embedded object and rep
@AerospikeReferencefield (object)lazy (false), type (key)Referenced object and loading
@AerospikeVersionfield (object)min (1), maxField version validity
@AerospikeOrdinalfield (object)valueField order in List rep
@AerospikeSettermethodfield nameCustom setter method
@AerospikeGettermethodfield nameCustom getter method
@ParamFromconstructor argumentsbin nameArgument in constructor
@AerospikeConstructorconstructorConstructor to be used
@ToAerospikedata converter methodCustom conversion
@FromAerospikedata converter methodCustom conversion

Takeaways

Persisting objects connected with complex relationships in Aerospike requires mastery of Aerospike APIs, and doing so manually can be tricky to maintain and error prone. The object mapper uses Java annotations to define the Aerospoke semantics for saving and loading behavior. As annontations appear next to the class, method, and field definitions, the object mapper makes persistence using Aerospike:

  • easier to implement,
  • easier to understand, and
  • less error prone.

Cleaning Up

Remove tutorial data and close connection.

truncateTestData();
client.close();
System.out.println("Removed tutorial data and closed server connection.");

Output:

Removed tutorial data and closed server connection.

Further Exploration and Resources

Here are some links for further exploration.

Resources

Exploring Other Notebooks

Visit Aerospike notebooks repo to run additional Aerospike notebooks. To run a different notebook, download the notebook from the repo to your local machine, and then click on File->Open in the notebook menu, and select Upload.