Jump To …

philosophy.rb

Internals: Nest and the Ohm Philosophy

Ohm does not want to hide Redis from you

In contrast to the usual philosophy of ORMs in the wild, Ohm actually just provides a basic object mapping where you can safely tuck away attributes and declare grouping of data.

Beyond that, Ohm doesn’t try to hide Redis, but rather exposes it in a simple way, through key hierarchies provided by the library Nest.

Let’s require Ohm. We also require Ohm::Contrib so we can make use of its module Ohm::Callbacks.

require "ohm"
require "ohm/contrib"

Let’s quickly declare our Post model and include Ohm::Callbacks. We define an attribute title and also index it.

In addition we specify our Post to have a list of comments.

class Post < Ohm::Model
  include Ohm::Callbacks

  attribute :title
  index :title

  list :comments, Comment

This is one example of using the underlying library Nest directly. As you can see, we can easily drop down to using raw Redis commands, in this case we use ZREVRANGE.

Note: Since Ohm::Model defines a to_proc, we can use the & syntax together with map to make our code a little more terse.

  def self.latest
    key[:latest].zrevrange(0, -1).map(&Post)
  end

Here we just quickly push this instance of Post to our latest SORTED SET. We use the current time as the score.

protected
  def after_save
    self.class.key[:latest].zadd(Time.now.to_i, id)
  end

Since we add every Post to our SORTED SET, we have to make sure that we removed it from our latest SORTED SET as soon as we delete a Post.

In this case we use the raw Redis command ZREM.

  def after_delete
    self.class.key[:latest].zrem(id)
  end
end

Now let’s quickly define our Comment model.

class Comment < Ohm::Model
end

Test it out

For this example, we’ll use Cutest for our testing framework.

require "cutest"

To make it simple, we also ensure that every test run has a clean Redis instance.

prepare { Ohm.flush }

Now let’s create Post. Cutest by default yields the return value of the block to each and every one of the test blocks.

setup { Post.create }

We then verify the behavior for our Post:latest ZSET. Our created post should automatically be part of Post:latest.

test "created post is inserted into latest" do |p|
  assert [p.id] == Post.key[:latest].zrange(0, -1)
end

And it should automatically be removed from it as soon as we delete our Post.

test "deleting the created post removes it from latest" do |p|
  p.delete

  assert Post.key[:latest].zrange(0, -1).empty?
end

You might be curious what happens when we do Post.all. The test here demonstrates more or less what’s happening when you do that.

test "querying Post:all using raw Redis commands" do |p|
  assert [p.id] == Post.key[:all].smembers

  assert [p] == Post.key[:all].smembers.map(&Post)
end

Understanding post.comments.

Let’s pop the hood and see how we can do LIST operations on our post.comments object.

Getting the current size of our comments is just a wrapper for LLEN.

test "checking the number of comments for a given post" do |p|
  assert 0 == p.comments.key.llen
  assert 0 == p.comments.size
end

Also, pushing a comment to our post.comments object is equivalent to doing an RPUSH of its id.

test "pushing a Comment manually and checking for its presence" do |p|
  comment = Comment.create

  p.comments.key.rpush(comment.id)
  assert [comment.id] == p.comments.key.lrange(0, -1)
end

Now for some interesting judo

test "now what if we want to find all Ohm or Redis posts" do
  ohm = Post.create(:title => "Ohm")
  redis = Post.create(:title => "Redis")

Let’s first choose an arbitrary key name to hold our Set.

  ohm_redis = Post.key.volatile["ohm-redis"]

A volatile key just simply means it will be prefixed with a ~.

  assert "~:Post:ohm-redis" == ohm_redis

Finding all Ohm or Redis posts now will just be a call to SUNIONSTORE on our volatile ohm-redis key.

  ohm_redis.sunionstore(
    Post.index_key_for(:title, "Ohm"),
    Post.index_key_for(:title, "Redis")
  )

And voila, they have been found!

  assert [ohm.id, redis.id] == ohm_redis.smembers.sort
end

The command reference is your friend

If you invest a little time reading through all the different Redis commands, I’m pretty sure you will enjoy your experience hacking with Ohm, Nest and Redis a lot more.