1. Introduction

Most tutorials use SQL databases, but the Crails Framework also supports other databases, such as MongoDB. In this tutorial, we'll see:

This tutorial assumes that you already have a MongoDB server running on your machine.

1.1 Installation

MongoDB support can optionally be installed by Crails' installation script, assuming that you already have the the MongoDB C++ driver installed on your system.
If you didn't install it then, or didn't use the installation script at all, here's how to install libcrails-mongodb and its dependencies on your system:

1.1.1 mongocxx

We will first need to install the MongoDB C++ driver.
Go to the installation manual and follow the instructions.

1.1.2 libcrails-mongodb

Next we will install libcrails-mongodb, using build2:

bpkg create -d crails-mongodb-build-gcc cc config.cxx=g++
cd crails-mongodb-build-gcc
bpkg add 'https://github.com/crails-framework/libcrails-mongodb.git#master'
bpkg fetch
bpkg build libcrails-mongodb '?sys:libmongocxx/*' '?sys:libcrails-databases/*' '?sys:libcrails/*'
bpkg install --all --recursive config.install.root=/usr/local config.install.sudo=sudo

Note that the bpkg build command imports several packages from your system: libmongocxx, libcrails-databases and libcrails. These package should already be installed on your system.

And that's it, you're all set for the next step.

2. Enabling MongoDB

2.1 Creating an application with a MongoDB database

Creating an application from scratch with mongodb support is pretty simple. Just run the following command:

$ crails new --name blog --database mongodb --formats html

Your new application has been created in the blog directory.

2.2 Adding a MongoDB database to an existing application

If you want to add mongodb support to an already existing application, go to the application's folder and run the following command:

$ crails plugins mongodb install

2.3 MongoDB database settings

Like with all database supported by Crails, the connection details are specified in the config/databases.cpp file. Let's see how it looks like after having installed MongoDB support:

config/databases.cpp
#include <crails/databases.hpp>

using namespace Crails;
using namespace std;

const Databases::Settings Databases::settings = {
  {
    Production, {
    }
  },

  {
    Development, {
      {"mongo",{{"name", "blog"},{"port", static_cast<unsigned int>(27017)}}}
    }
  },

  {
    Test, {
    }
  }
};

Right now, we only have one database configuration, internally named mongo, which a MongoDB will store in a database named blog. This configuration is only valid in the "Production" environment: let's see how we could setup MongoDB servers for other environments:

config/databases.cpp
#include <crails/databases.hpp>

using namespace Crails;
using namespace std;

const Databases::Settings Databases::settings = {
  {
    Production, {
      {"mongo",{{"name", "blog"},{"host", "mongo-server.org"},{"port", static_cast<unsigned int>(27017)}}}
    }
  },

  {
    Development, {
      {"mongo",{{"name", "blog_dev"},{"port", static_cast<unsigned int>(27017)}}}
    }
  },

  {
    Test, {
      {"mongo",{{"name", "blog_test"},{"port", static_cast<unsigned int>(27017)}}}
    }
  }
};

mongo is the default internal name for MongoDB databases: you should not change it unless your application needs to work with multiple MongoDB databases.

3. Controllers & Models

3.1 Scaffolding

If mongodb is the only database backend enabled in your application, then the scaffolding commands will generate your controllers and models with MongoDB support in mind.

Let's generate a whole set of controller, model and views with the following command:

$ crails scaffold resource \
  --model article \
  --format html \
  --property std::string/title std::string/author std::string/content

3.2 Models

Let's take a look at the generated MongoDB model:

app/models/article.hpp
#pragma once
#include <crails/mongodb/model.hpp>
class Article : public Crails::MongoDB::ModelInterface
{
public:
  static const std::string collection_name;
  static const std::string scope;
  static const std::string plural_scope;
  static const std::string view;

  void set_id(const std::string& value) { id = value; }
  std::string get_id() const override { return id; }
  std::string to_json() const override;
  void edit(Data) override;
  void merge_data(Data) const;
  
  void set_author(const std::string& value) { this->author = value; }
  const std::string& get_author() const { return author; }
  void set_content(const std::string& value) { this->content = value; }
  const std::string& get_content() const { return content; }
  void set_title(const std::string& value) { this->title = value; }
  const std::string& get_title() const { return title; }


private:
  std::string id;
  std::string author;
  std::string content;
  std::string title;
};

First thing to note is that the model inherits Crails::MongoDB::ModelInterface. This interface helps other Crails MongoDB related tools to persist and load your models from the results of requests made through mongocxx's API.

Next thing to notice are the global constants defined by this class:

Lastly, there are two methods that you should keep track of, as you'll need to update them whenver you add or remove properties to the model:

  void edit(Data) override;
  void merge_data(Data) const;

The edit method updates the model from the content of a Data object.

The merge_data does the opposite, putting the content of a model into a Data object.

Let's take a look at their implementation:

app/models/article.cpp
#include "article.hpp"

const std::string Article::collection_name = "Article";
const std::string Article::scope = "article";
const std::string Article::plural_scope = "articles";
const std::string Article::view = "";

void Article::edit(Data params)
{
  if (params["author"].exists())
    set_author(params["author"]);
  if (params["content"].exists())
    set_content(params["content"]);
  if (params["title"].exists())
    set_title(params["title"]);
}

void Article::merge_data(Data out) const
{
  out["author"] = this->author;
  out["content"] = this->content;
  out["title"] = this->title;
}

std::string Article::to_json() const
{
  DataTree out;

  merge_data(out);
  return out.to_json();
}

3.3 Controllers

Let's now take a look at our controller:

app/controllers/article.cpp
#pragma once
#include "app/controllers/application.hpp"
#include "app/models/article.hpp"
#include <crails/mongodb/controller.hpp>

class Article;

class ArticleController : public Crails::MongoDB::Controller<ApplicationController>
{
public:
  ArticleController(Crails::Context&);

  void initialize() override;
  void finalize() override;
  void index();
  void show();
  void create();
  void update();
  void destroy();
  void new_();
  void edit();
protected:
  std::shared_ptr<Article> find_model(std::string id);
  void require_model(std::string id);
  void find_list();

  std::shared_ptr<Article> model;
  std::vector<Article> model_list;
};

Our controller inherits from Crails::MongoDB::Controller: this allows you to interact with your MongoDB databases simply by using a Crails::MongoDB::Connection object always available from your controller, under the property name database.
On top of it, mongodb related operation will be timed, and time spent on these operations will be reported in the application logs after each query.

3.1 Querying for models

Our controller uses the method find_list to figure out what the article index should contain. In this chapter, we'll learn how to make more advanced queries by adding options to our article index. Let's first see what the method looks like:

app/controllers/article.cpp
void ArticleController::find_list()
{
  Crails::MongoDB::Result<Article> results;

  database.find(results);
  model_list = results.to_vector();
  std::cout << model_list.size() << std::endl;
  vars["models"] = &model_list;
}

We first create a Crails::MongoDB::Result object, before calling database.find(results) to fetch all the objects from the article collection.
These objects are then converted to the Article model type and stored into the model_list vector. Lastly, the vector is made available to the views through the models shared variable.

Let's try to add an option to search among articles. We'll be using libmongocxx document builder to create a query that checks the articles' title attribute against a regular expression:

app/controllers/article.cpp
void ArticleController::find_list()
{
  Crails::MongoDB::Result<Article> results;
  std::string search_query = params["query"].defaults_to<std::string>("");
  bsoncxx::document::view index_query{};

  if (search_query.length() > 0)
  {
    index_query = bsoncxx::builder::stream::document{}
      << "title"
      << bsoncxx::builder::stream::open_document
        << "$regex" << search_query
        << bsoncxx::builder::stream::close_document
      << bsoncxx::builder::stream::finalize;
  }
  database.find(results, index_query);
  model_list = results.to_vector();
  std::cout << model_list.size() << std::endl;
  vars["models"] = &model_list;
}

This example show you that database.find(results) can also take a query as an optional second argument. This type of this second parameter is bsoncxx::document::view_or_value, meaning you are allowed to provide either bsoncxx::document::view or bsoncxx::document::value.

In this example, we built a bsoncxx::document::view using bsoncxx's stream-based document builder. You can learn more about how to provide bson documents from mongocxx "Working with BSON" manual.