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.

NoSQLRubyRedisODMLightweightSimpleKey-Value

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