In Progress
Unit 1, Lesson 1
In Progress

Getting started with rom-rb

Because of the ubiquity of the Ruby on Rails web framework, the Rails ActiveRecord library has become the de-facto standard for managing database persistence in Ruby applications. But ActiveRecord—both the library and the pattern it embodies—is not a one-size-fits-all solution. When Martin Fowler first defined the Active Record pattern, he noted that it only works well:
…if the Active Record objects correspond directly to the database tables… if your business logic is complex, you’ll soon want to use your objects direct relationships, collections, inheritance, and so forth. These don’t map easily to Active Record, and adding them piecemeal gets very messy.

Most Ruby developers I know have a story or two about business logic in Active Record objects getting very messy indeed. But what is the alternative?

The ROM library is one option. This library seeks to cleanly separate various aspects of database persistence into small, focused objects. Today, guest chef Tim Riley joins us for the first in a three-part exploration of ROM. In today’s episode, you’ll get a first look at what makes ROM’s approach unique. Enjoy!

Video transcript & code


ruby logo

If you’ve built applications in Ruby...


ruby logo + database

...you’ve likely worked with databases. They enable so much for us.

But too often they can also cause us problems. The way we work with the database can end up constraining our applications in unwanted ways.


We can end up with too many details of the database spread throughout our domain logic.

Or we can end up with an application coupled too tightly to our database’s structure, simply because that's what our tools made it easiest to do.

These things all make our applications harder to maintain.


rom-rb logo

But what if we could find a toolkit that enabled powerful integration with a database, while also encouraging cleaner application design?


rom-rb logo with text

That toolkit is rom-rb, and we'll be spending 3 episodes together exploring how it works, and how it helps improve our application architecture.

If you've mostly worked with Rails, some of this may feel foreign to you, but stick with it, and you might just see your work with the database in a new light.

OK! Let's get started!


One of the defining patterns within rom is the repository pattern.


ROM::Repository

rom repositories serve as the key interface betwen our application's domain layer -- where our core business logic resides -- and the persistence layer -- where we care about the details of interacting with the the database.


So if we were building a blog, we would want an ArticleRepo to offer access to our articles.


class ArticleRepo < ROM::Repository
end

Let's say we wanted to show a list of most recent articles on the home page. For this, we can add a #latest method:


class ArticleRepo < ROM::Repository
  def latest
  end
end

To be able to write this method, we first need to move away from our repository over to a relation class.

Relations contain the low-level query logic for each data source.


In this case, we want an Articles relation...


module Relations
  class Articles < ROM::Relation[:sql]
  end
end

... corresponding to the articles table in our SQL database.

Here was also tell rom to infer this relation's attributes by reading the table's columns.


module Relations
  class Articles < ROM::Relation[:sql]
    schema :articles, infer: true
  end
end

Now we can build an #ordered_by_recency method to give us our articles in the right order.


module Relations
  class Articles < ROM::Relation[:sql]
    schema :articles, infer: true

    def ordered_by_recency
    end
  end
end

For SQL relations like this one, rom gives us a powerful API for querying the database. We can use it to achieve our ordering here.

We want to order by publishing date and time, descending:


module Relations
  class Articles < ROM::Relation[:sql]
    schema :articles, infer: true

    def ordered_by_recency
      order { published_at.desc }
    end
  end
end

This is enough for us to back to our repository and flesh out that #latest method. To start with, we need to access our relation. rom sets things up for us so we can refer to relations here by their names, in this case, articles:


class ArticleRepo < ROM::Repository
  def latest
    articles
  end
end

Now, let's pause at this point to get an instance of this repository.

We need to provide a configured rom environment to its initializer; rom doesn't rely on any global state, so everything must be explicitly passed in.

In real apps, a framework integration will help take care of this for you.


class ArticleRepo < ROM::Repository
  def latest
    articles
  end
end

repo = ArticleRepo.new(rom)

Now we have our instance, we can call our #latest method and see the articles relation being returned.

We can also see that this relation is indeed wrapping a query to our articles table:


class ArticleRepo < ROM::Repository
  def latest
    articles
  end
end

repo = ArticleRepo.new(rom)
repo.latest
# => #<Relations::Articles name=ROM::Relation::Name(articles) dataset=#<Sequel::SQLite::Dataset: "SELECT `articles`.`id`, `articles`.`title`, `articles`.`published_at` FROM `articles` ORDER BY `articles`.`id`">>

So let's go ahead and call #ordered_by_recency on this relation.

We can now see the relation has changed to wrap a different query, with the ORDER BY clause working on that published_at column.


class ArticleRepo < ROM::Repository
  def latest
    articles.ordered_by_recency
  end
end

repo = ArticleRepo.new(rom)
repo.latest
# => #<Relations::Articles name=ROM::Relation::Name(articles) dataset=#<Sequel::SQLite::Dataset: "SELECT `articles`.`id`, `articles`.`title`, `articles`.`published_at` FROM `articles` ORDER BY `articles`.`published_at` DESC">>

So far we've only seen relation objects returned, not any actual article records.

This is by design: relations are intended to be chainable and composable.

Every method we call on a relation returns a modified copy of that relation, and we can refine our queries by continuing to chain these method calls.


For our repository to act as an effective interface, however, we want to materialize our relation, asking it to go ahead and load its data from the database.


We can do this by calling #to_a on the relation, which coerces it into an array of records:


class ArticleRepo < ROM::Repository
  def latest
    articles.ordered_by_recency.to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest
# => [#<ROM::Struct::Article id=2 title="Cat fingers" published_at=2018-10-15 00:00:00 +1100>,
#     #<ROM::Struct::Article id=1 title="Together breakfast" published_at=2018-10-14 00:00:00 +1100>]

Our article records are structs, simple value objects.

These objects carry their data only; no connection at all back to the database.

This is worth dwelling on for a moment, because this represents a major improvement to our application architecture.

Since our records are data only, the repositories serve as the sole interface to the database.

If we need to discover anything at all about how we work with the database, we now have one clear place to look. And there can be no creeping source of bugs from unfettered database access being passed around with our application records.


# => [
#      #<ROM::Struct::Article
#        id=2
#        title="Cat fingers"
#        published_at=2018-10-15 00:00:00 +1100>,
#
#      #<ROM::Struct::Article
#        id=1
#        title="Together breakfast"
#        published_at=2018-10-14 00:00:00 +1100>
#    ]

We aren't limited to working with raw data only. We can customize our article structs. To do this, we want to make our own structs module, then define an Article class within it, inheriting from ROM::Struct.


module Entities
  class Article < ROM::Struct
    def display_title
      [published_at.strftime('%d %m'), title].join(" / ")
    end
  end
end

Then we can configure our repository to use this module as its struct_namespace:


class ArticleRepo < ROM::Repository
  struct_namespace Entities

  def latest
    articles.ordered_by_recency.to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest

And now the results are instances of our own Article class:


class ArticleRepo < ROM::Repository
  struct_namespace Entities

  def latest
    articles.ordered_by_recency.to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest
# => [#<Entities::Article id=2 title="Cat fingers" published_at=2018-10-15 00:00:00 +1100>,
#     #<Entities::Article id=1 title="Together breakfast" published_at=2018-10-14 00:00:00 +1100>]

Which means we can go ahead and call on any extra behaviour we've defined:


class ArticleRepo < ROM::Repository
  struct_namespace Entities

  def latest
    articles.ordered_by_recency.to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest
repo.latest.map(&:display_title)
# => ["Oct 15 - Cat fingers", "Oct 14 - Together breakfast"]

Let's go one step further. Let's say we want to associate each article to an author, via an author_id foreign key column, and we want to show these authors when we present our listing of articles.


To make this happen, we start by informing rom of our authors by defining an Authors relation.


module Relations
  class Authors < ROM::Relation[:sql]
    schema :authors, infer: true
  end
end

Then we can go back into our Articles relation, and update its schema to include an association: every article belongs_to an author:



module Relations
  class Articles < ROM::Relation[:sql]
    schema :articles, infer: true do
      associations do
        belongs_to :author
      end
    end

    def ordered_by_recency
      order { published_at.desc }
    end
  end
end

Now, with this association in place, we can go over to our repository and update our method there. We tell the articles relation to combine itself with authors:


class ArticleRepo < ROM::Repository
  struct_namespace Entities

  def latest
    articles
      .combine(:author)
      .ordered_by_recency
      .to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest

And as we can see, combine does what it says on the tin.


class ArticleRepo < ROM::Repository
  struct_namespace Entities

  def latest
    articles
      .combine(:author)
      .ordered_by_recency
      .to_a
  end
end

repo = ArticleRepo.new(rom)
repo.latest
# => [#<Entities::Article id=2 title="Cat fingers" author_id=2 published_at=2018-10-15 00:00:00 +1100 author=#<Entities::Author id=2 name="Kat Morris">>,
#     #<Entities::Article id=1 title="Together breakfast" author_id=1 published_at=2018-10-14 00:00:00 +1100 author=#<Entities::Author id=1 name="Rebecca Sugar">>]

Every article we're returned is combined with its associated author.


We've taken the time to set up this nested data structure right here in our repository, because unlike ActiveRecord, the records we pass around our application are those simple structs, so we can't just go and load our authors later on.

Instead, rom encourages us to be mindful of our data requirements up front, and fetch everything in one go.

And as a bonus, this entirely eliminates any chance of n+1 problems in our application!


Let's recap. To work with articles in our database, we built a repository to act as the interface to those articles.


# Recap

- Repository

That repository worked with one or more relations, each relation corresponding to a database table and providing the place to store the detailed query logic.


# Recap

- Repository
- Relations

And back from the repository, the database records were returned to us as structs, simple value objects, which we could customise as desired.


# Recap

- Repository
- Relations
- Structs

So we touched on 3 different things to fetch our list of articles. That might seem like a lot of effort for what we otherwise get with a single ActiveRecord subclass, but it was all for a reason:


# Benefits

It separated our persistence logic into clear layers: repositories as the front door, relations for detailed query logic, and structs for modelling our records.

This helps ensure persistence details don't leak out into the rest of our application, and as our requirements for working with the database grow, it will be important for keeping things organised.


# Benefits

- Layers

The repositories also come with an intentionally narrow API. To build our repositories, we must consider every requirement our app has for database persistence, then name and write our own methods to satisfy those requirements.

This gives us a persistence API that is 100% fit for purpose, with usage that is much easier to trace through the rest of our app.


# Benefits

- Layers
- Fit for purpose

And finally, much of this arrangement can be defined by it's focus, by the things that it doesn't do. rom as a persistence toolkit is dedicated to persistence only.

What you won't find in rom are features for validations or callbacks or HTTP form post handling. All of those are much better handled by dedicated tools and in other parts of our application.

This should result in a much more even spread of logic across our application, across a larger number of smaller, better focused components, all of which should be easier to understand, test, and work on over time.


# Benefits

- Layers
- Fit for purpose
- Focus

So that's our first look into rom-rb.

Next time, we'll see how rom can help us write data back to our database.

Until then, happy hacking!

Responses