ohm 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fb164eaa5fbec17afbcceb9a4c7f8ded8ace93b5
4
- data.tar.gz: 4ab08c00e7edf980f16e3d854012b3a5e9503b1a
5
- SHA512:
6
- metadata.gz: 5f3c9c4a0457fd31a0b14eb783840c23e8996e69205fffaacea18ae22efb1c5aa643188d2ddcaf1c2fe584efe2fa5231b58d5997627e7f9f7249b6e52c5b446e
7
- data.tar.gz: 6d0e08cd9092c0fce4c41a326163c1cf13a37f2a781020be389bd83d46fa650b8af05785873fb57343cdeb3b7721d341c4c063b1dca38163f835f7e7b8975ee7
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZDVlZWJhMjlkNWUzM2Y2NzY3ODVmN2E3YmIxYjQ3ZmU3OGQ3N2JiOA==
5
+ data.tar.gz: !binary |-
6
+ ZGMyODFlYTAwYzU0MzE4MTQ0Y2FhZTNlZTdhODU0YzgwYTgyZjA4Mw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NGY2YmU4YTE0NWQ3N2E4Y2QyYWViZDBiOWUwM2NmMTFhNjY5YjNmMTIxNmE3
10
+ OTRhNmJiMjQ2ZjcwNDQwMjU4YTQ3ZWRjZmJjMmZlOTIxMTY3OTA5ZGJkNDYy
11
+ Yjg0ZDllODdkNjM4NGFmNWM4YzY5MTQ0ODBlYTNkYjhkODhmOGE=
12
+ data.tar.gz: !binary |-
13
+ ZTJiYWNjNGUxZTk1ZTUxNTQ5Nzk2NTZmOGQ1YTAzY2QzY2Y2NDk4MGE5MDU3
14
+ Zjk1ODY3MThlYTdmOGZhZWFiY2QyNzg1MTg2YjA5NjFlODE4NDNjNmE4N2Mx
15
+ MmJlNjg3ZjViYzc4NmU5ZGNlNmU1MmEwMmNkMDBhOTg1ZDUyZjk=
data/.gems ADDED
@@ -0,0 +1,4 @@
1
+ nest -v 1.1.1
2
+ scrivener -v 0.0.3
3
+ redis -v 3.0.1
4
+ cutest -v 1.2.0.rc2
@@ -0,0 +1,4 @@
1
+ /pkg
2
+ /.yardoc
3
+ /doc
4
+ .rbenv-gemsets
@@ -0,0 +1,52 @@
1
+ 1.3.1
2
+
3
+ - Improve memory consumption when indexing persisted attributes.
4
+
5
+ No migration is needed and old indices will be cleaned up as you save
6
+ instances.
7
+
8
+ 1.3.0
9
+
10
+ - Add Model.attributes.
11
+
12
+ 1.2.0
13
+
14
+ - Enumerable fix.
15
+ - Merge Ohm::PipelinedFetch into Ohm::Collection.
16
+ - Fix Set, MultiSet, and List enumerable behavior.
17
+ - Change dependencies to use latest cutest.
18
+
19
+ 1.1.0
20
+
21
+ - Compatible with redis-rb 3.
22
+
23
+ 1.0.0
24
+
25
+ - Fetching a batch of objects is now done through one pipeline, effectively
26
+ reducing the IO to just 2 operations (one for SMEMBERS / LRANGE, one for
27
+ the actual HGET of all the individual HASHes.)
28
+ - write_remote / read_remote have been replaced with set / get respectively.
29
+ - Ohm::Model.unique has been added.
30
+ - Ohm::Model::Set has been renamed to Ohm::Set
31
+ - Ohm::Model::List has been renamed to Ohm::List
32
+ - Ohm::Model::Collection is gone.
33
+ - Ohm::Validations is gone. Ohm now uses Scrivener::Validations.
34
+ - Ohm::Key is gone. Ohm now uses Nest directly.
35
+ - No more concept of volatile keys.
36
+ - Ohm::Model::Wrapper is gone.
37
+ - Use Symbols for constants instead of relying on Ohm::Model.const_missing.
38
+ - #sort / #sort_by now uses `limit` as it's used in redis-rb, e.g. you
39
+ have to pass in an array like so: sort(limit: [0, 1]).
40
+ - Set / List have been trimmed to contain only the minimum number
41
+ of necessary methods.
42
+ - You can no longer mutate a collection / set as before, e.g. doing
43
+ User.find(...).add(User[1]) will throw an error.
44
+ - The #union operation has been added. You can now chain it with your filters.
45
+ - Temporary keys when doing finds are now automatically cleaned up.
46
+ - Counters are now stored in their own key instead, i.e. in
47
+ User:<id>:counters.
48
+ - JSON support has to be explicitly required by doing `require
49
+ "ohm/json"`.
50
+ - All save / delete / update operations are now done using
51
+ transactions (see http://redis.io/topics/transactions).
52
+ - All indices are now stored without converting the values to base64.
@@ -0,0 +1,33 @@
1
+ require "bench"
2
+ require_relative "../lib/ohm"
3
+
4
+ Ohm.connect(:port => 6379, :db => 15)
5
+ Ohm.flush
6
+
7
+ class Event < Ohm::Model
8
+ attribute :name
9
+ attribute :location
10
+
11
+ index :name
12
+ index :location
13
+
14
+ def validate
15
+ assert_present :name
16
+ assert_present :location
17
+ end
18
+ end
19
+
20
+ class Sequence
21
+ def initialize
22
+ @value = 0
23
+ end
24
+
25
+ def succ!
26
+ Thread.exclusive { @value += 1 }
27
+ end
28
+
29
+ def self.[](name)
30
+ @@sequences ||= Hash.new { |hash, key| hash[key] = Sequence.new }
31
+ @@sequences[name]
32
+ end
33
+ 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,162 @@
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://code.google.com/p/redis/wiki/LrangeCommand) directly.
33
+ def activities(start = 0, limit = 100)
34
+ key[:activities].lrange(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://code.google.com/p/redis/wiki/RpushCommand) directly.
41
+ def broadcast(str)
42
+ followers.each do |user|
43
+ user.key[:activities].lpush(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 [john] == jane.following.to_a
85
+ assert [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 check if the number of activities exceeds
129
+ # `MAX`, and use
130
+ # [ltrim](http://code.google.com/p/redis/wiki/LtrimCommand) to truncate
131
+ # the activities.
132
+ def broadcast(str)
133
+ followers.each do |user|
134
+ user.key[:activities].lpush(str)
135
+
136
+ if user.key[:activities].llen > MAX
137
+ user.key[:activities].ltrim(0, MAX - 1)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ # Now let's verify that this new behavior is enforced.
144
+ test "pushing 11 activities maintains the list to 10" do |john, jane|
145
+ jane.follow(john)
146
+
147
+ 11.times { john.broadcast("Flooding your feed!") }
148
+
149
+ assert 10 == jane.activities.size
150
+ end
151
+
152
+
153
+ #### Conclusion
154
+
155
+ # As you can see, choosing a more straightforward approach (in this case,
156
+ # actually having a list per user, instead of maintaining a separate
157
+ # `activities` table) will greatly simplify the design of your system.
158
+ #
159
+ # As a final note, keep in mind that the Ohm solution would still need
160
+ # sharding for large datasets, but that would be again trivial to implement
161
+ # using [redis-rb](http://github.com/ezmobius/redis-rb)'s distributed support
162
+ # and sharding it against the *user_id*.
@@ -0,0 +1,203 @@
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 authorized orders. The tricky part
85
+ # is trying to find an order that is either *captured* or *authorized*,
86
+ # since `Ohm` as of this writing doesn't support unions in its
87
+ # finder syntax.
88
+ test "finding authorized and/or captured orders" do
89
+ assert @user.orders.find(:state => "authorized").include?(@authorized)
90
+ assert @user.orders.find(:state => "captured").include?(@captured)
91
+
92
+ assert @user.orders.find(:state => ["authorized", "captured"]).empty?
93
+
94
+ auth_or_capt = @user.orders.key.volatile[:auth_or_capt]
95
+ auth_or_capt.sunionstore(
96
+ @user.orders.find(:state => "authorized").key,
97
+ @user.orders.find(:state => "captured").key
98
+ )
99
+
100
+ assert auth_or_capt.smembers.include?(@authorized.id)
101
+ assert auth_or_capt.smembers.include?(@captured.id)
102
+ end
103
+
104
+ #### Creating shortcuts
105
+
106
+ # You can of course define methods to make that code more readable.
107
+ class User < Ohm::Model
108
+ def authorized_orders
109
+ orders.find(:state => "authorized")
110
+ end
111
+
112
+ def captured_orders
113
+ orders.find(:state => "captured")
114
+ end
115
+ end
116
+
117
+ # And we can now test these new methods.
118
+ test "finding authorized and/or captured orders" do
119
+ assert @user.authorized_orders.include?(@authorized)
120
+ assert @user.captured_orders.include?(@captured)
121
+ end
122
+
123
+ # In most cases this is fine, but if you want to have a little fun,
124
+ # then we can play around with some chainability.
125
+
126
+ #### Chaining Kung-Fu
127
+
128
+ # The `Ohm::Model::Set` takes a *Redis* key and a *class monad*
129
+ # for its arguments.
130
+ #
131
+ # We can simply subclass it and define the monad to always be an
132
+ # `Order` so we don't have to manually set it everytime.
133
+ class UserOrders < Ohm::Model::Set
134
+ def initialize(key)
135
+ super key, Ohm::Model::Wrapper.wrap(Order)
136
+ end
137
+
138
+ # Here is the crux of the chaining pattern. Instead of
139
+ # just doing a straight up `find(:state => "pending")`, we return
140
+ # `UserOrders` again.
141
+ def pending
142
+ self.class.new(model.index_key_for(:state, "pending"))
143
+ end
144
+
145
+ def authorized
146
+ self.class.new(model.index_key_for(:state, "authorized"))
147
+ end
148
+
149
+ def captured
150
+ self.class.new(model.index_key_for(:state, "captured"))
151
+ end
152
+
153
+ # Now we wrap the implementation of doing an `SUNIONSTORE` and also
154
+ # make it return a `UserOrders` object.
155
+ #
156
+ # NOTE: `volatile` just returns the key prepended with a `~:`, so in
157
+ # this case it would be `~:Order:accepted`.
158
+ def accepted
159
+ model.key.volatile[:accepted].sunionstore(
160
+ authorized.key, captured.key
161
+ )
162
+
163
+ self.class.new(model.key.volatile[:accepted])
164
+ end
165
+ end
166
+
167
+ # Now let's re-open the `User` class and add a customized `orders` method.
168
+ class User < Ohm::Model
169
+ def orders
170
+ UserOrders.new(Order.index_key_for(:user_id, id))
171
+ end
172
+ end
173
+
174
+ # Ok! Let's put all of that chaining code to good use.
175
+ test "finding pending orders using a chainable style" do
176
+ assert @user.orders.pending.include?(@pending)
177
+ assert @user.orders.pending.find(:product_id => @ipod.id).include?(@pending)
178
+
179
+ assert @user.orders.pending.find(:product_id => @ipad.id).empty?
180
+ end
181
+
182
+ test "finding authorized and/or captured orders using a chainable style" do
183
+ assert @user.orders.authorized.include?(@authorized)
184
+ assert @user.orders.captured.include?(@captured)
185
+
186
+ assert @user.orders.accepted.include?(@authorized)
187
+ assert @user.orders.accepted.include?(@captured)
188
+
189
+ accepted = @user.orders.accepted
190
+
191
+ assert accepted.find(:product_id => @ipad.id).include?(@authorized)
192
+ assert accepted.find(:product_id => @ipad.id).include?(@captured)
193
+ end
194
+
195
+ #### Conclusion
196
+
197
+ # This design pattern is something that really depends upon the situation. In
198
+ # the example above, you can add more complicated querying on the `UserOrders`
199
+ # class.
200
+ #
201
+ # The most important takeaway here is the ease of which we can weild the
202
+ # different components of Ohm, and mold it accordingly to our preferences,
203
+ # without having to monkey-patch anything.