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.
@@ -0,0 +1,149 @@
1
+ ### All Kinds of Slugs
2
+
3
+ # The problem of making semantic URLs have definitely been a prevalent one.
4
+ # There has been quite a lot of solutions around this theme, so we'll discuss
5
+ # a few simple ways to handle slug generation.
6
+
7
+ #### ID Prefixed slugs
8
+
9
+ # This is by far the simplest (and most cost-effective way) of generating
10
+ # slugs. Implementing this is pretty simple too.
11
+
12
+ # Let's first require `Ohm`.
13
+ require "ohm"
14
+
15
+ # Now let's define our `Post` model, with just a single
16
+ # `attribute` *title*.
17
+ class Post < Ohm::Model
18
+ attribute :title
19
+
20
+ # To make it more convenient, we override the finder syntax,
21
+ # so doing a `Post["1-my-post-title"]` will in effect just call
22
+ # `Post[1]`.
23
+ def self.[](id)
24
+ super(id.to_i)
25
+ end
26
+
27
+ # This pattern was mostly borrowed from Rails' style of generating
28
+ # URLs. Here we just concatenate the `id` and a sanitized form
29
+ # of our title.
30
+ def to_param
31
+ "#{id}-#{title.to_s.gsub(/\p{^Alnum}/u, " ").gsub(/\s+/, "-").downcase}"
32
+ end
33
+ end
34
+
35
+ # Let's verify our code using the
36
+ # [Cutest](http://github.com/djanowski/cutest)
37
+ # testing framework.
38
+ require "cutest"
39
+
40
+ # Also we ensure every test run is guaranteed to have a clean
41
+ # *Redis* instance.
42
+ prepare { Ohm.flush }
43
+
44
+ # For each and every test, we create a post with
45
+ # the title "ID Prefixed Slugs". Since it's the last
46
+ # line of our `setup`, it will also be yielded to
47
+ # each of our test blocks.
48
+ setup do
49
+ Post.create(:title => "ID Prefixed Slugs")
50
+ end
51
+
52
+ # Now let's verify the behavior of our `to_param` method.
53
+ # Note that we make it dash-separated and lowercased.
54
+ test "to_param" do |post|
55
+ assert "1-id-prefixed-slugs" == post.to_param
56
+ end
57
+
58
+ # We also check that our easier finder syntax works.
59
+ test "finding the post" do |post|
60
+ assert post == Post[post.to_param]
61
+ end
62
+
63
+ #### We don't have to code it everytime
64
+
65
+ # Because of the prevalence, ease of use, and efficiency of this style of slug
66
+ # generation, it has been extracted to a module in
67
+ # [Ohm::Contrib](http://github.com/cyx/ohm-contrib/) called `Ohm::Slug`.
68
+
69
+ # Let's create a different model to demonstrate how to use it.
70
+ # (Run `[sudo] gem install ohm-contrib` to install ohm-contrib).
71
+
72
+ # When using `ohm-contrib`, we simply require it, and then
73
+ # directly reference the specific module. In this case, we
74
+ # use `Ohm::Slug`.
75
+ require "ohm/contrib"
76
+
77
+ class Video < Ohm::Model
78
+ include Ohm::Slug
79
+
80
+ attribute :title
81
+
82
+ # `Ohm::Slug` just uses the value of the object's `to_s`.
83
+ def to_s
84
+ title.to_s
85
+ end
86
+ end
87
+
88
+ # Now to quickly verify that everything works similar to our
89
+ # example above!
90
+ test "video slugging" do
91
+ video = Video.create(:title => "A video about ohm")
92
+
93
+ assert "1-a-video-about-ohm" == video.to_param
94
+ assert video == Video[video.id]
95
+ end
96
+
97
+ # That's it, and it works similarly to the example above.
98
+
99
+ #### What if I want a slug without an ID prefix?
100
+
101
+ # For this case, we can still make use of `Ohm::Slug`'s ability to
102
+ # make a clean string.
103
+
104
+ # Let's create an `Article` class which has a single attribute `title`.
105
+ class Article < Ohm::Model
106
+ include Ohm::Callbacks
107
+
108
+ attribute :title
109
+
110
+ # Now before creating this object, we just call `Ohm::Slug.slug` directly.
111
+ # We also check if the generated slug exists, and repeatedly try
112
+ # appending numbers.
113
+ protected
114
+ def before_create
115
+ temp = Ohm::Slug.slug(title)
116
+ self.id = temp
117
+
118
+ counter = 0
119
+ while Article.exists?(id)
120
+ self.id = "%s-%d" % [temp, counter += 1]
121
+ end
122
+ end
123
+ end
124
+
125
+ # We now verify the behavior of our `Article` class
126
+ # by creating an article with the same title 3 times.
127
+ test "create an article with the same title" do
128
+ a1 = Article.create(:title => "All kinds of slugs")
129
+ a2 = Article.create(:title => "All kinds of slugs")
130
+ a3 = Article.create(:title => "All kinds of slugs")
131
+
132
+ assert a1.id == "all-kinds-of-slugs"
133
+ assert a2.id == "all-kinds-of-slugs-1"
134
+ assert a3.id == "all-kinds-of-slugs-2"
135
+ end
136
+
137
+ #### Conclusion
138
+
139
+ # Slug generation comes in all different flavors.
140
+ #
141
+ # 1. The first solution is good enough for most cases. The primary advantage
142
+ # of this solution is that we don't have to check for ID clashes.
143
+ #
144
+ # 2. The second solution may be needed for cases where you must make
145
+ # the URLs absolutely clean and readable, and you hate having those
146
+ # number prefixes.
147
+ #
148
+ # *NOTE:* The example we used for the second solution has potential
149
+ # race conditions. I'll leave fixing it as an exercise to you.
@@ -0,0 +1,234 @@
1
+ ### Tagging
2
+
3
+ #### Intro
4
+
5
+ # When building a Web 2.0 application, tagging will probably come up
6
+ # as one of the most requested features. Popularized by Delicious,
7
+ # it has quickly become a useful way to organize crowd sourced data.
8
+
9
+ #### How it was done
10
+
11
+ # Typically, when you do tagging using an RDBMS, you'll probably end up
12
+ # having a taggings and a tags table, hence a many-to-many design.
13
+ # Here is a quick sketch just to illustrate:
14
+ #
15
+ #
16
+ #
17
+ # Post Taggings Tag
18
+ # ---- -------- ---
19
+ # id tag_id id
20
+ # title post_id name
21
+ #
22
+ # As you can see, this design leads to a lot of problems:
23
+ #
24
+ # 1. Trying to find the tags of a post will have to go through taggings, and
25
+ # then individually find the actual tag.
26
+ # 2. One might be inclined to use a JOIN query, but we all know
27
+ # [joins are evil](http://stackoverflow.com/questions/1020847).
28
+ # 3. Building a tag cloud or some form of tag ranking is unintuitive.
29
+
30
+ #### The Ohm approach
31
+
32
+ # Here is a basic outline of what we'll need:
33
+ #
34
+ # 1. We should be able to tag a post (separated by commas).
35
+ # 2. We should be able to find a post with a given tag.
36
+
37
+ #### Beginning with our Post model
38
+
39
+ # Let's first require ohm.
40
+ require 'ohm'
41
+
42
+ # We then declare our class, inheriting from `Ohm::Model` in the process.
43
+ class Post < Ohm::Model
44
+
45
+ # The structure, fields, and other associations are defined in a declarative
46
+ # manner. Ohm allows us to declare *attributes*, *sets*, *lists* and
47
+ # *counters*. For our usecase here, only two *attributes* will get the job
48
+ # done. The `body` will just
49
+ # be a plain string, and the `tags` will contain our comma-separated list of
50
+ # words, i.e. "ruby, redis, ohm". We then declare an `index` (which can be
51
+ # an `attribute` or just a plain old method), which we point to our method
52
+ # `tag`.
53
+ attribute :body
54
+ attribute :tags
55
+ index :tag
56
+
57
+ # One very interesting thing about Ohm indexes is that it can either be a
58
+ # *String* or an *Enumerable* data structure. When we declare it as an
59
+ # *Enumerable*, `Ohm` will create an index for every element. So if `tag`
60
+ # returned `[ruby, redis, ohm]` then we can search it using any of the
61
+ # following:
62
+ #
63
+ # 1. ruby
64
+ # 2. redis
65
+ # 3. ohm
66
+ # 4. ruby, redis
67
+ # 5. ruby, ohm
68
+ # 6. redis, ohm
69
+ # 7. ruby, redis, ohm
70
+ #
71
+ # Pretty neat ain't it?
72
+ def tag
73
+ tags.to_s.split(/\s*,\s*/).uniq
74
+ end
75
+ end
76
+
77
+ #### Testing it out
78
+
79
+ # It's a very good habit to test all the time. In the Ruby community,
80
+ # a lot of test frameworks have been created.
81
+
82
+ # For our purposes in this example, we'll use cutest.
83
+ require "cutest"
84
+
85
+ # Cutest allows us to define callbacks which are guaranteed to be executed
86
+ # every time a new `test` begins. Here, we just make sure that the Redis
87
+ # instance of `Ohm` is empty everytime.
88
+ prepare { Ohm.flush }
89
+
90
+ # Next, let's create a simple `Post` instance. The return value of the `setup`
91
+ # block will be passed to every `test` block, so we don't actually have to
92
+ # assign it to an instance variable.
93
+ setup do
94
+ Post.create(:body => "Ohm Tagging", :tags => "tagging, ohm, redis")
95
+ end
96
+
97
+ # For our first run, let's verify the fact that we can find a `Post`
98
+ # using any of the tags we gave.
99
+ test "find using a single tag" do |p|
100
+ assert Post.find(tag: "tagging").include?(p)
101
+ assert Post.find(tag: "ohm").include?(p)
102
+ assert Post.find(tag: "redis").include?(p)
103
+ end
104
+
105
+ # Now we verify our claim earlier, that it is possible to find a tag
106
+ # using any one of the combinations for the given set of tags.
107
+ #
108
+ # We also verify that if we pass in a non-existent tag name that
109
+ # we'll fail to find the `Post` we just created.
110
+ test "find using an intersection of multiple tag names" do |p|
111
+ assert Post.find(tag: ["tagging", "ohm"]).include?(p)
112
+ assert Post.find(tag: ["tagging", "redis"]).include?(p)
113
+ assert Post.find(tag: ["ohm", "redis"]).include?(p)
114
+ assert Post.find(tag: ["tagging", "ohm", "redis"]).include?(p)
115
+
116
+ assert ! Post.find(tag: ["tagging", "foo"]).include?(p)
117
+ end
118
+
119
+ #### Adding a Tag model
120
+
121
+ # Let's pretend that the client suddenly requested that we keep track
122
+ # of the number of times a tag has been used. It's a pretty fair requirement
123
+ # after all. Updating our requirements, we will now have:
124
+ #
125
+ # 1. We should be able to tag a post (separated by commas).
126
+ # 2. We should be able to find a post with a given tag.
127
+ # 3. We should be able to find top tags, and their count.
128
+
129
+ # Continuing from our example above, let's require `ohm-contrib`, which we
130
+ # will be using for callbacks.
131
+ require "ohm/contrib"
132
+
133
+ # Let's quickly re-open our Post class.
134
+ class Post
135
+ # When we want our class to have extended functionality like callbacks,
136
+ # we simply include the necessary modules, in this case `Ohm::Callbacks`,
137
+ # which will be responsible for inserting `before_*` and `after_*` methods
138
+ # in the object's lifecycle.
139
+ include Ohm::Callbacks
140
+
141
+ # To make our code more concise, we just quickly change our implementation
142
+ # of `tag` to receive a default parameter:
143
+ def tag(tags = self.tags)
144
+ tags.to_s.split(/\s*,\s*/).uniq
145
+ end
146
+
147
+ # For all but the most simple cases, we would probably need to define
148
+ # callbacks. When we included `Ohm::Callbacks` above, it actually gave us
149
+ # the following:
150
+ #
151
+ # 1. `before_validate` and `after_validate`
152
+ # 2. `before_create` and `after_create`
153
+ # 3. `before_update` and `after_update`
154
+ # 4. `before_save` and `after_save`
155
+ # 5. `before_delete` and `after_delete`
156
+
157
+ # For our scenario, we only need a `before_update` and `after_save`.
158
+ # The idea for our `before_update` is to decrement the `total` of
159
+ # all existing tags. We use `read_remote(:tags)` to make sure that
160
+ # we actually get the original `tags` for a particular record.
161
+ protected
162
+ def before_update
163
+ tag(read_remote(:tags)).map(&Tag).each { |t| t.decr :total }
164
+ end
165
+
166
+ # And of course, we increment all new tags for a particular record
167
+ # after successfully saving it.
168
+ def after_save
169
+ tag.map(&Tag).each { |t| t.incr :total }
170
+ end
171
+ end
172
+
173
+ #### Our Tag model
174
+
175
+ # The `Tag` model has only one type, which is a `counter` for the `total`.
176
+ # Since `Ohm` allows us to use any kind of ID (not just numeric sequences),
177
+ # we can actually use the tag name to identify a `Tag`.
178
+ class Tag < Ohm::Model
179
+ counter :total
180
+
181
+ # The syntax for finding a record by its ID is `Tag["ruby"]`. The standard
182
+ # behavior in `Ohm` is to return `nil` when the ID does not exist.
183
+ #
184
+ # To simplify our code, we override `Tag["ruby"]`, and make it create a
185
+ # new `Tag` if it doesn't exist yet. One important implementation detail
186
+ # though is that we need to encode the tag name, so special characters
187
+ # and spaces won't produce an invalid key.
188
+ def self.[](id)
189
+ super(encode(id)) || create(:id => encode(id))
190
+ end
191
+ end
192
+
193
+ #### Verifying our third requirement
194
+
195
+ # Continuing from our test cases above, let's add test coverage for the
196
+ # behavior of counting tags.
197
+
198
+ # For each and every tag we initially create, we need to make sure they have a
199
+ # total of 1.
200
+ test "verify total to be exactly 1" do
201
+ assert 1 == Tag["ohm"].total
202
+ assert 1 == Tag["redis"].total
203
+ assert 1 == Tag["tagging"].total
204
+ end
205
+
206
+ # If we try and create another post tagged "ruby", "redis", `Tag["redis"]`
207
+ # should then have a total of 2. All of the other tags will still have
208
+ # a total of 1.
209
+ test "verify totals increase" do
210
+ Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
211
+
212
+ assert 1 == Tag["ohm"].total
213
+ assert 1 == Tag["tagging"].total
214
+ assert 1 == Tag["ruby"].total
215
+ assert 2 == Tag["redis"].total
216
+ end
217
+
218
+ # Finally, let's verify the scenario where we create a `Post` tagged
219
+ # "ruby", "redis" and update it to only have the tag "redis",
220
+ # effectively removing the tag "ruby" from our `Post`.
221
+ test "updating an existing post decrements the tags removed" do
222
+ p = Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
223
+ p.update(:tags => "redis")
224
+
225
+ assert 0 == Tag["ruby"].total
226
+ assert 2 == Tag["redis"].total
227
+ end
228
+
229
+ ## Conclusion
230
+
231
+ # Most of the time we tend to think in terms of an RDBMS way, and this is in
232
+ # no way a negative thing. However, it is important to try and switch your
233
+ # frame of mind when working with Ohm (and Redis) because it will greatly save
234
+ # you time, and possibly lead to a great design.
data/lib/ohm.rb CHANGED
@@ -1147,7 +1147,7 @@ module Ohm
1147
1147
  return self
1148
1148
  end
1149
1149
 
1150
- # Read an attribute remotly from Redis. Useful if you want to get
1150
+ # Read an attribute remotely from Redis. Useful if you want to get
1151
1151
  # the most recent value of the attribute and not rely on locally
1152
1152
  # cached value.
1153
1153
  #
@@ -1305,9 +1305,13 @@ module Ohm
1305
1305
  uniques = nil
1306
1306
  _indices = nil
1307
1307
  indices = nil
1308
+ existing_indices = nil
1309
+ existing_uniques = nil
1308
1310
 
1309
1311
  t.read do
1310
1312
  _verify_uniques
1313
+ existing_indices = _read_attributes(model.indices) if model.indices.any?
1314
+ existing_uniques = _read_attributes(model.uniques) if model.uniques.any?
1311
1315
  _uniques = db.hgetall(key[:_uniques])
1312
1316
  _indices = db.smembers(key[:_indices])
1313
1317
  uniques = _read_index_type(:uniques)
@@ -1316,8 +1320,10 @@ module Ohm
1316
1320
 
1317
1321
  t.write do
1318
1322
  db.sadd(model.key[:all], id)
1319
- _delete_uniques(_uniques)
1323
+ _delete_existing_indices(existing_indices)
1324
+ _delete_existing_uniques(existing_uniques)
1320
1325
  _delete_indices(_indices)
1326
+ _delete_uniques(_uniques)
1321
1327
  _save
1322
1328
  _save_indices(indices)
1323
1329
  _save_uniques(uniques)
@@ -1337,6 +1343,7 @@ module Ohm
1337
1343
  transaction do |t|
1338
1344
  _uniques = nil
1339
1345
  _indices = nil
1346
+ existing = nil
1340
1347
 
1341
1348
  t.watch(*_unique_keys)
1342
1349
 
@@ -1345,6 +1352,7 @@ module Ohm
1345
1352
  t.watch(key[:_uniques]) if model.uniques.any?
1346
1353
 
1347
1354
  t.read do
1355
+ existing = _read_attributes(model.indices) if model.indices.any?
1348
1356
  _uniques = db.hgetall(key[:_uniques])
1349
1357
  _indices = db.smembers(key[:_indices])
1350
1358
  end
@@ -1352,6 +1360,7 @@ module Ohm
1352
1360
  t.write do
1353
1361
  _delete_uniques(_uniques)
1354
1362
  _delete_indices(_indices)
1363
+ _delete_existing_indices(existing)
1355
1364
  model.collections.each { |e| db.del(key[e]) }
1356
1365
  db.srem(model.key[:all], id)
1357
1366
  db.del(key[:counters])
@@ -1498,11 +1507,13 @@ module Ohm
1498
1507
  end
1499
1508
 
1500
1509
  def _save_uniques(uniques)
1510
+ attrs = model.attributes
1511
+
1501
1512
  uniques.each do |att, val|
1502
1513
  unique = model.key[:uniques][att]
1503
1514
 
1504
1515
  db.hset(unique, val, id)
1505
- db.hset(key[:_uniques], unique, val)
1516
+ db.hset(key[:_uniques], unique, val) unless attrs.include?(att)
1506
1517
  end
1507
1518
  end
1508
1519
 
@@ -1513,6 +1524,23 @@ module Ohm
1513
1524
  end
1514
1525
  end
1515
1526
 
1527
+ def _delete_existing_indices(existing)
1528
+ return unless existing
1529
+
1530
+ existing = existing.map { |key, value| model.to_indices(key, value) }
1531
+ existing.flatten!(1)
1532
+
1533
+ _delete_indices(existing)
1534
+ end
1535
+
1536
+ def _delete_existing_uniques(existing)
1537
+ return unless existing
1538
+
1539
+ _delete_uniques(existing.map { |key, value|
1540
+ [model.key[:uniques][key], value]
1541
+ })
1542
+ end
1543
+
1516
1544
  def _delete_indices(indices)
1517
1545
  indices.each do |index|
1518
1546
  db.srem(index, id)
@@ -1521,12 +1549,18 @@ module Ohm
1521
1549
  end
1522
1550
 
1523
1551
  def _save_indices(indices)
1552
+ attrs = model.attributes
1553
+
1524
1554
  indices.each do |att, val|
1525
1555
  model.to_indices(att, val).each do |index|
1526
1556
  db.sadd(index, id)
1527
- db.sadd(key[:_indices], index)
1557
+ db.sadd(key[:_indices], index) unless attrs.include?(att)
1528
1558
  end
1529
1559
  end
1530
1560
  end
1561
+
1562
+ def _read_attributes(attrs)
1563
+ Hash[attrs.zip(db.hmget(key, *attrs))]
1564
+ end
1531
1565
  end
1532
1566
  end