one-to-many.rb |
|
|---|---|
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:
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 |
require "ohm" |
|
We define both a |
class Video < Ohm::Model
list :comments, Comment
end
class Audio < Ohm::Model
list :comments, Comment
end |
|
The |
class Comment < Ohm::Model
attribute :body
end |
|
Now let’s require the test framework we’re going to use called 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 |
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 |
DiscussionAs you can see above, the design is very simple, and leaves little to be desired. |
|
|
Latest first ordering can simply be achieved by using |
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 |
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
|
|