ohm_util 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,13 @@
1
+ require_relative "common"
2
+
3
+ 1000.times do |i|
4
+ Event.create(:name => "Redis Meetup #{i}", :location => "At my place")
5
+ end
6
+
7
+ benchmark "Delete event" do
8
+ Event.all.each do |event|
9
+ event.delete
10
+ end
11
+ end
12
+
13
+ run 1
@@ -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.