ohm 1.3.0 → 1.3.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 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.