### One to Many Ohm style

#### Problem

# Let's say you want to implement a commenting system, and you need to have
# comments on different models. In order to do this using an RDBMS you have
# one of two options:
#
# 1. Have multiple comment tables per type i.e. VideoComments, AudioComments,
#    etc.
# 2. Use a polymorphic schema.
#
# The problem with option 1 is that you'll may possibly run into an explosion
# of tables.
#
# The problem with option 2 is that if you have many comments across the whole
# site, you'll quickly hit the limit on a table, and eventually need to shard.

#### Solution

# In *Redis*, possibly the best data structure to model a comment would be to
# use a *List*, mainly because comments are always presented within the
# context of the parent entity, and are typically ordered in a predefined way
# (i.e. latest at the top, or latest at the bottom).
#

# Let's start by requiring `Ohm`.
require "ohm"

# We define both a `Video` and `Audio` model, with a `list` of *comments*.
class Video < Ohm::Model
  list :comments, Comment
end

class Audio < Ohm::Model
  list :comments, Comment
end

# The `Comment` model for this example will just contain one attribute called
# `body`.
class Comment < Ohm::Model
  attribute :body
end

# Now let's require the test framework we're going to use called
# [cutest](http://github.com/djanowski/cutest)
require "cutest"

# And make sure that every run of our test suite has a clean Redis instance.
prepare { Ohm.flush }

# Let's begin testing. The important thing to verify is that
# video comments and audio comments don't munge with each other.
#
# We can see that they don't since each of the `comments` list only has
# one element.
test "adding all sorts of comments" do
  video = Video.create

  video_comment = Comment.create(:body => "First Video Comment")
  video.comments.push(video_comment)

  audio = Audio.create
  audio_comment = Comment.create(:body => "First Audio Comment")
  audio.comments.push(audio_comment)

  assert video.comments.include?(video_comment)
  assert video.comments.size == 1

  assert audio.comments.include?(audio_comment)
  assert audio.comments.size == 1
end


#### Discussion
#
# As you can see above, the design is very simple, and leaves little to be
# desired.

# Latest first ordering can simply be achieved by using `unshift` instead of
# `push`.
test "latest first ordering" do
  video = Video.create

  first = Comment.create(:body => "First")
  second = Comment.create(:body => "Second")

  video.comments.unshift(first)
  video.comments.unshift(second)

  assert [second, first] == video.comments.to_a
end

# In addition, since Lists are optimized for doing `LRANGE` operations,
# pagination of Comments would be very fast compared to doing a LIMIT / OFFSET
# query in SQL (some sites also use `WHERE id > ? LIMIT N` and pass the
# previous last ID in the set).
test "getting paged chunks of comments" do
  video = Video.create

  20.times { |i| video.comments.push(Comment.create(:body => "C#{i + 1}")) }

  assert %w(C1 C2 C3 C4 C5)      ==  video.comments[0, 4].map(&:body)
  assert %w(C6 C7 C8 C9 C10)     ==  video.comments[5, 9].map(&:body)

  # ** Range style is also supported.
  assert %w(C11 C12 C13 C14 C15) ==  video.comments[10..14].map(&:body)

  # ** Also you can just pass in a single number.
  assert "C16" == video.comments[15].body
end

#### Caveats

# Sometimes you need to be able to delete comments. For these cases, you might
# possibly need to store a reference back to the parent entity. Also, if you
# expect to store millions of comments for a single entity, it might be tricky
# to delete comments, as you need to manually loop through the entire LIST.
#
# Luckily, there is a clean alternative solution, which would be to use a
# `SORTED SET`, and to use the timestamp (or the negative of the timestamp) as
# the score to maintain the desired order. Deleting a comment from a
# `SORTED SET` would be a simple
# [ZREM](http://code.google.com/p/redis/wiki/ZremCommand) call.