Getting started with Nitrite

When I prepare my blog posts or when I need to create a proof-of-concept application, I often face the following issue: generally speaking, any minimal functionality requires to have any kind of data store. Relational databases, like MySQL or Postgresql do not solve this problem, because they often need preparation process, like schema generation etc. Also, as an author, you could not assume, that readers do have installed and configured database servers. For sure, with containers (as docker), things became simpler, but not so much. Again, and again, when I read posts, I note, that a quite long part of them is still devoted to setup and configuration of databases. I do not want to have such inconvenience for readers, because I assume, that they came to get information about particular cases regarding Vert.x, Spring Webflux or other technologies, that I observe in my posts. I would not like to put databases configuration sections in each post just to make them longer. There is an area where embedded databases really shine. In this field, we have obvious candidates, like SQLite, H2 or Derby (if you want to demonstrate a usage of relational databases) or embedded MongoDB. They are also very good choices for testing purposes.

Recently, I found a interesting project, called Nitrite. This is an embedded NoSQL storage, but comparing to the Embedded MongoDB, it is easy to use and to configure. It is also lightweight. This data storage, likewise Mongo, is document-oriented and schema less. It can keep data both on disk (in temporary files) or in-memory. Therefore, I decided to write a small post about this technology and to highlight its core features. Please note, that this article covers the 3.x release.

How does Nitrite work?

As it was mentioned in the introduction, the project is an implementation of a document model. This means, that data is not kept in tables, that are defined in a table format, but data is stored in a form of JSON-like entities. Being schema less, it comes with both advantages and disadvantages. An obvious strength of such storage is that they are easy to use, as no initial setup is required. Also, document entities “look” similar to data classes and that make a transition easier for developers. Document based databases scale horizontally and allow to keep to in a single collection (this term can be different across specific platforms) entities of the different structure. But here comes the major problem as well. When you affect the schema of relational databases (for instance, you add new columns), you are sure, that all rows will have these fields. However, document based databases do not perform any schema checking, and you can rely only on your application’s data classes in this question. Another issue comes with joining. While document entities can have very sophisticated structure with many embedded objects, it is harder than with RDBMS to join several collections. However, that are general considerations about using document-based databases, and for sure they have their use cases.

So, you got an idea, what is the Nitrite project. From the technical point of view, main components, that you would use are:

  • Nitrite – this class represents the database instance; it is created using the builder and practically you use this class in order to get a reference to particular collections with the getCollection() method
  • NitriteCollection – a collection is a place where documents are kept; the collection class exposes an API for CRUD operations
  • Document – this class defines a type-safe container to keep data of the particular item; it is implemented in a form of key-value storage; each document has generated NitriteId, that is a long value under the hood

Most of your time working with Nitrite, you will handle these three classes. As I have said before, Nitrite is configured in an easy way, and all what you need to do in order to initialize it, is to use the builder and to obtain an instance of the Nitrite class. Take a look on the following code snippet below, that demonstrates how to create a new Nitrite instance:

Nitrite nitrite = Nitrite.builder().openOrCreate();

This is a basic use case, but that is enough for most situations. For more precise configuration, you can do following:

  • NitriteBuilder.filePath(String path) – set up the file path to be used (if you need to store data between sessions on the disk)
  • NitriteBuilder.openOrCreate(String username, String password) – set up the username and password to access a database

Once, you have initialized the Nitrite database, you can use it to retrieve collections. Note, that a collection is retrieved using the getCollection(String name) method. This function returns a collection (if it does exist) or create a new collection (if it does not exist):

NitriteCollection collection = nitrite.getCollection("my-collection");

Note, that you can also use the hasCollection method in order to check if the collection does exist already. The collection class provides methods for CRUD actions. Let observe how to use them.

Writing operations

The first group of methods can be defined as writing operations. These functions affect data items and therefore these methods create, update and remove documents.

Insertion of new documents

In order to create a new entity into the Nitrite collection, we use the insert() method. This function returns a WriteResult value, that is technically an iterable, which keeps IDs of all items, modified by a writing operation (same also goes for update and delete). Also, the insert() method can accept many documents using varargs, so we can insert as many objects as we need in the single step. In order to access generated IDs, we can utilize an iterator.

@Test
void writeTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    WriteResult result = collection.insert(document);
    Iterator<NitriteId> iterator = result.iterator();
    if (iterator.hasNext()){
        NitriteId id = iterator.next();
        Assertions.assertThat(id).isNotNull();
        logger.info(id.getIdValue().toString());
    } else {
        Assertions.fail("No ids");
    }
}

What could we note here? There are interesting points, which I think we need to highlight. The first is that we use an iterator (because under the hood, the WriteResult implements an Iterable) to access IDs. Therefore, we can say, that if the iterator.hasNext() returns the false value, no documents were inserted. The second thing is that the NitriteId is a container class. An actual ID is a long value, that we can retrieve with the getIdValue() method.

Update documents

In order to modify documents we call the update() method. This operation takes two arguments:

  • Filters to select documents for modifications
  • Document object, that represents actual modifications to be made

Note, that if the first argument is null, changes will apply to all documents in a collection. Take a look on my example, that updates a name of the person, based on an email address:

@Test
void updateTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    WriteResult ir = collection.insert(document);
    long iid = ir.iterator().next().getIdValue();

    Document update = new Document();
    update.put("name", "Not John Doe anymore");
    WriteResult ur = collection.update(Filters.eq("email", "john.doe@email.com"), update);
    long uid = ur.iterator().next().getIdValue();

    Assertions.assertThat(iid).isEqualTo(uid);
}

Deletion

The last writing operation in this section is a remove operation. We can do it in two ways:

  • Using filters to find documents that needed to be deleted from the collection
  • Delete the particular item by passing it to the remove() method; in this case the element has to have an id

Let demonstrate how to remove a document in Nitrite:

@Test
void removeTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    WriteResult result = collection.insert(document);
    Iterator<NitriteId> iterator = result.iterator();
    if (iterator.hasNext()){
        NitriteId id = iterator.next();
        Assertions.assertThat(id).isNotNull();        
        collection.remove(Filters.eq("email", "john.doe@email.com"));
        Cursor cursor = collection.find();
        List<Document> list = cursor.toList();
        Assertions.assertThat(list).isEmpty();
    } else {
        Assertions.fail("No ids");
    }
}

Query documents

The second group of operation consists of query methods or selection methods. These actions do not affect documents in a collection; they allow to retrieve them using certain criteria. There are three approaches to get documents from the Nitrite collection:

  • find() – the method without arguments get all documents that are kept by the collection
  • find(Filters filters) – this version allows to pass filters to select only documents that meet criteria
  • getById(NitriteId id)– select a single document based its ID; note, that the method accepts NitriteId instance as an argument

Let first have a look on how to retrieve all documents with Nitrite:

@Test
void getAllTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    WriteResult write = collection.insert(document);

    Cursor cursor = collection.find();
    List<Document> list = cursor.toList();

    Assertions.assertThat(list).hasSize(1);

    Document d = list.get(0);
    Assertions.assertThat(d.get("name").toString()).isEqualTo("John Doe");
    Assertions.assertThat(d.get("email").toString()).isEqualTo("john.doe@email.com");
    Assertions.assertThat(d.getId().toString()).isNotNull().isNotEmpty();
}

Consider the fact, that the method returns a Cursor object. This class works in the same way as cursors in other databases, and it allows to access elements in two main ways:

  • Using an iterator and to iterate over documents
  • Convert the cursor’s content to a Java list (this approach allows to use streams in the next step)

If you need to select documents based on some criteria, you can set filters:

@Test
void selectTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    collection.insert(document);

    Cursor cursor = collection.find(Filters.eq("email", "john.doe@email.com"));
    List<Document> list = cursor.toList();
    Assertions.assertThat(list).hasSize(1);
    Document d = list.get(0);
    Assertions.assertThat(d.get("name").toString()).isEqualTo("John Doe");
    Assertions.assertThat(d.get("email").toString()).isEqualTo("john.doe@email.com");
    Assertions.assertThat(d.getId().toString()).isNotNull().isNotEmpty();
}

Finally, we can retrieve a single document by passing its ID to the getById method. Note, that if document is not available in the collection, the result would be null:

@Test
void findOneTest(){
    NitriteCollection collection = nitrite.getCollection("my-collection");
    Document document = new Document();
    document.put("name", "John Doe");
    document.put("email", "john.doe@email.com");
    WriteResult ir = collection.insert(document);
    NitriteId iid = ir.iterator().next();

    Document result = collection.getById(iid);
    Assertions.assertThat(result.get("email").toString()).isEqualTo("john.doe@email.com");
}

Filters

The last thing I would like to cover in this guide, before we will finish is to cover built-in filters. If you are familiar with MongoDB, you will find them familiar. Filters are created using static methods from the Filters class and can be combined with the Filters.and() method. Filters include helpers for comparison, logical operations and matching. There is a list of most common filter operations in Nitrite:

  • eq – equals
  • gt – greater
  • gte – greate or equal
  • “`lt““ – less
  • lte – less or equal
  • in – takes an array of objects and checks that value is in it
  • not – inverts thepassed filter
  • or – accepts an array of filters and performs a logical OR operation
  • and – accepts an array of filters and performs a logical AND operation
  • regex – checks that the value matches a certain regular expression

Conclusion

In this post we observed Nitrite. It is Java-based embedded NoSQL storage, that can be used in small applications, proof-of-concept objects or for tutorials. It is quite interesting solution and allows to store data both in memory or in the file. We reviewed how to perform writing operations (create, update, delete), how to select documents. Additionally, we list common built-in filters. You can access source code for this post – please note, that it lives in the Vertx examples repository. If you like this post, don’t be shy to share it and connect me in social media to be notified about new posts and projects. If you have questions regarding the usage of Nitrite, you can contact me.