ohm_util 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gems +4 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +408 -0
- data/CONTRIBUTING +19 -0
- data/LICENSE +19 -0
- data/README.md +570 -0
- data/benchmarks/common.rb +28 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +157 -0
- data/examples/chaining.rb +162 -0
- data/examples/json-hash.rb +75 -0
- data/examples/one-to-many.rb +118 -0
- data/examples/philosophy.rb +137 -0
- data/examples/redis-logging.txt +179 -0
- data/examples/slug.rb +149 -0
- data/examples/tagging.rb +237 -0
- data/lib/lua/delete.lua +72 -0
- data/lib/lua/save.lua +126 -0
- data/lib/ohm_util.rb +116 -0
- data/makefile +9 -0
- data/ohm-util.gemspec +14 -0
- data/test/association.rb +33 -0
- data/test/connection.rb +16 -0
- data/test/core.rb +24 -0
- data/test/counters.rb +67 -0
- data/test/enumerable.rb +79 -0
- data/test/filtering.rb +185 -0
- data/test/hash_key.rb +31 -0
- data/test/helper.rb +23 -0
- data/test/indices.rb +138 -0
- data/test/json.rb +62 -0
- data/test/list.rb +83 -0
- data/test/model.rb +819 -0
- data/test/set.rb +37 -0
- data/test/thread_safety.rb +67 -0
- data/test/to_hash.rb +29 -0
- data/test/uniques.rb +108 -0
- metadata +97 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
### One to Many Ohm style
|
2
|
+
|
3
|
+
#### Problem
|
4
|
+
|
5
|
+
# Let's say you want to implement a commenting system, and you need to have
|
6
|
+
# comments on different models. In order to do this using an RDBMS you have
|
7
|
+
# one of two options:
|
8
|
+
#
|
9
|
+
# 1. Have multiple comment tables per type i.e. VideoComments, AudioComments,
|
10
|
+
# etc.
|
11
|
+
# 2. Use a polymorphic schema.
|
12
|
+
#
|
13
|
+
# The problem with option 1 is that you'll may possibly run into an explosion
|
14
|
+
# of tables.
|
15
|
+
#
|
16
|
+
# The problem with option 2 is that if you have many comments across the whole
|
17
|
+
# site, you'll quickly hit the limit on a table, and eventually need to shard.
|
18
|
+
|
19
|
+
#### Solution
|
20
|
+
|
21
|
+
# In *Redis*, possibly the best data structure to model a comment would be to
|
22
|
+
# use a *List*, mainly because comments are always presented within the
|
23
|
+
# context of the parent entity, and are typically ordered in a predefined way
|
24
|
+
# (i.e. latest at the top, or latest at the bottom).
|
25
|
+
#
|
26
|
+
|
27
|
+
# Let's start by requiring `Ohm`.
|
28
|
+
require "ohm"
|
29
|
+
|
30
|
+
# We define both a `Video` and `Audio` model, with a `list` of *comments*.
|
31
|
+
class Video < Ohm::Model
|
32
|
+
list :comments, :Comment
|
33
|
+
end
|
34
|
+
|
35
|
+
class Audio < Ohm::Model
|
36
|
+
list :comments, :Comment
|
37
|
+
end
|
38
|
+
|
39
|
+
# The `Comment` model for this example will just contain one attribute called
|
40
|
+
# `body`.
|
41
|
+
class Comment < Ohm::Model
|
42
|
+
attribute :body
|
43
|
+
end
|
44
|
+
|
45
|
+
# Now let's require the test framework we're going to use called
|
46
|
+
# [cutest](http://github.com/djanowski/cutest)
|
47
|
+
require "cutest"
|
48
|
+
|
49
|
+
# And make sure that every run of our test suite has a clean Redis instance.
|
50
|
+
prepare { Ohm.flush }
|
51
|
+
|
52
|
+
# Let's begin testing. The important thing to verify is that
|
53
|
+
# video comments and audio comments don't munge with each other.
|
54
|
+
#
|
55
|
+
# We can see that they don't since each of the `comments` list only has
|
56
|
+
# one element.
|
57
|
+
test "adding all sorts of comments" do
|
58
|
+
video = Video.create
|
59
|
+
|
60
|
+
video_comment = Comment.create(:body => "First Video Comment")
|
61
|
+
video.comments.push(video_comment)
|
62
|
+
|
63
|
+
audio = Audio.create
|
64
|
+
audio_comment = Comment.create(:body => "First Audio Comment")
|
65
|
+
audio.comments.push(audio_comment)
|
66
|
+
|
67
|
+
assert video.comments.include?(video_comment)
|
68
|
+
assert_equal video.comments.size, 1
|
69
|
+
|
70
|
+
assert audio.comments.include?(audio_comment)
|
71
|
+
assert_equal audio.comments.size, 1
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
#### Discussion
|
76
|
+
#
|
77
|
+
# As you can see above, the design is very simple, and leaves little to be
|
78
|
+
# desired.
|
79
|
+
|
80
|
+
# Latest first ordering can simply be achieved by using `unshift` instead of
|
81
|
+
# `push`.
|
82
|
+
test "latest first ordering" do
|
83
|
+
video = Video.create
|
84
|
+
|
85
|
+
first = Comment.create(:body => "First")
|
86
|
+
second = Comment.create(:body => "Second")
|
87
|
+
|
88
|
+
video.comments.unshift(first)
|
89
|
+
video.comments.unshift(second)
|
90
|
+
|
91
|
+
assert [second, first] == video.comments.to_a
|
92
|
+
end
|
93
|
+
|
94
|
+
# In addition, since Lists are optimized for doing `LRANGE` operations,
|
95
|
+
# pagination of Comments would be very fast compared to doing a LIMIT / OFFSET
|
96
|
+
# query in SQL (some sites also use `WHERE id > ? LIMIT N` and pass the
|
97
|
+
# previous last ID in the set).
|
98
|
+
test "getting paged chunks of comments" do
|
99
|
+
video = Video.create
|
100
|
+
|
101
|
+
20.times { |i| video.comments.push(Comment.create(:body => "C#{i + 1}")) }
|
102
|
+
|
103
|
+
assert_equal %w(C1 C2 C3 C4 C5), video.comments.range(0, 4).map(&:body)
|
104
|
+
assert_equal %w(C6 C7 C8 C9 C10), video.comments.range(5, 9).map(&:body)
|
105
|
+
end
|
106
|
+
|
107
|
+
#### Caveats
|
108
|
+
|
109
|
+
# Sometimes you need to be able to delete comments. For these cases, you might
|
110
|
+
# possibly need to store a reference back to the parent entity. Also, if you
|
111
|
+
# expect to store millions of comments for a single entity, it might be tricky
|
112
|
+
# to delete comments, as you need to manually loop through the entire LIST.
|
113
|
+
#
|
114
|
+
# Luckily, there is a clean alternative solution, which would be to use a
|
115
|
+
# `SORTED SET`, and to use the timestamp (or the negative of the timestamp) as
|
116
|
+
# the score to maintain the desired order. Deleting a comment from a
|
117
|
+
# `SORTED SET` would be a simple
|
118
|
+
# [ZREM](http://redis.io/commands/zrem) call.
|
@@ -0,0 +1,137 @@
|
|
1
|
+
### Internals: Nest and the Ohm Philosophy
|
2
|
+
|
3
|
+
#### Ohm does not want to hide Redis from you
|
4
|
+
|
5
|
+
# In contrast to the usual philosophy of ORMs in the wild, Ohm actually
|
6
|
+
# just provides a basic object mapping where you can safely tuck away
|
7
|
+
# attributes and declare grouping of data.
|
8
|
+
#
|
9
|
+
# Beyond that, Ohm doesn't try to hide Redis, but rather exposes it in
|
10
|
+
# a simple way, through key hierarchies provided by the library
|
11
|
+
# [Nido](http://github.com/soveran/nido).
|
12
|
+
|
13
|
+
# Let's require `Ohm`. We also require `Ohm::Contrib` so we can make
|
14
|
+
# use of its module `Ohm::Callbacks`.
|
15
|
+
require "ohm"
|
16
|
+
require "ohm/contrib"
|
17
|
+
|
18
|
+
# Let's quickly declare our `Post` model and include `Ohm::Callbacks`.
|
19
|
+
# We define an *attribute* `title` and also *index* it.
|
20
|
+
#
|
21
|
+
# In addition we specify our `Post` to have a list of *comments*.
|
22
|
+
class Post < Ohm::Model
|
23
|
+
include Ohm::Callbacks
|
24
|
+
|
25
|
+
attribute :title
|
26
|
+
index :title
|
27
|
+
|
28
|
+
list :comments, :Comment
|
29
|
+
|
30
|
+
# This is one example of using Redic simple API and
|
31
|
+
# the underlying library `Nido`.
|
32
|
+
def self.latest
|
33
|
+
fetch(redis.call("ZRANGE", key[:latest], 0, -1))
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Here we just quickly push this instance of `Post` to our `latest`
|
39
|
+
# *SORTED SET*. We use the current time as the score.
|
40
|
+
def after_save
|
41
|
+
redis.call("ZADD", model.key[:latest], Time.now.to_i, id)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Since we add every `Post` to our *SORTED SET*, we have to make sure that
|
45
|
+
# we removed it from our `latest` *SORTED SET* as soon as we delete a
|
46
|
+
# `Post`.
|
47
|
+
def after_delete
|
48
|
+
redis.call("ZREM", model.key[:latest], id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Now let's quickly define our `Comment` model.
|
53
|
+
class Comment < Ohm::Model
|
54
|
+
end
|
55
|
+
|
56
|
+
#### Test it out
|
57
|
+
|
58
|
+
# For this example, we'll use [Cutest](http://github.com/djanowski/cutest)
|
59
|
+
# for our testing framework.
|
60
|
+
require "cutest"
|
61
|
+
|
62
|
+
# To make it simple, we also ensure that every test run has a clean
|
63
|
+
# *Redis* instance.
|
64
|
+
prepare do
|
65
|
+
Ohm.flush
|
66
|
+
end
|
67
|
+
|
68
|
+
# Now let's create a post. `Cutest` by default yields the return value of the
|
69
|
+
# block to each and every one of the test blocks.
|
70
|
+
setup do
|
71
|
+
Post.create
|
72
|
+
end
|
73
|
+
|
74
|
+
# We then verify the behavior for our `Post:latest` ZSET. Our created
|
75
|
+
# post should automatically be part of `Post:latest`.
|
76
|
+
test "created post is inserted into latest" do |post|
|
77
|
+
assert [post.id] == Post.latest.map(&:id)
|
78
|
+
end
|
79
|
+
|
80
|
+
# And it should automatically be removed from it as soon as we delete our
|
81
|
+
# `Post`.
|
82
|
+
test "deleting the created post removes it from latest" do |post|
|
83
|
+
post.delete
|
84
|
+
|
85
|
+
assert Post.latest.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
# You might be curious what happens when we do `Post.all`. The test here
|
89
|
+
# demonstrates more or less what's happening when you do that.
|
90
|
+
test "querying Post:all using raw Redis commands" do |post|
|
91
|
+
assert [post.id] == Post.all.ids
|
92
|
+
assert [post] == Post.all.to_a
|
93
|
+
end
|
94
|
+
|
95
|
+
#### Understanding `post.comments`.
|
96
|
+
|
97
|
+
# Let's pop the hood and see how we can do *LIST* operations on our
|
98
|
+
# `post.comments` object.
|
99
|
+
|
100
|
+
# Getting the current size of our comments is just a wrapper for
|
101
|
+
# [LLEN](http://redis.io/commands/LLEN).
|
102
|
+
test "checking the number of comments for a given post" do |post|
|
103
|
+
assert_equal 0, post.comments.size
|
104
|
+
assert_equal 0, Post.redis.call("LLEN", post.comments.key)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Also, pushing a comment to our `post.comments` object is equivalent
|
108
|
+
# to doing an [RPUSH](http://redis.io/commands/RPUSH) of its `id`.
|
109
|
+
test "pushing a comment manually and checking for its presence" do |post|
|
110
|
+
comment = Comment.create
|
111
|
+
|
112
|
+
Post.redis.call("RPUSH", post.comments.key, comment.id)
|
113
|
+
assert_equal comment, post.comments.last
|
114
|
+
|
115
|
+
post.comments.push(comment)
|
116
|
+
assert_equal comment, post.comments.last
|
117
|
+
end
|
118
|
+
|
119
|
+
# Now for some interesting judo.
|
120
|
+
test "now what if we want to find all Ohm or Redis posts" do
|
121
|
+
ohm = Post.create(title: "Ohm")
|
122
|
+
redis = Post.create(title: "Redis")
|
123
|
+
|
124
|
+
# Finding all *Ohm* or *Redis* posts now will just be a call to
|
125
|
+
# [SUNIONSTORE](http://redis.io/commands/UNIONSTORE).
|
126
|
+
result = Post.find(title: "Ohm").union(title: "Redis")
|
127
|
+
|
128
|
+
# And voila, they have been found!
|
129
|
+
assert_equal [ohm, redis], result.to_a
|
130
|
+
end
|
131
|
+
|
132
|
+
#### The command reference is your friend
|
133
|
+
|
134
|
+
# If you invest a little time reading through all the different
|
135
|
+
# [Redis commands](http://redis.io/commands).
|
136
|
+
# I'm pretty sure you will enjoy your experience hacking with Ohm, Nido,
|
137
|
+
# Redic and Redis a lot more.
|
@@ -0,0 +1,179 @@
|
|
1
|
+
### Understanding Ohm Internals through Logging
|
2
|
+
|
3
|
+
#### Benefits
|
4
|
+
|
5
|
+
# Ohm is actually a very thin wrapper for Redis, and most of the design
|
6
|
+
# patterns implemented in Ohm are based on the prescribed patterns for using
|
7
|
+
# Redis.
|
8
|
+
#
|
9
|
+
# If you take a little time to grok the internals of Ohm and Redis, the more
|
10
|
+
# effective you will be in weilding it efficiently.
|
11
|
+
|
12
|
+
#### Keys
|
13
|
+
#
|
14
|
+
# Most key-value stores have standardized on a structured design pattern for
|
15
|
+
# organizing data. Let's fire up a console:
|
16
|
+
#
|
17
|
+
# >> irb -r ohm -r logger
|
18
|
+
#
|
19
|
+
# Ohm.redis.client.logger = Logger.new(STDOUT)
|
20
|
+
#
|
21
|
+
# class Post < Ohm::Model
|
22
|
+
# attribute :title
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Post.create(:title => "Grokking Ohm")
|
26
|
+
#
|
27
|
+
# After executing `Post.create(...)`, you'll see a lot of output from the
|
28
|
+
# logger. Let's go through every line in detail.
|
29
|
+
|
30
|
+
#### Post.create
|
31
|
+
|
32
|
+
# *Generate a new `Post:id`.*
|
33
|
+
INCR Post:id
|
34
|
+
|
35
|
+
# *Lock `Post:2`
|
36
|
+
# (see [SETNX](http://code.google.com/p/redis/wiki/SetnxCommand)
|
37
|
+
# for an explanation of the locking design pattern).
|
38
|
+
SETNX Post:2:_lock 1285060009.409451
|
39
|
+
|
40
|
+
# *Add the newly generated ID 2 to the `Post:all` SET.*
|
41
|
+
SADD Post:all 2
|
42
|
+
|
43
|
+
# *Start transaction.*
|
44
|
+
MULTI
|
45
|
+
|
46
|
+
# *Delete HASH `Post:2`.*
|
47
|
+
DEL Post:2
|
48
|
+
|
49
|
+
# *Set the attributes.*
|
50
|
+
HMSET Post:2 title "Grokking Ohm"
|
51
|
+
|
52
|
+
# *End transaction.*
|
53
|
+
EXEC
|
54
|
+
|
55
|
+
# *Release the lock.*
|
56
|
+
DEL Post:2:_lock
|
57
|
+
|
58
|
+
#### Sets and Lists
|
59
|
+
|
60
|
+
# Let's do a little `SET` and `LIST` manipulation and see what Ohm does.
|
61
|
+
# Here's the code for this section:
|
62
|
+
#
|
63
|
+
# class User < Ohm::Model
|
64
|
+
# collection :posts, Post
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class Post < Ohm::Model
|
68
|
+
# attribute :title
|
69
|
+
# reference :user, User
|
70
|
+
#
|
71
|
+
# list :comments, Comment
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# class Comment < Ohm::Model
|
75
|
+
# end
|
76
|
+
|
77
|
+
#### User.create
|
78
|
+
|
79
|
+
# *Generate a new ID for the `User`.*
|
80
|
+
INCR User:id
|
81
|
+
|
82
|
+
# *Lock User:9.*
|
83
|
+
SETNX User:9:_lock 1285061032.174834
|
84
|
+
|
85
|
+
# *Add this new user to the SET User:all.*
|
86
|
+
SADD User:all 9
|
87
|
+
|
88
|
+
# *Release the lock.*
|
89
|
+
DEL User:9:_lock
|
90
|
+
|
91
|
+
#### Post.create(:title => "Foo", :user => User.create)
|
92
|
+
|
93
|
+
# *Generate an ID for this Post*
|
94
|
+
INCR Post:id
|
95
|
+
|
96
|
+
# *Lock Post:3*
|
97
|
+
SETNX Post:3:_lock 1285061032.180314
|
98
|
+
|
99
|
+
# *Add the ID 3 to the SET Post:all*
|
100
|
+
SADD Post:all 3
|
101
|
+
|
102
|
+
# *Start transaction*
|
103
|
+
MULTI
|
104
|
+
|
105
|
+
# *Remove existing Post:3 HASH*
|
106
|
+
DEL Post:3
|
107
|
+
|
108
|
+
# *Assign the attributes to Post:3*
|
109
|
+
HMSET Post:3 title Foo user_id 9
|
110
|
+
|
111
|
+
# *End transaction*
|
112
|
+
EXEC
|
113
|
+
|
114
|
+
# *Add the ID of this post to the SET index (more on this below)*
|
115
|
+
SADD Post:user_id:OQ== 3
|
116
|
+
|
117
|
+
# *Book keeping for Post:3 indices*
|
118
|
+
SADD Post:3:_indices Post:user_id:OQ==
|
119
|
+
|
120
|
+
# *Release lock*
|
121
|
+
DEL Post:3:_lock
|
122
|
+
|
123
|
+
#### post.comments << Comment.create
|
124
|
+
|
125
|
+
# *Generate an ID for this comment*
|
126
|
+
INCR Comment:id
|
127
|
+
|
128
|
+
# *Lock Comment:1*
|
129
|
+
SETNX Comment:1:_lock 1285061034.335855
|
130
|
+
|
131
|
+
# *Add this comment to the SET Comment:all*
|
132
|
+
SADD Comment:all 1
|
133
|
+
|
134
|
+
# *Release lock*
|
135
|
+
DEL Comment:1:_lock
|
136
|
+
|
137
|
+
# *Append the comment to the LIST Post:3:comments*
|
138
|
+
RPUSH Post:3:comments 1
|
139
|
+
|
140
|
+
#### Understanding indexes
|
141
|
+
|
142
|
+
# reference :user, User
|
143
|
+
#
|
144
|
+
# is actually more or less the same as:
|
145
|
+
#
|
146
|
+
# attribute :user_id
|
147
|
+
# index :user_id
|
148
|
+
#
|
149
|
+
# def user
|
150
|
+
# User[user_id]
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# def user_id=(user_id)
|
154
|
+
# self.user_id = user_id
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# To further explain the [example above](#section-29), let's
|
158
|
+
# run through the commands that was issued. Remember that
|
159
|
+
# we had a *user_id* of *9* and a *post_id* of *3*.
|
160
|
+
|
161
|
+
# *OQ== is Base64 of *9*, with newlines removed. The post_id 3 is added
|
162
|
+
# effectively to Post:user_id:9, if we ignore the encoding.*
|
163
|
+
SADD Post:user_id:OQ== 3
|
164
|
+
|
165
|
+
# *Just keep track that Post:user_id:OQ== was created*
|
166
|
+
SADD Post:3:_indices Post:user_id:OQ==
|
167
|
+
|
168
|
+
# *Get all *posts* with user_id *9**
|
169
|
+
SMEMBERS Post:user_id:OQ==
|
170
|
+
|
171
|
+
#### What's next?
|
172
|
+
|
173
|
+
# We tackled a fair bit amount regarding Ohm internals. More or less this is
|
174
|
+
# the foundation of everything else found in Ohm, and other code are just
|
175
|
+
# built around the same fundamental concepts.
|
176
|
+
#
|
177
|
+
# If there's anything else you want to know, you can hang out on #ohm at
|
178
|
+
# irc.freenode.net, or you can visit the
|
179
|
+
# [Ohm google group](http://groups.google.com/group/ohm-ruby).
|
data/examples/slug.rb
ADDED
@@ -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.
|