CrackedRuby logo

CrackedRuby

ROM

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