sohm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gems +4 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +312 -0
- data/LICENSE +19 -0
- data/README.md +10 -0
- data/benchmarks/common.rb +33 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +162 -0
- data/examples/chaining.rb +162 -0
- data/examples/json-hash.rb +75 -0
- data/examples/one-to-many.rb +124 -0
- data/examples/philosophy.rb +137 -0
- data/examples/redis-logging.txt +179 -0
- data/examples/slug.rb +149 -0
- data/examples/tagging.rb +237 -0
- data/lib/sample.rb +14 -0
- data/lib/sohm/command.rb +51 -0
- data/lib/sohm/json.rb +17 -0
- data/lib/sohm/lua/delete.lua +72 -0
- data/lib/sohm/lua/save.lua +13 -0
- data/lib/sohm.rb +1576 -0
- data/makefile +4 -0
- data/sohm.gemspec +18 -0
- data/test/association.rb +33 -0
- data/test/command.rb +55 -0
- data/test/connection.rb +16 -0
- data/test/core.rb +24 -0
- data/test/counters.rb +67 -0
- data/test/enumerable.rb +79 -0
- data/test/filtering.rb +185 -0
- data/test/hash_key.rb +31 -0
- data/test/helper.rb +23 -0
- data/test/indices.rb +133 -0
- data/test/json.rb +62 -0
- data/test/list.rb +83 -0
- data/test/model.rb +789 -0
- data/test/set.rb +37 -0
- data/test/thread_safety.rb +67 -0
- data/test/to_hash.rb +29 -0
- data/test/uniques.rb +98 -0
- metadata +142 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
### Chaining Ohm Sets
|
2
|
+
|
3
|
+
#### Doing the straight forward approach
|
4
|
+
|
5
|
+
# Let's design our example around the following requirements:
|
6
|
+
#
|
7
|
+
# 1. a `User` has many orders.
|
8
|
+
# 2. an `Order` can be pending, authorized or captured.
|
9
|
+
# 3. a `Product` is referenced by an `Order`.
|
10
|
+
|
11
|
+
#### Doing it the normal way
|
12
|
+
|
13
|
+
# Let's first require `Ohm`.
|
14
|
+
require "ohm"
|
15
|
+
|
16
|
+
# A `User` has a `collection` of *orders*. Note that a collection
|
17
|
+
# is actually just a convenience, which implemented simply will look like:
|
18
|
+
#
|
19
|
+
# def orders
|
20
|
+
# Order.find(user_id: self.id)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
class User < Ohm::Model
|
24
|
+
collection :orders, :Order
|
25
|
+
end
|
26
|
+
|
27
|
+
# The product for our purposes will only contain a name.
|
28
|
+
class Product < Ohm::Model
|
29
|
+
attribute :name
|
30
|
+
end
|
31
|
+
|
32
|
+
# We define an `Order` with just a single `attribute` called `state`, and
|
33
|
+
# also add an `index` so we can search an order given its state.
|
34
|
+
#
|
35
|
+
# The `reference` to the `User` is actually required for the `collection`
|
36
|
+
# of *orders* in the `User` declared above, because the `reference` defines
|
37
|
+
# an index called `:user_id`.
|
38
|
+
#
|
39
|
+
# We also define a `reference` to a `Product`.
|
40
|
+
class Order < Ohm::Model
|
41
|
+
attribute :state
|
42
|
+
index :state
|
43
|
+
|
44
|
+
reference :user, User
|
45
|
+
reference :product, Product
|
46
|
+
end
|
47
|
+
|
48
|
+
##### Testing what we have so far.
|
49
|
+
|
50
|
+
# For the purposes of this tutorial, we'll use cutest for our test framework.
|
51
|
+
require "cutest"
|
52
|
+
|
53
|
+
# Make sure that every run of our test suite has a clean Redis instance.
|
54
|
+
prepare { Ohm.flush }
|
55
|
+
|
56
|
+
# Let's create a *user*, a *pending*, *authorized* and a captured order.
|
57
|
+
# We also create two products named *iPod* and *iPad*.
|
58
|
+
setup do
|
59
|
+
@user = User.create
|
60
|
+
|
61
|
+
@ipod = Product.create(name: "iPod")
|
62
|
+
@ipad = Product.create(name: "iPad")
|
63
|
+
|
64
|
+
@pending = Order.create(user: @user, state: "pending",
|
65
|
+
product: @ipod)
|
66
|
+
@authorized = Order.create(user: @user, state: "authorized",
|
67
|
+
product: @ipad)
|
68
|
+
@captured = Order.create(user: @user, state: "captured",
|
69
|
+
product: @ipad)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Now let's try and grab all pending orders, and also pending
|
73
|
+
# *iPad* and *iPod* ones.
|
74
|
+
test "finding pending orders" do
|
75
|
+
assert @user.orders.find(state: "pending").include?(@pending)
|
76
|
+
|
77
|
+
assert @user.orders.find(state: "pending",
|
78
|
+
product_id: @ipod.id).include?(@pending)
|
79
|
+
|
80
|
+
assert @user.orders.find(state: "pending",
|
81
|
+
product_id: @ipad.id).empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Now we try and find captured and/or authorized orders.
|
85
|
+
test "finding authorized and/or captured orders" do
|
86
|
+
assert @user.orders.find(state: "authorized").include?(@authorized)
|
87
|
+
assert @user.orders.find(state: "captured").include?(@captured)
|
88
|
+
|
89
|
+
results = @user.orders.find(state: "authorized").union(state: "captured")
|
90
|
+
|
91
|
+
assert results.include?(@authorized)
|
92
|
+
assert results.include?(@captured)
|
93
|
+
end
|
94
|
+
|
95
|
+
#### Creating shortcuts
|
96
|
+
|
97
|
+
# You can of course define methods to make that code more readable.
|
98
|
+
class User < Ohm::Model
|
99
|
+
def authorized_orders
|
100
|
+
orders.find(state: "authorized")
|
101
|
+
end
|
102
|
+
|
103
|
+
def captured_orders
|
104
|
+
orders.find(state: "captured")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# And we can now test these new methods.
|
109
|
+
test "finding authorized and/or captured orders" do
|
110
|
+
assert @user.authorized_orders.include?(@authorized)
|
111
|
+
assert @user.captured_orders.include?(@captured)
|
112
|
+
end
|
113
|
+
|
114
|
+
# In most cases this is fine, but if you want to have a little fun,
|
115
|
+
# then we can play around with some chainability using scopes.
|
116
|
+
|
117
|
+
# Let's require `ohm-contrib`, which we will be using for scopes.
|
118
|
+
require "ohm/contrib"
|
119
|
+
|
120
|
+
# Include `Ohm::Scope` module and desired scopes.
|
121
|
+
class Order
|
122
|
+
include Ohm::Scope
|
123
|
+
|
124
|
+
scope do
|
125
|
+
def pending
|
126
|
+
find(state: "pending")
|
127
|
+
end
|
128
|
+
|
129
|
+
def authorized
|
130
|
+
find(state: "authorized")
|
131
|
+
end
|
132
|
+
|
133
|
+
def captured
|
134
|
+
find(state: "captured")
|
135
|
+
end
|
136
|
+
|
137
|
+
def accepted
|
138
|
+
find(state: "authorized").union(state: "captured")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Ok! Let's put all of that chaining code to good use.
|
144
|
+
test "finding pending orders using a chainable style" do
|
145
|
+
assert @user.orders.pending.include?(@pending)
|
146
|
+
assert @user.orders.pending.find(product_id: @ipod.id).include?(@pending)
|
147
|
+
|
148
|
+
assert @user.orders.pending.find(product_id: @ipad.id).empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
test "finding authorized and/or captured orders using a chainable style" do
|
152
|
+
assert @user.orders.authorized.include?(@authorized)
|
153
|
+
assert @user.orders.captured.include?(@captured)
|
154
|
+
|
155
|
+
assert @user.orders.accepted.include?(@authorized)
|
156
|
+
assert @user.orders.accepted.include?(@captured)
|
157
|
+
|
158
|
+
accepted = @user.orders.accepted
|
159
|
+
|
160
|
+
assert accepted.find(product_id: @ipad.id).include?(@authorized)
|
161
|
+
assert accepted.find(product_id: @ipad.id).include?(@captured)
|
162
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
### Make Peace wih JSON and Hash
|
2
|
+
|
3
|
+
#### Why do I care?
|
4
|
+
|
5
|
+
# If you've ever needed to build an AJAX route handler, you may have noticed
|
6
|
+
# the prevalence of the design pattern where you return a JSON response.
|
7
|
+
#
|
8
|
+
# on get, "comments" do
|
9
|
+
# res.write Comment.all.to_json
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# `Ohm` helps you here by providing sensible defaults. It's not very popular,
|
13
|
+
# but `Ohm` actually has a `to_hash` method.
|
14
|
+
|
15
|
+
# Let's start by requiring `ohm` and `ohm/json`.
|
16
|
+
require "ohm"
|
17
|
+
require "ohm/json"
|
18
|
+
|
19
|
+
# Here we define our `Post` model with just a single `attribute` called `title`.
|
20
|
+
class Post < Ohm::Model
|
21
|
+
attribute :title
|
22
|
+
end
|
23
|
+
|
24
|
+
# Now let's load the test framework `cutest` to test our code.
|
25
|
+
require "cutest"
|
26
|
+
|
27
|
+
# We also call `Ohm.flush` for each test run.
|
28
|
+
prepare { Ohm.flush }
|
29
|
+
|
30
|
+
# When we successfully create a `Post`, we can see that it returns
|
31
|
+
# only the *id* and its value in the hash.
|
32
|
+
test "hash representation when created" do
|
33
|
+
post = Post.create(title: "my post")
|
34
|
+
|
35
|
+
assert_equal Hash[id: post.id], post.to_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# The JSON representation is actually just `post.to_hash.to_json`, so the
|
39
|
+
# same result, only in JSON, is returned.
|
40
|
+
test "json representation when created" do
|
41
|
+
post = Post.create(title: "my post")
|
42
|
+
|
43
|
+
assert_equal "{\"id\":\"#{post.id}\"}", post.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
#### Whitelisted approach
|
47
|
+
|
48
|
+
# Unlike other frameworks which dumps out all attributes by default,
|
49
|
+
# `Ohm` favors a whitelisted approach where you have to explicitly
|
50
|
+
# declare which attributes you want.
|
51
|
+
#
|
52
|
+
# By default, only `:id` will be available if the model is persisted.
|
53
|
+
|
54
|
+
# Let's re-open our Post class, and add a `to_hash` method.
|
55
|
+
class Post
|
56
|
+
def to_hash
|
57
|
+
super.merge(title: title)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Now, let's test that the title is in fact part of `to_hash`.
|
62
|
+
test "customized to_hash" do
|
63
|
+
post = Post.create(title: "Override FTW?")
|
64
|
+
|
65
|
+
assert_equal Hash[id: post.id, title: post.title], post.to_hash
|
66
|
+
end
|
67
|
+
|
68
|
+
#### Conclusion
|
69
|
+
|
70
|
+
# Ohm has a lot of neat intricacies like this. Some of the things to keep
|
71
|
+
# in mind from this tutorial would be:
|
72
|
+
#
|
73
|
+
# 1. `Ohm` doesn't assume too much about your needs.
|
74
|
+
# 2. If you need a customized version, you can always define it yourself.
|
75
|
+
# 3. Customization is easy using basic OOP principles.
|
@@ -0,0 +1,124 @@
|
|
1
|
+
### One to Many Ohm style
|
2
|
+
|
3
|
+
#### Problem
|
4
|
+
|
5
|
+
# Let's say you want to implement a commenting system, and you need to have
|
6
|
+
# comments on different models. In order to do this using an RDBMS you have
|
7
|
+
# one of two options:
|
8
|
+
#
|
9
|
+
# 1. Have multiple comment tables per type i.e. VideoComments, AudioComments,
|
10
|
+
# etc.
|
11
|
+
# 2. Use a polymorphic schema.
|
12
|
+
#
|
13
|
+
# The problem with option 1 is that you'll may possibly run into an explosion
|
14
|
+
# of tables.
|
15
|
+
#
|
16
|
+
# The problem with option 2 is that if you have many comments across the whole
|
17
|
+
# site, you'll quickly hit the limit on a table, and eventually need to shard.
|
18
|
+
|
19
|
+
#### Solution
|
20
|
+
|
21
|
+
# In *Redis*, possibly the best data structure to model a comment would be to
|
22
|
+
# use a *List*, mainly because comments are always presented within the
|
23
|
+
# context of the parent entity, and are typically ordered in a predefined way
|
24
|
+
# (i.e. latest at the top, or latest at the bottom).
|
25
|
+
#
|
26
|
+
|
27
|
+
# Let's start by requiring `Ohm`.
|
28
|
+
require "ohm"
|
29
|
+
|
30
|
+
# We define both a `Video` and `Audio` model, with a `list` of *comments*.
|
31
|
+
class Video < Ohm::Model
|
32
|
+
list :comments, Comment
|
33
|
+
end
|
34
|
+
|
35
|
+
class Audio < Ohm::Model
|
36
|
+
list :comments, Comment
|
37
|
+
end
|
38
|
+
|
39
|
+
# The `Comment` model for this example will just contain one attribute called
|
40
|
+
# `body`.
|
41
|
+
class Comment < Ohm::Model
|
42
|
+
attribute :body
|
43
|
+
end
|
44
|
+
|
45
|
+
# Now let's require the test framework we're going to use called
|
46
|
+
# [cutest](http://github.com/djanowski/cutest)
|
47
|
+
require "cutest"
|
48
|
+
|
49
|
+
# And make sure that every run of our test suite has a clean Redis instance.
|
50
|
+
prepare { Ohm.flush }
|
51
|
+
|
52
|
+
# Let's begin testing. The important thing to verify is that
|
53
|
+
# video comments and audio comments don't munge with each other.
|
54
|
+
#
|
55
|
+
# We can see that they don't since each of the `comments` list only has
|
56
|
+
# one element.
|
57
|
+
test "adding all sorts of comments" do
|
58
|
+
video = Video.create
|
59
|
+
|
60
|
+
video_comment = Comment.create(:body => "First Video Comment")
|
61
|
+
video.comments.push(video_comment)
|
62
|
+
|
63
|
+
audio = Audio.create
|
64
|
+
audio_comment = Comment.create(:body => "First Audio Comment")
|
65
|
+
audio.comments.push(audio_comment)
|
66
|
+
|
67
|
+
assert video.comments.include?(video_comment)
|
68
|
+
assert video.comments.size == 1
|
69
|
+
|
70
|
+
assert audio.comments.include?(audio_comment)
|
71
|
+
assert audio.comments.size == 1
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
#### Discussion
|
76
|
+
#
|
77
|
+
# As you can see above, the design is very simple, and leaves little to be
|
78
|
+
# desired.
|
79
|
+
|
80
|
+
# Latest first ordering can simply be achieved by using `unshift` instead of
|
81
|
+
# `push`.
|
82
|
+
test "latest first ordering" do
|
83
|
+
video = Video.create
|
84
|
+
|
85
|
+
first = Comment.create(:body => "First")
|
86
|
+
second = Comment.create(:body => "Second")
|
87
|
+
|
88
|
+
video.comments.unshift(first)
|
89
|
+
video.comments.unshift(second)
|
90
|
+
|
91
|
+
assert [second, first] == video.comments.to_a
|
92
|
+
end
|
93
|
+
|
94
|
+
# In addition, since Lists are optimized for doing `LRANGE` operations,
|
95
|
+
# pagination of Comments would be very fast compared to doing a LIMIT / OFFSET
|
96
|
+
# query in SQL (some sites also use `WHERE id > ? LIMIT N` and pass the
|
97
|
+
# previous last ID in the set).
|
98
|
+
test "getting paged chunks of comments" do
|
99
|
+
video = Video.create
|
100
|
+
|
101
|
+
20.times { |i| video.comments.push(Comment.create(:body => "C#{i + 1}")) }
|
102
|
+
|
103
|
+
assert %w(C1 C2 C3 C4 C5) == video.comments[0, 4].map(&:body)
|
104
|
+
assert %w(C6 C7 C8 C9 C10) == video.comments[5, 9].map(&:body)
|
105
|
+
|
106
|
+
# ** Range style is also supported.
|
107
|
+
assert %w(C11 C12 C13 C14 C15) == video.comments[10..14].map(&:body)
|
108
|
+
|
109
|
+
# ** Also you can just pass in a single number.
|
110
|
+
assert "C16" == video.comments[15].body
|
111
|
+
end
|
112
|
+
|
113
|
+
#### Caveats
|
114
|
+
|
115
|
+
# Sometimes you need to be able to delete comments. For these cases, you might
|
116
|
+
# possibly need to store a reference back to the parent entity. Also, if you
|
117
|
+
# expect to store millions of comments for a single entity, it might be tricky
|
118
|
+
# to delete comments, as you need to manually loop through the entire LIST.
|
119
|
+
#
|
120
|
+
# Luckily, there is a clean alternative solution, which would be to use a
|
121
|
+
# `SORTED SET`, and to use the timestamp (or the negative of the timestamp) as
|
122
|
+
# the score to maintain the desired order. Deleting a comment from a
|
123
|
+
# `SORTED SET` would be a simple
|
124
|
+
# [ZREM](http://code.google.com/p/redis/wiki/ZremCommand) call.
|
@@ -0,0 +1,137 @@
|
|
1
|
+
### Internals: Nest and the Ohm Philosophy
|
2
|
+
|
3
|
+
#### Ohm does not want to hide Redis from you
|
4
|
+
|
5
|
+
# In contrast to the usual philosophy of ORMs in the wild, Ohm actually
|
6
|
+
# just provides a basic object mapping where you can safely tuck away
|
7
|
+
# attributes and declare grouping of data.
|
8
|
+
#
|
9
|
+
# Beyond that, Ohm doesn't try to hide Redis, but rather exposes it in
|
10
|
+
# a simple way, through key hierarchies provided by the library
|
11
|
+
# [Nido](http://github.com/soveran/nido).
|
12
|
+
|
13
|
+
# Let's require `Ohm`. We also require `Ohm::Contrib` so we can make
|
14
|
+
# use of its module `Ohm::Callbacks`.
|
15
|
+
require "ohm"
|
16
|
+
require "ohm/contrib"
|
17
|
+
|
18
|
+
# Let's quickly declare our `Post` model and include `Ohm::Callbacks`.
|
19
|
+
# We define an *attribute* `title` and also *index* it.
|
20
|
+
#
|
21
|
+
# In addition we specify our `Post` to have a list of *comments*.
|
22
|
+
class Post < Ohm::Model
|
23
|
+
include Ohm::Callbacks
|
24
|
+
|
25
|
+
attribute :title
|
26
|
+
index :title
|
27
|
+
|
28
|
+
list :comments, :Comment
|
29
|
+
|
30
|
+
# This is one example of using Redic simple API and
|
31
|
+
# the underlying library `Nido`.
|
32
|
+
def self.latest
|
33
|
+
fetch(redis.call("ZRANGE", key[:latest], 0, -1))
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Here we just quickly push this instance of `Post` to our `latest`
|
39
|
+
# *SORTED SET*. We use the current time as the score.
|
40
|
+
def after_save
|
41
|
+
redis.call("ZADD", model.key[:latest], Time.now.to_i, id)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Since we add every `Post` to our *SORTED SET*, we have to make sure that
|
45
|
+
# we removed it from our `latest` *SORTED SET* as soon as we delete a
|
46
|
+
# `Post`.
|
47
|
+
def after_delete
|
48
|
+
redis.call("ZREM", model.key[:latest], id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Now let's quickly define our `Comment` model.
|
53
|
+
class Comment < Ohm::Model
|
54
|
+
end
|
55
|
+
|
56
|
+
#### Test it out
|
57
|
+
|
58
|
+
# For this example, we'll use [Cutest](http://github.com/djanowski/cutest)
|
59
|
+
# for our testing framework.
|
60
|
+
require "cutest"
|
61
|
+
|
62
|
+
# To make it simple, we also ensure that every test run has a clean
|
63
|
+
# *Redis* instance.
|
64
|
+
prepare do
|
65
|
+
Ohm.flush
|
66
|
+
end
|
67
|
+
|
68
|
+
# Now let's create a post. `Cutest` by default yields the return value of the
|
69
|
+
# block to each and every one of the test blocks.
|
70
|
+
setup do
|
71
|
+
Post.create
|
72
|
+
end
|
73
|
+
|
74
|
+
# We then verify the behavior for our `Post:latest` ZSET. Our created
|
75
|
+
# post should automatically be part of `Post:latest`.
|
76
|
+
test "created post is inserted into latest" do |post|
|
77
|
+
assert [post.id] == Post.latest.map(&:id)
|
78
|
+
end
|
79
|
+
|
80
|
+
# And it should automatically be removed from it as soon as we delete our
|
81
|
+
# `Post`.
|
82
|
+
test "deleting the created post removes it from latest" do |post|
|
83
|
+
post.delete
|
84
|
+
|
85
|
+
assert Post.latest.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
# You might be curious what happens when we do `Post.all`. The test here
|
89
|
+
# demonstrates more or less what's happening when you do that.
|
90
|
+
test "querying Post:all using raw Redis commands" do |post|
|
91
|
+
assert [post.id] == Post.all.ids
|
92
|
+
assert [post] == Post.all.to_a
|
93
|
+
end
|
94
|
+
|
95
|
+
#### Understanding `post.comments`.
|
96
|
+
|
97
|
+
# Let's pop the hood and see how we can do *LIST* operations on our
|
98
|
+
# `post.comments` object.
|
99
|
+
|
100
|
+
# Getting the current size of our comments is just a wrapper for
|
101
|
+
# [LLEN](http://redis.io/commands/LLEN).
|
102
|
+
test "checking the number of comments for a given post" do |post|
|
103
|
+
assert_equal 0, post.comments.size
|
104
|
+
assert_equal 0, Post.redis.call("LLEN", post.comments.key)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Also, pushing a comment to our `post.comments` object is equivalent
|
108
|
+
# to doing an [RPUSH](http://redis.io/commands/RPUSH) of its `id`.
|
109
|
+
test "pushing a comment manually and checking for its presence" do |post|
|
110
|
+
comment = Comment.create
|
111
|
+
|
112
|
+
Post.redis.call("RPUSH", post.comments.key, comment.id)
|
113
|
+
assert_equal comment, post.comments.last
|
114
|
+
|
115
|
+
post.comments.push(comment)
|
116
|
+
assert_equal comment, post.comments.last
|
117
|
+
end
|
118
|
+
|
119
|
+
# Now for some interesting judo.
|
120
|
+
test "now what if we want to find all Ohm or Redis posts" do
|
121
|
+
ohm = Post.create(title: "Ohm")
|
122
|
+
redis = Post.create(title: "Redis")
|
123
|
+
|
124
|
+
# Finding all *Ohm* or *Redis* posts now will just be a call to
|
125
|
+
# [SUNIONSTORE](http://redis.io/commands/UNIONSTORE).
|
126
|
+
result = Post.find(title: "Ohm").union(title: "Redis")
|
127
|
+
|
128
|
+
# And voila, they have been found!
|
129
|
+
assert_equal [ohm, redis], result.to_a
|
130
|
+
end
|
131
|
+
|
132
|
+
#### The command reference is your friend
|
133
|
+
|
134
|
+
# If you invest a little time reading through all the different
|
135
|
+
# [Redis commands](http://redis.io/commands).
|
136
|
+
# I'm pretty sure you will enjoy your experience hacking with Ohm, Nido,
|
137
|
+
# Redic and Redis a lot more.
|
@@ -0,0 +1,179 @@
|
|
1
|
+
### Understanding Ohm Internals through Logging
|
2
|
+
|
3
|
+
#### Benefits
|
4
|
+
|
5
|
+
# Ohm is actually a very thin wrapper for Redis, and most of the design
|
6
|
+
# patterns implemented in Ohm are based on the prescribed patterns for using
|
7
|
+
# Redis.
|
8
|
+
#
|
9
|
+
# If you take a little time to grok the internals of Ohm and Redis, the more
|
10
|
+
# effective you will be in weilding it efficiently.
|
11
|
+
|
12
|
+
#### Keys
|
13
|
+
#
|
14
|
+
# Most key-value stores have standardized on a structured design pattern for
|
15
|
+
# organizing data. Let's fire up a console:
|
16
|
+
#
|
17
|
+
# >> irb -r ohm -r logger
|
18
|
+
#
|
19
|
+
# Ohm.redis.client.logger = Logger.new(STDOUT)
|
20
|
+
#
|
21
|
+
# class Post < Ohm::Model
|
22
|
+
# attribute :title
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Post.create(:title => "Grokking Ohm")
|
26
|
+
#
|
27
|
+
# After executing `Post.create(...)`, you'll see a lot of output from the
|
28
|
+
# logger. Let's go through every line in detail.
|
29
|
+
|
30
|
+
#### Post.create
|
31
|
+
|
32
|
+
# *Generate a new `Post:id`.*
|
33
|
+
INCR Post:id
|
34
|
+
|
35
|
+
# *Lock `Post:2`
|
36
|
+
# (see [SETNX](http://code.google.com/p/redis/wiki/SetnxCommand)
|
37
|
+
# for an explanation of the locking design pattern).
|
38
|
+
SETNX Post:2:_lock 1285060009.409451
|
39
|
+
|
40
|
+
# *Add the newly generated ID 2 to the `Post:all` SET.*
|
41
|
+
SADD Post:all 2
|
42
|
+
|
43
|
+
# *Start transaction.*
|
44
|
+
MULTI
|
45
|
+
|
46
|
+
# *Delete HASH `Post:2`.*
|
47
|
+
DEL Post:2
|
48
|
+
|
49
|
+
# *Set the attributes.*
|
50
|
+
HMSET Post:2 title "Grokking Ohm"
|
51
|
+
|
52
|
+
# *End transaction.*
|
53
|
+
EXEC
|
54
|
+
|
55
|
+
# *Release the lock.*
|
56
|
+
DEL Post:2:_lock
|
57
|
+
|
58
|
+
#### Sets and Lists
|
59
|
+
|
60
|
+
# Let's do a little `SET` and `LIST` manipulation and see what Ohm does.
|
61
|
+
# Here's the code for this section:
|
62
|
+
#
|
63
|
+
# class User < Ohm::Model
|
64
|
+
# collection :posts, Post
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class Post < Ohm::Model
|
68
|
+
# attribute :title
|
69
|
+
# reference :user, User
|
70
|
+
#
|
71
|
+
# list :comments, Comment
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# class Comment < Ohm::Model
|
75
|
+
# end
|
76
|
+
|
77
|
+
#### User.create
|
78
|
+
|
79
|
+
# *Generate a new ID for the `User`.*
|
80
|
+
INCR User:id
|
81
|
+
|
82
|
+
# *Lock User:9.*
|
83
|
+
SETNX User:9:_lock 1285061032.174834
|
84
|
+
|
85
|
+
# *Add this new user to the SET User:all.*
|
86
|
+
SADD User:all 9
|
87
|
+
|
88
|
+
# *Release the lock.*
|
89
|
+
DEL User:9:_lock
|
90
|
+
|
91
|
+
#### Post.create(:title => "Foo", :user => User.create)
|
92
|
+
|
93
|
+
# *Generate an ID for this Post*
|
94
|
+
INCR Post:id
|
95
|
+
|
96
|
+
# *Lock Post:3*
|
97
|
+
SETNX Post:3:_lock 1285061032.180314
|
98
|
+
|
99
|
+
# *Add the ID 3 to the SET Post:all*
|
100
|
+
SADD Post:all 3
|
101
|
+
|
102
|
+
# *Start transaction*
|
103
|
+
MULTI
|
104
|
+
|
105
|
+
# *Remove existing Post:3 HASH*
|
106
|
+
DEL Post:3
|
107
|
+
|
108
|
+
# *Assign the attributes to Post:3*
|
109
|
+
HMSET Post:3 title Foo user_id 9
|
110
|
+
|
111
|
+
# *End transaction*
|
112
|
+
EXEC
|
113
|
+
|
114
|
+
# *Add the ID of this post to the SET index (more on this below)*
|
115
|
+
SADD Post:user_id:OQ== 3
|
116
|
+
|
117
|
+
# *Book keeping for Post:3 indices*
|
118
|
+
SADD Post:3:_indices Post:user_id:OQ==
|
119
|
+
|
120
|
+
# *Release lock*
|
121
|
+
DEL Post:3:_lock
|
122
|
+
|
123
|
+
#### post.comments << Comment.create
|
124
|
+
|
125
|
+
# *Generate an ID for this comment*
|
126
|
+
INCR Comment:id
|
127
|
+
|
128
|
+
# *Lock Comment:1*
|
129
|
+
SETNX Comment:1:_lock 1285061034.335855
|
130
|
+
|
131
|
+
# *Add this comment to the SET Comment:all*
|
132
|
+
SADD Comment:all 1
|
133
|
+
|
134
|
+
# *Release lock*
|
135
|
+
DEL Comment:1:_lock
|
136
|
+
|
137
|
+
# *Append the comment to the LIST Post:3:comments*
|
138
|
+
RPUSH Post:3:comments 1
|
139
|
+
|
140
|
+
#### Understanding indexes
|
141
|
+
|
142
|
+
# reference :user, User
|
143
|
+
#
|
144
|
+
# is actually more or less the same as:
|
145
|
+
#
|
146
|
+
# attribute :user_id
|
147
|
+
# index :user_id
|
148
|
+
#
|
149
|
+
# def user
|
150
|
+
# User[user_id]
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# def user_id=(user_id)
|
154
|
+
# self.user_id = user_id
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# To further explain the [example above](#section-29), let's
|
158
|
+
# run through the commands that was issued. Remember that
|
159
|
+
# we had a *user_id* of *9* and a *post_id* of *3*.
|
160
|
+
|
161
|
+
# *OQ== is Base64 of *9*, with newlines removed. The post_id 3 is added
|
162
|
+
# effectively to Post:user_id:9, if we ignore the encoding.*
|
163
|
+
SADD Post:user_id:OQ== 3
|
164
|
+
|
165
|
+
# *Just keep track that Post:user_id:OQ== was created*
|
166
|
+
SADD Post:3:_indices Post:user_id:OQ==
|
167
|
+
|
168
|
+
# *Get all *posts* with user_id *9**
|
169
|
+
SMEMBERS Post:user_id:OQ==
|
170
|
+
|
171
|
+
#### What's next?
|
172
|
+
|
173
|
+
# We tackled a fair bit amount regarding Ohm internals. More or less this is
|
174
|
+
# the foundation of everything else found in Ohm, and other code are just
|
175
|
+
# built around the same fundamental concepts.
|
176
|
+
#
|
177
|
+
# If there's anything else you want to know, you can hang out on #ohm at
|
178
|
+
# irc.freenode.net, or you can visit the
|
179
|
+
# [Ohm google group](http://groups.google.com/group/ohm-ruby).
|