ohm_util 0.1

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