Aerospike - How to deserialize a map containing an object in Java?

Dante Adams :

I am having problem loading the map from Aerospike DB. When I fetch the record and try to print it, I'm getting the error below

Main

Key key = new Key( "test", "users", 2 );
Map<Integer, Widgets> map = new HashMap<>();
map.put(1, new Widgets(2, 2));
map.put(2, new Widgets(3, 0));
Bin bin = new Bin("widgets", map);
client.put( policy, key, bin );
Record record = client.get(policy, key); // using same key for testing
map = (Map<Integer, Widgets>) record.getMap("widgets"); // here, I do get a map back... but its serialized 
map.forEach( (k,v) -> System.out.println(k)); <------- ERROR

Error

Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer

Data stored in Aerospike

|                    | 2  | MAP('{1:AC ED 00 05 73 72 00 07 57 69 64 67 65 74 73 6F F3 7E F4 7F CD 1C 92 02 00 02 49 00 0A 63 6C 69 63 6B 43 6F 75 6E 74 49 00 09 76 69 65 77 43 6F 75 6E 74 78 70 00 00 00 02 00 00 00 02, 2:AC ED 00 05 73 72 00 07 57 69 64 67 65 74 73 6F F3 7E F4 7F  |
+--------------------+----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.098 secs)

Widgets class

public class Widgets implements Serializable{
    private int viewCount;
    private int clickCount;
    // getters, setters and constructors here
}

Data is getting stored in the database without a problem, but it is being stored as byte array. I'm having trouble deserializing it.

EDIT :

When I try to print the map, I do get an output, but when I try to use foreach, it shows an error

System.out.println(map); // works fine

OUTPUT

{1=Widgets@7e774085, 2=Widgets@3f8f9dd6} 
Tim Faulkes :

Aerospike always stores integer types (short, int, long, etc) as 64-bin longs in the database. If you insert a shorter numeric type, it will automatically upcast it to a long. This is to support languages which do not have shorter numeric types.

So when you retrieve your map, your map keys will be returned as longs. Hence, this code should replace your retrieval code:

        Record record = client.get(null, key); // using same key for testing
        Map<Long, Widgets> map2 = (Map<Long, Widgets>) record.getMap("widgets"); // here, I do get a map back... but its serialized 
        map2.forEach( (k,v) -> System.out.println(k));

Note: You do correctly mention that the Widgets() classes will be stored in Aerospike as a Java serialized object. This is probably not desirable. If, at some point in the future you change technology to something else, your data will not be readable. Aerospike is language agnostic so you can write your data in Java and read it back in C for example. By storing Java serialized objects you're preventing this ability.

You can see this in your AQL (note the 0xaced Java magic number):

aql> select * from test.users
*************************** 1. row ***************************
widgets: MAP('{1:AC ED 00 05 73 72 00 34 63 6F 6D 2E 74 69 6D 2E...

Typically it's better to either use Spring Data or to serialize them yourself. For example, you could change your code to be:

package com.aerospike.play;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import com.aerospike.client.AerospikeClient;
import com.aerospike.client.Bin;
import com.aerospike.client.Key;
import com.aerospike.client.Record;

public class StackOverflowQuestion {
    public static class Widgets implements Serializable{
        private static final String VIEW_FIELD = "view";
        private static final String CLICK_FIELD = "click";

        private int viewCount;
        private int clickCount;

        public Widgets(int viewCount, int clickCount) {
                this.viewCount = viewCount;
                this.clickCount = clickCount;
        }
        public int getViewCount() {
            return viewCount;
        }
        public int getClickCount() {
            return clickCount;
        }

        @Override
        public String toString() {
                return String.format("{view: %d, count: %s}", viewCount, clickCount);
        }

        public Map<String, Object> asMap() {
                Map<String, Object> values = new HashMap<>();
                values.put(VIEW_FIELD, this.viewCount);
                values.put(CLICK_FIELD, this.clickCount);
                return values;
        }

        public static Widgets fromMap(Map<String, Object> map) {
                return new Widgets((int)(long)map.get(VIEW_FIELD), (int)(long)map.get(CLICK_FIELD));
        }
    }

    public static void main(String[] args) {
        AerospikeClient client = new AerospikeClient("172.28.128.4", 3000);
        Key key = new Key( "test", "users", 2 );
        Map<Integer, Map<String, Object>> map = new HashMap<>();
        map.put(1, new Widgets(2, 2).asMap());
        map.put(2, new Widgets(3, 0).asMap());
        Bin bin = new Bin("widgets", map);
        client.put( null, key, bin );

        Record record = client.get(null, key); // using same key for testing
        Map<Long, Map<String, Object>> map2 = (Map<Long, Map<String, Object>>) record.getMap("widgets"); 
        map2.forEach( (k,v) -> {
            Widgets w = Widgets.fromMap(v);
            System.out.printf("%d -> %s\n", k, w);
        });

        client.close();
    }
}

In this case the output is what you would expect:

1 -> {view: 2, count: 2}
2 -> {view: 3, count: 0}

But the data stored in the database is using native types:

aql> select * from test.users
*************************** 1. row ***************************
widgets: MAP('{1:{"view":2, "click":2}, 2:{"view":3, "click":0}}')

As a side note, there are more efficient ways of storing this data such as a list, but this representation is more illustrative.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=416643&siteId=1