ohm 1.3.0 → 1.3.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,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).