Couchbase on Rails: A Guide to Introducing Dynamic and Adaptive Data to Your Application

Couchbase on Rails: A Guide to Introducing Dynamic and Adaptive Data to Your Application

Discover how Couchbase's flexible document model and robust features can transform your Rails projects

Ruby on Rails is often the framework of choice for getting new projects off the ground. The speed of development and iteration is in many ways unparalleled. As such, perhaps you want to use Rails as the framework for your application that requires a lot of flexibility in data and has a fluid structure. Maybe you are building an Internet of Things application or a Content Management System that must handle all sorts of data. The traditional database solutions for Rails will just not cut it. What do you do?

In this guide, you will discover the considerations and steps to utilizing Couchbase as your database fully integrated into your Rails application. The decision to build with Couchbase introduces not only technical implementation changes, but also conceptual changes in the way you approach your data. Let’s dive into them.

tl;dr Interested in just seeing code? Check out a real world fully built example app demonstrating all CRUD actions with Ruby on Rails and the Couchbase Ruby ORM on GitHub.

Document model vs Relational model

In Rails, you’re accustomed to working with a relational model, typically using ActiveRecord. Data is organized in tables with rows and columns, and relationships between entities are defined using foreign keys. Couchbase, on the other hand, uses a document model, where data is stored in JSON documents.

The document model allows for more flexibility in data structures. For example, you now have the ability to store nested data directly into the same document of the parent data. This means that if you are building a blogging platform, comments on articles can be appended directly into the JSON document of each article, instead of associating comments to articles by the comment ID. When you would choose to use this ability or not depends on access patterns and performance considerations that you must give thought to as you design your application.

The difference in data will look like the following examples.

Embedded comments:

{
  "title": "My Article",
  "content": "This is the content of the article...",
  "comments": [
    {"author": "User1", "text": "Great article!"},
    {"author": "User2", "text": "Thanks for the info."}
  ]
}

Whereas, the traditional model that most Rails developers are familiar with looks like this:

{
  "title": "My Article",
  "content": "This is the content of the article...",
  "comment_ids": ["comment1", "comment2"]
}

When deciding how to model your data in Couchbase, it’s essential to consider how your application will read and write data. If you choose to embed comments within the article document, you can achieve faster read operations since all the data is contained in a single document. This approach is beneficial when you need to retrieve an article along with all its comments quickly. However, the downside is that any updates to the article or its comments require rewriting the entire document. This can be inefficient, especially if the document is large or if updates are frequent. Therefore, embedding comments is suitable for scenarios where comments are rarely updated independently of the article, and read performance is crucial.

On the other hand, referencing comments by their IDs allows for more granular updates. Each comment can be updated independently of the article, making write operations more efficient. However, this approach may result in slower read operations since retrieving an article with all its comments requires multiple document fetches. This pattern is advantageous when comments are frequently updated or when the overall document size needs to be kept smaller for performance reasons.

Understanding these trade-offs helps you make informed decisions on how to structure your data in your application. By carefully considering your application’s read and write patterns, you can optimize performance and ensure efficient data management.

Rails caching helps reduces any performance trade-offs

There is one method that you can utilize in your Rails application to mitigate the need to even deliberate on the potential trade-offs between using an embedded document approach or a referenced document approach. That approach is caching and leveraging ActiveSupport::Cache. Did you know you can do so with Couchbase with minimal extra configuration?

The Couchbase Ruby SDK includes support for an ActiveSupport cache store specifically for Couchbase data. This gives you all the benefits of ActiveSupport for your JSON document data. Let’s take a quick look at how this would work.

Once you have the Couchbase SDK installed by adding gem couchbase to your Gemfile and running bundle install from the command line, you are ready to integrate the caching support in your Rails application.

First, define the Couchbase store in your config.rb:

config.cache_store = :couchbase_store, {
  connection_string: # YOUR_COUCHBASE_CAPELLA_CONNECTION_STRING",
  username: YOUR_COUCHBASE_ACCESS_CREDENTIALS_USERNAME,
  password: YOUR_COUCHBASE_ACCESS_CREDENTIALS_PASSWORD",
  bucket: YOUR_COUCHBASE_BUCKET_NAME
}

Then, you can create a helper method in your application to fetch any data and store it in cache. For example, let’s say you are creating a blogging platform. Once an article is published, it often stays the same for a long period of time, and therefore is safe to keep in cache. Similarly, if you choose to embed comments in the article JSON document on Couchbase, you may only need to update the document and a fetch a new copy in cache whenever a new comment is added, which will certainly be less frequent than fetching the document for every single request regardless.

Your code may look like the following example. We create a method called #fetch_article_with_caching that fetches the article from Couchbase and parses the results to get the article contents and the CAS value. The CAS (Compare and Swap) value represents the current state of the document, ensuring concurrency control for every write operation. It helps check if the state of data in the local cache matches the most recent state in the database. Our method uses the CAS value to either update the application cache or return the article from the cache, reducing trade-offs between embedded data in Couchbase and traditional relational data models.

# Fetch an article with caching, checking for updates to comments or article
def fetch_article_with_caching(article_id)
  cache_key = "article_#{article_id}"

  # Fetch the article metadata (including CAS value)
  # CAS value is a token for concurrency control, check for document updates
  result = Article.bucket.default_collection.get(article_id, Couchbase::Options::Get(with_expiry: true))
  article = result.content
  cas = result.meta.cas

  # Fetch the cached article along with its CAS value
  cached_article, cached_cas = Rails.cache.read(cache_key)

  # Update the cache if the article or its comments have changed
  if cached_article.nil? || cached_cas != cas
    Rails.cache.write(cache_key, [article, cas], expires_in: 12.hours)
  else
    article = cached_article
  end

  article
end

# Example usage
article = fetch_article_with_caching("your_article_id")

No ActiveRecord… or, is there?

You might think that transitioning to Couchbase means saying goodbye to the familiar ActiveRecord library. However, thanks to the new Couchbase Ruby ORM, you can still enjoy an ActiveRecord-like experience when working with Couchbase in your Rails applications.

The Couchbase Ruby ORM provides an ORM layer that mimics the functionality and syntax of ActiveRecord, making the transition smoother for Rails developers. This library bridges the gap between the relational model you’re accustomed to and the document model used by Couchbase. It offers a syntax that Rails developers are familiar with, reducing the learning curve. You can define models, set attributes, and interact with the database in a manner similar to ActiveRecord.

For example, perhaps you need to define an Article class and create a new instance of it. Using the new ORM, doing so with Couchbase looks exactly like doing so with ActiveRecord.

class Article < CouchbaseOrm::Base
  attribute :title, type: String
  attribute :content, type: String
  attribute :comments, type: Array
end

article = Article.create(title: "My Article", content: "This is the content of the article...")

You are even able to define validations in the Article class just like you would with ActiveRecord.

class Article < CouchbaseOrm::Base
  attribute :title, type: String
  attribute :content, type: String
  attribute :comments, type: Array
end

## Ensure that every new article has a title
validates :title, presence: true

article = Article.create(title: "My Article", content: "This is the content of the article...")

What about creating associations between different models? Perhaps you want to make sure that an article can fetch its comments by invoking a #comments method using the has_many macro. This, too, is possible.

class Article < CouchbaseOrm::Base
  # Define the association and make it a destruction 
  # dependency when an article is deleted
  has_many :comments, dependent: destroy

  attribute :title, type: String
  attribute :content, type: String
  attribute :comments, type: Array
end

validates :title, presence: true

article = Article.create(title: "My Article", content: "This is the content of the article...")

Unique considerations

As you introduce Couchbase as your database in your Rails application, there are some things to consider that by doing so early will make your development work smoother and more efficient. First, as the Ruby ORM is very new, there is not yet a testing library that you can integrate into RSpec to create your mocks, stubs and define matchers as you build your testing.

This means that you will need to define your own mocks and other testing related items. For example, you can create a mock of an article and define an expectation around it.

RSpec.describe Article, type: :model do
 let(:article) do
    Article.new(id: 'article-id', title: 'Test Title', description: 'Test Description', body: 'Test Body', author_id: author.id)
  end

context 'when saving an article' do
    describe '#save' do
      it 'creates a new article record in the database' do
        allow(Article).to receive(:new).and_return(article)
        allow(article).to receive(:save).and_return(true)

        article.save
        expect(article.id).to eq('article-id')
      end
    end
  end
end

Another important feature of Couchbase data to remember is the role of Metadata in a Couchbase document. Metadata includes various pieces of information about the document, such as the document ID, the CAS value, expiration time, and more. This metadata can be helpful for managing and interacting with your data.

One of the key components of metadata is the document ID. The document ID is a unique identifier for each document in Couchbase. It allows you to retrieve, update, and delete documents based on this ID. Unlike relational databases where you often use primary keys, Couchbase relies on these document IDs to uniquely identify each JSON document.

To access the metadata of any of your JSON documents, you can create a custom query using the Ruby ORM. In the example below, the document ID is fetched by defining first the query and then a method to use it. The method returns both the document ID and the rest of the article data.

class Article < CouchbaseOrm::Base
  attribute :title, type: String
  attribute :content, type: String
  attribute :comments, type: Array

  # Custom query to fetch metadata ID along with article data
  n1ql :by_id_with_meta, emit_key: [:id], query_fn: proc { |bucket, values, options|
    cluster.query(
      "SELECT META(a).id AS meta_id, a.*
      FROM `#{bucket.name}` AS a
      WHERE META(a).id = $1",
      Couchbase::Options::Query(positional_parameters: values)
    )
  }
end

# Fetch and display the article with its metadata ID
def fetch_article_with_meta_id(article_id)
  results = Article.by_id_with_meta(article_id)
  results.each do |row|
    meta_id = row["meta_id"]
    article_data = row.reject { |key| key == "meta_id" }
    puts "Meta ID: #{meta_id}"
    puts "Article Data: #{article_data}"
  end
end

# Example usage
fetch_article_with_meta_id("your_article_id")

If you are not familiar with Couchbase yet, you may look at that query and think it looks a lot like SQL, and you would be correct! One of the great things about Couchbase is it introduced a query language – SQL++ – for NoSQL documents that provides the same experience for interacting with them as one would with a SQL table. Working with your data in Couchbase with SQL++ provides the same functionality and ergonomics you are familiar with in any SQL database. There is no need to introduce any additional cognitive overhead to your work. That’s the last thing any of us needs!

SELECT META(a).id AS meta_id, a.* FROM `#{bucket.name}` AS a WHERE META(a).id = $1

Wrapping Up

The combination of an ORM to provide an ActiveRecord-like experience for your dynamic NoSQL data in Rails and a SQL-like query language to cover other use cases offers a fully versatile database and data management system for your application. If you are interested in exploring what a complete functioning implementation looks like, you can clone and dive into a real world example Rails application that covers all create, read, update and delete operations using the Ruby ORM on GitHub.

Whenever your application requires data that does not easily conform to a rigid schema or a strictly defined structure and you are looking for how to accommodate that, Couchbase with the new Ruby ORM offers a compelling solution.

Did you find this article valuable?

Support Hummus on Rails by becoming a sponsor. Any amount is appreciated!