ohm_util 0.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,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.