sohm 0.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,162 @@
1
+ ### Chaining Ohm Sets
2
+
3
+ #### Doing the straight forward approach
4
+
5
+ # Let's design our example around the following requirements:
6
+ #
7
+ # 1. a `User` has many orders.
8
+ # 2. an `Order` can be pending, authorized or captured.
9
+ # 3. a `Product` is referenced by an `Order`.
10
+
11
+ #### Doing it the normal way
12
+
13
+ # Let's first require `Ohm`.
14
+ require "ohm"
15
+
16
+ # A `User` has a `collection` of *orders*. Note that a collection
17
+ # is actually just a convenience, which implemented simply will look like:
18
+ #
19
+ # def orders
20
+ # Order.find(user_id: self.id)
21
+ # end
22
+ #
23
+ class User < Ohm::Model
24
+ collection :orders, :Order
25
+ end
26
+
27
+ # The product for our purposes will only contain a name.
28
+ class Product < Ohm::Model
29
+ attribute :name
30
+ end
31
+
32
+ # We define an `Order` with just a single `attribute` called `state`, and
33
+ # also add an `index` so we can search an order given its state.
34
+ #
35
+ # The `reference` to the `User` is actually required for the `collection`
36
+ # of *orders* in the `User` declared above, because the `reference` defines
37
+ # an index called `:user_id`.
38
+ #
39
+ # We also define a `reference` to a `Product`.
40
+ class Order < Ohm::Model
41
+ attribute :state
42
+ index :state
43
+
44
+ reference :user, User
45
+ reference :product, Product
46
+ end
47
+
48
+ ##### Testing what we have so far.
49
+
50
+ # For the purposes of this tutorial, we'll use cutest for our test framework.
51
+ require "cutest"
52
+
53
+ # Make sure that every run of our test suite has a clean Redis instance.
54
+ prepare { Ohm.flush }
55
+
56
+ # Let's create a *user*, a *pending*, *authorized* and a captured order.
57
+ # We also create two products named *iPod* and *iPad*.
58
+ setup do
59
+ @user = User.create
60
+
61
+ @ipod = Product.create(name: "iPod")
62
+ @ipad = Product.create(name: "iPad")
63
+
64
+ @pending = Order.create(user: @user, state: "pending",
65
+ product: @ipod)
66
+ @authorized = Order.create(user: @user, state: "authorized",
67
+ product: @ipad)
68
+ @captured = Order.create(user: @user, state: "captured",
69
+ product: @ipad)
70
+ end
71
+
72
+ # Now let's try and grab all pending orders, and also pending
73
+ # *iPad* and *iPod* ones.
74
+ test "finding pending orders" do
75
+ assert @user.orders.find(state: "pending").include?(@pending)
76
+
77
+ assert @user.orders.find(state: "pending",
78
+ product_id: @ipod.id).include?(@pending)
79
+
80
+ assert @user.orders.find(state: "pending",
81
+ product_id: @ipad.id).empty?
82
+ end
83
+
84
+ # Now we try and find captured and/or authorized orders.
85
+ test "finding authorized and/or captured orders" do
86
+ assert @user.orders.find(state: "authorized").include?(@authorized)
87
+ assert @user.orders.find(state: "captured").include?(@captured)
88
+
89
+ results = @user.orders.find(state: "authorized").union(state: "captured")
90
+
91
+ assert results.include?(@authorized)
92
+ assert results.include?(@captured)
93
+ end
94
+
95
+ #### Creating shortcuts
96
+
97
+ # You can of course define methods to make that code more readable.
98
+ class User < Ohm::Model
99
+ def authorized_orders
100
+ orders.find(state: "authorized")
101
+ end
102
+
103
+ def captured_orders
104
+ orders.find(state: "captured")
105
+ end
106
+ end
107
+
108
+ # And we can now test these new methods.
109
+ test "finding authorized and/or captured orders" do
110
+ assert @user.authorized_orders.include?(@authorized)
111
+ assert @user.captured_orders.include?(@captured)
112
+ end
113
+
114
+ # In most cases this is fine, but if you want to have a little fun,
115
+ # then we can play around with some chainability using scopes.
116
+
117
+ # Let's require `ohm-contrib`, which we will be using for scopes.
118
+ require "ohm/contrib"
119
+
120
+ # Include `Ohm::Scope` module and desired scopes.
121
+ class Order
122
+ include Ohm::Scope
123
+
124
+ scope do
125
+ def pending
126
+ find(state: "pending")
127
+ end
128
+
129
+ def authorized
130
+ find(state: "authorized")
131
+ end
132
+
133
+ def captured
134
+ find(state: "captured")
135
+ end
136
+
137
+ def accepted
138
+ find(state: "authorized").union(state: "captured")
139
+ end
140
+ end
141
+ end
142
+
143
+ # Ok! Let's put all of that chaining code to good use.
144
+ test "finding pending orders using a chainable style" do
145
+ assert @user.orders.pending.include?(@pending)
146
+ assert @user.orders.pending.find(product_id: @ipod.id).include?(@pending)
147
+
148
+ assert @user.orders.pending.find(product_id: @ipad.id).empty?
149
+ end
150
+
151
+ test "finding authorized and/or captured orders using a chainable style" do
152
+ assert @user.orders.authorized.include?(@authorized)
153
+ assert @user.orders.captured.include?(@captured)
154
+
155
+ assert @user.orders.accepted.include?(@authorized)
156
+ assert @user.orders.accepted.include?(@captured)
157
+
158
+ accepted = @user.orders.accepted
159
+
160
+ assert accepted.find(product_id: @ipad.id).include?(@authorized)
161
+ assert accepted.find(product_id: @ipad.id).include?(@captured)
162
+ end
@@ -0,0 +1,75 @@
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
+ # on get, "comments" do
9
+ # res.write Comment.all.to_json
10
+ # end
11
+ #
12
+ # `Ohm` helps you here by providing sensible defaults. It's not very popular,
13
+ # but `Ohm` actually has a `to_hash` method.
14
+
15
+ # Let's start by requiring `ohm` and `ohm/json`.
16
+ require "ohm"
17
+ require "ohm/json"
18
+
19
+ # Here we define our `Post` model with just a single `attribute` called `title`.
20
+ class Post < Ohm::Model
21
+ attribute :title
22
+ end
23
+
24
+ # Now let's load the test framework `cutest` to test our code.
25
+ require "cutest"
26
+
27
+ # We also call `Ohm.flush` for each test run.
28
+ prepare { Ohm.flush }
29
+
30
+ # When we successfully create a `Post`, we can see that it returns
31
+ # only the *id* and its value in the hash.
32
+ test "hash representation when created" do
33
+ post = Post.create(title: "my post")
34
+
35
+ assert_equal Hash[id: post.id], post.to_hash
36
+ end
37
+
38
+ # The JSON representation is actually just `post.to_hash.to_json`, so the
39
+ # same result, only in JSON, is returned.
40
+ test "json representation when created" do
41
+ post = Post.create(title: "my post")
42
+
43
+ assert_equal "{\"id\":\"#{post.id}\"}", post.to_json
44
+ end
45
+
46
+ #### Whitelisted approach
47
+
48
+ # Unlike other frameworks which dumps out all attributes by default,
49
+ # `Ohm` favors a whitelisted approach where you have to explicitly
50
+ # declare which attributes you want.
51
+ #
52
+ # By default, only `:id` will be available if the model is persisted.
53
+
54
+ # Let's re-open our Post class, and add a `to_hash` method.
55
+ class Post
56
+ def to_hash
57
+ super.merge(title: title)
58
+ end
59
+ end
60
+
61
+ # Now, let's test that the title is in fact part of `to_hash`.
62
+ test "customized to_hash" do
63
+ post = Post.create(title: "Override FTW?")
64
+
65
+ assert_equal Hash[id: post.id, title: post.title], post.to_hash
66
+ end
67
+
68
+ #### Conclusion
69
+
70
+ # Ohm has a lot of neat intricacies like this. Some of the things to keep
71
+ # in mind from this tutorial would be:
72
+ #
73
+ # 1. `Ohm` doesn't assume too much about your needs.
74
+ # 2. If you need a customized version, you can always define it yourself.
75
+ # 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,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).