sabato 7 marzo 2015

Overview of pluggable codecs in MongoDB 3.0 Java Driver

MongoDB 3.0 has been officially released the 3rd of March, along with the new java driver (still in BETA).
The driver introduces a lot of new features. Among them brand new Objects have been implemented to represent database entities:
  • Document replaces DBObject as preferred way to access database documents
  • MongoCollection replaces DBCollection
  • MongoDatabase replaces DB


A very interesting feature are the new pluggable codecs. Codec is an interface that allow converting s Java object into a BSON. For example in the driver we have codecs for String, List, Map and so on.

CollectibleCodec interface extends Codec to allow BSON encoded from a Java object to be stored in MongoDB, by checking that the document has an id or by generating a new one through the implemented methods of the interface. For example we have a CollectibleCodec implementation for the Document class or for the old DBObject class.

Let's stop talking and see a concrete example. Imagine we have a Java Bean like the following:


public class User implements Bson{
    private String id;
    private String name;
    private String surname;

    @Override
    public String toString()
    {
        return new StringBuilder("{ _id : '")
                .append(String.valueOf(id))
                .append("', name : '")
                .append(String.valueOf(name))
                .append("', surname : '")
                .append(String.valueOf(surname))
                .append("' }").toString();
    }


    public User() {

    }

    public User(String name, String surname) {
        this.id = id;
        this.name = name;
        this.surname = surname;
    }

    public String getId()
    {
        return id;
    }

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

    public String getName()
    {
        return name;
    }

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

    public String getSurname()
    {
        return surname;
    }

    public void setSurname(String surname)
    {
        this.surname = surname;
    }

    @Override
    public  BsonDocument toBsonDocument(final Class documentClass, final CodecRegistry codecRegistry) {
        return new BsonDocumentWrapper(this, codecRegistry.get(User.class));
    }
}


EDIT: From the release candidate 1 of the driver, to be used as query filter, our Bean must implement the Bson interface and override the method toBsonDocument

We want to write a CollectibleCodec that allow us to insert to, or read from MongoDB an instance of the User class. So let's implement a CollectibleCodec.
Our user codec will serve as a proxy to the document codec. In other words in case we are decoding a BSON to User we will first call the decode method of the DocumentCodec to convert the BSON into a Document and than we will build an User from the document. The other way round for encoding, we will first convert the User into a Document and then call the encode method of the DocumentCodec to convert it into a BSON. As you can see in the listing below, we will initialize our UserCodec with a DocumentCodec instance.

public class UserCodec implements CollectibleCodec<User>
{
    private Codec<Document> documentCodec;

    public UserCodec() {
        this.documentCodec = new DocumentCodec();
    }

    public UserCodec(Codec<Document> codec) {
        this.documentCodec = codec;
    }
...


The decode method implementation:

    @Override
    public User decode(BsonReader reader, 
                       DecoderContext decoderContext)
    {
        Document document = documentCodec.decode(reader, 
                                                 decoderContext);

        User user = new User();

        String id = (String) document.get("_id");

        user.setId(id);

        String name = (String) document.get("name");

        user.setName(name);

        String surname = (String) document.get("surname");

        user.setSurname(surname);

        return user;
    }


and the encode method implementation:

    @Override
    public void encode(BsonWriter writer, 
                       User user, 
                       EncoderContext encoderContext)
    {
        Document document = new Document();

        String id = user.getId();
        String name = user.getName();
        String surname = user.getSurname();

        if (id != null)
        {
            document.put("_id", id);
        }

        if (name != null)
        {
            document.put("name", name);
        }

        if (surname != null)
        {
            document.put("surname", surname);
        }

        documentCodec.encode(writer, document, encoderContext);
    }


In order for the clients of the codec to know which Class the codec decode/encode from/to, you must implement the following method:

    @Override
    public Class<User> getEncoderClass()
    {
        return User.class;
    }


then to handle id generation, the following methods (of the CollectibleCodec interface):

    @Override
    public User generateIdIfAbsentFromDocument(User user)
    {
        if (!documentHasId(user))
        {
            user.setId(UUID.randomUUID().toString());
        }
        return user;
    }

    @Override
    public boolean documentHasId(User user)
    {
        return user.getId() != null;
    }

    @Override
    public BsonValue getDocumentId(User user)
    {
        if (!documentHasId(user))
        {
            throw new IllegalStateException("The document does not contain an _id");
        }

        return new BsonString(user.getId());
    }


Now we have a codec. We need to register the codec in the driver. First of all, we get the default DocumentCodec instance:

Codec<Document> defaultDocumentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);


then we instantiate a new UserCodec, passing the DocumentCodec as constructor parameter:

UserCodec userCodec = new UserCodec(defaultDocumentCodec);


and we get a MongoClient to our local database, adding the new UserCodec to the Default CodecRegistry (that includes for example DocumentCodec and DBObjectCodec instancies):

   
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
    MongoClient.getDefaultCodecRegistry(),
    CodecRegistries.fromCodecs(userCodec)
);

MongoClientOptions options = 
        MongoClientOptions.builder()
                .codecRegistry(codecRegistry)
                .build();

MongoClient mongoClient = 
        new MongoClient("localhost:27017", options);


Now we can get a collection on the database and a create a new User:

   
MongoDatabase mongoDatabase = 
                mongoClient.getDatabase("test");

MongoCollection<User> mongoCollection = 
    mongoDatabase.getCollection("user", 
                                User.class);

User me = new User("Massimo", "Esperto");

mongoCollection.insertOne(me);

User meAgain = mongoCollection.find(me)
                              .first();

System.out.println(meAgain);

User improvedMe = new User("Massimissimo", "Esperto");

FindOneAndReplaceOptions replaceOptions = 
        new FindOneAndReplaceOptions().returnOriginal(false);
        
improvedMe = mongoCollection.findOneAndReplace(me, 
                improvedMe, 
                replaceOptions);

System.out.println(improvedMe);

mongoClient.close();


And you can see the result of the database writes:


 

And that's it! An example of how to implement the Codec interface of the new MongoDB Java Driver. MongoDB developers state that this new feature:

should be particularly useful for ODMs or other libraries, as they can write their own codecs to convert Java objects to BSON bytes

and they are probably right!

The whole example in this post can be downloaded as a Maven project here.

2 commenti:

  1. Hi Matteo. I've been exploring this codec feature and liked it so much, to the point to be willing to migrate my code to use it. So this question arised http://stackoverflow.com/questions/32417267/is-there-any-way-for-creating-mongo-codecs-automatically. As I couldn't find a way, I started this open source project that basically uses Java Annotation Processing to generate the codecs at compile time. You may be interested in it and maybe even willing to contribute. https://github.com/caeus/vertigo

    RispondiElimina