Persistence with ROM in Hanami
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:
- Relations map database tables to queryable objects
- Repositories encapsulate data access and provide an entity interface
- Migrations manage your schema evolution
- Commands generate CRUD methods automatically
- Transactions ensure atomicity for complex operations
In the next tutorial, we’ll explore Hanami Slices—Hanami’s approach to organizing code in larger applications.