{ "cells": [ { "cell_type": "markdown", "metadata": { "toc": true }, "source": [ "

Table of Contents

\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Java Object Mapper\n", "\n", "This notebook demostrates the use of the [Java Object Mapper](https://github.com/aerospike/java-object-mapper), which provides a convenient way of saving objects and their relationships in Aerospike database, and also retrieving them. \n", "\n", "This notebook requires Aerospike database running on localhost. Visit [Aerospike notebooks repo](https://github.com/aerospike-examples/interactive-notebooks) for additional details and the docker container." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction\n", "The Java Object Mapper code and documentation is available in [this repo](https://github.com/aerospike/java-object-mapper).\n", " \n", "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.\n", "\n", "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:\n", "- easier to implement, \n", "- easier to understand, and \n", "- less error prone.\n", "\n", "The main topics in this notebook include:\n", "- Basic operations\n", "- Mapping between Java and Aerospike types\n", "- Specifying fields to persist\n", "- List and Map object representation\n", "- Embedding an object vs storing a reference" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prerequisites\n", "This tutorial assumes familiarity with the following topics:\n", "- [Aerospike Notebooks - Readme and Tips](../readme_tips.ipynb)\n", "- [Hello World](hello_world.ipynb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ensure database is running\n", "This notebook requires that Aerospike datbase is running." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import io.github.spencerpark.ijava.IJava;\n", "import io.github.spencerpark.jupyter.kernel.magic.common.Shell;\n", "IJava.getKernelInstance().getMagics().registerMagics(Shell.class);\n", "%sh asd" ] }, { "cell_type": "markdown", "metadata": { "hide_input": false }, "source": [ "### Download and Install Additional Components\n", "Aerospike Java client 5.1.3 and the object mapper library 2.0.0." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "%%loadFromPOM\n", "\n", " \n", " com.aerospike\n", " aerospike-client\n", " 5.1.3\n", " \n", " \n", " com.aerospike\n", " java-object-mapper\n", " 2.0.0\n", " \n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Initialize Client and Define Convenience Functions\n", "Initialize the client, and define a convenience functions `truncateTestData` to delete test data, and `toJsonString` to convert and object to JSON notation for printing." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initialized Aerospike client and connected to the cluster.\n" ] } ], "source": [ "import com.aerospike.client.AerospikeClient;\n", "import com.aerospike.client.AerospikeException;\n", "\n", "final String NAMESPACE = \"test\";\n", "\n", "AerospikeClient client = new AerospikeClient(\"localhost\", 3000);\n", "System.out.println(\"Initialized Aerospike client and connected to the cluster.\");\n", "\n", "// convenience function to truncate test data\n", "void truncateTestData() {\n", " try {\n", " client.truncate(null, NAMESPACE, null, null);\n", " }\n", " catch (AerospikeException e) {\n", " // ignore\n", " }\n", "}\n", "\n", "import com.google.gson.Gson; \n", "import com.google.gson.GsonBuilder; \n", "\n", "Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();\n", "String toJsonString(Object object) {\n", " return gson.toJson(object).toString();\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Access Shell Commands\n", "You may execute shell commands including Aerospike tools like [aql](https://docs.aerospike.com/docs/tools/aql/index.html) and [asadm](https://docs.aerospike.com/docs/tools/asadm/index.html) in the terminal tab throughout this tutorial. Open a terminal tab by selecting File->Open from the notebook menu, and then New->Terminal." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Basic Operations \n", "\n", "The basic annotations to save and object are @AerospikeRecord and @AerospikeKey.\n", "- @AerospikeRecord: Used with a class definition. It defines how the objects of the class should be stored in Aerospike. \n", "- @AerospikeKey: Used with an attribute within the class to define the object id or “primary key”.\n", "\n", "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`.\n", "\n", "NOTE: \n", "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.\n", "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.\n", "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import com.aerospike.mapper.annotations.AerospikeRecord;\n", "import com.aerospike.mapper.annotations.AerospikeKey;\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Saving Object\n", "Let's instantiate an object of class `Person`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "Person p = new Person();\n", "p.ssn = \"111-11-1111\";\n", "p.name = \"John Doe\";;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to map the object, we first create an `AeroMapper` object by passing in the `AerospikeClient` object." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import com.aerospike.mapper.tools.AeroMapper;\n", "\n", "AeroMapper mapper = new AeroMapper.Builder(client).build();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then to save the object, we pass it in the `save` operation on the Aeromapper instance. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "mapper.save(p);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You may view the state of the database by running the following command in the terminal tab:\n", "\n", "```\n", "aql -c \"set output raw; select * from test.om-persons\"\n", "```\n", "The output should look like:\n", "```\n", "*************************** 1. row ***************************\n", "name: \"John Doe\"\n", "ssn: \"111-11-1111\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Reading Object\n", "The object is instantiated in memory through the `read`operation on the `AeroMapper` instance, which takes two parameters:\n", "- object class\n", "- object id (i.e., the value of the field annotated by `@AerospikeKey` annotation)\n", "\n", "In our example, these parameters are class `Person` and string \"111-11-1111\" respectively." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Instantiated Person object: {\n", " \"ssn\": \"111-11-1111\",\n", " \"name\": \"John Doe\"\n", "}\n" ] } ], "source": [ "Person person = mapper.read(Person.class, \"111-11-1111\");\n", "System.out.format(\"Instantiated Person object: %s\\n\", toJsonString(person));;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Processing Objects\n", "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`." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jane Doe\n", "John Doe\n" ] } ], "source": [ "import java.util.function.Function;\n", "\n", "// let's add another Person object\n", "Person p2 = new Person();\n", "p2.ssn = \"222-22-2222\";\n", "p2.name = \"Jane Doe\";\n", "mapper.save(p2);\n", "\n", "// the function simply prints the name of a retrieved person record\n", "Function function = person -> {\n", " System.out.println(person.name);\n", " return true; \n", "};\n", "\n", "// scan records and process with the function\n", "mapper.find(Person.class, function);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Deleting Object\n", "A stored object may be deleted from the database with mapper's `delete` operation." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Instantiated Person object: null\n" ] } ], "source": [ "// delete the person object\n", "mapper.delete(person);\n", "\n", "//now try to read back\n", "Person gone = mapper.read(Person.class, \"111-11-1111\");\n", "System.out.format(\"Instantiated Person object: %s\\n\", toJsonString(gone));;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Mapping Java and Aerospike Types\n", "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.\n", "\n", "Java Type | Aerospike Type\n", ":-------- | :---- \n", "byte, char, short, int, long | Integer\n", "boolean | Boolean\n", "float, double | Double\n", "java.util.date, java.util.instant | Integer\n", "string | String\n", "byte[] | Blob\n", "enum | String\n", "arrays, List | List\n", "Map | Map" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Specifying Fields for Mapping\n", "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.\n", "\n", "## Selecting Specific Fields\n", "In order to specify specific fields to persist, use`@AerospikeBin` and `AerospikeExclude` annotations, and the `mapAll` parameter in `@AerospikeRecord`.\n", "\n", "If it is desired to save only specific bins, annotate those bins with `@AerospikeBin` and use`mapAll = 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.\n", "\n", "In the example below, only the fields explicitly annotated with `@AerospikeBin` will be stored in the database." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Instantiated object: {\n", " \"ssn\": \"222-22-2222\",\n", " \"name\": \"Jane Doe\",\n", " \"notStored\": null\n", "}\n" ] } ], "source": [ "import com.aerospike.mapper.annotations.AerospikeBin;\n", "\n", "// drop old data\n", "truncateTestData();\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\", mapAll=false) // note mapAll is false\n", "public class Person2 { \n", " @AerospikeBin // explicit @AerospikeBin -> persisted \n", " @AerospikeKey\n", " public String ssn;\n", " @AerospikeBin\n", " public String name; // explicit @AerospikeBin -> persisted\n", " public String notStored; // no @AerospikeBin -> not persisted \n", "}\n", "Person2 p = new Person2();\n", "p.ssn = \"222-22-2222\";\n", "p.name = \"Jane Doe\";\n", "p.notStored = \"Does Not Persist\";\n", "\n", "mapper.save(p);\n", "\n", "Person2 person = mapper.read(Person2.class, \"222-22-2222\");\n", "System.out.format(\"Instantiated object: %s\\n\", toJsonString(person));;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also exclude a specific attribute with an `@AerospikeExclude` annotation. It would be used with the default setting of `mapAll = true`. \n", "\n", "The above example can be implemented with `@AerospikeExclude` as follows." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Instantiated object: {\n", " \"ssn\": \"333-33-3333\",\n", " \"name\": \"Jack Doe\",\n", " \"notStored\": null\n", "}\n" ] } ], "source": [ "import com.aerospike.mapper.annotations.AerospikeExclude;\n", "\n", "// drop old data\n", "truncateTestData();\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\") // note, default mapAll is true\n", "public class Person3 { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", " @AerospikeExclude\n", " public String notStored; // explicitly excluded -> not persisted \n", "}\n", "\n", "Person3 p = new Person3();\n", "p.ssn = \"333-33-3333\";\n", "p.name = \"Jack Doe\";\n", "p.notStored = \"Does Not Persist\";\n", "\n", "mapper.save(p);\n", "\n", "Person3 person = mapper.read(Person3.class, \"333-33-3333\");\n", "System.out.format(\"Instantiated object: %s\\n\", toJsonString(person));;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Specifying Bin Names\n", "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.\n", "\n", "In the following example, we change the bin name for the `name` field to `full_name`." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "// drop the old data\n", "truncateTestData();\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person4 { \n", " @AerospikeKey\n", " public String ssn;\n", " @AerospikeBin(name=\"full_name\")\n", " public String name; // stored in bin full_name\n", "}\n", "\n", "Person4 p = new Person4();\n", "p.ssn = \"444-44-4444\";\n", "p.name = \"Jill Doe\";\n", "\n", "mapper.save(p);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Confirm the field `name` is saved in bin `full_name` by running the following command in the terminal tab:\n", "\n", "```\n", "aql -c \"set output raw; select * from test.om-persons\"\n", "```\n", "The output should be like:\n", "```\n", "*************************** 1. row ***************************\n", "full_name: \"Jill Doe\"\n", "ssn: \"444-44-4444\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Specifying Object Representation\n", "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. \n", "\n", "An object can be stored in Aerospike as a `List` or a `Map`.\n", "\n", "For example, consider the `Address` object:\n", "```\n", "Address {\n", " street = \"100 Main St\"\n", " city = \"Smartville\"\n", " state = \"CA\"\n", " zipcode = \"911001\"\n", "}\n", "```\n", "The object can be stored as a `Map` using the `EmbedType` parameter set to `MAP` in `@AerospikeEmbed` annotation:\n", "```\n", "{\"street\":\"100 Main St\", \"city\":\"Smartville\", \"state\":\"CA\", \"zipcode\":\"911001\"}\n", "```\n", "\n", "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.\n", "\n", "Alternatively, the above object can be stored as a `List` of its field values using the `EmbedType` parameter set to `LIST` in `@AerospikeEmbed` annotation::\n", "\n", "```\n", "[\"100 Main St\", \"Smartville\", \"CA\", \"911001\"]\n", "```\n", "\n", "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.\n", "\n", "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`.\n", "\n", "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`." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "import com.aerospike.mapper.annotations.AerospikeEmbed;\n", "import com.aerospike.mapper.annotations.AerospikeEmbed.EmbedType;\n", "\n", "// drop the old data\n", "truncateTestData();\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"object-mapper\")\n", "public class Address {\n", " public String street;\n", " public String city;\n", " public String state;\n", " public String zipcode;\n", "}\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person5 { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", " @AerospikeEmbed(type = EmbedType.MAP) \n", " public Address home_addr; // embedded object in Map representation\n", " @AerospikeEmbed(type = EmbedType.LIST) \n", " public Address office_addr; // embedded object in List representation\n", "}\n", "\n", "// home address object\n", "Address home = new Address();\n", "home.street = \"555 Burb St\";\n", "home.city = \"Smartville\";\n", "home.state = \"CA\";\n", "home. zipcode = \"911011\";\n", "\n", "// office address object\n", "Address office = new Address();\n", "office.street = \"100 Main St\";\n", "office.city = \"Smartville\";\n", "office.state = \"CA\";\n", "office. zipcode = \"911001\";\n", "\n", "Person5 p = new Person5();\n", "p.ssn = \"555-55-5555\";\n", "p.name = \"Joey Doe\";\n", "p.home_addr = home;\n", "p.office_addr = office;\n", "\n", "mapper.save(p);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the following commands in the terminal tab:\n", "\n", "```\n", "aql -c \"set output raw; select * from test.om-persons\"\n", "```\n", "\n", "It should show an output like:\n", "```\n", "*************************** 1. row ***************************\n", "home_addr: MAP('{\"street\":\"555 Burb St\", \"city\":\"Smartville\", \"zipcode\":\"911011\", \"state\":\"CA\"}')\n", "name: \"Joey Doe\"\n", "office_addr: LIST('[\"Smartville\", \"CA\", \"100 Main St\", \"911001\"]')\n", "ssn: \"555-55-5555\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Embedding Object Vs Storing Reference\n", "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. \n", "\n", "Note below, when we read the parent object that was saved above, the two embedded objects are retrieved with it." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Instantiated object: {\n", " \"ssn\": \"555-55-5555\",\n", " \"name\": \"Joey Doe\",\n", " \"home_addr\": {\n", " \"street\": \"555 Burb St\",\n", " \"city\": \"Smartville\",\n", " \"state\": \"CA\",\n", " \"zipcode\": \"911011\"\n", " },\n", " \"office_addr\": {\n", " \"street\": \"100 Main St\",\n", " \"city\": \"Smartville\",\n", " \"state\": \"CA\",\n", " \"zipcode\": \"911001\"\n", " }\n", "}\n" ] } ], "source": [ "Person5 person = mapper.read(Person5.class, \"555-55-5555\");\n", "System.out.format(\"Instantiated object: %s\\n\", toJsonString(person));;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. \n", "\n", "Below, we save the course that a person is enrolled for by reference. \n", "\n", "Courses: \n", " Number: 100, Title: English\n", " Number: 200, Title: Math\n", " Number: 300, Title: Science\n", " Number: 400, Title: History\n", " \n", "John's courses:\n", " English, Math, History\n", "Jill's courses:\n", " Math, Science\n", "\n", "The following cell shows the code. \n", "\n", "Some points to highlight:\n", "- 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. \n", "- Use generics (as in `List` below) to describe the type as fully as possible. " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "import com.aerospike.mapper.annotations.AerospikeReference;\n", "\n", "// drop old data\n", "truncateTestData();\n", "\n", "// course object saved in set \"om-courses\"\n", "@AerospikeRecord(namespace=\"test\", set=\"om-courses\")\n", "public class Course { \n", " @AerospikeKey\n", " public Integer course_num;\n", " public String title;\n", " \n", " public Course(Integer course_num, String title) {\n", " this.course_num = course_num;\n", " this.title = title;\n", " }\n", "}\n", "\n", "// define and save courses\n", "Course english = new Course(100, \"English\");\n", "//mapper.save(english); // reference object need not already exist in database\n", "\n", "Course math = new Course(200, \"Math\");\n", "//mapper.save(math); // reference object need not already exist in database\n", "\n", "Course science = new Course(300, \"Science\");\n", "//mapper.save(science); // reference object need not already exist in database\n", "\n", "Course history = new Course(400, \"History\");\n", "//mapper.save(history); // reference object need not already exist in database\n", "\n", "// person object\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person6 { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", " @AerospikeReference \n", " public List courses; // list of courses by reference; note fully defined generics\n", "\n", " public Person6(String ssn, String name) {\n", " this.ssn = ssn;\n", " this.name = name;\n", " }\n", "}\n", "\n", "Person6 john = new Person6(\"111-11-1111\", \"John Doe\");\n", "john.courses = new ArrayList<>(Arrays.asList(english, science, history));\n", "mapper.save(john);\n", "\n", "Person6 jane = new Person6(\"222-22-2222\", \"Jane Doe\");\n", "jane.courses = new ArrayList<>(Arrays.asList(math, science));\n", "mapper.save(jane);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the following commands in the terminal tab to see how a list by reference is stored:\n", "```\n", "aql -c \"set output raw; select * from test.om-persons\"\n", "```\n", "It should show an output like:\n", "```\n", "*************************** 1. row ***************************\n", "courses: LIST('[200, 300]')\n", "name: \"Jane Doe\"\n", "ssn: \"222-22-2222\"\n", "*************************** 2. row ***************************\n", "courses: LIST('[100, 300, 400]')\n", "name: \"John Doe\"\n", "ssn: \"111-11-1111\"\n", "```\n", "And to view the courses (if stored separately):\n", "```\n", "aql> select * from test.om-courses\n", "*************************** 1. row ***************************\n", "course_num: 300\n", "title: \"Science\"\n", "*************************** 2. row ***************************\n", "course_num: 200\n", "title: \"Math\"\n", "*************************** 3. row ***************************\n", "course_num: 400\n", "title: \"History\"\n", "*************************** 4. row ***************************\n", "course_num: 100\n", "title: \"English\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Mapping Circular References\n", "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." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "// drop old data\n", "truncateTestData();\n", "\n", "// person object\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person7 { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", " @AerospikeReference \n", " public Person7 spouse; // reference to Person7\n", " @AerospikeReference \n", " public List children; // list of Person7 references\n", "\n", " public Person7(String ssn, String name) {\n", " this.ssn = ssn;\n", " this.name = name;\n", " }\n", "}\n", "\n", "Person7 john = new Person7(\"111-11-1111\", \"John Doe\");\n", "Person7 jane = new Person7(\"222-22-2222\", \"Jane Doe\");\n", "Person7 jack = new Person7(\"333-33-3333\", \"Jack Doe\");\n", "Person7 jill = new Person7(\"444-44-4444\", \"Jill Doe\");\n", "\n", "john.spouse = jane;\n", "john.children = new ArrayList<>(Arrays.asList(jack, jill));\n", "jane.spouse = john;\n", "jane.children = new ArrayList<>(Arrays.asList(jack, jill));\n", "mapper.save(john);\n", "mapper.save(jane);\n", "// chidren objects saved with a null spouse and children\n", "mapper.save(jack);\n", "mapper.save(jill);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the following commands in the terminal tab to see how the references are stored:\n", "```\n", "aql -c \"set output raw; select * from test.om-persons\"\n", "```\n", "It should show an output like:\n", "```\n", "*************************** 1. row ***************************\n", "children: LIST('[\"333-33-3333\", \"444-44-4444\"]')\n", "name: \"Jane Doe\"\n", "spouse: \"111-11-1111\"\n", "ssn: \"222-22-2222\"\n", "*************************** 2. row ***************************\n", "name: \"Jack Doe\"\n", "ssn: \"333-33-3333\"\n", "*************************** 3. row ***************************\n", "name: \"Jill Doe\"\n", "ssn: \"444-44-4444\"\n", "*************************** 4. row ***************************\n", "children: LIST('[\"333-33-3333\", \"444-44-4444\"]')\n", "name: \"John Doe\"\n", "spouse: \"222-22-2222\"\n", "ssn: \"111-11-1111\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Nested Object Graphs\n", "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`.\n", "\n", "We will extend the object model with the following relationships:\n", "\n", "Person\n", "\n", "AccountHolder is-a Person\n", "\n", "AccountHolder has-a Account (1:N)\n", "\n", "Student is-a Person \n", "\n", "Student registersFor Course (M:N)\n", "\n", "Course taughtBy Person (N:1)\n", "\n", "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.\n", "\n", "A few other things to note: \n", "- an object may be stored without other objects it references already existing in the database\n", "- 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.\n", "- In the database, the actual class name is stored along with an object reference if the field definition uses its parent class." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "// drop old data\n", "truncateTestData();\n", "\n", "// person object definition\n", "@AerospikeRecord(namespace=\"test\", set=\"om-persons\")\n", "public class Person8 { \n", " @AerospikeKey\n", " public String ssn;\n", " public String name;\n", "\n", " public Person8(String ssn, String name) {\n", " this.ssn = ssn;\n", " this.name = name;\n", " }\n", "}\n", "\n", "// account object defiition\n", "public static enum AccountType {\n", "\tSAVING, CHECKING\n", "}\n", "\n", "@AerospikeRecord(namespace=\"test\", set=\"om-accounts\")\n", "public class Account { \n", " @AerospikeKey\n", " public Integer account_num;\n", " public AccountType type;\n", " public Integer balance;\n", " @AerospikeReference\n", " public Person8 owner;\n", " \n", " public Account(Integer account_num, AccountType type, Integer balance) {\n", " this.account_num = account_num;\n", " this.type = type;\n", " this.balance = balance;\n", " }\n", "}\n", "\n", "// create account objects\n", "Account johnsChecking = new Account(1, AccountType.CHECKING, 1000);\n", "Account johnsSaving = new Account(11, AccountType.SAVING, 100);\n", "Account janesSaving = new Account(12, AccountType.SAVING, 200);\n", "Account petesChecking = new Account(2, AccountType.CHECKING, 2000);\n", "Account willsChecking = new Account(3, AccountType.CHECKING, 3000);\n", "Account willsSaving = new Account(13, AccountType.SAVING, 300);\n", "\n", "// account-holder definition\n", "@AerospikeRecord(namespace=\"test\", set=\"om-acct-holders\")\n", "public class AccountHolder extends Person8 { \n", " @AerospikeReference \n", " public List accounts;\n", " AccountHolder(String ssn, String name) {\n", " super(ssn, name);\n", " }\n", "}\n", "\n", "// create account holder objects\n", "AccountHolder john = new AccountHolder(\"111-11-1111\", \"John Doe\");\n", "AccountHolder jane = new AccountHolder(\"222-22-2222\", \"Jane Doe\");\n", "AccountHolder pete = new AccountHolder(\"555-55-5555\", \"Pete Poe\");\n", "AccountHolder will = new AccountHolder(\"666-66-6666\", \"Will Woe\");\n", "\n", "// define account and account-holder relationships\n", "johnsChecking.owner = john;\n", "johnsSaving.owner = john;\n", "john.accounts = new ArrayList<>(Arrays.asList(johnsChecking, johnsSaving));\n", "janesSaving.owner = jane;\n", "jane.accounts = new ArrayList<>(Arrays.asList(janesSaving));\n", "petesChecking.owner = pete;\n", "pete.accounts = new ArrayList<>(Arrays.asList(petesChecking));\n", "willsChecking.owner = will;\n", "willsSaving.owner = will;\n", "will.accounts = new ArrayList<>(Arrays.asList(willsChecking, willsSaving));\n", "\n", "// save account objects\n", "mapper.save(johnsChecking); // reference object need not already exist in database\n", "mapper.save(johnsSaving); \n", "mapper.save(janesSaving); \n", "mapper.save(petesChecking); \n", "mapper.save(willsChecking); \n", "mapper.save(willsSaving); \n", "\n", "// save account holder objects\n", "mapper.save(john);\n", "mapper.save(jane);\n", "mapper.save(pete);\n", "mapper.save(will);\n", "\n", "// course object definition\n", "// course objects are saved in set \"om-courses\"\n", "@AerospikeRecord(namespace=\"test\", set=\"om-courses\")\n", "public class Course2 { \n", " @AerospikeKey\n", " public Integer course_num;\n", " public String title;\n", " @AerospikeReference\n", " public Person8 teacher;\n", " @AerospikeReference\n", " public List students;\n", " \n", " public Course2(Integer course_num, String title) {\n", " this.course_num = course_num;\n", " this.title = title;\n", " }\n", "}\n", "\n", "// create course objects\n", "Course2 english = new Course2(100, \"English\");\n", "Course2 math = new Course2(200, \"Math\");\n", "Course2 science = new Course2(300, \"Science\");\n", "Course2 history = new Course2(400, \"History\");\n", "\n", "// student object definition\n", "// student objects are saved in set \"om-students\"\n", "@AerospikeRecord(namespace=\"test\", set=\"om-students\")\n", "public class Student extends Person8 { \n", " @AerospikeReference \n", " public List courses; \n", " Student(String ssn, String name) {\n", " super(ssn, name);\n", " }\n", "}\n", "\n", "// create student objects\n", "Student jack = new Student(\"333-33-3333\", \"Jack Doe\");\n", "Student jill = new Student(\"444-44-4444\", \"Jill Doe\");\n", "\n", "// define course and student relationships\n", "english.teacher = pete;\n", "english.students = new ArrayList<>(Arrays.asList(jack));\n", "math.teacher = pete;\n", "math.students = new ArrayList<>(Arrays.asList(jill));\n", "science.teacher = will;\n", "science.students = new ArrayList<>(Arrays.asList(jack, jill));\n", "history.teacher = will;\n", "history.students = new ArrayList<>(Arrays.asList(jack));\n", "jack.courses = new ArrayList<>(Arrays.asList(english, science, history));\n", "jill.courses = new ArrayList<>(Arrays.asList(math, science));\n", "\n", "// save course objects\n", "mapper.save(english); \n", "mapper.save(math); \n", "mapper.save(science); \n", "mapper.save(history); \n", "\n", "// save student objects\n", "mapper.save(jack);\n", "mapper.save(jill);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the following commands in the terminal tab to see how the object graphs is stored:\n", "```\n", "aql -c \"set output raw; select * from test.om-accounts\"\n", "```\n", "Output:\n", "```\n", "*************************** 1. row ***************************\n", "account_num: 12\n", "balance: 200\n", "owner: LIST('[\"222-22-2222\", \"AccountHolder\"]')\n", "type: \"SAVING\"\n", "\n", "...\n", "\n", "*************************** 6. row ***************************\n", "account_num: 2\n", "balance: 2000\n", "owner: LIST('[\"555-55-5555\", \"AccountHolder\"]')\n", "type: \"CHECKING\"\n", "```\n", "Run:\n", "```\n", "aql -c \"set output raw; select * from test.om-acct-holders\"\n", "```\n", "Output:\n", "```\n", "*************************** 1. row ***************************\n", "accounts: LIST('[1, 11]')\n", "name: \"John Doe\"\n", "ssn: \"111-11-1111\"\n", "\n", "...\n", "\n", "*************************** 4. row ***************************\n", "accounts: LIST('[3, 13]')\n", "name: \"Will Woe\"\n", "ssn: \"666-66-6666\"\n", "```\n", "Run:\n", "```\n", "aql -c \"set output raw; select * from test.om-courses\"\n", "```\n", "Output:\n", "```\n", "*************************** 1. row ***************************\n", "course_num: 300\n", "students: LIST('[[\"333-33-3333\", \"Student\"], [\"444-44-4444\", \"Student\"]]')\n", "teacher: LIST('[\"666-66-6666\", \"AccountHolder\"]')\n", "title: \"Science\"\n", "\n", "...\n", "\n", "*************************** 4. row ***************************\n", "course_num: 100\n", "students: LIST('[[\"333-33-3333\", \"Student\"]]')\n", "teacher: LIST('[\"555-55-5555\", \"AccountHolder\"]')\n", "title: \"English\"\n", "```\n", "```\n", "aql -c \"set output raw; select * from test.om-students\"\n", "```\n", "Output:\n", "```\n", "*************************** 1. row ***************************\n", "courses: LIST('[200, 300]')\n", "name: \"Jill Doe\"\n", "ssn: \"444-44-4444\"\n", "*************************** 2. row ***************************\n", "courses: LIST('[100, 300, 400]')\n", "name: \"Jack Doe\"\n", "ssn: \"333-33-3333\"\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Account object: account_num: 1, type: CHECKING, owner: John Doe\n", "AccountHolder object: ssn: 111-11-1111, name: John Doe, accounts: [1, 11]\n", "Course object: course_num: 100, title: English, students: [Jack Doe]\n", "Student object: ssn: 333-33-3333, name: Jack Doe, courses: [English, Science, History]\n" ] } ], "source": [ "// read back the account record with key 1\n", "Account object = mapper.read(Account.class, 1);\n", "System.out.format(\"Account object: account_num: %d, type: %s, owner: %s\\n\", object.account_num, object.type, object.owner.name);;\n", "\n", "// read back the account holder record with key \"111-11-1111\"\n", "AccountHolder object = mapper.read(AccountHolder.class, \"111-11-1111\");\n", "System.out.format(\"AccountHolder object: ssn: %s, name: %s, \", object.ssn, object.name);\n", "ListIterator iter = object.accounts.listIterator();\n", "ArrayList list = new ArrayList();\n", "while (iter.hasNext()) {\n", " list.add(iter.next().account_num);\n", "}\n", "System.out.format(\"accounts: %s\\n\", list);\n", "\n", "// read back the course record with key 100\n", "Course2 object = mapper.read(Course2.class, 100);\n", "System.out.format(\"Course object: course_num: %s, title: %s, \", object.course_num, object.title);\n", "ListIterator iter = object.students.listIterator();\n", "ArrayList list = new ArrayList();\n", "while (iter.hasNext()) {\n", " list.add(iter.next().name);\n", "}\n", "System.out.format(\"students: %s\\n\", list);\n", "\n", "// read back the student record with key \"333-33-3333\"\n", "Student object = mapper.read(Student.class, \"333-33-3333\");\n", "System.out.format(\"Student object: ssn: %s, name: %s, \", object.ssn, object.name);\n", "ListIterator iter = object.courses.listIterator();\n", "ArrayList list = new ArrayList();\n", "while (iter.hasNext()) {\n", " list.add(iter.next().title);\n", "}\n", "System.out.format(\"courses: %s\\n\", list);;\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# More Advanced Topics\n", "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](https://github.com/aerospike/java-object-mapper) for their description and examples.\n", "- Config via YAML string, and config file with precedence rules.\n", "- Policy specification and precedence rules\n", "- Versioning\n", "- Class hierarchies\n", "- Object deserialization using constructor factories \n", "- Custom data converters, custom getter/setter methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Annotations Summary\n", "The table below provides a summary of various annotations available in the object mapper.\n", "\n", "Annotation | Applied To | Parameters (default) | Description\n", ":-------- | :---- | :---- | :----\n", "@AerospikeRecord | class | namespace, set, mapAll (true), version (1), | Details of record location\n", " | | factoryMethod (null), factoryClass (null), | and metadata\n", " | | shortName(null), ttl, durableDelete, sendKey\n", "@AerospikeKey | field or method | | Object id\n", "@AerospikeBin | field | bin name (field name) | Persisted field and bin name\n", "@AerospikeExclude | field | | Excluded field\n", "@AerospikeEmbed | field (object) | type (Map or List) | Embedded object and rep\n", "@AerospikeReference | field (object) | lazy (false), type (key) | Referenced object and loading\n", "@AerospikeVersion | field (object) | min (1), max | Field version validity\n", "@AerospikeOrdinal | field (object) | value | Field order in List rep \n", "@AerospikeSetter | method | field name | Custom setter method\n", "@AerospikeGetter | method | field name | Custom getter method\n", "@ParamFrom | constructor arguments | bin name | Argument in constructor\n", "@AerospikeConstructor | constructor | | Constructor to be used\n", "@ToAerospike | data converter method | | Custom conversion\n", "@FromAerospike | data converter method | | Custom conversion " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Takeaways\n", "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:\n", "- easier to implement,\n", "- easier to understand, and\n", "- less error prone." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Cleaning Up\n", "Remove tutorial data and close connection." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Removed tutorial data and closed server connection.\n" ] } ], "source": [ "truncateTestData();\n", "client.close();\n", "System.out.println(\"Removed tutorial data and closed server connection.\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Further Exploration and Resources\n", "Here are some links for further exploration.\n", "\n", "## Resources\n", "- Video\n", " - [Java Object Mapper Workshop - Summit 2021](https://www.youtube.com/watch?v=QQhdX661raM&list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&index=6)\n", "- Github repo\n", " - [Java Object Mapper](https://github.com/aerospike/java-object-mapper)\n", "- Related notebooks\n", " - [Introduction to Modeling](java-intro_to_data_modeling.ipynb)\n", " - [Modeling Using Lists](java-modeling_using_lists.ipynb)\n", " - [Modeling Using Maps](java-modeling_using_maps.ipynb)\n", "- Aerospike Developer Hub\n", " - [Java Developers Resources](https://developer.aerospike.com/java-developers)\n", "- Other Github repos\n", " - [Aerospike Document API Library](https://github.com/aerospike/aerospike-document-lib)\n", " - [Java code examples](https://github.com/aerospike/aerospike-client-java/tree/master/examples/src/com/aerospike/examples)\n", " - [Java Client](https://www.aerospike.com/docs/client/java/index.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exploring Other Notebooks\n", "\n", "Visit [Aerospike notebooks repo](https://github.com/aerospike-examples/interactive-notebooks) 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." ] } ], "metadata": { "kernelspec": { "display_name": "Java", "language": "java", "name": "java" }, "language_info": { "codemirror_mode": "java", "file_extension": ".jshell", "mimetype": "text/x-java-source", "name": "Java", "pygments_lexer": "java", "version": "11.0.8+10-LTS" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 2 }