Categories: database

NoSQL with Couchbase: Working with JSON

Welcome to the third episode of the Couchbase series. In Couchbase everything (except for counters) are stored in JSON and in this article, we will look into how we can work with it in an easy and generic way.

Make sure to check out the other articles in the series as well:

  1. NoSQL with Couchbase: Setting everything up with Docker
  2. NoSQL with Couchbase: Getting started
  3. NoSQL with Couchbase: Working with JSON
  4. NoSQL with Couchbase: Querying with N1QL
  5. NoSQL with Couchbase: Creating Indexes that Scale

There are two terms that you should be aware of and understand what they mean in order to not get confused in this article. The first one is serialization which is the process of converting an object to a stream of bytes so that it can be sent over the network or persisted to disk. Deserialization is the opposite, meaning how we can transform that stream of bytes into an object. There are different ways to interpret or manipulate these stream of bytes, popular options are XML or JSON. We will work with JSON in these articles.

Serializing and deserializing JSON

In the last article, we coded a repository for persisting and reading person objects to/from Couchbase. We used an ObjectMapper from Jackson to do the serialization and deserialization. But we didn’t do it in a very clean and re-usable way which is the first thing I want to go back and refactor.

We are going to make a serializer class that will handle it for us in a nice and generic way.


package org.thecuriousdev.demo.skeleton.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Optional;

public class Serializer {

    private static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class);

    private final ObjectMapper mapper;

    private final Class typeClass;

    public Serializer(ObjectMapper mapper, Class typeClass) {
        this.mapper = mapper;
        this.typeClass = typeClass;
    }

    public Optional seralize(Object value) {
        try {
            return Optional.ofNullable(mapper.writeValueAsString(value));
        } catch (JsonProcessingException e) {
            LOGGER.info("Failed to serialize object {}", value, e);
            return Optional.empty();
        }
    }

    public Optional deserialize(String json) {
        try {
            return Optional.ofNullable(mapper.readValue(json, typeClass));
        } catch (IOException e) {
            LOGGER.info("Failed to deserialize json {}", json, e);
            return Optional.empty();
        }
    }
}

This class is able to both serialize and deserialize any different types of objects. This means that we can put all the error handling and avoid having try-catch in our repository. It also means that we easy can re-use this serializer for any other repository that we later create.

Our updated PersonRepository class utilizing this new serializer looks like the following:


package org.thecuriousdev.demo.skeleton.db;

import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.document.RawJsonDocument;
import com.couchbase.client.java.document.json.JsonObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.thecuriousdev.demo.skeleton.db.domain.Person;
import org.thecuriousdev.demo.skeleton.util.Serializer;

import java.util.Optional;

@Repository
public class PersonRepository {

    private static final Logger LOG = LoggerFactory.getLogger(PersonRepository.class);

    private Bucket bucket;
    private Serializer serializer;

    @Autowired
    public PersonRepository(Bucket bucket, ObjectMapper objectMapper) {
        this.bucket = bucket;
        this.serializer = new Serializer(objectMapper, Person.class);
    }

    public Optional findById(String name) {
        Optional json = Optional.ofNullable(bucket.get(name))
                .map(JsonDocument::content)
                .map(JsonObject::toString);

        if (json.isPresent()) {
            return serializer.deserialize(json.get());
        }

        return Optional.empty();
    }

    public void save(Person person) {
        Optional json = serializer.seralize(person);
        if (json.isPresent()) {
            bucket.upsert(RawJsonDocument.create(person.getName(), json.get()));
            LOG.info("Saved person : {}", person);
        } else {
            LOG.warn("Failed to save user {}", person);
        }
    }

    public void delete(String name) {
        bucket.remove(name);
        LOG.info("Deleted person: {}", name);
    }
}

Much more neat and cleaner than the previous version.

Improvements in our Person class

The next thing we need to do is to make our Person object more accessible for working with JSON when it is persisted in Couchbase. We also are going to use make use of CAS (Check and Set) which is a very powerful way to handle concurrent modifications of our documents.

First of all, we are going to add a type to our Person class. This type has a very important purpose later for our N1QL queries. N1QL is like SQL, but for JSON, which is one of Couchbase specialties. You get the incredible speed of key/value fetches, but at the same time, it also supports more advanced fetches via N1QL. The type field is important when we are going to create our indexes that will support N1QL queries, but more on that in a future blog but while we are doing work in our Person class we might as well add this type value already.

For every Couchbase document we want to store, we want it to have 3 different values: type, cas and id, it doesn’t matter if its a person or a car or any other arbitrary object. That sounds like perfect for an interface.


package org.thecuriousdev.demo.skeleton.db;

import com.fasterxml.jackson.annotation.JsonIgnore;

public interface CouchbaseDocument {

    String getType();
    void setType(String type);

    @JsonIgnore String getId();
    @JsonIgnore void setId(String id);

    @JsonIgnore long getCas();
    @JsonIgnore void setCas(long cas);
}

We use @JsonIgnore to instruct the ObjectMapper to not serialize and deserialize id and cas. The reason for this is that these are already stored in the metadata inside Couchbase, and we are not interested in storing them again inside the actual document. But at the same time, we do need these values in our application while working with the objects that the documents represent.

Our updated Person class which implements this interface looks like the following.


package org.thecuriousdev.demo.skeleton.db.domain;

import com.google.common.base.MoreObjects;
import org.thecuriousdev.demo.skeleton.db.CouchbaseDocument;

public class Person implements CouchbaseDocument {

    private static final String DB_TYPE = "tcd:person";

    private String id;
    private long cas;

    private String name;
    private int age;
    private String favouriteFood;
    private String type;

    public Person() {
        this.type = DB_TYPE;
    }

    public Person(String name, int age, String favouriteFood) {
        this.name = name;
        this.age = age;
        this.favouriteFood = favouriteFood;
        this.type = DB_TYPE;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getFavouriteFood() {
        return favouriteFood;
    }

    public void setFavouriteFood(String favouriteFood) {
        this.favouriteFood = favouriteFood;
    }

    @Override
    public String getType() {
        return type;
    }

    @Override
    public void setType(String type) {
        this.type = type;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void setId(String id) {
        this.id = id;
    }

    @Override
    public long getCas() {
        return cas;
    }

    @Override
    public void setCas(long cas) {
        this.cas = cas;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this.getClass())
                .add("name", name)
                .add("age", age)
                .add("favouriteFood", favouriteFood)
                .omitNullValues()
                .toString();
    }
}

We put the handling of cas and id inside our serializer class so that we never have to remember to bother with it and risk forgetting it.


package org.thecuriousdev.demo.skeleton.util;

import com.couchbase.client.java.document.JsonDocument;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thecuriousdev.demo.skeleton.db.CouchbaseDocument;


import java.io.IOException;
import java.util.Optional;

public class Serializer {

    private static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class);

    private final ObjectMapper mapper;

    private final Class typeClass;

    public Serializer(ObjectMapper mapper, Class typeClass) {
        this.mapper = mapper;
        this.typeClass = typeClass;
    }

    public Optional seralize(Object value) {
        try {
            return Optional.ofNullable(mapper.writeValueAsString(value));
        } catch (JsonProcessingException e) {
            LOGGER.info("Failed to serialize object {}", value, e);
            return Optional.empty();
        }
    }

    public Optional deserialize(JsonDocument doc) {
        try {
            Optional json = Optional.ofNullable(doc.content().toString());
            if (json.isPresent()) {
                T obj = mapper.readValue(json.get(), typeClass);
                obj.setCas(doc.cas());
                obj.setId(doc.id());
                return Optional.of(obj);
            }

            return Optional.empty();

        } catch (IOException e) {
            LOGGER.info("Failed to deserialize document {}", doc, e);
            return Optional.empty();
        }
    }
}

We also make some minor adjustments in our PersonRepository class that uses the serializer.


package org.thecuriousdev.demo.skeleton.db;

import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.document.RawJsonDocument;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.thecuriousdev.demo.skeleton.db.domain.Person;
import org.thecuriousdev.demo.skeleton.util.Serializer;

import java.util.Optional;

@Repository
public class PersonRepository {

    private static final Logger LOG = LoggerFactory.getLogger(PersonRepository.class);

    private Bucket bucket;
    private Serializer serializer;

    @Autowired
    public PersonRepository(Bucket bucket, ObjectMapper objectMapper) {
        this.bucket = bucket;
        this.serializer = new Serializer(objectMapper, Person.class);
    }

    public Optional findById(String name) {
        Optional doc = Optional.ofNullable(bucket.get(name));

        if (doc.isPresent()) {
            return serializer.deserialize(doc.get());
        }

        return Optional.empty();
    }

    public void save(Person person) {
        Optional json = serializer.seralize(person);
        if (json.isPresent()) {
            bucket.upsert(RawJsonDocument.create(person.getName(), json.get(), person.getCas()));
            LOG.info("Saved person : {}", person);
        } else {
            LOG.warn("Failed to save user {}", person);
        }
    }

    public void delete(String name) {
        bucket.remove(name);
        LOG.info("Deleted person: {}", name);
    }
}

Notice how we pass the cas value in the RawJsonDocument.create(String, String, long) method. This is a security mechanic against concurrent modifications. If the cas value has been updated by someone else before we did our upsert we will receive a ConcurrentModificationException, and we will know that someone else concurrently modified our document. At this point there are many different approaches to take, we can abort the procedure and let the user know that someone already updated the document, or we can attempt to receive the document and try to merge in our changes as well. This really depends on the type of application, and there is no generic right answer on how to handle ConcurrentModificationExceptions. But at least if we pass the cas value, we can be sure that we never accidentally override some other update to the document, if we do not care about this we can simply avoid passing the cas value.

Final words

With this, we have implemented a nice and reliable way to handle serializing and deserializing JSON. We did it in a generic way that very easily can be re-used by any other type of objects that we want to store in Couchbase, all they have to do is to implement the CouchbaseDocument interface.

In the next article, we will evolve our Person class and add some more fields to it so that we can start running some N1QL queries against it.

snieking

Share
Published by
snieking
Tags: couchbasedeserializationjavajsonnosqlserialization

Recent Posts

  • development
  • java

Handle Stream Exceptions with an Attempt

Streams has become a very popular way to process a collection of elements. But a…

2 years ago
  • deployment
  • development

Deploying Spring Boot in Pivotal Cloud Foundry

A lot of focus on my previous blogs has been on how to build micro…

2 years ago
  • python

Working with High-Quality Reference Genomes

Learn how to work with high-quality reference genomes in this article by Tiago Antao, a…

2 years ago
  • java

Garbage Collection in JDK 12 and onward

Garbage collection is one of the key concepts of Java programming and up to now…

2 years ago
  • python

Understanding Convolution

Learn about convolution in this article by Sandipan Dey, a data scientist with a wide…

2 years ago
  • java

Lombok Builder with Jackson

Lombok comes with a very convenient way of creating immutable objects with the builder pattern.…

2 years ago