Persistence with ROM in Hanami

· 4 min read · Updated March 7, 2026 · intermediate
hanami rom persistence database ruby

In the previous tutorial, we explored Hanami’s action and view layer. Now we’ll dive into persistence using ROM (Ruby Object Mapper). ROM is Hanami’s default data persistence library, providing a clean, functional approach to working with databases.

What is ROM?

ROM is a Ruby library that maps Ruby objects to database records. Unlike ActiveRecord, ROM takes a more minimal approach—it’s not an ORM in the traditional sense. Instead, ROM focuses on providing building blocks that you compose to fit your needs.

Hanami 2 integrates ROM through the hanami-model gem, giving you:

  • Relations — Database table mappings
  • Repositories — Data access objects that wrap relations
  • Commands — Database operations (create, update, delete)

Setting Up Your Database

First, configure your database connection. In a Hanami app, this typically goes in config/boot.rb or your environment configuration:

# config/boot.rb
Hanami.configure do
  environment :development do
    database do
      adapter :sqlite, Pathname.new("db/development.sqlite")
      migrations_path Pathname.new("db/migrations")
    end
  end
end

For PostgreSQL or MySQL, change the adapter:

database do
  adapter :postgres, host: "localhost", database: "myapp_development"
end

Defining Relations

A relation represents a database table. Create a relation for your entities:

# app/relations/books.rb
module App
  module Relations
    class Books < ROM::Relation[:sql]
      schema do
        attribute :id, Types::Integer
        attribute :title, Types::String
        attribute :author, Types::String
        attribute :published_at, Types:::timestamp
        attribute :created_at, Types:::timestamp
        attribute :updated_at, Types:::timestamp
      end
    end
  end
end

The schema block defines how database columns map to Ruby types. Hanami uses Dry::Types for type safety.

Creating Repositories

Repositories provide an interface between your application and relations. They encapsulate data access logic:

# app/repositories/books.rb
module App
  module Repositories
    class BookRepository < ROM::Repository[:books]
      commands :create, :update, :delete

      def all
        books.order(:created_at).to_a
      end

      def find(id)
        books.by_id(id).one
      end

      def find_by_title(title)
        books.where(title: title).one
      end

      def latest(limit = 10)
        books.order(published_at: :desc).limit(limit).to_a
      end

      def create_with_author(title, author_name)
        transaction do
          author = authors.create(name: author_name)
          books.create(title: title, author_id: author[:id])
        end
      end
    end
  end
end

The commands :create, :update, :delete line automatically generates CRUD methods.

Working with Entities

Entities are domain objects that wrap relation data:

# app/entities/book.rb
module App
  module Entities
    class Book < Hanami::Entity
      attributes do
        attribute :id, Types::Integer
        attribute :title, Types::String
        attribute :author, Types::String
        attribute :published_at, Types:::timestamp
      end
    end
  end
end

Repositories automatically map relation tuples to entities:

# In an action or service
book_repo = App::Repositories::BookRepository.new

# Create returns an entity
book = book_repo.create(title: "The Pragmatic Programmer", author: "David Thomas")

book.id          # => 1
book.title       # => "The Pragmatic Programmer"
book.class       # => App::Entities::Book

Database Migrations

Create migrations to manage your schema:

bundle exec hanami generate migration create_books

Edit the generated migration:

# db/migrations/20260307120000_create_books.rb
ROM::SQL.migration do
  change do
    create_table :books do
      primary_key :id
      column :title, String, null: false
      column :author, String, null: false
      column :published_at, :timestamp
      column :created_at, :timestamp, null: false
      column :updated_at, :timestamp, null: false
    end
  end
end

Run migrations:

bundle exec hanami db migrate

Complex Queries

ROM provides a powerful query DSL:

class BookRepository < ROM::Repository[:books]
  def search(query)
    books.where { title.like("%#{query}%") | author.like("%#{query}%") }
  end

  def published_between(start_date, end_date)
    books.where {
      (published_at >= start_date) & (published_at <= end_date)
    }.order(:published_at)
  end

  def with_recent_updates(limit = 5)
    books.order(updated_at: :desc).limit(limit)
  end
end

Associations

Define relationships between relations:

class Books < ROM::Relation[:sql]
  schema do
    attribute :id, Types::Integer
    attribute :title, Types::String
    attribute :author_id, Types::Integer

    associations do
      belongs_to :author
    end
  end
end

class Authors < ROM::Relation[:sql]
  schema do
    attribute :id, Types::Integer
    attribute :name, Types::String

    associations do
      has_many :books
    end
  end
end

Then combine data through associations:

class BookRepository < ROM::Repository[:books]
  def with_authors
    books.combine(:author).to_a
  end
end

# Returns books with author data nested
book = book_repo.with_authors.first
book.author.name  # => "David Thomas"

Transactions

Use transactions for atomic operations:

class OrderRepository < ROM::Repository[:orders]
  def create_order_with_items(order_data, items_data)
    transaction do
      order = orders.create(order_data)
      items_data.each do |item|
        order_items.create(order_id: order[:id], **item)
      end
      order
    end
  end
end

If any operation fails, the entire transaction rolls back.

When to Use ROM

ROM works well when you want:

  • Fine-grained control over database operations
  • A minimal, composable data layer
  • Explicit queries over magic methods
  • Type-safe database interactions

Consider alternatives if you need:

  • Heavy Rails integration (use ActiveRecord)
  • Very rapid prototyping with conventions
  • Complex migrations and schema management

Summary

You’ve learned how to set up ROM in Hanami 2, define relations, create repositories, and work with entities. Key takeaways:

  1. Relations map database tables to queryable objects
  2. Repositories encapsulate data access and provide an entity interface
  3. Migrations manage your schema evolution
  4. Commands generate CRUD methods automatically
  5. Transactions ensure atomicity for complex operations

In the next tutorial, we’ll explore Hanami Slices—Hanami’s approach to organizing code in larger applications.