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,102 @@
1
+ ### Make Peace wih JSON and Hash
2
+
3
+ #### Why do I care?
4
+
5
+ # If you've ever needed to build an AJAX route handler, you may have noticed
6
+ # the prevalence of the design pattern where you return a JSON response.
7
+ #
8
+ # post "/comments.json" do
9
+ # comment = Comment.create(params[:comment])
10
+ # comment.to_json
11
+ # end
12
+ #
13
+ # `Ohm` helps you here by providing sensible defaults. It's not very popular,
14
+ # but `Ohm` actually has a `to_hash` method.
15
+
16
+ # Let's start by requiring `ohm` and `json`. In ruby 1.9, `json` is
17
+ # actually part of the standard library, so you don't have to install a gem
18
+ # for it. For ruby 1.8.x, a simple `[sudo] gem install json` will do it.
19
+ require "ohm"
20
+ require "json"
21
+
22
+ # Here we define our `Post` model with just a single `attribute` called
23
+ # `title`.
24
+ #
25
+ # We also define a validation, asserting the presence of the `title`.
26
+ class Post < Ohm::Model
27
+ attribute :title
28
+
29
+ def validate
30
+ assert_present :title
31
+ end
32
+ end
33
+
34
+ # Now let's load the test framework `cutest` to verify our code. We
35
+ # also call `Ohm.flush` for each test run.
36
+ require "cutest"
37
+
38
+ prepare { Ohm.flush }
39
+
40
+ # When we successfully create a `Post`, we can see that it returns
41
+ # only the *id* and its value in the hash.
42
+ test "hash representation when created" do
43
+ post = Post.create(:title => "my post")
44
+
45
+ assert({ :id => "1" } == post.to_hash)
46
+ end
47
+
48
+ # The JSON representation is actually just `post.to_hash.to_json`, so the
49
+ # same result, only in JSON, is returned.
50
+ test "json representation when created" do
51
+ post = Post.create(:title => "my post")
52
+
53
+ assert("{\"id\":\"1\"}" == post.to_json)
54
+ end
55
+
56
+ # Let's try and do the opposite now -- that is, purposely try and create
57
+ # an invalid `Post`. We can see that it returns the `errors` of the
58
+ # `Post`, because we added an `assert_present :title` in our code above.
59
+ test "hash representation when validation failed" do
60
+ post = Post.create
61
+
62
+ assert({ :errors => [[:title, :not_present]]} == post.to_hash)
63
+ end
64
+
65
+ # As is the case for a valid record, the JSON representation is
66
+ # still equivalent to `post.to_hash.to_json`.
67
+ test "json representation when validation failed" do
68
+ post = Post.create
69
+
70
+ assert("{\"errors\":[[\"title\",\"not_present\"]]}" == post.to_json)
71
+ end
72
+
73
+ #### Whitelisted approach
74
+
75
+ # Unlike in other frameworks which dumps out all attributes by default,
76
+ # `Ohm` favors a whitelisted approach where you have to explicitly
77
+ # declare which attributes you want.
78
+ #
79
+ # By default, only `:id` and `:errors` will be available, depending if
80
+ # it was successfully saved or if there were validation errors.
81
+
82
+ # Let's re-open our Post class, and add a `to_hash` method.
83
+ class Post
84
+ def to_hash
85
+ super.merge(:title => title)
86
+ end
87
+ end
88
+
89
+ # Now, let's test that the title is in fact part of `to_hash`.
90
+ test "customized to_hash" do
91
+ post = Post.create(:title => "Override FTW?")
92
+ assert({ :id => "1", :title => "Override FTW?" } == post.to_hash)
93
+ end
94
+
95
+ #### Conclusion
96
+
97
+ # Ohm has a lot of neat intricacies like this. Some of the things to keep
98
+ # in mind from this tutorial would be:
99
+ #
100
+ # 1. `Ohm` doesn't assume too much about your needs.
101
+ # 2. If you need a customized version, you can always define it yourself.
102
+ # 3. Customization is easy using basic OOP principles.
@@ -0,0 +1,124 @@
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 video.comments.size == 1
69
+
70
+ assert audio.comments.include?(audio_comment)
71
+ assert 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 %w(C1 C2 C3 C4 C5) == video.comments[0, 4].map(&:body)
104
+ assert %w(C6 C7 C8 C9 C10) == video.comments[5, 9].map(&:body)
105
+
106
+ # ** Range style is also supported.
107
+ assert %w(C11 C12 C13 C14 C15) == video.comments[10..14].map(&:body)
108
+
109
+ # ** Also you can just pass in a single number.
110
+ assert "C16" == video.comments[15].body
111
+ end
112
+
113
+ #### Caveats
114
+
115
+ # Sometimes you need to be able to delete comments. For these cases, you might
116
+ # possibly need to store a reference back to the parent entity. Also, if you
117
+ # expect to store millions of comments for a single entity, it might be tricky
118
+ # to delete comments, as you need to manually loop through the entire LIST.
119
+ #
120
+ # Luckily, there is a clean alternative solution, which would be to use a
121
+ # `SORTED SET`, and to use the timestamp (or the negative of the timestamp) as
122
+ # the score to maintain the desired order. Deleting a comment from a
123
+ # `SORTED SET` would be a simple
124
+ # [ZREM](http://code.google.com/p/redis/wiki/ZremCommand) call.
@@ -0,0 +1,149 @@
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
+ # [Nest](http://github.com/soveran/nest).
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 the underlying library `Nest` directly.
31
+ # As you can see, we can easily drop down to using raw *Redis* commands,
32
+ # in this case we use
33
+ # [ZREVRANGE](http://code.google.com/p/redis/wiki/ZrangeCommand).
34
+ #
35
+ # *Note:* Since `Ohm::Model` defines a `to_proc`, we can use the `&` syntax
36
+ # together with `map` to make our code a little more terse.
37
+ def self.latest
38
+ key[:latest].zrevrange(0, -1).map(&Post)
39
+ end
40
+
41
+ # Here we just quickly push this instance of `Post` to our `latest`
42
+ # *SORTED SET*. We use the current time as the score.
43
+ protected
44
+ def after_save
45
+ self.class.key[:latest].zadd(Time.now.to_i, id)
46
+ end
47
+
48
+ # Since we add every `Post` to our *SORTED SET*, we have to make sure that
49
+ # we removed it from our `latest` *SORTED SET* as soon as we delete a
50
+ # `Post`.
51
+ #
52
+ # In this case we use the raw *Redis* command
53
+ # [ZREM](http://code.google.com/p/redis/wiki/ZremCommand).
54
+ def after_delete
55
+ self.class.key[:latest].zrem(id)
56
+ end
57
+ end
58
+
59
+ # Now let's quickly define our `Comment` model.
60
+ class Comment < Ohm::Model
61
+ end
62
+
63
+ #### Test it out
64
+
65
+ # For this example, we'll use [Cutest](http://github.com/djanowski/cutest)
66
+ # for our testing framework.
67
+ require "cutest"
68
+
69
+ # To make it simple, we also ensure that every test run has a clean
70
+ # *Redis* instance.
71
+ prepare { Ohm.flush }
72
+
73
+ # Now let's create Post. `Cutest` by default yields the return value of the
74
+ # block to each and every one of the test blocks.
75
+ setup { Post.create }
76
+
77
+ # We then verify the behavior for our `Post:latest` ZSET. Our created
78
+ # post should automatically be part of `Post:latest`.
79
+ test "created post is inserted into latest" do |p|
80
+ assert [p.id] == Post.key[:latest].zrange(0, -1)
81
+ end
82
+
83
+ # And it should automatically be removed from it as soon as we delete our
84
+ # `Post`.
85
+ test "deleting the created post removes it from latest" do |p|
86
+ p.delete
87
+
88
+ assert Post.key[:latest].zrange(0, -1).empty?
89
+ end
90
+
91
+ # You might be curious what happens when we do `Post.all`. The test here
92
+ # demonstrates more or less what's happening when you do that.
93
+ test "querying Post:all using raw Redis commands" do |p|
94
+ assert [p.id] == Post.key[:all].smembers
95
+
96
+ assert [p] == Post.key[:all].smembers.map(&Post)
97
+ end
98
+
99
+ #### Understanding `post.comments`.
100
+
101
+ # Let's pop the hood and see how we can do *LIST* operations on our
102
+ # `post.comments` object.
103
+
104
+ # Getting the current size of our comments is just a wrapper for
105
+ # [LLEN](http://code.google.com/p/redis/wiki/LlenCommand).
106
+ test "checking the number of comments for a given post" do |p|
107
+ assert 0 == p.comments.key.llen
108
+ assert 0 == p.comments.size
109
+ end
110
+
111
+ # Also, pushing a comment to our `post.comments` object is equivalent
112
+ # to doing an [RPUSH](http://code.google.com/p/redis/wiki/RpushCommand)
113
+ # of its `id`.
114
+ test "pushing a Comment manually and checking for its presence" do |p|
115
+ comment = Comment.create
116
+
117
+ p.comments.key.rpush(comment.id)
118
+ assert [comment.id] == p.comments.key.lrange(0, -1)
119
+ end
120
+
121
+ # Now for some interesting judo
122
+ test "now what if we want to find all Ohm or Redis posts" do
123
+ ohm = Post.create(:title => "Ohm")
124
+ redis = Post.create(:title => "Redis")
125
+
126
+ # Let's first choose an arbitrary key name to hold our `Set`.
127
+ ohm_redis = Post.key.volatile["ohm-redis"]
128
+
129
+ # A *volatile* key just simply means it will be prefixed with a `~`.
130
+ assert "~:Post:ohm-redis" == ohm_redis
131
+
132
+ # Finding all *Ohm* or *Redis* posts now will just be a call to
133
+ # [SUNIONSTORE](http://code.google.com/p/redis/wiki/SunionstoreCommand)
134
+ # on our *volatile* `ohm-redis` key.
135
+ ohm_redis.sunionstore(
136
+ Post.index_key_for(:title, "Ohm"),
137
+ Post.index_key_for(:title, "Redis")
138
+ )
139
+
140
+ # And voila, they have been found!
141
+ assert [ohm.id, redis.id] == ohm_redis.smembers.sort
142
+ end
143
+
144
+ #### The command reference is your friend
145
+
146
+ # If you invest a little time reading through all the different
147
+ # [Redis commands](http://code.google.com/p/redis/wiki/CommandReference),
148
+ # I'm pretty sure you will enjoy your experience hacking with Ohm, Nest and
149
+ # 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).