ohm 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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