Java Object Mapper
For an interactive Jupyter notebook experience:
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 database 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 andasadm 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:
- 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.
- 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 likeClassName2
,ClassName3
, and so on. - 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 read
operation on the
AeroMapper
instance, which takes two parameters:
- object class
- object id (value of the field annotated by the
@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 Type | Aerospike Type |
---|---|
byte, char, short, int, long | Integer |
boolean | Boolean |
float, double | Double |
java.util.date, java.util.instant | Integer |
string | String |
byte[] | Blob |
enum | String |
arrays, List<?> | List |
Map | Map |
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.
Annotation | Applied To | Parameters (default) | Description |
---|---|---|---|
@AerospikeRecord | class | namespace, set, mapAll (true), version (1), factoryMethod (null), factoryClass (null), shortName(null), ttl, durableDelete, sendKey | Details of record location and metadata |
@AerospikeKey | field or method | Object id | |
@AerospikeBin | field | bin name (field name) | Persisted field and bin name |
@AerospikeExclude | field | Excluded field | |
@AerospikeEmbed | field (object) | type (Map or List) | Embedded object and rep |
@AerospikeReference | field (object) | lazy (false), type (key) | Referenced object and loading |
@AerospikeVersion | field (object) | min (1), max | Field version validity |
@AerospikeOrdinal | field (object) | value | Field order in List rep |
@AerospikeSetter | method | field name | Custom setter method |
@AerospikeGetter | method | field name | Custom getter method |
@ParamFrom | constructor arguments | bin name | Argument in constructor |
@AerospikeConstructor | constructor | Constructor to be used | |
@ToAerospike | data converter method | Custom conversion | |
@FromAerospike | data converter method | Custom 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
- Video
- Github repo
- Related notebooks
- Other Github repos
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.