ohm_util 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gems +4 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +408 -0
- data/CONTRIBUTING +19 -0
- data/LICENSE +19 -0
- data/README.md +570 -0
- data/benchmarks/common.rb +28 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +157 -0
- data/examples/chaining.rb +162 -0
- data/examples/json-hash.rb +75 -0
- data/examples/one-to-many.rb +118 -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/lua/delete.lua +72 -0
- data/lib/lua/save.lua +126 -0
- data/lib/ohm_util.rb +116 -0
- data/makefile +9 -0
- data/ohm-util.gemspec +14 -0
- data/test/association.rb +33 -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 +138 -0
- data/test/json.rb +62 -0
- data/test/list.rb +83 -0
- data/test/model.rb +819 -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 +108 -0
- metadata +97 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require "bench"
|
2
|
+
require_relative "../lib/ohm"
|
3
|
+
|
4
|
+
Ohm.redis = Redic.new("redis://127.0.0.1:6379/15")
|
5
|
+
Ohm.flush
|
6
|
+
|
7
|
+
class Event < Ohm::Model
|
8
|
+
attribute :name
|
9
|
+
attribute :location
|
10
|
+
|
11
|
+
index :name
|
12
|
+
index :location
|
13
|
+
end
|
14
|
+
|
15
|
+
class Sequence
|
16
|
+
def initialize
|
17
|
+
@value = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def succ!
|
21
|
+
Thread.exclusive { @value += 1 }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.[](name)
|
25
|
+
@@sequences ||= Hash.new { |hash, key| hash[key] = Sequence.new }
|
26
|
+
@@sequences[name]
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "common"
|
2
|
+
|
3
|
+
benchmark "Create Events" do
|
4
|
+
i = Sequence[:events].succ!
|
5
|
+
|
6
|
+
Event.create(:name => "Redis Meetup #{i}", :location => "London #{i}")
|
7
|
+
end
|
8
|
+
|
9
|
+
benchmark "Find by indexed attribute" do
|
10
|
+
Event.find(:name => "Redis Meetup 1").first
|
11
|
+
end
|
12
|
+
|
13
|
+
benchmark "Mass update" do
|
14
|
+
Event[1].update(:name => "Redis Meetup II")
|
15
|
+
end
|
16
|
+
|
17
|
+
benchmark "Load events" do
|
18
|
+
Event[1].name
|
19
|
+
end
|
20
|
+
|
21
|
+
run 5000
|
@@ -0,0 +1,157 @@
|
|
1
|
+
### Building an activity feed
|
2
|
+
|
3
|
+
#### Common solutions using a relational design
|
4
|
+
|
5
|
+
# When faced with this application requirement, the most common approach by
|
6
|
+
# far have been to create an *activities* table, and rows in this table would
|
7
|
+
# reference a *user*. Activities would typically be generated for each
|
8
|
+
# follower (or friend) when a certain user performs an action, like posting a
|
9
|
+
# new status update.
|
10
|
+
|
11
|
+
#### Problems
|
12
|
+
|
13
|
+
# The biggest issue with this design, is that the *activities* table will
|
14
|
+
# quickly get very huge, at which point you would need to shard it on
|
15
|
+
# *user_id*. Also, inserting thousands of entries per second would quickly
|
16
|
+
# bring your database to its knees.
|
17
|
+
|
18
|
+
#### Ohm Solution
|
19
|
+
|
20
|
+
# As always we need to require `Ohm`.
|
21
|
+
require "ohm"
|
22
|
+
|
23
|
+
# We create a `User` class, with a `set` for all the other users he
|
24
|
+
# would be `following`, and another `set` for all his `followers`.
|
25
|
+
class User < Ohm::Model
|
26
|
+
set :followers, User
|
27
|
+
set :following, User
|
28
|
+
|
29
|
+
# Because a `User` literally has a `list` of activities, using a Redis
|
30
|
+
# `list` to model the activities would be a good choice. We default to
|
31
|
+
# getting the first 100 activities, and use
|
32
|
+
# [lrange](http://redis.io/commands/lrange) directly.
|
33
|
+
def activities(start = 0, limit = 100)
|
34
|
+
redis.call 'LRANGE', key[:activities], start, start + limit
|
35
|
+
end
|
36
|
+
|
37
|
+
# Broadcasting a message to all the `followers` of a user would simply
|
38
|
+
# be prepending the message for each if his `followers`. We also use
|
39
|
+
# the Redis command
|
40
|
+
# [lpush](http://redis.io/commands/lpush) directly.
|
41
|
+
def broadcast(str)
|
42
|
+
followers.each do |user|
|
43
|
+
redis.call 'LPUSH', user.key[:activities], str
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Given that *Jane* wants to follow *John*, we simply do the following
|
48
|
+
# steps:
|
49
|
+
#
|
50
|
+
# 1. *John* is added to *Jane*'s `following` list.
|
51
|
+
# 2. *Jane* is added to *John*'s `followers` list.
|
52
|
+
def follow(other)
|
53
|
+
following << other
|
54
|
+
other.followers << self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#### Testing
|
60
|
+
|
61
|
+
# We'll use cutest for our testing framework.
|
62
|
+
require "cutest"
|
63
|
+
|
64
|
+
# The database is flushed before each test.
|
65
|
+
prepare { Ohm.flush }
|
66
|
+
|
67
|
+
# We define two users, `john` and `jane`, and yield them so all
|
68
|
+
# other tests are given access to these 2 users.
|
69
|
+
setup do
|
70
|
+
john = User.create
|
71
|
+
jane = User.create
|
72
|
+
|
73
|
+
[john, jane]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Let's verify our model for `follow`. When `jane` follows `john`,
|
77
|
+
# the following conditions should hold:
|
78
|
+
#
|
79
|
+
# 1. The followers list of `john` is comprised *only* of `jane`.
|
80
|
+
# 2. The list of users `jane` is following is comprised *only* of `john`.
|
81
|
+
test "jane following john" do |john, jane|
|
82
|
+
jane.follow(john)
|
83
|
+
|
84
|
+
assert_equal [john], jane.following.to_a
|
85
|
+
assert_equal [jane], john.followers.to_a
|
86
|
+
end
|
87
|
+
|
88
|
+
# Broadcasting a message should simply notify all the followers of the
|
89
|
+
# `broadcaster`.
|
90
|
+
test "john broadcasting a message" do |john, jane|
|
91
|
+
jane.follow(john)
|
92
|
+
john.broadcast("Learning about Redis and Ohm")
|
93
|
+
|
94
|
+
assert jane.activities.include?("Learning about Redis and Ohm")
|
95
|
+
end
|
96
|
+
|
97
|
+
#### Total Denormalization: Adding HTML
|
98
|
+
|
99
|
+
# This may be a real edge case design decision, but for some scenarios this
|
100
|
+
# may work. The beauty of this solution is that you only have to generate the
|
101
|
+
# output once, and successive refreshes of the end user will help you save
|
102
|
+
# some CPU cycles.
|
103
|
+
#
|
104
|
+
# This example of course assumes that the code that generates this does all
|
105
|
+
# the conditional checks (possibly changing the point of view like *Me:*
|
106
|
+
# instead of *John says:*).
|
107
|
+
test "broadcasting the html directly" do |john, jane|
|
108
|
+
jane.follow(john)
|
109
|
+
|
110
|
+
snippet = '<a href="/1">John</a> says: How\'s it going ' +
|
111
|
+
'<a href="/user/2">jane</a>?'
|
112
|
+
|
113
|
+
john.broadcast(snippet)
|
114
|
+
|
115
|
+
assert jane.activities.include?(snippet)
|
116
|
+
end
|
117
|
+
|
118
|
+
#### Saving Space
|
119
|
+
|
120
|
+
# In most cases, users don't really care about keeping their entire activity
|
121
|
+
# history. This application requirement would be fairly trivial to implement.
|
122
|
+
|
123
|
+
# Let's reopen our `User` class and define a new broadcast method.
|
124
|
+
class User
|
125
|
+
# We define a constant where we set the maximum number of activity entries.
|
126
|
+
MAX = 10
|
127
|
+
|
128
|
+
# Using `MAX` as the reference, we truncate the activities feed using
|
129
|
+
# [ltrim](http://redis.io/commands/ltrim).
|
130
|
+
def broadcast(str)
|
131
|
+
followers.each do |user|
|
132
|
+
redis.call 'LPUSH', user.key[:activities], str
|
133
|
+
redis.call 'LTRIM', user.key[:activities], 0, MAX - 1
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Now let's verify that this new behavior is enforced.
|
139
|
+
test "pushing 11 activities maintains the list to 10" do |john, jane|
|
140
|
+
jane.follow(john)
|
141
|
+
|
142
|
+
11.times { john.broadcast("Flooding your feed!") }
|
143
|
+
|
144
|
+
assert 10 == jane.activities.size
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
#### Conclusion
|
149
|
+
|
150
|
+
# As you can see, choosing a more straightforward approach (in this case,
|
151
|
+
# actually having a list per user, instead of maintaining a separate
|
152
|
+
# `activities` table) will greatly simplify the design of your system.
|
153
|
+
#
|
154
|
+
# As a final note, keep in mind that the Ohm solution would still need
|
155
|
+
# sharding for large datasets, but that would be again trivial to implement
|
156
|
+
# using [redis-rb](http://github.com/redis/redis-rb)'s distributed support
|
157
|
+
# and sharding it against the *user_id*.
|
@@ -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.
|