ROM (Ruby Object Mapper)
ROM (Ruby Object Mapper) is "a Ruby data mapping toolkit based on functional programming principles" designed as a new approach to data persistence that serves as an alternative to the traditional Active Record pattern. Adopting the concepts of "Command Query Responsibility Segregation (CQRS)" and "functional programming," it achieves high maintainability and predictability through immutable objects, pure functions, and explicit dependencies. It can interact with multiple databases (PostgreSQL, MySQL, SQLite, Redis, Elasticsearch, etc.) through a unified API, significantly improving data layer design in complex enterprise applications.
Library
ROM (Ruby Object Mapper)
Overview
ROM (Ruby Object Mapper) is "a Ruby data mapping toolkit based on functional programming principles" designed as a new approach to data persistence that serves as an alternative to the traditional Active Record pattern. Adopting the concepts of "Command Query Responsibility Segregation (CQRS)" and "functional programming," it achieves high maintainability and predictability through immutable objects, pure functions, and explicit dependencies. It can interact with multiple databases (PostgreSQL, MySQL, SQLite, Redis, Elasticsearch, etc.) through a unified API, significantly improving data layer design in complex enterprise applications.
Details
ROM 2025 edition fully leverages the latest features of Ruby 3.3, providing a more expressive and safe data access layer. Through the combination of Repository Pattern, Data Mapper Pattern, and Unit of Work patterns, it has very high affinity with Domain-Driven Design (DDD) and strongly supports clean architecture implementation. It comprehensively provides features necessary for modern web application development, including multi-tenant support, read-write separation, custom data type support, and comprehensive validation systems. Additionally, leveraging the advantages of functional programming, it achieves predictable data operations with minimal side effects.
Key Features
- Functional Approach: Predictable processing through immutable objects and pure functions
- CQRS Support: Clear separation of commands and queries
- Repository Pattern: Data access abstraction and dependency inversion
- Multi-Adapter: Unified operations across multiple databases
- Schema Definition: Type-safe schema definition and validation
- DDD Integration: Natural integration with Domain-Driven Design
Pros and Cons
Pros
- Functional design solving Active Record's "god class" problem
- Improved predictability and safety through immutable objects
- High flexibility to operate multiple databases with the same API
- Significant improvement in testability and explicit side effect management
- Excellent affinity with Domain-Driven Design and clean architecture
- Maintainable code leveraging functional programming advantages
Cons
- High learning curve requiring understanding of functional programming
- Conceptual shift difficult for developers accustomed to Active Record
- Complex configuration and setup, excessive for small projects
- Integration with Rails ecosystem not as natural as Active Record
- Limited Japanese documentation and community resources
- Cannot leverage rich Active Record plugins
Reference Pages
Code Examples
Setup
# Gemfile
gem 'rom', '~> 5.3'
gem 'rom-sql', '~> 3.6'
gem 'rom-postgres', '~> 3.0' # For PostgreSQL
gem 'rom-repository', '~> 5.3'
# For development/testing
gem 'rom-factory', '~> 0.11', group: [:development, :test]
# Install
bundle install
# config/database.yml (when using Rails)
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
username: postgres
password: password
host: localhost
test:
<<: *default
database: myapp_test
username: postgres
password: password
host: localhost
production:
<<: *default
database: myapp_production
username: <%= ENV['DATABASE_USER'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
host: <%= ENV['DATABASE_HOST'] %>
# config/rom.rb
require 'rom'
require 'rom-sql'
require 'rom-repository'
# ROM configuration
ROM_CONFIG = ROM::Configuration.new(:sql, ENV['DATABASE_URL'])
# Load schema definitions
ROM_CONFIG.auto_registration('./app/persistence')
# Create container
ROM_CONTAINER = ROM.container(ROM_CONFIG)
Schema and Relation Definition
# app/persistence/relations/users.rb
module Relations
class Users < ROM::Relation[:sql]
schema(:users, infer: true) do
attribute :id, ROM::Types::Integer.meta(primary_key: true)
attribute :name, ROM::Types::String
attribute :email, ROM::Types::String
attribute :age, ROM::Types::Integer
attribute :is_active, ROM::Types::Bool
attribute :created_at, ROM::Types::DateTime
attribute :updated_at, ROM::Types::DateTime
primary_key :id
associations do
has_many :posts, foreign_key: :user_id
has_one :profile, foreign_key: :user_id
end
end
# Query methods
def active
where(is_active: true)
end
def by_age_range(min_age, max_age)
where { age >= min_age }.where { age <= max_age }
end
def search_by_name(pattern)
where { name.ilike("%#{pattern}%") }
end
def recent(limit = 20)
order(:created_at).reverse.limit(limit)
end
def with_post_count
select_append { integer::count(posts[:id]).as(:post_count) }
.left_join(:posts)
.group(:users[:id])
end
def adults
where { age >= 18 }
end
def by_email(email)
where(email: email)
end
end
end
# app/persistence/relations/posts.rb
module Relations
class Posts < ROM::Relation[:sql]
schema(:posts, infer: true) do
attribute :id, ROM::Types::Integer.meta(primary_key: true)
attribute :title, ROM::Types::String
attribute :content, ROM::Types::String
attribute :excerpt, ROM::Types::String.optional
attribute :is_published, ROM::Types::Bool
attribute :published_at, ROM::Types::DateTime.optional
attribute :user_id, ROM::Types::Integer.meta(foreign_key: true)
attribute :category_id, ROM::Types::Integer.meta(foreign_key: true).optional
attribute :view_count, ROM::Types::Integer
attribute :created_at, ROM::Types::DateTime
attribute :updated_at, ROM::Types::DateTime
primary_key :id
associations do
belongs_to :user, foreign_key: :user_id
belongs_to :category, foreign_key: :category_id
has_many :comments, foreign_key: :post_id
end
end
def published
where(is_published: true)
end
def by_user(user_id)
where(user_id: user_id)
end
def by_category(category_id)
where(category_id: category_id)
end
def popular(limit = 10)
where(is_published: true)
.order(:view_count)
.reverse
.limit(limit)
end
def recent_published(limit = 20)
published
.order(:published_at)
.reverse
.limit(limit)
end
def search_content(pattern)
where { title.ilike("%#{pattern}%") | content.ilike("%#{pattern}%") }
end
def with_author
combine(:user)
end
def with_category
combine(:category)
end
def with_comments_count
select_append { integer::count(comments[:id]).as(:comments_count) }
.left_join(:comments)
.group(:posts[:id])
end
end
end
# app/persistence/relations/categories.rb
module Relations
class Categories < ROM::Relation[:sql]
schema(:categories, infer: true) do
attribute :id, ROM::Types::Integer.meta(primary_key: true)
attribute :name, ROM::Types::String
attribute :description, ROM::Types::String.optional
attribute :color, ROM::Types::String
attribute :is_active, ROM::Types::Bool
attribute :sort_order, ROM::Types::Integer
attribute :created_at, ROM::Types::DateTime
primary_key :id
associations do
has_many :posts, foreign_key: :category_id
end
end
def active
where(is_active: true)
end
def ordered
order(:sort_order, :name)
end
def with_post_count
select_append { integer::count(posts[:id]).as(:post_count) }
.left_join(:posts)
.group(:categories[:id])
end
end
end
Repository Pattern Implementation
# app/repositories/user_repository.rb
class UserRepository < ROM::Repository[:users]
include ROM::Repository::Plugins::CommandAPIDefinition
commands :create, :update, :delete
# Relation loading configuration
def all
users.to_a
end
def find(id)
users.by_pk(id).one!
end
def find_by_email(email)
users.by_email(email).one
end
def active_users
users.active.to_a
end
def users_by_age_range(min_age, max_age)
users.by_age_range(min_age, max_age).to_a
end
def search_users(name_pattern)
users.search_by_name(name_pattern).to_a
end
def recent_users(limit = 20)
users.recent(limit).to_a
end
def users_with_post_count
users.with_post_count.to_a
end
def adult_users
users.adults.to_a
end
# User creation (with validation)
def create_user(attributes)
changeset = users.changeset(:create, attributes)
command = users.command(:create)
if changeset.valid?
command.call(changeset)
else
raise ROM::ValidationError, changeset.errors
end
end
# User update
def update_user(id, attributes)
user = find(id)
changeset = users.changeset(:update, attributes)
command = users.command(:update)
if changeset.valid?
command.by_pk(id).call(changeset)
else
raise ROM::ValidationError, changeset.errors
end
end
# User deletion (with related data)
def delete_user(id)
# Delete related posts first
posts.command(:delete).where(user_id: id).call
# Delete user
users.command(:delete).by_pk(id).call
end
# User statistics
def user_statistics
total_users = users.count
active_users_count = users.active.count
average_age = users.dataset.avg(:age)
{
total: total_users,
active: active_users_count,
average_age: average_age&.to_f&.round(2) || 0.0
}
end
# Complex query example
def users_with_recent_posts(days = 7)
cutoff_date = Date.current - days.days
users
.join(:posts)
.where { posts[:created_at] >= cutoff_date }
.select(:users[:id], :users[:name], :users[:email])
.group(:users[:id])
.to_a
end
# Transaction processing
def transfer_posts(from_user_id, to_user_id)
ROM_CONTAINER.gateways[:default].transaction do
from_user = find(from_user_id)
to_user = find(to_user_id)
raise ArgumentError, "One or both users not found" unless from_user && to_user
posts.command(:update)
.where(user_id: from_user_id)
.call(user_id: to_user_id, updated_at: Time.current)
end
end
private
def posts
ROM_CONTAINER.relations[:posts]
end
end
# app/repositories/post_repository.rb
class PostRepository < ROM::Repository[:posts]
include ROM::Repository::Plugins::CommandAPIDefinition
commands :create, :update, :delete
def all
posts.to_a
end
def find(id)
posts.by_pk(id).one!
end
def published_posts
posts.published.to_a
end
def posts_by_user(user_id)
posts.by_user(user_id).to_a
end
def posts_by_category(category_id)
posts.by_category(category_id).to_a
end
def popular_posts(limit = 10)
posts.popular(limit).to_a
end
def recent_published_posts(limit = 20)
posts.recent_published(limit).to_a
end
def search_posts(search_term)
posts.search_content(search_term).to_a
end
def posts_with_author
posts.combine(:user).to_a
end
def posts_with_category
posts.combine(:category).to_a
end
def posts_with_comments_count
posts.with_comments_count.to_a
end
# Post creation
def create_post(attributes)
# Automatic timestamp setting
attributes_with_timestamps = attributes.merge(
created_at: Time.current,
updated_at: Time.current,
view_count: 0
)
# Automatic excerpt generation
if attributes_with_timestamps[:excerpt].blank? && attributes_with_timestamps[:content].present?
attributes_with_timestamps[:excerpt] = attributes_with_timestamps[:content][0..200]
end
changeset = posts.changeset(:create, attributes_with_timestamps)
if changeset.valid?
posts.command(:create).call(changeset)
else
raise ROM::ValidationError, changeset.errors
end
end
# Post update
def update_post(id, attributes)
attributes_with_timestamp = attributes.merge(updated_at: Time.current)
changeset = posts.changeset(:update, attributes_with_timestamp)
if changeset.valid?
posts.command(:update).by_pk(id).call(changeset)
else
raise ROM::ValidationError, changeset.errors
end
end
# Publish post
def publish_post(id)
update_post(id, {
is_published: true,
published_at: Time.current
})
end
# Unpublish post
def unpublish_post(id)
update_post(id, {
is_published: false,
published_at: nil
})
end
# Increment view count
def increment_view_count(id)
post = find(id)
update_post(id, view_count: post.view_count + 1)
end
# Delete post
def delete_post(id)
posts.command(:delete).by_pk(id).call
end
# Post statistics
def post_statistics
total_posts = posts.count
published_posts_count = posts.published.count
draft_posts_count = total_posts - published_posts_count
average_view_count = posts.dataset.avg(:view_count)
{
total: total_posts,
published: published_posts_count,
drafts: draft_posts_count,
average_views: average_view_count&.to_f&.round(2) || 0.0
}
end
# Recent post statistics
def recent_post_statistics(days = 7)
cutoff_date = Date.current - days.days
recent_posts = posts.where { created_at >= cutoff_date }
recent_published = recent_posts.where(is_published: true)
{
recent_total: recent_posts.count,
recent_published: recent_published.count,
recent_drafts: recent_posts.count - recent_published.count
}
end
# Posts count by user
def posts_count_by_user
posts
.join(:users)
.select(:users[:id], :users[:name])
.select_append { integer::count(posts[:id]).as(:post_count) }
.group(:users[:id])
.to_a
end
end
Commands and Validation
# app/persistence/commands/create_user.rb
module Commands
class CreateUser < ROM::Commands::Create[:sql]
relation :users
register_as :create_user
# Validation rules
schema do
required(:name).filled(:string)
required(:email).filled(:string, format?: /@/)
required(:age).filled(:integer, gt?: 0, lt?: 150)
optional(:is_active).filled(:bool)
end
# Hook processing
def call(attributes)
# Default value setting
attributes_with_defaults = attributes.merge(
is_active: attributes.fetch(:is_active, true),
created_at: Time.current,
updated_at: Time.current
)
# Email duplicate check
if users.by_email(attributes_with_defaults[:email]).exist?
raise ROM::ValidationError, { email: ['already exists'] }
end
super(attributes_with_defaults)
end
end
end
# app/persistence/commands/update_user.rb
module Commands
class UpdateUser < ROM::Commands::Update[:sql]
relation :users
register_as :update_user
schema do
optional(:name).filled(:string)
optional(:email).filled(:string, format?: /@/)
optional(:age).filled(:integer, gt?: 0, lt?: 150)
optional(:is_active).filled(:bool)
end
def call(attributes)
attributes_with_timestamp = attributes.merge(updated_at: Time.current)
# Email duplicate check (excluding self)
if attributes[:email] && relation.dataset.exclude(id: restriction[:id]).where(email: attributes[:email]).any?
raise ROM::ValidationError, { email: ['already exists'] }
end
super(attributes_with_timestamp)
end
end
end
# app/persistence/commands/create_post.rb
module Commands
class CreatePost < ROM::Commands::Create[:sql]
relation :posts
register_as :create_post
schema do
required(:title).filled(:string, min_size?: 1)
required(:content).filled(:string, min_size?: 10)
required(:user_id).filled(:integer)
optional(:category_id).filled(:integer)
optional(:is_published).filled(:bool)
optional(:excerpt).filled(:string)
end
def call(attributes)
# Validation
user_exists = ROM_CONTAINER.relations[:users].by_pk(attributes[:user_id]).exist?
raise ROM::ValidationError, { user_id: ['does not exist'] } unless user_exists
if attributes[:category_id]
category_exists = ROM_CONTAINER.relations[:categories].by_pk(attributes[:category_id]).exist?
raise ROM::ValidationError, { category_id: ['does not exist'] } unless category_exists
end
# Default values and timestamp setting
attributes_with_defaults = attributes.merge(
is_published: attributes.fetch(:is_published, false),
view_count: 0,
created_at: Time.current,
updated_at: Time.current
)
# Automatic excerpt generation
if attributes_with_defaults[:excerpt].blank?
attributes_with_defaults[:excerpt] = attributes_with_defaults[:content][0..200]
end
# Published date setting
if attributes_with_defaults[:is_published]
attributes_with_defaults[:published_at] = Time.current
end
super(attributes_with_defaults)
end
end
end
Service Layer Implementation
# app/services/user_service.rb
class UserService
include ROM::Repository::Plugins::Helpers
def initialize(container = ROM_CONTAINER)
@container = container
@user_repo = UserRepository.new(container)
@post_repo = PostRepository.new(container)
end
# User creation service
def create_user(params)
# Parameter validation
clean_params = sanitize_user_params(params)
@container.gateways[:default].transaction do
user = @user_repo.create_user(clean_params)
# Welcome processing (example)
# send_welcome_email(user)
# create_default_profile(user)
user
end
rescue ROM::ValidationError => e
raise ServiceError, "User creation failed: #{e.message}"
end
# User update service
def update_user(id, params)
clean_params = sanitize_user_params(params)
@container.gateways[:default].transaction do
user = @user_repo.update_user(id, clean_params)
# Post-update processing
# update_search_index(user) if user
user
end
rescue ROM::ValidationError => e
raise ServiceError, "User update failed: #{e.message}"
end
# User deletion service (safe deletion)
def delete_user(id)
@container.gateways[:default].transaction do
# Pre-deletion check
user = @user_repo.find(id)
posts_count = @post_repo.posts_by_user(id).length
if posts_count > 0
# Processing choice when posts exist
# Option 1: Delete posts as well
@user_repo.delete_user(id)
# Option 2: Throw error
# raise ServiceError, "Cannot delete user with existing posts"
# Option 3: Transfer posts to another user
# transfer_posts_to_system_user(id)
# @user_repo.delete_user(id)
else
@user_repo.delete_user(id)
end
end
rescue ROM::TupleCountMismatchError
raise ServiceError, "User not found"
end
# Complex business logic example
def activate_user_with_verification(id, verification_code)
@container.gateways[:default].transaction do
user = @user_repo.find(id)
# Verification code check (hypothetical implementation)
unless verify_activation_code(user, verification_code)
raise ServiceError, "Invalid verification code"
end
# User activation
@user_repo.update_user(id, {
is_active: true,
verified_at: Time.current
})
# Post-activation processing
# send_activation_confirmation(user)
# grant_default_permissions(user)
@user_repo.find(id)
end
end
# User search service
def search_users(query_params)
results = @user_repo.all
if query_params[:name_pattern]
results = @user_repo.search_users(query_params[:name_pattern])
end
if query_params[:min_age] || query_params[:max_age]
min_age = query_params[:min_age] || 0
max_age = query_params[:max_age] || 150
results = @user_repo.users_by_age_range(min_age, max_age)
end
if query_params[:active_only]
results = results.select { |user| user.is_active }
end
results
end
# Data export service
def export_user_data(format = :json)
users_with_stats = @user_repo.users_with_post_count
case format
when :json
users_with_stats.map do |user|
{
id: user.id,
name: user.name,
email: user.email,
age: user.age,
is_active: user.is_active,
post_count: user.post_count || 0,
created_at: user.created_at.iso8601
}
end
when :csv
# CSV format output implementation
CSV.generate do |csv|
csv << ['ID', 'Name', 'Email', 'Age', 'Active', 'Post Count', 'Created At']
users_with_stats.each do |user|
csv << [user.id, user.name, user.email, user.age, user.is_active, user.post_count || 0, user.created_at.iso8601]
end
end
else
raise ServiceError, "Unsupported export format: #{format}"
end
end
private
def sanitize_user_params(params)
{
name: params[:name]&.strip,
email: params[:email]&.strip&.downcase,
age: params[:age]&.to_i,
is_active: params.key?(:is_active) ? params[:is_active] : true
}.compact
end
def verify_activation_code(user, code)
# In actual implementation, retrieve and compare verification code from database
true # Simplified
end
# Custom exception
class ServiceError < StandardError; end
end
# app/services/post_service.rb
class PostService
def initialize(container = ROM_CONTAINER)
@container = container
@post_repo = PostRepository.new(container)
@user_repo = UserRepository.new(container)
end
# Post creation service
def create_post(params)
clean_params = sanitize_post_params(params)
@container.gateways[:default].transaction do
# Author existence check
author = @user_repo.find(clean_params[:user_id])
raise ServiceError, "Author not found" unless author
post = @post_repo.create_post(clean_params)
# Post-creation processing
# notify_followers(author, post) if post.is_published
# update_search_index(post)
post
end
rescue ROM::ValidationError => e
raise ServiceError, "Post creation failed: #{e.message}"
end
# Post publishing service
def publish_post(id, publish_at = nil)
@container.gateways[:default].transaction do
post = @post_repo.find(id)
# Pre-publishing validation
validate_post_for_publishing(post)
@post_repo.publish_post(id)
# Post-publishing processing
# notify_subscribers(post)
# update_sitemap
# social_media_post(post)
@post_repo.find(id)
end
rescue ROM::TupleCountMismatchError
raise ServiceError, "Post not found"
end
# Bulk operation service
def bulk_update_posts(post_ids, attributes)
@container.gateways[:default].transaction do
updated_posts = []
post_ids.each do |id|
updated_post = @post_repo.update_post(id, attributes)
updated_posts << updated_post if updated_post
end
updated_posts
end
rescue => e
raise ServiceError, "Bulk update failed: #{e.message}"
end
# Content analysis service
def analyze_post_performance(days = 30)
cutoff_date = Date.current - days.days
# Get posts within period
recent_posts = @post_repo.posts.where { created_at >= cutoff_date }.to_a
total_views = recent_posts.sum(&:view_count)
avg_views = recent_posts.empty? ? 0 : total_views.to_f / recent_posts.length
top_posts = recent_posts
.select { |post| post.is_published }
.sort_by(&:view_count)
.reverse
.first(10)
{
period_days: days,
total_posts: recent_posts.length,
published_posts: recent_posts.count(&:is_published),
total_views: total_views,
average_views: avg_views.round(2),
top_posts: top_posts.map { |post|
{
id: post.id,
title: post.title,
views: post.view_count
}
}
}
end
private
def sanitize_post_params(params)
{
title: params[:title]&.strip,
content: params[:content]&.strip,
excerpt: params[:excerpt]&.strip,
user_id: params[:user_id]&.to_i,
category_id: params[:category_id]&.to_i,
is_published: params.key?(:is_published) ? params[:is_published] : false
}.compact
end
def validate_post_for_publishing(post)
errors = []
errors << "Title cannot be empty" if post.title.blank?
errors << "Content too short" if post.content.length < 100
errors << "Author must be active" unless post.user&.is_active
raise ServiceError, errors.join(", ") if errors.any?
end
class ServiceError < StandardError; end
end
Testing
# spec/repositories/user_repository_spec.rb
require 'spec_helper'
RSpec.describe UserRepository, type: :repository do
let(:repo) { described_class.new(ROM_CONTAINER) }
describe '#create_user' do
it 'creates a user with valid attributes' do
attributes = {
name: 'John Doe',
email: '[email protected]',
age: 30
}
user = repo.create_user(attributes)
expect(user.name).to eq('John Doe')
expect(user.email).to eq('[email protected]')
expect(user.age).to eq(30)
expect(user.is_active).to be(true)
end
it 'raises error for invalid email' do
attributes = {
name: 'John Doe',
email: 'invalid-email',
age: 30
}
expect { repo.create_user(attributes) }.to raise_error(ROM::ValidationError)
end
end
describe '#search_users' do
before do
repo.create_user(name: 'Alice Johnson', email: '[email protected]', age: 25)
repo.create_user(name: 'Bob Smith', email: '[email protected]', age: 35)
end
it 'finds users by name pattern' do
results = repo.search_users('Alice')
expect(results.length).to eq(1)
expect(results.first.name).to eq('Alice Johnson')
end
end
describe '#user_statistics' do
before do
repo.create_user(name: 'User 1', email: '[email protected]', age: 25)
repo.create_user(name: 'User 2', email: '[email protected]', age: 35)
end
it 'returns correct statistics' do
stats = repo.user_statistics
expect(stats[:total]).to eq(2)
expect(stats[:active]).to eq(2)
expect(stats[:average_age]).to eq(30.0)
end
end
end
# spec/services/user_service_spec.rb
require 'spec_helper'
RSpec.describe UserService do
let(:service) { described_class.new }
describe '#create_user' do
it 'creates user successfully' do
params = {
name: 'Jane Doe',
email: '[email protected]',
age: 28
}
user = service.create_user(params)
expect(user.name).to eq('Jane Doe')
expect(user.email).to eq('[email protected]')
end
it 'handles validation errors gracefully' do
params = {
name: '',
email: 'invalid',
age: -5
}
expect { service.create_user(params) }.to raise_error(UserService::ServiceError)
end
end
describe '#search_users' do
before do
service.create_user(name: 'Alice', email: '[email protected]', age: 25)
service.create_user(name: 'Bob', email: '[email protected]', age: 35)
end
it 'searches by name pattern' do
results = service.search_users(name_pattern: 'Ali')
expect(results.length).to eq(1)
expect(results.first.name).to eq('Alice')
end
it 'filters by age range' do
results = service.search_users(min_age: 30, max_age: 40)
expect(results.length).to eq(1)
expect(results.first.name).to eq('Bob')
end
end
end
# spec/factories/user_factory.rb (using ROM::Factory)
require 'rom-factory'
ROM::Factory.configure do |config|
config.rom = ROM_CONTAINER
end
module Factories
extend ROM::Factory::DSL
define(:user) do |f|
f.sequence(:name) { |n| "User #{n}" }
f.sequence(:email) { |n| "user#{n}@example.com" }
f.age { rand(18..80) }
f.is_active { true }
f.created_at { Time.current }
f.updated_at { Time.current }
end
define(:post) do |f|
f.sequence(:title) { |n| "Post #{n}" }
f.content { "This is the content of the post." * 10 }
f.association(:user)
f.is_published { false }
f.view_count { rand(0..1000) }
f.created_at { Time.current }
f.updated_at { Time.current }
end
end
# Factory usage example in tests
RSpec.describe 'Integration test' do
it 'creates users and posts' do
user = Factories[:user]
post = Factories[:post, user: user, title: 'Custom Title']
expect(user.name).to start_with('User')
expect(post.title).to eq('Custom Title')
expect(post.user_id).to eq(user.id)
end
end
Error Handling and Exception Processing
# app/errors/rom_errors.rb
module ROMErrors
class BaseError < StandardError
attr_reader :details
def initialize(message, details = {})
super(message)
@details = details
end
end
class ValidationError < BaseError; end
class NotFoundError < BaseError; end
class DuplicateError < BaseError; end
class TransactionError < BaseError; end
class ConfigurationError < BaseError; end
end
# app/services/safe_user_service.rb
class SafeUserService
include ROMErrors
def initialize(container = ROM_CONTAINER)
@container = container
@user_repo = UserRepository.new(container)
end
# Safe user creation
def create_user_safely(params)
# Input validation
validate_user_input(params)
@container.gateways[:default].transaction do
@user_repo.create_user(params)
end
rescue ROM::ValidationError => e
raise ValidationError.new("User validation failed", e.errors)
rescue ROM::ConstraintError => e
if e.message.include?('email')
raise DuplicateError.new("Email already exists", { email: params[:email] })
else
raise ValidationError.new("Data constraint violation", { error: e.message })
end
rescue => e
raise TransactionError.new("User creation failed", { original_error: e.message })
end
# Safe user retrieval
def get_user_safely(id)
@user_repo.find(id)
rescue ROM::TupleCountMismatchError
raise NotFoundError.new("User not found", { user_id: id })
rescue => e
raise BaseError.new("Failed to retrieve user", { user_id: id, error: e.message })
end
# Safe user update
def update_user_safely(id, params)
validate_user_input(params, updating: true)
@container.gateways[:default].transaction do
# User existence check
user = get_user_safely(id)
# Email duplicate check
if params[:email] && params[:email] != user.email
existing_user = @user_repo.find_by_email(params[:email])
if existing_user && existing_user.id != id
raise DuplicateError.new("Email already exists", { email: params[:email] })
end
end
@user_repo.update_user(id, params)
end
rescue NotFoundError, DuplicateError
raise
rescue ROM::ValidationError => e
raise ValidationError.new("User validation failed", e.errors)
rescue => e
raise TransactionError.new("User update failed", { user_id: id, error: e.message })
end
# Safe user deletion
def delete_user_safely(id)
@container.gateways[:default].transaction do
user = get_user_safely(id)
# Pre-deletion check
posts_count = @container.relations[:posts].where(user_id: id).count
if posts_count > 0
raise ValidationError.new(
"Cannot delete user with existing posts",
{ user_id: id, posts_count: posts_count }
)
end
@user_repo.delete_user(id)
true
end
rescue NotFoundError, ValidationError
raise
rescue => e
raise TransactionError.new("User deletion failed", { user_id: id, error: e.message })
end
# Safe execution of bulk operations
def bulk_create_users_safely(users_data)
results = { success: [], failed: [] }
@container.gateways[:default].transaction do
users_data.each_with_index do |user_params, index|
begin
user = create_user_safely(user_params)
results[:success] << { index: index, user: user }
rescue BaseError => e
results[:failed] << {
index: index,
params: user_params,
error: e.message,
details: e.details
}
end
end
# Rollback if many failures
if results[:failed].length > users_data.length * 0.5
raise TransactionError.new(
"Bulk operation failed: too many errors",
{ success_count: results[:success].length, failed_count: results[:failed].length }
)
end
end
results
rescue TransactionError
raise
rescue => e
raise TransactionError.new("Bulk operation failed", { error: e.message })
end
# Database integrity check
def perform_data_integrity_check
issues = []
begin
# Orphaned data check
orphaned_posts = @container.relations[:posts]
.left_join(:users, id: :user_id)
.where(users[:id] => nil)
if orphaned_posts.count > 0
issues << {
type: :orphaned_data,
table: :posts,
count: orphaned_posts.count,
message: "Posts without valid user references found"
}
end
# Duplicate email check
duplicate_emails = @container.relations[:users]
.dataset
.group(:email)
.having { count.function.* > 1 }
.select(:email)
.count
if duplicate_emails > 0
issues << {
type: :duplicate_data,
table: :users,
count: duplicate_emails,
message: "Duplicate email addresses found"
}
end
# Invalid data check
invalid_users = @container.relations[:users]
.where { (age < 0) | (age > 150) }
.count
if invalid_users > 0
issues << {
type: :invalid_data,
table: :users,
count: invalid_users,
message: "Users with invalid age values found"
}
end
rescue => e
raise BaseError.new("Integrity check failed", { error: e.message })
end
{
healthy: issues.empty?,
issues: issues,
checked_at: Time.current
}
end
# Data cleanup
def cleanup_invalid_data
cleanup_results = {}
@container.gateways[:default].transaction do
# Delete orphaned posts
orphaned_posts_count = @container.relations[:posts]
.left_join(:users, id: :user_id)
.where(users[:id] => nil)
.delete
cleanup_results[:orphaned_posts_removed] = orphaned_posts_count
# Fix invalid user data
invalid_users_fixed = @container.relations[:users]
.where { (age < 0) | (age > 150) }
.update(age: 0) # Reset invalid age to 0
cleanup_results[:invalid_users_fixed] = invalid_users_fixed
cleanup_results[:cleaned_at] = Time.current
end
cleanup_results
rescue => e
raise TransactionError.new("Data cleanup failed", { error: e.message })
end
private
def validate_user_input(params, updating: false)
errors = []
if !updating || params.key?(:name)
name = params[:name]
errors << "Name cannot be empty" if name.nil? || name.strip.empty?
end
if !updating || params.key?(:email)
email = params[:email]
if email.nil? || email.strip.empty?
errors << "Email cannot be empty"
elsif !email.match?(/\A[^@\s]+@[^@\s]+\z/)
errors << "Invalid email format"
end
end
if !updating || params.key?(:age)
age = params[:age]
if age.nil?
errors << "Age cannot be empty"
elsif age < 0 || age > 150
errors << "Age must be between 0 and 150"
end
end
raise ValidationError.new("Input validation failed", { errors: errors }) if errors.any?
end
end
# Usage example and error handling
class UserController
def initialize
@user_service = SafeUserService.new
end
def create
user = @user_service.create_user_safely(params)
render json: { success: true, user: user }
rescue ROMErrors::ValidationError => e
render json: {
success: false,
error: "Validation failed",
details: e.details
}, status: 422
rescue ROMErrors::DuplicateError => e
render json: {
success: false,
error: "Duplicate data",
details: e.details
}, status: 409
rescue ROMErrors::TransactionError => e
render json: {
success: false,
error: "Transaction failed",
details: e.details
}, status: 500
rescue => e
render json: {
success: false,
error: "Unexpected error",
message: e.message
}, status: 500
end
def update
user = @user_service.update_user_safely(params[:id], params)
render json: { success: true, user: user }
rescue ROMErrors::NotFoundError => e
render json: {
success: false,
error: "User not found",
details: e.details
}, status: 404
rescue ROMErrors::ValidationError, ROMErrors::DuplicateError => e
render json: {
success: false,
error: e.message,
details: e.details
}, status: 422
rescue => e
render json: {
success: false,
error: "Update failed",
message: e.message
}, status: 500
end
def health_check
integrity_result = @user_service.perform_data_integrity_check
if integrity_result[:healthy]
render json: {
status: "healthy",
checked_at: integrity_result[:checked_at]
}
else
render json: {
status: "issues_found",
issues: integrity_result[:issues],
checked_at: integrity_result[:checked_at]
}, status: 200
end
rescue => e
render json: {
status: "check_failed",
error: e.message
}, status: 500
end
end