ohm_util 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 +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.
|