Ohm
Ohm is a Redis-specific object mapping library. It enables treating Redis data structures as Ruby objects through a simple and beautiful API, optimized for NoSQL approaches. By combining Redis's speed with Ruby's expressiveness, it enables scalable application development.
Library
Ohm
Overview
Ohm is a Redis-specific object mapping library. It enables treating Redis data structures as Ruby objects through a simple and beautiful API, optimized for NoSQL approaches. By combining Redis's speed with Ruby's expressiveness, it enables scalable application development.
Details
Ohm 2025 edition continues to be used in Redis-specific applications. It is adopted in projects utilizing Redis for cache layer, session management, and real-time features, particularly showing strength in web applications where performance is critical. By avoiding the complexity of Active Record and directly leveraging Redis data types (String, Hash, Set, List, Sorted Set), it achieves a simple and fast persistence layer.
Key Features
- Redis-specific Design: Utilizes all Redis features
- Simple API: Intuitive and easy to learn
- High Performance: Directly leverages Redis's in-memory performance
- Indexing: Supports efficient searching
- Validation: Built-in data validation features
- Collection Operations: Relationship management through Set operations
Pros and Cons
Pros
- Extremely fast read/write performance
- Simple and understandable code
- Maximum utilization of Redis features
- Memory-efficient data structures
- Ideal for real-time applications
- Lightweight with minimal overhead
Cons
- Limited to Redis, cannot use other databases
- Complex queries and JOINs are difficult
- Data capacity limitations due to memory constraints
- Limited transaction features
- Lack of migration management tools
- Not suitable for large datasets
References
Examples
Basic Setup
# Gemfile
gem 'ohm'
gem 'redis'
# Initial configuration
require 'ohm'
# Redis connection setup
Ohm.redis = Redic.new("redis://localhost:6379")
Model Definition
# models/user.rb
class User < Ohm::Model
attribute :name
attribute :email
attribute :age
attribute :created_at
# Index definitions
index :email
index :age
# Unique constraint
unique :email
# Validations
def validate
assert_present :name
assert_present :email
assert_email :email
assert_numeric :age if age
end
# Custom validation
def assert_email(attr)
assert_format attr, /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
end
# Collection (one-to-many)
collection :posts, :Post
# Reference (many-to-one)
reference :company, :Company
# Set (many-to-many)
set :roles, :Role
# Hooks
def before_create
self.created_at = Time.now.to_i
end
# Custom methods
def adult?
age.to_i >= 18
end
def posts_count
posts.size
end
end
# models/post.rb
class Post < Ohm::Model
attribute :title
attribute :content
attribute :published
attribute :view_count
attribute :created_at
index :published
index :created_at
# References
reference :author, :User
# Collections
collection :comments, :Comment
# Counter operations
counter :view_count
def validate
assert_present :title
assert_present :content
assert_present :author_id
end
def publish!
update(published: "true")
end
def unpublish!
update(published: "false")
end
def published?
published == "true"
end
end
# models/comment.rb
class Comment < Ohm::Model
attribute :content
attribute :created_at
reference :post, :Post
reference :user, :User
def validate
assert_present :content
assert_present :post_id
assert_present :user_id
end
end
# models/role.rb
class Role < Ohm::Model
attribute :name
unique :name
set :users, :User
def validate
assert_present :name
end
end
# models/company.rb
class Company < Ohm::Model
attribute :name
attribute :industry
index :industry
collection :employees, :User
def validate
assert_present :name
end
end
Basic CRUD Operations
# CREATE - Create new records
user = User.create(
name: "John Doe",
email: "[email protected]",
age: 30
)
# Handling validation errors
user = User.new(name: "", email: "invalid")
if user.valid?
user.save
else
puts user.errors # Display validation errors
end
# READ - Read records
# Search by ID
user = User[1]
# Search by attribute
user = User.find(email: "[email protected]").first
# Search using index
adults = User.find(age: 18..99)
young_users = User.find(age: 18..25)
# Get all records
all_users = User.all
# UPDATE - Update records
user = User[1]
user.update(name: "John Smith", age: 31)
# Partial update
user.name = "John Johnson"
user.save
# DELETE - Delete records
user = User[1]
user.delete
# Conditional deletion
User.find(age: 0..17).each(&:delete)
Advanced Search and Filtering
# Search with multiple conditions
# Ohm uses intersection
active_adults = User.find(age: 18..99).find(active: "true")
# Sort and limit
recent_posts = Post.find(published: "true").sort_by(:created_at, order: "DESC", limit: [0, 10])
# Custom search methods
class User < Ohm::Model
# ... existing definitions ...
def self.adults
find(age: 18..Float::INFINITY)
end
def self.by_email_domain(domain)
all.select { |user| user.email.end_with?("@#{domain}") }
end
def self.with_posts
all.select { |user| user.posts.size > 0 }
end
def self.recent(days = 7)
threshold = Time.now.to_i - (days * 24 * 60 * 60)
all.select { |user| user.created_at.to_i > threshold }
end
end
# Usage examples
adults = User.adults
gmail_users = User.by_email_domain("gmail.com")
active_bloggers = User.with_posts
new_users = User.recent(30)
Relationship Operations
# One-to-many relationship
user = User.create(name: "Author", email: "[email protected]")
# Create post
post = Post.create(
title: "How to use Ohm",
content: "Redis ORM basics...",
author: user
)
# Get user's posts
user_posts = user.posts
puts "Post count: #{user.posts_count}"
# Get post's author
author = post.author
puts "Author: #{author.name}"
# Many-to-many relationship
admin_role = Role.create(name: "admin")
editor_role = Role.create(name: "editor")
# Add roles to user
user.roles.add(admin_role)
user.roles.add(editor_role)
# Check user's roles
user.roles.each do |role|
puts "Role: #{role.name}"
end
# Find users with role
admin_users = admin_role.users
# Company and employee relationship
company = Company.create(name: "Tech Corp", industry: "IT")
user.company = company
user.save
# Get company employees
employees = company.employees
Transactions and Atomic Operations
# Redis multi/exec
class TransferService
def self.transfer_posts(from_user_id, to_user_id)
from_user = User[from_user_id]
to_user = User[to_user_id]
return false unless from_user && to_user
# Redis transaction
Ohm.redis.queue("MULTI")
from_user.posts.each do |post|
# Change post author
Ohm.redis.queue("HSET", post.key, "author_id", to_user_id)
end
Ohm.redis.queue("EXEC")
Ohm.redis.commit
true
rescue => e
Ohm.redis.queue("DISCARD")
Ohm.redis.commit
false
end
end
# Atomic counter operations
post = Post[1]
post.incr(:view_count) # Increment view count by 1
post.incr(:view_count, 10) # Increment view count by 10
post.decr(:view_count) # Decrement view count by 1
Practical Example
# Sinatra application usage example
require 'sinatra'
require 'json'
require_relative 'models/user'
require_relative 'models/post'
class BlogAPI < Sinatra::Base
# User list
get '/users' do
users = User.all.map do |user|
{
id: user.id,
name: user.name,
email: user.email,
age: user.age,
posts_count: user.posts_count
}
end
content_type :json
users.to_json
end
# User detail
get '/users/:id' do
user = User[params[:id]]
halt 404, { error: 'User not found' }.to_json unless user
content_type :json
{
id: user.id,
name: user.name,
email: user.email,
age: user.age,
posts: user.posts.map { |p| { id: p.id, title: p.title } }
}.to_json
end
# Create user
post '/users' do
data = JSON.parse(request.body.read)
user = User.new(
name: data['name'],
email: data['email'],
age: data['age']
)
if user.save
status 201
{ id: user.id, name: user.name, email: user.email }.to_json
else
status 422
{ errors: user.errors }.to_json
end
end
# Post list
get '/posts' do
posts = Post.find(published: "true")
.sort_by(:created_at, order: "DESC", limit: [0, 20])
.map do |post|
{
id: post.id,
title: post.title,
content: post.content,
author: post.author.name,
view_count: post.view_count,
created_at: Time.at(post.created_at.to_i).iso8601
}
end
content_type :json
posts.to_json
end
# Create post
post '/posts' do
data = JSON.parse(request.body.read)
author = User[data['author_id']]
halt 404, { error: 'Author not found' }.to_json unless author
post = Post.new(
title: data['title'],
content: data['content'],
author: author,
published: data['published'] || "false"
)
if post.save
status 201
{ id: post.id, title: post.title }.to_json
else
status 422
{ errors: post.errors }.to_json
end
end
# Increment view count
post '/posts/:id/view' do
post = Post[params[:id]]
halt 404 unless post
post.incr(:view_count)
{ view_count: post.view_count }.to_json
end
end
# Cache service
class CacheService
CACHE_TTL = 3600 # 1 hour
def self.cache_key(type, id)
"cache:#{type}:#{id}"
end
def self.get_user_data(user_id)
key = cache_key("user", user_id)
cached = Ohm.redis.call("GET", key)
if cached
JSON.parse(cached)
else
user = User[user_id]
return nil unless user
data = {
id: user.id,
name: user.name,
email: user.email,
posts_count: user.posts_count
}
Ohm.redis.call("SETEX", key, CACHE_TTL, data.to_json)
data
end
end
def self.invalidate_user(user_id)
Ohm.redis.call("DEL", cache_key("user", user_id))
end
end
# Real-time statistics
class StatsService
def self.increment_page_view(page)
key = "stats:pageviews:#{Date.today}"
Ohm.redis.call("HINCRBY", key, page, 1)
Ohm.redis.call("EXPIRE", key, 7 * 24 * 3600) # Keep for 7 days
end
def self.get_popular_pages(date = Date.today)
key = "stats:pageviews:#{date}"
views = Ohm.redis.call("HGETALL", key)
Hash[*views]
.map { |page, count| { page: page, views: count.to_i } }
.sort_by { |item| -item[:views] }
.first(10)
end
def self.active_users_count
# Active users in last 5 minutes
cutoff = Time.now.to_i - 300
Ohm.redis.call("ZCOUNT", "active_users", cutoff, "+inf").to_i
end
def self.track_user_activity(user_id)
Ohm.redis.call("ZADD", "active_users", Time.now.to_i, user_id)
# Remove old entries
cutoff = Time.now.to_i - 3600
Ohm.redis.call("ZREMRANGEBYSCORE", "active_users", "-inf", cutoff)
end
end