Overview
ROM is a data mapping and persistence toolkit that provides a functional approach to data access in Ruby applications. ROM separates data mapping concerns from domain logic through adapters, relations, repositories, and mappers. The architecture emphasizes immutability, explicit data transformation, and clear separation between reading and writing operations.
ROM consists of several core components working together. Relations define data queries and transformations. Repositories encapsulate data access patterns. Mappers transform raw data into domain objects. Adapters connect to different data sources including SQL databases, HTTP APIs, and memory stores.
require 'rom'
# Basic ROM setup with SQL adapter
config = ROM::Configuration.new(:sql, 'sqlite://./database.db')
# Define a relation
class Users < ROM::Relation[:sql]
schema(:users, infer: true)
def by_email(email)
where(email: email)
end
end
# Register and finalize
config.register_relation(Users)
container = ROM.container(config)
ROM operates through explicit data flow patterns. Applications define schemas that describe data structure. Relations provide query interfaces. Repositories coordinate between relations and mappers. Mappers handle object construction and data transformation.
# Repository pattern
class UserRepository < ROM::Repository[:users]
def find_by_email(email)
users.by_email(email).one!
end
def create(attributes)
users.changeset(:create, attributes).commit
end
end
repo = UserRepository.new(container)
user = repo.find_by_email('user@example.com')
The functional approach means ROM relations are immutable. Each query method returns a new relation object rather than modifying the existing one. This enables composition and prevents side effects during query building.
Basic Usage
ROM applications start with configuration and adapter setup. The configuration object registers components and establishes connections to data sources. After registration, ROM creates a container that provides access to repositories and relations.
# Configuration with multiple adapters
config = ROM::Configuration.new(:sql, 'postgresql://localhost/myapp')
config.use :macros
# Define schema and relations
config.relation(:users) do
schema(:users, infer: true) do
attribute :id, Types::Integer
attribute :email, Types::String
attribute :created_at, Types::Time
primary_key :id
end
def active
where(status: 'active')
end
def recent(days = 30)
where { created_at >= Date.today - days }
end
end
Repositories provide high-level interfaces for data operations. They combine relations with commands to handle both reading and writing. Repository methods typically return domain objects rather than raw database records.
class UserRepository < ROM::Repository[:users]
commands :create, :update, :delete
def all_active
users.active.to_a
end
def recent_signups(days = 7)
users.recent(days).order(:created_at).to_a
end
def create_user(attributes)
create(attributes)
end
def update_user(id, attributes)
update(id, attributes)
end
end
Commands handle data modifications through changesets. Changesets validate and transform input data before persistence. ROM provides built-in commands for common operations and supports custom command definitions.
# Using repository with commands
repo = UserRepository.new(container)
# Create with validation
changeset = repo.users.changeset(:create, {
email: 'new@example.com',
name: 'New User'
})
if changeset.valid?
user = changeset.commit
else
puts changeset.errors
end
Relations support method chaining for complex queries. Each method returns a new relation, enabling composition without mutation. Relations automatically handle SQL generation and parameter binding.
# Query composition
active_recent_users = users
.active
.recent(14)
.where(verified: true)
.order(:created_at)
.limit(50)
# Execute and iterate
active_recent_users.each do |user_data|
# Process raw database record
puts "#{user_data[:name]} - #{user_data[:email]}"
end
Advanced Usage
ROM supports complex data transformations through mapper pipelines. Mappers receive raw data and return structured objects. Multiple mappers can compose together for sophisticated transformations.
# Custom mapper with nested data
class UserMapper < ROM::Transformer
import_functions ROM::Transformer::HashTransformations
define! do
map_array do
rename_keys user_name: :name
nest :profile, %i[bio avatar_url]
map_value :created_at do |timestamp|
Time.parse(timestamp) if timestamp
end
end
end
end
# Repository with custom mapper
class UserRepository < ROM::Repository[:users]
def all_with_profiles
users
.combine(:profile)
.map_with(UserMapper)
.to_a
end
end
Associations define relationships between different data sources. ROM handles loading strategies, including lazy loading, eager loading, and custom association definitions.
# Define associations in schema
config.relation(:users) do
schema(:users, infer: true) do
associations do
has_many :posts
has_one :profile
end
end
end
config.relation(:posts) do
schema(:posts, infer: true) do
associations do
belongs_to :user
end
end
end
# Repository methods with associations
class UserRepository < ROM::Repository[:users]
def with_posts(user_id)
users
.by_pk(user_id)
.combine(:posts)
.one!
end
def with_recent_posts
users
.combine(posts: posts.recent)
.to_a
end
end
Custom commands enable complex business logic during data persistence. Commands can validate, transform, and coordinate multiple operations.
# Custom command with business logic
class CreateUserCommand < ROM::Commands::Create[:sql]
def call(attributes)
# Pre-processing
processed_attrs = attributes.merge(
slug: generate_slug(attributes[:name]),
created_at: Time.now
)
# Execute with transaction
relation.transaction do
user = super(processed_attrs)
# Post-processing
create_welcome_message(user[:id])
user
end
end
private
def generate_slug(name)
name.downcase.gsub(/\W/, '-')
end
def create_welcome_message(user_id)
# Custom logic for side effects
end
end
ROM supports multi-adapter configurations for applications using different data sources. Each adapter handles its specific data format and query capabilities.
# Multiple data sources
config = ROM::Configuration.new
# SQL database
config.register_adapter(:sql, ROM::SQL::Adapter, 'postgresql://localhost/app')
# HTTP API
config.register_adapter(:http, ROM::HTTP::Adapter, {
uri: 'https://api.example.com',
headers: {
'Accept' => 'application/json'
}
})
# Define relations for different sources
config.relation(:users, adapter: :sql) do
# SQL-specific methods
def by_status(status)
where(status: status)
end
end
config.relation(:analytics, adapter: :http) do
dataset :events
def by_date_range(start_date, end_date)
with_params(from: start_date, to: end_date)
end
end
Production Patterns
ROM applications in production require careful attention to connection management and resource allocation. Connection pools prevent resource exhaustion during high concurrent load.
# Production database configuration
config = ROM::Configuration.new(:sql, ENV['DATABASE_URL'], {
pool: {
max_connections: 25,
initial_connections: 5,
connection_timeout: 5000
},
logging: Rails.env.development?
})
# Connection health monitoring
class DatabaseHealthCheck
def initialize(container)
@container = container
end
def healthy?
@container[:users].count
true
rescue => e
Rails.logger.error("Database health check failed: #{e}")
false
end
end
Repository patterns in web applications benefit from dependency injection and service layer organization. Repositories become service objects that encapsulate complex business operations.
# Service layer with repositories
class UserService
def initialize(user_repo: nil, notification_service: nil)
@user_repo = user_repo || UserRepository.new(ROM.env)
@notification_service = notification_service || NotificationService.new
end
def register_user(attributes)
ROM.env[:users].transaction do |t|
user = @user_repo.create_user(attributes)
@notification_service.send_welcome_email(user[:email])
user
rescue => e
t.rollback
raise e
end
end
def activate_user(user_id)
user = @user_repo.update_user(user_id, { status: 'active', activated_at: Time.now })
@notification_service.send_activation_confirmation(user[:email])
user
end
end
# Rails controller integration
class UsersController < ApplicationController
def create
@user_service = UserService.new
@user = @user_service.register_user(user_params)
redirect_to @user
rescue ROM::SQL::NotNullConstraintError
flash[:error] = "Required fields missing"
render :new
end
end
Caching strategies improve performance for frequently accessed data. ROM integrates with Rails cache stores and custom caching layers.
# Repository with caching
class CachedUserRepository < ROM::Repository[:users]
def initialize(*)
super
@cache = Rails.cache
end
def find_with_cache(id)
cache_key = "user:#{id}"
@cache.fetch(cache_key, expires_in: 1.hour) do
users.by_pk(id).one!
end
end
def popular_users
@cache.fetch('users:popular', expires_in: 30.minutes) do
users
.join(:posts)
.group(:users__id)
.having { count(:posts__id) > 10 }
.order { count(:posts__id).desc }
.limit(50)
.to_a
end
end
def invalidate_cache(user_id)
@cache.delete("user:#{user_id}")
@cache.delete('users:popular')
end
end
Background job integration requires careful transaction management and error handling. ROM transactions ensure data consistency across job processing.
# Background job with ROM
class UserProcessingJob
include Sidekiq::Worker
def perform(user_id)
container = ROM.env
user_repo = UserRepository.new(container)
container[:users].transaction do |t|
user = user_repo.find(user_id)
# Process user data
processed_data = expensive_processing(user)
# Update with results
user_repo.update(user_id, {
processed_at: Time.now,
processing_result: processed_data
})
rescue ProcessingError => e
t.rollback
# Mark as failed for retry
user_repo.update(user_id, {
processing_failed: true,
last_error: e.message
})
raise e
end
end
end
Performance & Memory
ROM queries generate optimized SQL through relation composition. Understanding query execution helps identify performance bottlenecks and optimization opportunities.
# Query optimization techniques
class OptimizedUserRepository < ROM::Repository[:users]
# Batch loading to avoid N+1
def users_with_post_counts
users
.left_join(:posts)
.select_group(:users)
.select_append { count(:posts__id).as(:post_count) }
.to_a
end
# Selective field loading
def user_summaries
users
.select(:id, :name, :email, :created_at)
.limit(1000)
.to_a
end
# Streaming for large datasets
def process_all_users_in_batches(batch_size = 1000)
users.each_batch(batch_size) do |batch|
batch.each do |user|
yield user
end
end
end
end
Memory management becomes critical when processing large datasets. ROM provides streaming interfaces and batch processing capabilities to control memory usage.
# Memory-efficient data processing
class DataExportService
def initialize(container)
@container = container
end
def export_users_csv(filename)
File.open(filename, 'w') do |file|
file.puts "ID,Name,Email,Created At"
# Stream results to avoid loading all records
@container[:users].each do |user|
file.puts "#{user[:id]},#{user[:name]},#{user[:email]},#{user[:created_at]}"
end
end
end
def update_users_in_batches
total_processed = 0
@container[:users].each_batch(500) do |batch|
@container[:users].transaction do |t|
batch.each do |user|
# Process individual user
update_user_data(user)
total_processed += 1
end
puts "Processed #{total_processed} users"
end
end
end
end
Connection pooling configuration affects both performance and resource usage. Pool sizing depends on application concurrency patterns and database capabilities.
# Performance monitoring integration
class PerformanceAwareRepository < ROM::Repository[:users]
def initialize(*)
super
@metrics = ActiveSupport::Notifications
end
def find_with_metrics(id)
@metrics.instrument('rom.query', {
relation: 'users',
operation: 'find'
}) do
users.by_pk(id).one!
end
end
def complex_search_with_timing(criteria)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
results = users
.join(:profiles)
.where(criteria)
.combine(:posts)
.to_a
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
duration_ms = ((end_time - start_time) * 1000).round(2)
Rails.logger.info("Complex search completed in #{duration_ms}ms, #{results.size} results")
results
end
end
Index usage and query plan analysis help optimize database performance. ROM exposes query information for analysis and monitoring.
# Query analysis tools
class QueryAnalyzer
def initialize(container)
@container = container
end
def analyze_slow_queries
# Enable query logging
@container.gateways[:default].connection.loggers << Logger.new(STDOUT)
# Execute and measure queries
queries = [
-> { @container[:users].where(status: 'active').to_a },
-> { @container[:users].join(:posts).group(:users__id).to_a },
-> { @container[:posts].combine(:user, :comments).to_a }
]
queries.each_with_index do |query, index|
time = Benchmark.measure { query.call }
puts "Query #{index + 1}: #{time.real}s"
end
end
end
Reference
Core Classes
Class | Purpose | Key Methods |
---|---|---|
ROM::Configuration |
Setup and registration | #register_relation , #register_command , #use |
ROM::Container |
Component access | #[] , #relations , #commands |
ROM::Repository |
High-level data access | #initialize , #transaction |
ROM::Relation |
Query building | #where , #select , #order , #limit |
ROM::Commands::Create |
Record creation | #call , #[] |
ROM::Commands::Update |
Record updates | #call , #by_pk |
ROM::Commands::Delete |
Record deletion | #call , #by_pk |
Repository Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#commands(*names) |
*names (Symbol) |
nil |
Defines command methods |
#auto_struct(enabled) |
enabled (Boolean) |
self |
Enables automatic struct creation |
#struct_namespace(namespace) |
namespace (Module) |
self |
Sets struct namespace |
Relation Query Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#where(conditions) |
conditions (Hash/Proc) |
Relation |
Filters records |
#select(*fields) |
*fields (Symbol) |
Relation |
Projects specific columns |
#order(*fields) |
*fields (Symbol) |
Relation |
Orders results |
#limit(count) |
count (Integer) |
Relation |
Limits result count |
#offset(count) |
count (Integer) |
Relation |
Skips records |
#combine(*associations) |
*associations (Symbol) |
Relation |
Eager loads associations |
#by_pk(id) |
id (Object) |
Relation |
Filters by primary key |
Schema Definition
Method | Parameters | Returns | Description |
---|---|---|---|
attribute(name, type, **options) |
name (Symbol), type (Type), options (Hash) |
self |
Defines attribute |
primary_key(*names) |
*names (Symbol) |
self |
Sets primary key |
foreign_key(name, relation) |
name (Symbol), relation (Symbol) |
self |
Defines foreign key |
associations(&block) |
block (Proc) |
self |
Defines relationships |
Association Types
Type | Syntax | Description |
---|---|---|
has_many |
has_many :posts, foreign_key: :user_id |
One-to-many relationship |
has_one |
has_one :profile |
One-to-one relationship |
belongs_to |
belongs_to :user |
Many-to-one relationship |
has_and_belongs_to_many |
has_and_belongs_to_many :tags |
Many-to-many relationship |
Command Types
Command | Usage | Description |
---|---|---|
:create |
commands :create |
Insert new records |
:update |
commands :update |
Modify existing records |
:delete |
commands :delete |
Remove records |
Adapter Configuration Options
Adapter | Option | Type | Description |
---|---|---|---|
:sql |
pool |
Hash | Connection pool settings |
:sql |
logging |
Boolean | Enable query logging |
:http |
uri |
String | Base API URL |
:http |
headers |
Hash | Default request headers |
:memory |
dataset |
String | Memory dataset name |
Transaction Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#transaction(&block) |
block (Proc) |
Object |
Executes block in transaction |
#rollback |
None | nil |
Rolls back current transaction |
Error Classes
Error | Inheritance | Description |
---|---|---|
ROM::Error |
StandardError |
Base ROM error |
ROM::RelationNotDefinedError |
ROM::Error |
Missing relation definition |
ROM::NoRelationError |
ROM::Error |
Relation not found |
ROM::TupleCountMismatchError |
ROM::Error |
Unexpected result count |
ROM::SQL::NotNullConstraintError |
ROM::Error |
Database constraint violation |