ohm 0.1.0.rc6 → 0.1.0
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.
- data/README.markdown +212 -26
- data/Rakefile +15 -0
- data/lib/ohm/key.rb +19 -1
- data/lib/ohm/validations.rb +116 -0
- data/lib/ohm/version.rb +1 -1
- data/lib/ohm.rb +1145 -86
- data/test/model_test.rb +8 -0
- metadata +6 -9
data/lib/ohm.rb
CHANGED
@@ -11,26 +11,97 @@ require File.join(File.dirname(__FILE__), "ohm", "key")
|
|
11
11
|
|
12
12
|
module Ohm
|
13
13
|
|
14
|
-
# Provides access to the
|
14
|
+
# Provides access to the _Redis_ database. It is highly recommended that you
|
15
|
+
# use this sparingly, and only if you really know what you're doing.
|
16
|
+
#
|
17
|
+
# The better way to access the _Redis_ database and do raw _Redis_
|
18
|
+
# commands would be one of the following:
|
19
|
+
#
|
20
|
+
# 1. Use {Ohm::Model.key} or {Ohm::Model#key}. So if the name of your
|
21
|
+
# model is *Post*, it would be *Post.key* or the protected method
|
22
|
+
# *#key* which should be used within your *Post* model.
|
23
|
+
#
|
24
|
+
# 2. Use {Ohm::Model.db} or {Ohm::Model#db}. Although this is also
|
25
|
+
# accessible, it is much cleaner and terse to use {Ohm::Model.key}.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
#
|
29
|
+
# class Post < Ohm::Model
|
30
|
+
# def comment_ids
|
31
|
+
# key[:comments].zrange(0, -1)
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# def add_comment_id(id)
|
35
|
+
# key[:comments].zadd(Time.now.to_i, id)
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# def remove_comment_id(id)
|
39
|
+
# # Let's use the db style here just to demonstrate.
|
40
|
+
# db.zrem key[:comments], id
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Post.key[:latest].sadd(1)
|
45
|
+
# Post.key[:latest].smembers == ["1"]
|
46
|
+
# # => true
|
47
|
+
#
|
48
|
+
# Post.key[:latest] == "Post:latest"
|
49
|
+
# # => true
|
50
|
+
#
|
51
|
+
# p = Post.create
|
52
|
+
# p.comment_ids == []
|
53
|
+
# # => true
|
54
|
+
#
|
55
|
+
# p.add_comment_id(101)
|
56
|
+
# p.comment_ids == ["101"]
|
57
|
+
# # => true
|
58
|
+
#
|
59
|
+
# p.remove_comment_id(101)
|
60
|
+
# p.comment_ids == []
|
61
|
+
# # => true
|
15
62
|
def self.redis
|
16
63
|
threaded[:redis] ||= connection(*options)
|
17
64
|
end
|
18
65
|
|
66
|
+
# Assign a new _Redis_ connection. Internally used by {Ohm.connect}
|
67
|
+
# to clear the cached _Redis_ instance.
|
68
|
+
#
|
69
|
+
# If you're looking to change the connection or reconnect with different
|
70
|
+
# parameters, try {Ohm.connect} or {Ohm::Model.connect}.
|
71
|
+
# @see connect
|
72
|
+
# @see Model.connect
|
73
|
+
# @param connection [Redis] an instance created using `Redis.new`.
|
19
74
|
def self.redis=(connection)
|
20
75
|
threaded[:redis] = connection
|
21
76
|
end
|
22
77
|
|
78
|
+
# @private Used internally by Ohm for thread safety.
|
23
79
|
def self.threaded
|
24
80
|
Thread.current[:ohm] ||= {}
|
25
81
|
end
|
26
82
|
|
27
|
-
# Connect to a
|
83
|
+
# Connect to a _Redis_ database.
|
84
|
+
#
|
85
|
+
# It is also worth mentioning that you can pass in a *URI* e.g.
|
86
|
+
#
|
87
|
+
# Ohm.connect :url => "redis://127.0.0.1:6379/0"
|
88
|
+
#
|
89
|
+
# Note that the value *0* refers to the database number for the given
|
90
|
+
# _Redis_ instance.
|
91
|
+
#
|
92
|
+
# Also you can use {Ohm.connect} without any arguments. The behavior will
|
93
|
+
# be as follows:
|
94
|
+
#
|
95
|
+
# # Connect to redis://127.0.0.1:6379/0
|
96
|
+
# Ohm.connect
|
97
|
+
#
|
98
|
+
# # Connect to redis://10.0.0.100:22222/5
|
99
|
+
# ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
|
100
|
+
# Ohm.connect
|
101
|
+
#
|
102
|
+
# @param options [{Symbol => #to_s}] An options hash.
|
103
|
+
# @see file:README.html#connecting Ohm.connect options documentation.
|
28
104
|
#
|
29
|
-
# @param options [Hash] options to create a message with.
|
30
|
-
# @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
|
31
|
-
# @option options [#to_s] :port (6379) Port number.
|
32
|
-
# @option options [#to_s] :db (0) Database number.
|
33
|
-
# @option options [#to_s] :timeout (0) Database timeout in seconds.
|
34
105
|
# @example Connect to a database in port 6380.
|
35
106
|
# Ohm.connect(:port => 6380)
|
36
107
|
def self.connect(*options)
|
@@ -38,29 +109,102 @@ module Ohm
|
|
38
109
|
@options = options
|
39
110
|
end
|
40
111
|
|
41
|
-
# Return a connection to Redis.
|
112
|
+
# @private Return a connection to Redis.
|
42
113
|
#
|
43
|
-
# This is a
|
114
|
+
# This is a wrapper around Redis.connect(options)
|
44
115
|
def self.connection(*options)
|
45
116
|
Redis.connect(*options)
|
46
117
|
end
|
47
118
|
|
119
|
+
# @private Stores the connection options for Ohm.redis.
|
48
120
|
def self.options
|
49
121
|
@options = [] unless defined? @options
|
50
122
|
@options
|
51
123
|
end
|
52
124
|
|
53
|
-
# Clear the database.
|
125
|
+
# Clear the database. You typically use this only during testing,
|
126
|
+
# or when you seed your site.
|
127
|
+
#
|
128
|
+
# @see http://code.google.com/p/redis/wiki/FlushdbCommand FLUSHDB in the
|
129
|
+
# Redis Command Reference.
|
54
130
|
def self.flush
|
55
131
|
redis.flushdb
|
56
132
|
end
|
57
133
|
|
134
|
+
# The base class of all *Ohm* errors. Can be used as a catch all for
|
135
|
+
# Ohm related errors.
|
58
136
|
class Error < StandardError; end
|
59
137
|
|
138
|
+
# This is the class that you need to extend in order to define your
|
139
|
+
# own models.
|
140
|
+
#
|
141
|
+
# Probably the most magic happening within {Ohm::Model} is the catching
|
142
|
+
# of {Ohm::Model.const_missing} exceptions to allow the use of constants
|
143
|
+
# even before they are defined.
|
144
|
+
#
|
145
|
+
# @example
|
146
|
+
#
|
147
|
+
# class Post < Ohm::Model
|
148
|
+
# reference :author, User # no User definition yet!
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# class User < Ohm::Model
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# @see Model.const_missing
|
60
155
|
class Model
|
61
156
|
|
62
157
|
# Wraps a model name for lazy evaluation.
|
63
158
|
class Wrapper < BasicObject
|
159
|
+
|
160
|
+
# Allows you to use a constant even before it is defined. This solves
|
161
|
+
# the issue of having to require inter-project dependencies in a very
|
162
|
+
# simple and "magic-free" manner.
|
163
|
+
#
|
164
|
+
# Example of how it was done before Wrapper existed:
|
165
|
+
#
|
166
|
+
# require "./app/models/user"
|
167
|
+
# require "./app/models/comment"
|
168
|
+
#
|
169
|
+
# class Post < Ohm::Model
|
170
|
+
# reference :author, User
|
171
|
+
# list :comments, Comment
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# Now, you can simply do the following:
|
175
|
+
# class Post < Ohm::Model
|
176
|
+
# reference :author, User
|
177
|
+
# list :comments, Comment
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# @example
|
181
|
+
#
|
182
|
+
# module Commenting
|
183
|
+
# def self.included(base)
|
184
|
+
# base.list :comments, Ohm::Model::Wrapper.new(:Comment) {
|
185
|
+
# Object.const_get(:Comment)
|
186
|
+
# }
|
187
|
+
# end
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# # In your classes:
|
191
|
+
# class Post < Ohm::Model
|
192
|
+
# include Commenting
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# class Comment < Ohm::Model
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# p = Post.create
|
199
|
+
# p.comments.empty?
|
200
|
+
# # => true
|
201
|
+
#
|
202
|
+
# p.comments.push(Comment.create)
|
203
|
+
# p.comments.size == 1
|
204
|
+
# # => true
|
205
|
+
#
|
206
|
+
# @param [Symbol, String] name Canonical name of wrapped class.
|
207
|
+
# @param [#to_proc] block Closure for getting the name of the constant.
|
64
208
|
def initialize(name, &block)
|
65
209
|
@name = name
|
66
210
|
@caller = ::Kernel.caller[2]
|
@@ -68,51 +212,134 @@ module Ohm
|
|
68
212
|
|
69
213
|
class << self
|
70
214
|
def method_missing(method_id, *args)
|
71
|
-
::Kernel.raise
|
215
|
+
::Kernel.raise(
|
216
|
+
::NoMethodError,
|
217
|
+
"You tried to call %s#%s, but %s is not defined on %s" % [
|
218
|
+
@name, method_id, @name, @caller
|
219
|
+
]
|
220
|
+
)
|
72
221
|
end
|
73
222
|
end
|
74
223
|
end
|
75
224
|
|
225
|
+
# Used as a convenience for wrapping an existing constant into a
|
226
|
+
# {Ohm::Model::Wrapper wrapper object}.
|
227
|
+
#
|
228
|
+
# This is used extensively within the library for points where a user
|
229
|
+
# defined class (e.g. _Post_, _User_, _Comment_) is expected.
|
230
|
+
#
|
231
|
+
# You can also use this if you need to do uncommon things, such as
|
232
|
+
# creating your own {Ohm::Model::Set Set}, {Ohm::Model::List List}, etc.
|
233
|
+
#
|
234
|
+
# (*NOTE:* Keep in mind that the following code is given only as an
|
235
|
+
# educational example, and is in no way prescribed as good design.)
|
236
|
+
#
|
237
|
+
# class User < Ohm::Model
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# User.create(:id => "1001")
|
241
|
+
#
|
242
|
+
# Ohm.redis.sadd("myset", 1001)
|
243
|
+
#
|
244
|
+
# key = Ohm::Key.new("myset", Ohm.redis)
|
245
|
+
# set = Ohm::Model::Set.new(key, Ohm::Model::Wrapper.wrap(User))
|
246
|
+
#
|
247
|
+
# [User[1001]] == set.all.to_a
|
248
|
+
# # => true
|
249
|
+
#
|
250
|
+
# @see http://ohm.keyvalue.org/tutorials/chaining Chaining Ohm Sets
|
76
251
|
def self.wrap(object)
|
77
252
|
object.class == self ? object : new(object.inspect) { object }
|
78
253
|
end
|
79
254
|
|
255
|
+
# Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
|
256
|
+
#
|
257
|
+
# @return [Class] The wrapped class.
|
80
258
|
def unwrap
|
81
259
|
@block.call
|
82
260
|
end
|
83
261
|
|
262
|
+
# Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
|
263
|
+
# to manually declare this.
|
264
|
+
#
|
265
|
+
# @return [Wrapper]
|
84
266
|
def class
|
85
267
|
Wrapper
|
86
268
|
end
|
87
269
|
|
270
|
+
# @return [String] A string describing this lazy object.
|
88
271
|
def inspect
|
89
272
|
"<Wrapper for #{@name} (in #{@caller})>"
|
90
273
|
end
|
91
274
|
end
|
92
275
|
|
276
|
+
# Defines the base implementation for all enumerable types in Ohm,
|
277
|
+
# which includes {Ohm::Model::Set Sets}, {Ohm::Model::List Lists} and
|
278
|
+
# {Ohm::Model::Index Indices}.
|
93
279
|
class Collection
|
94
280
|
include Enumerable
|
95
281
|
|
282
|
+
# An instance of {Ohm::Key}.
|
96
283
|
attr :key
|
284
|
+
|
285
|
+
# A subclass of {Ohm::Model}.
|
97
286
|
attr :model
|
98
287
|
|
288
|
+
# @param [Key] key A key which includes a _Redis_ connection.
|
289
|
+
# @param [Ohm::Model::Wrapper] model A wrapped subclass of {Ohm::Model}.
|
99
290
|
def initialize(key, model)
|
100
291
|
@key = key
|
101
292
|
@model = model.unwrap
|
102
293
|
end
|
103
294
|
|
295
|
+
# Adds an instance of {Ohm::Model} to this collection.
|
296
|
+
#
|
297
|
+
# @param [#id] model A model with an ID.
|
104
298
|
def add(model)
|
105
299
|
self << model
|
106
300
|
end
|
107
301
|
|
108
|
-
|
302
|
+
# Sort this collection using the ID by default, or an attribute defined
|
303
|
+
# in the elements of this collection.
|
304
|
+
#
|
305
|
+
# *NOTE:* It is worth mentioning that if you want to sort by a specific
|
306
|
+
# attribute instead of an ID, you would probably want to use
|
307
|
+
# {Ohm::Model::Collection#sort_by sort_by} instead.
|
308
|
+
#
|
309
|
+
# @example
|
310
|
+
# class Post < Ohm::Model
|
311
|
+
# attribute :title
|
312
|
+
# end
|
313
|
+
#
|
314
|
+
# p1 = Post.create(:title => "Alpha")
|
315
|
+
# p2 = Post.create(:title => "Beta")
|
316
|
+
# p3 = Post.create(:title => "Gamma")
|
317
|
+
#
|
318
|
+
# [p1, p2, p3] == Post.all.sort.to_a
|
319
|
+
# # => true
|
320
|
+
#
|
321
|
+
# [p3, p2, p1] == Post.all.sort(:order => "DESC").to_a
|
322
|
+
# # => true
|
323
|
+
#
|
324
|
+
# [p1, p2, p3] == Post.all.sort(:by => "Post:*->title",
|
325
|
+
# :order => "ASC ALPHA").to_a
|
326
|
+
# # => true
|
327
|
+
#
|
328
|
+
# [p3, p2, p1] == Post.all.sort(:by => "Post:*->title",
|
329
|
+
# :order => "DESC ALPHA").to_a
|
330
|
+
# # => true
|
331
|
+
#
|
332
|
+
# @see file:README.html#sorting Sorting in the README.
|
333
|
+
# @see http://code.google.com/p/redis/wiki/SortCommand SORT in the
|
334
|
+
# Redis Command Reference.
|
335
|
+
def sort(options = {})
|
109
336
|
return [] unless key.exists
|
110
337
|
|
111
|
-
|
112
|
-
|
113
|
-
|
338
|
+
opts = options.dup
|
339
|
+
opts[:start] ||= 0
|
340
|
+
opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
|
114
341
|
|
115
|
-
key.sort(
|
342
|
+
key.sort(opts).map(&model)
|
116
343
|
end
|
117
344
|
|
118
345
|
# Sort the model instances by the given attribute.
|
@@ -125,23 +352,73 @@ module Ohm
|
|
125
352
|
# user = User.all.sort_by(:name, :order => "ALPHA").first
|
126
353
|
# user.name == "A"
|
127
354
|
# # => true
|
128
|
-
|
355
|
+
#
|
356
|
+
# @see file:README.html#sorting Sorting in the README.
|
357
|
+
def sort_by(att, options = {})
|
129
358
|
return [] unless key.exists
|
130
359
|
|
131
|
-
|
132
|
-
|
360
|
+
opts = options.dup
|
361
|
+
opts.merge!(:by => model.key["*->#{att}"])
|
133
362
|
|
134
|
-
if
|
135
|
-
key.sort(
|
363
|
+
if opts[:get]
|
364
|
+
key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
|
136
365
|
else
|
137
|
-
sort(
|
366
|
+
sort(opts)
|
138
367
|
end
|
139
368
|
end
|
140
369
|
|
370
|
+
# Delete this collection.
|
371
|
+
#
|
372
|
+
# @example
|
373
|
+
#
|
374
|
+
# class Post < Ohm::Model
|
375
|
+
# list :comments, Comment
|
376
|
+
# end
|
377
|
+
#
|
378
|
+
# class Comment < Ohm::Model
|
379
|
+
# end
|
380
|
+
#
|
381
|
+
# post = Post.create
|
382
|
+
# post.comments << Comment.create
|
383
|
+
#
|
384
|
+
# post.comments.size == 1
|
385
|
+
# # => true
|
386
|
+
#
|
387
|
+
# post.comments.clear
|
388
|
+
# post.comments.size == 0
|
389
|
+
# # => true
|
390
|
+
# @see http://code.google.com/p/redis/wiki/DelCommand DEL in the Redis
|
391
|
+
# Command Reference.
|
141
392
|
def clear
|
142
393
|
key.del
|
143
394
|
end
|
144
395
|
|
396
|
+
# Simultaneously clear and add all models. This wraps all operations
|
397
|
+
# in a MULTI EXEC block to make the whole operation atomic.
|
398
|
+
#
|
399
|
+
# @example
|
400
|
+
#
|
401
|
+
# class Post < Ohm::Model
|
402
|
+
# list :comments, Comment
|
403
|
+
# end
|
404
|
+
#
|
405
|
+
# class Comment < Ohm::Model
|
406
|
+
# end
|
407
|
+
#
|
408
|
+
# post = Post.create
|
409
|
+
# post.comments << Comment.create(:id => 100)
|
410
|
+
#
|
411
|
+
# post.comments.map(&:id) == ["100"]
|
412
|
+
# # => true
|
413
|
+
#
|
414
|
+
# comments = (101..103).to_a.map { |i| Comment.create(:id => i) }
|
415
|
+
#
|
416
|
+
# post.comments.replace(comments)
|
417
|
+
# post.comments.map(&:id) == ["101", "102", "103"]
|
418
|
+
# # => true
|
419
|
+
#
|
420
|
+
# @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
|
421
|
+
# in the Redis Command Reference.
|
145
422
|
def replace(models)
|
146
423
|
model.db.multi do
|
147
424
|
clear
|
@@ -149,65 +426,265 @@ module Ohm
|
|
149
426
|
end
|
150
427
|
end
|
151
428
|
|
429
|
+
# @return [true, false] Whether or not this collection is empty.
|
152
430
|
def empty?
|
153
431
|
!key.exists
|
154
432
|
end
|
155
433
|
|
434
|
+
# @return [Array] Array representation of this collection.
|
156
435
|
def to_a
|
157
436
|
all
|
158
437
|
end
|
159
438
|
end
|
160
439
|
|
440
|
+
# Provides a Ruby-esque interface to a _Redis_ *SET*. The *SET* is assumed
|
441
|
+
# to be composed of ids which maps to {#model}.
|
161
442
|
class Set < Collection
|
443
|
+
# An implementation which relies on *SMEMBERS* and yields an instance
|
444
|
+
# of {#model}.
|
445
|
+
#
|
446
|
+
# @example
|
447
|
+
#
|
448
|
+
# class Author < Ohm::Model
|
449
|
+
# set :poems, Poem
|
450
|
+
# end
|
451
|
+
#
|
452
|
+
# class Poem < Ohm::Model
|
453
|
+
# end
|
454
|
+
#
|
455
|
+
# neruda = Author.create
|
456
|
+
# neruda.poems.add(Poem.create)
|
457
|
+
#
|
458
|
+
# neruda.poems.each do |poem|
|
459
|
+
# # do something with the poem
|
460
|
+
# end
|
461
|
+
#
|
462
|
+
# # if you look at the source, you'll quickly see that this can
|
463
|
+
# # easily be achieved by doing the following:
|
464
|
+
#
|
465
|
+
# neruda.poems.key.smembers.each do |id|
|
466
|
+
# poem = Poem[id]
|
467
|
+
# # do something with the poem
|
468
|
+
# end
|
469
|
+
#
|
470
|
+
# @see http://code.google.com/p/redis/wiki/SmembersCommand SMEMBERS
|
471
|
+
# in Redis Command Reference.
|
162
472
|
def each(&block)
|
163
473
|
key.smembers.each { |id| block.call(model[id]) }
|
164
474
|
end
|
165
475
|
|
476
|
+
# Convenient way to scope access to a predefined set, useful for access
|
477
|
+
# control.
|
478
|
+
#
|
479
|
+
# @example
|
480
|
+
#
|
481
|
+
# class User < Ohm::Model
|
482
|
+
# set :photos, Photo
|
483
|
+
# end
|
484
|
+
#
|
485
|
+
# class Photo < Ohm::Model
|
486
|
+
# end
|
487
|
+
#
|
488
|
+
# @user = User.create
|
489
|
+
# @user.photos.add(Photo.create(:id => "101"))
|
490
|
+
# @user.photos.add(Photo.create(:id => "102"))
|
491
|
+
#
|
492
|
+
# Photo.create(:id => "500")
|
493
|
+
#
|
494
|
+
# @user.photos[101] == Photo[101]
|
495
|
+
# # => true
|
496
|
+
#
|
497
|
+
# @user.photos[500] == nil
|
498
|
+
# # => true
|
499
|
+
#
|
500
|
+
# @param [#to_s] id Any id existing within this set.
|
501
|
+
# @return [Ohm::Model, nil] The model if it exists.
|
166
502
|
def [](id)
|
167
503
|
model[id] if key.sismember(id)
|
168
504
|
end
|
169
505
|
|
170
|
-
|
506
|
+
# Adds a model to this set.
|
507
|
+
#
|
508
|
+
# @param [#id] model Typically an instance of an {Ohm::Model} subclass.
|
509
|
+
#
|
510
|
+
# @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis
|
511
|
+
# Command Reference.
|
512
|
+
def <<(model)
|
171
513
|
key.sadd(model.id)
|
172
514
|
end
|
173
|
-
|
174
515
|
alias add <<
|
175
516
|
|
517
|
+
# Thin Ruby interface wrapper for *SCARD*.
|
518
|
+
#
|
519
|
+
# @return [Fixnum] The total number of members for this set.
|
520
|
+
# @see http://code.google.com/p/redis/wiki/ScardCommand SCARD in Redis
|
521
|
+
# Command Reference.
|
176
522
|
def size
|
177
523
|
key.scard
|
178
524
|
end
|
179
525
|
|
526
|
+
# Thin Ruby interface wrapper for *SREM*.
|
527
|
+
#
|
528
|
+
# @param [#id] member a member of this set.
|
529
|
+
# @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis
|
530
|
+
# Command Reference.
|
180
531
|
def delete(member)
|
181
532
|
key.srem(member.id)
|
182
533
|
end
|
183
534
|
|
535
|
+
# Array representation of this set.
|
536
|
+
#
|
537
|
+
# @example
|
538
|
+
#
|
539
|
+
# class Author < Ohm::Model
|
540
|
+
# set :posts, Post
|
541
|
+
# end
|
542
|
+
#
|
543
|
+
# class Post < Ohm::Model
|
544
|
+
# end
|
545
|
+
#
|
546
|
+
# author = Author.create
|
547
|
+
# author.posts.add(Author.create(:id => "101"))
|
548
|
+
# author.posts.add(Author.create(:id => "102"))
|
549
|
+
#
|
550
|
+
# author.posts.all.is_a?(Array)
|
551
|
+
# # => true
|
552
|
+
#
|
553
|
+
# author.posts.all.include?(Author[101])
|
554
|
+
# # => true
|
555
|
+
#
|
556
|
+
# author.posts.all.include?(Author[102])
|
557
|
+
# # => true
|
558
|
+
#
|
559
|
+
# @return [Array<Ohm::Model>] All members of this set.
|
184
560
|
def all
|
185
561
|
key.smembers.map(&model)
|
186
562
|
end
|
187
563
|
|
564
|
+
# Allows you to find members of this set which fits the given criteria.
|
565
|
+
#
|
566
|
+
# @example
|
567
|
+
#
|
568
|
+
# class Post < Ohm::Model
|
569
|
+
# attribute :title
|
570
|
+
# attribute :tags
|
571
|
+
#
|
572
|
+
# index :title
|
573
|
+
# index :tag
|
574
|
+
#
|
575
|
+
# def tag
|
576
|
+
# tags.split(/\s+/)
|
577
|
+
# end
|
578
|
+
# end
|
579
|
+
#
|
580
|
+
# post = Post.create(:title => "Ohm", :tags => "ruby ohm redis")
|
581
|
+
# Post.all.is_a?(Ohm::Model::Set)
|
582
|
+
# # => true
|
583
|
+
#
|
584
|
+
# Post.all.find(:tag => "ruby").include?(post)
|
585
|
+
# # => true
|
586
|
+
#
|
587
|
+
# # Post.find is actually just a wrapper around Post.all.find
|
588
|
+
# Post.find(:tag => "ohm", :title => "Ohm").include?(post)
|
589
|
+
# # => true
|
590
|
+
#
|
591
|
+
# Post.find(:tag => ["ruby", "python"]).empty?
|
592
|
+
# # => true
|
593
|
+
#
|
594
|
+
# # Alternatively, you may choose to chain them later on.
|
595
|
+
# ruby = Post.find(:tag => "ruby")
|
596
|
+
# ruby.find(:title => "Ohm").include?(post)
|
597
|
+
# # => true
|
598
|
+
#
|
599
|
+
# @param [Hash] options A hash of key value pairs.
|
600
|
+
# @return [Ohm::Model::Set] A set satisfying the filter passed.
|
188
601
|
def find(options)
|
189
602
|
source = keys(options)
|
190
603
|
target = source.inject(key.volatile) { |chain, other| chain + other }
|
191
604
|
apply(:sinterstore, key, source, target)
|
192
605
|
end
|
193
606
|
|
607
|
+
# Similar to find except that it negates the criteria.
|
608
|
+
#
|
609
|
+
# @example
|
610
|
+
# class Post < Ohm::Model
|
611
|
+
# attribute :title
|
612
|
+
# end
|
613
|
+
#
|
614
|
+
# ohm = Post.create(:title => "Ohm")
|
615
|
+
# ruby = Post.create(:title => "Ruby")
|
616
|
+
#
|
617
|
+
# Post.except(:title => "Ohm").include?(ruby)
|
618
|
+
# # => true
|
619
|
+
#
|
620
|
+
# Post.except(:title => "Ohm").size == 1
|
621
|
+
# # => true
|
622
|
+
#
|
623
|
+
# @param [Hash] options A hash of key value pairs.
|
624
|
+
# @return [Ohm::Model::Set] A set satisfying the filter passed.
|
194
625
|
def except(options)
|
195
626
|
source = keys(options)
|
196
627
|
target = source.inject(key.volatile) { |chain, other| chain - other }
|
197
628
|
apply(:sdiffstore, key, source, target)
|
198
629
|
end
|
199
630
|
|
200
|
-
|
201
|
-
|
202
|
-
|
631
|
+
# Returns by default the lowest id value for this set. You may also
|
632
|
+
# pass in options similar to {#sort}.
|
633
|
+
#
|
634
|
+
# @example
|
635
|
+
#
|
636
|
+
# class Post < Ohm::Model
|
637
|
+
# attribute :title
|
638
|
+
# end
|
639
|
+
#
|
640
|
+
# p1 = Post.create(:id => "101", :title => "Alpha")
|
641
|
+
# p2 = Post.create(:id => "100", :title => "Beta")
|
642
|
+
# p3 = Post.create(:id => "99", :title => "Gamma")
|
643
|
+
#
|
644
|
+
# Post.all.is_a?(Ohm::Model::Set)
|
645
|
+
# # => true
|
646
|
+
#
|
647
|
+
# p3 == Post.all.first
|
648
|
+
# # => true
|
649
|
+
#
|
650
|
+
# p1 == Post.all.first(:order => "DESC")
|
651
|
+
# # => true
|
652
|
+
#
|
653
|
+
# p1 == Post.all.first(:by => :title, :order => "ASC ALPHA")
|
654
|
+
# # => true
|
655
|
+
#
|
656
|
+
# # just ALPHA also means ASC ALPHA, for brevity.
|
657
|
+
# p1 == Post.all.first(:by => :title, :order => "ALPHA")
|
658
|
+
# # => true
|
659
|
+
#
|
660
|
+
# p3 == Post.all.first(:by => :title, :order => "DESC ALPHA")
|
661
|
+
# # => true
|
662
|
+
#
|
663
|
+
# @param [Hash] options Sort options hash.
|
664
|
+
# @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if this
|
665
|
+
# set is empty.
|
666
|
+
#
|
667
|
+
# @see file:README.html#sorting Sorting in the README.
|
668
|
+
def first(options = {})
|
669
|
+
opts = options.dup
|
670
|
+
opts.merge!(:limit => 1)
|
203
671
|
|
204
|
-
if
|
205
|
-
sort_by(
|
672
|
+
if opts[:by]
|
673
|
+
sort_by(opts.delete(:by), opts).first
|
206
674
|
else
|
207
|
-
sort(
|
675
|
+
sort(opts).first
|
208
676
|
end
|
209
677
|
end
|
210
678
|
|
679
|
+
# Ruby-like interface wrapper around *SISMEMBER*.
|
680
|
+
#
|
681
|
+
# @param [#id] model Typically an {Ohm::Model} instance.
|
682
|
+
#
|
683
|
+
# @return [true, false] Whether or not the {Ohm::Model model} instance
|
684
|
+
# is a member of this set.
|
685
|
+
#
|
686
|
+
# @see http://code.google.com/p/redis/wiki/SismemberCommand SISMEMBER
|
687
|
+
# in Redis Command Reference.
|
211
688
|
def include?(model)
|
212
689
|
key.sismember(model.id)
|
213
690
|
end
|
@@ -217,17 +694,19 @@ module Ohm
|
|
217
694
|
end
|
218
695
|
|
219
696
|
protected
|
220
|
-
|
697
|
+
# @private
|
221
698
|
def apply(operation, key, source, target)
|
222
699
|
target.send(operation, key, *source)
|
223
700
|
Set.new(target, Wrapper.wrap(model))
|
224
701
|
end
|
225
702
|
|
703
|
+
# @private
|
704
|
+
#
|
226
705
|
# Transform a hash of attribute/values into an array of keys.
|
227
706
|
def keys(hash)
|
228
707
|
[].tap do |keys|
|
229
708
|
hash.each do |key, values|
|
230
|
-
values = [values] unless values.kind_of?(Array)
|
709
|
+
values = [values] unless values.kind_of?(Array)
|
231
710
|
values.each do |v|
|
232
711
|
keys << model.index_key_for(key, v)
|
233
712
|
end
|
@@ -237,6 +716,34 @@ module Ohm
|
|
237
716
|
end
|
238
717
|
|
239
718
|
class Index < Set
|
719
|
+
# This method is here primarily as an optimization. Let's say you have
|
720
|
+
# the following model:
|
721
|
+
#
|
722
|
+
# class Post < Ohm::Model
|
723
|
+
# attribute :title
|
724
|
+
# index :title
|
725
|
+
# end
|
726
|
+
#
|
727
|
+
# ruby = Post.create(:title => "ruby")
|
728
|
+
# redis = Post.create(:title => "redis")
|
729
|
+
#
|
730
|
+
# Post.key[:all].smembers == [ruby.id, redis.id]
|
731
|
+
# # => true
|
732
|
+
#
|
733
|
+
# Post.index_key_for(:title, "ruby").smembers == [ruby.id]
|
734
|
+
# # => true
|
735
|
+
#
|
736
|
+
# Post.index_key_for(:title, "redis").smembers == [redis.id]
|
737
|
+
# # => true
|
738
|
+
#
|
739
|
+
# If we want to search for example all `Posts` entitled "ruby" or
|
740
|
+
# "redis", then it doesn't make sense to do an INTERSECTION with
|
741
|
+
# `Post.key[:all]` since it would be redundant.
|
742
|
+
#
|
743
|
+
# The implementation of {Ohm::Model::Index#find} avoids this redundancy.
|
744
|
+
#
|
745
|
+
# @see Ohm::Model::Set#find find in Ohm::Model::Set.
|
746
|
+
# @see Ohm::Model.find find in Ohm::Model.
|
240
747
|
def find(options)
|
241
748
|
keys = keys(options)
|
242
749
|
return super(options) if keys.size > 1
|
@@ -245,22 +752,96 @@ module Ohm
|
|
245
752
|
end
|
246
753
|
end
|
247
754
|
|
755
|
+
# Provides a Ruby-esque interface to a _Redis_ *LIST*. The *LIST* is
|
756
|
+
# assumed to be composed of ids which maps to {#model}.
|
248
757
|
class List < Collection
|
758
|
+
# An implementation which relies on *LRANGE* and yields an instance
|
759
|
+
# of {#model}.
|
760
|
+
#
|
761
|
+
# @example
|
762
|
+
#
|
763
|
+
# class Post < Ohm::Model
|
764
|
+
# list :comments, Comment
|
765
|
+
# end
|
766
|
+
#
|
767
|
+
# class Comment < Ohm::Model
|
768
|
+
# end
|
769
|
+
#
|
770
|
+
# post = Post.create
|
771
|
+
# post.comments.add(Comment.create)
|
772
|
+
# post.comments.add(Comment.create)
|
773
|
+
#
|
774
|
+
# post.comments.each do |comment|
|
775
|
+
# # do something with the comment
|
776
|
+
# end
|
777
|
+
#
|
778
|
+
# # reading the source reveals that this is achieved by doing:
|
779
|
+
# post.comments.key.lrange(0, -1).each do |id|
|
780
|
+
# comment = Comment[id]
|
781
|
+
# # do something with the comment
|
782
|
+
# end
|
783
|
+
#
|
784
|
+
# @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
|
785
|
+
# in Redis Command Reference.
|
249
786
|
def each(&block)
|
250
787
|
key.lrange(0, -1).each { |id| block.call(model[id]) }
|
251
788
|
end
|
252
789
|
|
790
|
+
# Thin wrapper around *RPUSH*.
|
791
|
+
#
|
792
|
+
# @example
|
793
|
+
#
|
794
|
+
# class Post < Ohm::Model
|
795
|
+
# list :comments, Comment
|
796
|
+
# end
|
797
|
+
#
|
798
|
+
# class Comment < Ohm::Model
|
799
|
+
# end
|
800
|
+
#
|
801
|
+
# p = Post.create
|
802
|
+
# p.comments << Comment.create
|
803
|
+
#
|
804
|
+
# @param [#id] model Typically an {Ohm::Model} instance.
|
805
|
+
#
|
806
|
+
# @see http://code.google.com/p/redis/wiki/RpushCommand RPUSH
|
807
|
+
# in Redis Command Reference.
|
253
808
|
def <<(model)
|
254
809
|
key.rpush(model.id)
|
255
810
|
end
|
256
|
-
|
257
811
|
alias push <<
|
258
812
|
|
259
813
|
# Returns the element at index, or returns a subarray starting at
|
260
|
-
# start and continuing for length elements, or returns a subarray
|
261
|
-
# specified by range
|
814
|
+
# `start` and continuing for `length` elements, or returns a subarray
|
815
|
+
# specified by `range`. Negative indices count backward from the end
|
262
816
|
# of the array (-1 is the last element). Returns nil if the index
|
263
817
|
# (or starting index) are out of range.
|
818
|
+
#
|
819
|
+
# @example
|
820
|
+
# class Post < Ohm::Model
|
821
|
+
# list :comments, Comment
|
822
|
+
# end
|
823
|
+
#
|
824
|
+
# class Comment < Ohm::Model
|
825
|
+
# end
|
826
|
+
#
|
827
|
+
# post = Post.create
|
828
|
+
#
|
829
|
+
# 10.times { post.comments << Comment.create }
|
830
|
+
#
|
831
|
+
# post.comments[0] == Comment[1]
|
832
|
+
# # => true
|
833
|
+
#
|
834
|
+
# post.comments[0, 4] == (1..5).map { |i| Comment[i] }
|
835
|
+
# # => true
|
836
|
+
#
|
837
|
+
# post.comments[0, 4] == post.comments[0..4]
|
838
|
+
# # => true
|
839
|
+
#
|
840
|
+
# post.comments.all == post.comments[0, -1]
|
841
|
+
# # => true
|
842
|
+
#
|
843
|
+
# @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
|
844
|
+
# in Redis Command Reference.
|
264
845
|
def [](index, limit = nil)
|
265
846
|
case [index, limit]
|
266
847
|
when Pattern[Fixnum, Fixnum] then
|
@@ -272,30 +853,76 @@ module Ohm
|
|
272
853
|
end
|
273
854
|
end
|
274
855
|
|
856
|
+
# Convience method for doing list[0], similar to Ruby's Array#first
|
857
|
+
# method.
|
858
|
+
#
|
859
|
+
# @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
|
860
|
+
# is empty.
|
275
861
|
def first
|
276
862
|
self[0]
|
277
863
|
end
|
278
864
|
|
865
|
+
# Returns the model at the tail of this list, while simultaneously
|
866
|
+
# removing it from the list.
|
867
|
+
#
|
868
|
+
# @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if the list
|
869
|
+
# is empty.
|
870
|
+
#
|
871
|
+
# @see http://code.google.com/p/redis/wiki/LpopCommand RPOP
|
872
|
+
# in Redis Command Reference.
|
279
873
|
def pop
|
280
874
|
model[key.rpop]
|
281
875
|
end
|
282
876
|
|
877
|
+
# Returns the model at the head of this list, while simultaneously
|
878
|
+
# removing it from the list.
|
879
|
+
#
|
880
|
+
# @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
|
881
|
+
# is empty.
|
882
|
+
#
|
883
|
+
# @see http://code.google.com/p/redis/wiki/LpopCommand LPOP
|
884
|
+
# in Redis Command Reference.
|
283
885
|
def shift
|
284
886
|
model[key.lpop]
|
285
887
|
end
|
286
888
|
|
889
|
+
# Prepends an {Ohm::Model} instance at the beginning of this list.
|
890
|
+
#
|
891
|
+
# @param [#id] model Typically an {Ohm::Model} instance.
|
892
|
+
#
|
893
|
+
# @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH
|
894
|
+
# in Redis Command Reference.
|
287
895
|
def unshift(model)
|
288
896
|
key.lpush(model.id)
|
289
897
|
end
|
290
898
|
|
899
|
+
# Returns an array representation of this list, with elements of the
|
900
|
+
# array being an instance of {#model}.
|
901
|
+
#
|
902
|
+
# @return [Array<Ohm::Model>] Instances of {Ohm::Model}.
|
291
903
|
def all
|
292
904
|
key.lrange(0, -1).map(&model)
|
293
905
|
end
|
294
906
|
|
907
|
+
# Thin Ruby interface wrapper for *LLEN*.
|
908
|
+
#
|
909
|
+
# @return [Fixnum] The total number of elements for this list.
|
910
|
+
#
|
911
|
+
# @see http://code.google.com/p/redis/wiki/LlenCommand LLEN in Redis
|
912
|
+
# Command Reference.
|
295
913
|
def size
|
296
914
|
key.llen
|
297
915
|
end
|
298
916
|
|
917
|
+
# Ruby-like interface wrapper around *LRANGE*.
|
918
|
+
#
|
919
|
+
# @param [#id] model Typically an {Ohm::Model} instance.
|
920
|
+
#
|
921
|
+
# @return [true, false] Whether or not the {Ohm::Model} instance is
|
922
|
+
# an element of this list.
|
923
|
+
#
|
924
|
+
# @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
|
925
|
+
# in Redis Command Reference.
|
299
926
|
def include?(model)
|
300
927
|
key.lrange(0, -1).include?(model.id)
|
301
928
|
end
|
@@ -305,30 +932,104 @@ module Ohm
|
|
305
932
|
end
|
306
933
|
end
|
307
934
|
|
935
|
+
# All validations which need to access the _Redis_ database goes here.
|
936
|
+
# As of this writing, {Ohm::Model::Validations#assert_unique} is the only
|
937
|
+
# assertion contained within this module.
|
308
938
|
module Validations
|
309
939
|
include Ohm::Validations
|
310
940
|
|
311
|
-
# Validates that the attribute or array of attributes are unique. For
|
312
|
-
# an index of the same kind must exist.
|
941
|
+
# Validates that the attribute or array of attributes are unique. For
|
942
|
+
# this, an index of the same kind must exist.
|
313
943
|
#
|
314
944
|
# @overload assert_unique :name
|
315
945
|
# Validates that the name attribute is unique.
|
316
946
|
# @overload assert_unique [:street, :city]
|
317
947
|
# Validates that the :street and :city pair is unique.
|
318
|
-
def assert_unique(
|
319
|
-
|
320
|
-
|
948
|
+
def assert_unique(atts, error = [atts, :not_unique])
|
949
|
+
indices = Array(atts).map { |att| index_key_for(att, send(att)) }
|
950
|
+
result = db.sinter(*indices)
|
951
|
+
|
952
|
+
assert result.empty? || !new? && result.include?(id.to_s), error
|
321
953
|
end
|
322
954
|
end
|
323
955
|
|
324
956
|
include Validations
|
325
957
|
|
958
|
+
# Raised when you try and get the *id* of an {Ohm::Model} without an id.
|
959
|
+
#
|
960
|
+
# class Post < Ohm::Model
|
961
|
+
# list :comments, Comment
|
962
|
+
# end
|
963
|
+
#
|
964
|
+
# class Comment < Ohm::Model
|
965
|
+
# end
|
966
|
+
#
|
967
|
+
# ex = nil
|
968
|
+
# begin
|
969
|
+
# Post.new.id
|
970
|
+
# rescue Exception => e
|
971
|
+
# ex = e
|
972
|
+
# end
|
973
|
+
#
|
974
|
+
# ex.kind_of?(Ohm::Model::MissingID)
|
975
|
+
# # => true
|
976
|
+
#
|
977
|
+
# This is also one of the most common errors you'll be faced with when
|
978
|
+
# you're new to {Ohm} coming from an ActiveRecord background, where you
|
979
|
+
# are used to just assigning associations even before the base model is
|
980
|
+
# persisted.
|
981
|
+
#
|
982
|
+
# # following from the example above:
|
983
|
+
# post = Post.new
|
984
|
+
#
|
985
|
+
# ex = nil
|
986
|
+
# begin
|
987
|
+
# post.comments << Comment.new
|
988
|
+
# rescue Exception => e
|
989
|
+
# ex = e
|
990
|
+
# end
|
991
|
+
#
|
992
|
+
# ex.kind_of?(Ohm::Model::MissingID)
|
993
|
+
# # => true
|
994
|
+
#
|
995
|
+
# # Correct way:
|
996
|
+
# post = Post.new
|
997
|
+
#
|
998
|
+
# if post.save
|
999
|
+
# post.comments << Comment.create
|
1000
|
+
# end
|
326
1001
|
class MissingID < Error
|
327
1002
|
def message
|
328
|
-
"You tried to perform an operation that needs the model ID,
|
1003
|
+
"You tried to perform an operation that needs the model ID, " +
|
1004
|
+
"but it's not present."
|
329
1005
|
end
|
330
1006
|
end
|
331
1007
|
|
1008
|
+
# Raised when you try and do an {Ohm::Model::Set#find} operation and use
|
1009
|
+
# a key which you did not define as an index.
|
1010
|
+
#
|
1011
|
+
# class Post < Ohm::Model
|
1012
|
+
# attribute :title
|
1013
|
+
# end
|
1014
|
+
#
|
1015
|
+
# post = Post.create(:title => "Ohm")
|
1016
|
+
#
|
1017
|
+
# ex = nil
|
1018
|
+
# begin
|
1019
|
+
# Post.find(:title => "Ohm")
|
1020
|
+
# rescue Exception => e
|
1021
|
+
# ex = e
|
1022
|
+
# end
|
1023
|
+
#
|
1024
|
+
# ex.kind_of?(Ohm::Model::IndexNotFound)
|
1025
|
+
# # => true
|
1026
|
+
#
|
1027
|
+
# To correct this problem, simply define a _:title_ *index* in your class.
|
1028
|
+
#
|
1029
|
+
# class Post < Ohm::Model
|
1030
|
+
# attribute :title
|
1031
|
+
# index :title
|
1032
|
+
# end
|
332
1033
|
class IndexNotFound < Error
|
333
1034
|
def initialize(att)
|
334
1035
|
@att = att
|
@@ -339,21 +1040,24 @@ module Ohm
|
|
339
1040
|
end
|
340
1041
|
end
|
341
1042
|
|
342
|
-
@@attributes
|
1043
|
+
@@attributes = Hash.new { |hash, key| hash[key] = [] }
|
343
1044
|
@@collections = Hash.new { |hash, key| hash[key] = [] }
|
344
|
-
@@counters
|
345
|
-
@@indices
|
346
|
-
|
347
|
-
attr_writer :id
|
1045
|
+
@@counters = Hash.new { |hash, key| hash[key] = [] }
|
1046
|
+
@@indices = Hash.new { |hash, key| hash[key] = [] }
|
348
1047
|
|
349
1048
|
def id
|
350
1049
|
@id or raise MissingID
|
351
1050
|
end
|
352
1051
|
|
353
|
-
# Defines a string attribute for the model. This attribute will be
|
354
|
-
# as a string. Any value stored here will be
|
1052
|
+
# Defines a string attribute for the model. This attribute will be
|
1053
|
+
# persisted by _Redis_ as a string. Any value stored here will be
|
1054
|
+
# retrieved in its string representation.
|
1055
|
+
#
|
1056
|
+
# If you're looking to have typecasting built in, you may want to look at
|
1057
|
+
# Ohm::Typecast in Ohm::Contrib.
|
355
1058
|
#
|
356
1059
|
# @param name [Symbol] Name of the attribute.
|
1060
|
+
# @see http://cyx.github.com/ohm-contrib/doc/Ohm/Typecast.html
|
357
1061
|
def self.attribute(name)
|
358
1062
|
define_method(name) do
|
359
1063
|
read_local(name)
|
@@ -366,10 +1070,10 @@ module Ohm
|
|
366
1070
|
attributes << name unless attributes.include?(name)
|
367
1071
|
end
|
368
1072
|
|
369
|
-
# Defines a counter attribute for the model. This attribute can't be
|
370
|
-
# or decremented. It will be zero by default.
|
1073
|
+
# Defines a counter attribute for the model. This attribute can't be
|
1074
|
+
# assigned, only incremented or decremented. It will be zero by default.
|
371
1075
|
#
|
372
|
-
# @param
|
1076
|
+
# @param [Symbol] name Name of the counter.
|
373
1077
|
def self.counter(name)
|
374
1078
|
define_method(name) do
|
375
1079
|
read_local(name).to_i
|
@@ -378,20 +1082,44 @@ module Ohm
|
|
378
1082
|
counters << name unless counters.include?(name)
|
379
1083
|
end
|
380
1084
|
|
381
|
-
# Defines a list attribute for the model. It can be accessed only after
|
382
|
-
# is created
|
1085
|
+
# Defines a list attribute for the model. It can be accessed only after
|
1086
|
+
# the model instance is created, or if you assign an :id during object
|
1087
|
+
# construction.
|
1088
|
+
#
|
1089
|
+
# @example
|
1090
|
+
#
|
1091
|
+
# class Post < Ohm::Model
|
1092
|
+
# list :comments, Comment
|
1093
|
+
# end
|
1094
|
+
#
|
1095
|
+
# class Comment < Ohm::Model
|
1096
|
+
# end
|
1097
|
+
#
|
1098
|
+
# # WRONG!!!
|
1099
|
+
# post = Post.new
|
1100
|
+
# post.comments << Comment.create
|
1101
|
+
#
|
1102
|
+
# # Right :-)
|
1103
|
+
# post = Post.create
|
1104
|
+
# post.comments << Comment.create
|
383
1105
|
#
|
384
|
-
#
|
1106
|
+
# # Alternative way if you want to have custom ids.
|
1107
|
+
# post = Post.new(:id => "my-id")
|
1108
|
+
# post.comments << Comment.create
|
1109
|
+
# post.create
|
1110
|
+
#
|
1111
|
+
# @param [Symbol] name Name of the list.
|
385
1112
|
def self.list(name, model)
|
386
1113
|
define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) }
|
387
1114
|
collections << name unless collections.include?(name)
|
388
1115
|
end
|
389
1116
|
|
390
|
-
# Defines a set attribute for the model. It can be accessed only after
|
391
|
-
# is created. Sets are recommended when insertion and
|
392
|
-
# operations like union, join, and
|
1117
|
+
# Defines a set attribute for the model. It can be accessed only after
|
1118
|
+
# the model instance is created. Sets are recommended when insertion and
|
1119
|
+
# retreival order is irrelevant, and operations like union, join, and
|
1120
|
+
# membership checks are important.
|
393
1121
|
#
|
394
|
-
# @param
|
1122
|
+
# @param [Symbol] name Name of the set.
|
395
1123
|
def self.set(name, model)
|
396
1124
|
define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
|
397
1125
|
collections << name unless collections.include?(name)
|
@@ -399,19 +1127,20 @@ module Ohm
|
|
399
1127
|
|
400
1128
|
# Creates an index (a set) that will be used for finding instances.
|
401
1129
|
#
|
402
|
-
# If you want to find a model instance by some attribute value, then an
|
403
|
-
# attribute must exist.
|
1130
|
+
# If you want to find a model instance by some attribute value, then an
|
1131
|
+
# index for that attribute must exist.
|
404
1132
|
#
|
405
1133
|
# @example
|
1134
|
+
#
|
406
1135
|
# class User < Ohm::Model
|
407
1136
|
# attribute :email
|
408
1137
|
# index :email
|
409
1138
|
# end
|
410
1139
|
#
|
411
1140
|
# # Now this is possible:
|
412
|
-
# User.find email
|
1141
|
+
# User.find :email => "ohm@example.com"
|
413
1142
|
#
|
414
|
-
# @param
|
1143
|
+
# @param [Symbol] name Name of the attribute to be indexed.
|
415
1144
|
def self.index(att)
|
416
1145
|
indices << att unless indices.include?(att)
|
417
1146
|
end
|
@@ -419,6 +1148,7 @@ module Ohm
|
|
419
1148
|
# Define a reference to another object.
|
420
1149
|
#
|
421
1150
|
# @example
|
1151
|
+
#
|
422
1152
|
# class Comment < Ohm::Model
|
423
1153
|
# attribute :content
|
424
1154
|
# reference :post, Post
|
@@ -446,7 +1176,8 @@ module Ohm
|
|
446
1176
|
# @comment.post
|
447
1177
|
# # => nil
|
448
1178
|
#
|
449
|
-
# @see
|
1179
|
+
# @see file:README.html#references References Explained.
|
1180
|
+
# @see Ohm::Model.collection
|
450
1181
|
def self.reference(name, model)
|
451
1182
|
model = Wrapper.wrap(model)
|
452
1183
|
|
@@ -476,8 +1207,8 @@ module Ohm
|
|
476
1207
|
end
|
477
1208
|
end
|
478
1209
|
|
479
|
-
# Define a collection of objects which have a
|
480
|
-
# to this model.
|
1210
|
+
# Define a collection of objects which have a
|
1211
|
+
# {Ohm::Model.reference reference} to this model.
|
481
1212
|
#
|
482
1213
|
# class Comment < Ohm::Model
|
483
1214
|
# attribute :content
|
@@ -499,7 +1230,8 @@ module Ohm
|
|
499
1230
|
# end
|
500
1231
|
#
|
501
1232
|
# @person = Person.create :name => "Albert"
|
502
|
-
# @post = Post.create :content => "Interesting stuff",
|
1233
|
+
# @post = Post.create :content => "Interesting stuff",
|
1234
|
+
# :author => @person
|
503
1235
|
# @comment = Comment.create :content => "Indeed!", :post => @post
|
504
1236
|
#
|
505
1237
|
# @post.comments.first.content
|
@@ -508,56 +1240,126 @@ module Ohm
|
|
508
1240
|
# @post.author.name
|
509
1241
|
# # => "Albert"
|
510
1242
|
#
|
511
|
-
# *Important*:
|
512
|
-
# you should not add or remove objects from this
|
1243
|
+
# *Important*: Please note that even though a collection is a
|
1244
|
+
# {Ohm::Model::Set set}, you should not add or remove objects from this
|
1245
|
+
# collection directly.
|
513
1246
|
#
|
514
|
-
# @see Ohm::Model
|
1247
|
+
# @see Ohm::Model.reference
|
515
1248
|
# @param name [Symbol] Name of the collection.
|
516
1249
|
# @param model [Constant] Model where the reference is defined.
|
517
|
-
# @param reference [Symbol] Reference as defined in the associated
|
1250
|
+
# @param reference [Symbol] Reference as defined in the associated
|
1251
|
+
# model.
|
1252
|
+
#
|
1253
|
+
# @see file:README.html#collections Collections Explained.
|
518
1254
|
def self.collection(name, model, reference = to_reference)
|
519
1255
|
model = Wrapper.wrap(model)
|
520
|
-
define_method(name) {
|
1256
|
+
define_method(name) {
|
1257
|
+
model.unwrap.find(:"#{reference}_id" => send(:id))
|
1258
|
+
}
|
521
1259
|
end
|
522
1260
|
|
1261
|
+
# Used by {Ohm::Model.collection} to infer the reference.
|
1262
|
+
#
|
1263
|
+
# @return [Symbol] Representation of this class in an all-lowercase
|
1264
|
+
# format, separated by underscores and demodulized.
|
523
1265
|
def self.to_reference
|
524
|
-
name.to_s.
|
1266
|
+
name.to_s.
|
1267
|
+
match(/^(?:.*::)*(.*)$/)[1].
|
1268
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
1269
|
+
downcase.to_sym
|
525
1270
|
end
|
526
1271
|
|
1272
|
+
# @private
|
527
1273
|
def self.define_memoized_method(name, &block)
|
528
1274
|
define_method(name) do
|
529
1275
|
@_memo[name] ||= instance_eval(&block)
|
530
1276
|
end
|
531
1277
|
end
|
532
1278
|
|
1279
|
+
# Allows you to find an {Ohm::Model} instance by its *id*.
|
1280
|
+
#
|
1281
|
+
# @param [#to_s] id The id of the model you want to find.
|
1282
|
+
# @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does
|
1283
|
+
# not exist.
|
533
1284
|
def self.[](id)
|
534
1285
|
new(:id => id) if id && exists?(id)
|
535
1286
|
end
|
536
1287
|
|
1288
|
+
# @private Used for conveniently doing [1, 2].map(&Post) for example.
|
537
1289
|
def self.to_proc
|
538
1290
|
Proc.new { |id| self[id] }
|
539
1291
|
end
|
540
1292
|
|
1293
|
+
# Returns a {Ohm::Model::Set set} containing all the members of a given
|
1294
|
+
# class.
|
1295
|
+
#
|
1296
|
+
# @example
|
1297
|
+
#
|
1298
|
+
# class Post < Ohm::Model
|
1299
|
+
# end
|
1300
|
+
#
|
1301
|
+
# post = Post.create
|
1302
|
+
#
|
1303
|
+
# Post.all.include?(post)
|
1304
|
+
# # => true
|
1305
|
+
#
|
1306
|
+
# post.delete
|
1307
|
+
#
|
1308
|
+
# Post.all.include?(post)
|
1309
|
+
# # => false
|
541
1310
|
def self.all
|
542
1311
|
Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
|
543
1312
|
end
|
544
1313
|
|
1314
|
+
# All the defined attributes within a class.
|
1315
|
+
# @see Ohm::Model.attribute
|
545
1316
|
def self.attributes
|
546
1317
|
@@attributes[self]
|
547
1318
|
end
|
548
1319
|
|
1320
|
+
# All the defined counters within a class.
|
1321
|
+
# @see Ohm::Model.counter
|
549
1322
|
def self.counters
|
550
1323
|
@@counters[self]
|
551
1324
|
end
|
552
1325
|
|
1326
|
+
# All the defined collections within a class. This will be comprised of
|
1327
|
+
# all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within
|
1328
|
+
# your class.
|
1329
|
+
#
|
1330
|
+
# @example
|
1331
|
+
# class Post < Ohm::Model
|
1332
|
+
# set :authors, Author
|
1333
|
+
# list :comments, Comment
|
1334
|
+
# end
|
1335
|
+
#
|
1336
|
+
# Post.collections == [:authors, :comments]
|
1337
|
+
# # => true
|
1338
|
+
#
|
1339
|
+
# @see Ohm::Model.list
|
1340
|
+
# @see Ohm::Model.set
|
553
1341
|
def self.collections
|
554
1342
|
@@collections[self]
|
555
1343
|
end
|
556
1344
|
|
1345
|
+
# All the defined indices within a class.
|
1346
|
+
# @see Ohm::Model.index
|
557
1347
|
def self.indices
|
558
1348
|
@@indices[self]
|
559
1349
|
end
|
560
1350
|
|
1351
|
+
# Convenience method to create and return the newly created object.
|
1352
|
+
#
|
1353
|
+
# @example
|
1354
|
+
#
|
1355
|
+
# class Post < Ohm::Model
|
1356
|
+
# attribute :title
|
1357
|
+
# end
|
1358
|
+
#
|
1359
|
+
# post = Post.create(:title => "A new post")
|
1360
|
+
#
|
1361
|
+
# @param [Hash] args attribute-value pairs for the object.
|
1362
|
+
# @return [Ohm::Model] an instance of the class you're trying to create.
|
561
1363
|
def self.create(*args)
|
562
1364
|
model = new(*args)
|
563
1365
|
model.create
|
@@ -571,16 +1373,66 @@ module Ohm
|
|
571
1373
|
# event2 = Event.create day: "2009-09-09", author: "Benoit"
|
572
1374
|
# event3 = Event.create day: "2009-09-10", author: "Albert"
|
573
1375
|
#
|
574
|
-
#
|
1376
|
+
# [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a
|
1377
|
+
# # => true
|
575
1378
|
def self.find(hash)
|
576
|
-
|
1379
|
+
unless hash.kind_of?(Hash)
|
1380
|
+
raise ArgumentError,
|
1381
|
+
"You need to supply a hash with filters. " +
|
1382
|
+
"If you want to find by ID, use #{self}[id] instead."
|
1383
|
+
end
|
1384
|
+
|
577
1385
|
all.find(hash)
|
578
1386
|
end
|
579
1387
|
|
1388
|
+
# Encode a value, making it safe to use as a key. Internally used by
|
1389
|
+
# {Ohm::Model.index_key_for} to canonicalize the indexed values.
|
1390
|
+
#
|
1391
|
+
# @param [#to_s] value Any object you want to be able to use as a key.
|
1392
|
+
# @return [String] A string which is safe to use as a key.
|
1393
|
+
# @see Ohm::Model.index_key_for
|
580
1394
|
def self.encode(value)
|
581
1395
|
Base64.encode64(value.to_s).gsub("\n", "")
|
582
1396
|
end
|
583
1397
|
|
1398
|
+
# Constructor for all subclasses of {Ohm::Model}, which optionally
|
1399
|
+
# takes a Hash of attribute value pairs.
|
1400
|
+
#
|
1401
|
+
# Starting with Ohm 0.1.0, you can use custom ids instead of being forced
|
1402
|
+
# to use auto incrementing numeric ids, but keep in mind that you have
|
1403
|
+
# to pass in the preferred id during object initialization.
|
1404
|
+
#
|
1405
|
+
# @example
|
1406
|
+
#
|
1407
|
+
# class User < Ohm::Model
|
1408
|
+
# end
|
1409
|
+
#
|
1410
|
+
# class Post < Ohm::Model
|
1411
|
+
# attribute :title
|
1412
|
+
# reference :user, User
|
1413
|
+
# end
|
1414
|
+
#
|
1415
|
+
# user = User.create
|
1416
|
+
# p1 = Post.new(:title => "Redis", :user_id => user.id)
|
1417
|
+
# p1.save
|
1418
|
+
#
|
1419
|
+
# p1.user_id == user.id
|
1420
|
+
# # => true
|
1421
|
+
#
|
1422
|
+
# p1.user == user
|
1423
|
+
# # => true
|
1424
|
+
#
|
1425
|
+
# # You can also just pass the actual User object, which is the better
|
1426
|
+
# # way to do it:
|
1427
|
+
# Post.new(:title => "Different way", :user => user).user == user
|
1428
|
+
# # => true
|
1429
|
+
#
|
1430
|
+
# # Let's try and generate custom ids
|
1431
|
+
# p2 = Post.new(:id => "ohm-redis-library", :title => "Lib")
|
1432
|
+
# p2 == Post["ohm-redis-library"]
|
1433
|
+
# # => true
|
1434
|
+
#
|
1435
|
+
# @param [Hash] attrs Attribute value pairs.
|
584
1436
|
def initialize(attrs = {})
|
585
1437
|
@id = nil
|
586
1438
|
@_memo = {}
|
@@ -588,10 +1440,15 @@ module Ohm
|
|
588
1440
|
update_attributes(attrs)
|
589
1441
|
end
|
590
1442
|
|
1443
|
+
# @return [true, false] Whether or not this object has an id.
|
591
1444
|
def new?
|
592
1445
|
!@id
|
593
1446
|
end
|
594
1447
|
|
1448
|
+
# Create this model if it passes all validations.
|
1449
|
+
#
|
1450
|
+
# @return [Ohm::Model, nil] The newly created object or nil if it fails
|
1451
|
+
# validation.
|
595
1452
|
def create
|
596
1453
|
return unless valid?
|
597
1454
|
initialize_id
|
@@ -603,6 +1460,10 @@ module Ohm
|
|
603
1460
|
end
|
604
1461
|
end
|
605
1462
|
|
1463
|
+
# Create or update this object based on the state of #new?.
|
1464
|
+
#
|
1465
|
+
# @return [Ohm::Model, nil] The saved object or nil if it fails
|
1466
|
+
# validation.
|
606
1467
|
def save
|
607
1468
|
return create if new?
|
608
1469
|
return unless valid?
|
@@ -613,17 +1474,32 @@ module Ohm
|
|
613
1474
|
end
|
614
1475
|
end
|
615
1476
|
|
1477
|
+
# Update this object, optionally accepting new attributes.
|
1478
|
+
#
|
1479
|
+
# @param [Hash] attrs Attribute value pairs to use for the updated
|
1480
|
+
# version
|
1481
|
+
# @return [Ohm::Model, nil] The updated object or nil if it fails
|
1482
|
+
# validation.
|
616
1483
|
def update(attrs)
|
617
1484
|
update_attributes(attrs)
|
618
1485
|
save
|
619
1486
|
end
|
620
1487
|
|
1488
|
+
# Locally update all attributes without persisting the changes.
|
1489
|
+
# Internally used by {Ohm::Model#initialize} and {Ohm::Model#update}
|
1490
|
+
# to set attribute value pairs.
|
1491
|
+
#
|
1492
|
+
# @param [Hash] attrs Attribute value pairs.
|
621
1493
|
def update_attributes(attrs)
|
622
1494
|
attrs.each do |key, value|
|
623
1495
|
send(:"#{key}=", value)
|
624
1496
|
end
|
625
1497
|
end
|
626
1498
|
|
1499
|
+
# Delete this object from the _Redis_ datastore, ensuring that all
|
1500
|
+
# indices, attributes, collections, etc are also deleted with it.
|
1501
|
+
#
|
1502
|
+
# @return [Ohm::Model] Returns a reference of itself.
|
627
1503
|
def delete
|
628
1504
|
delete_from_indices
|
629
1505
|
delete_attributes(collections) unless collections.empty?
|
@@ -633,22 +1509,27 @@ module Ohm
|
|
633
1509
|
|
634
1510
|
# Increment the counter denoted by :att.
|
635
1511
|
#
|
636
|
-
# @param
|
1512
|
+
# @param [Symbol] att Attribute to increment.
|
1513
|
+
# @param [Fixnum] count An optional increment step to use.
|
637
1514
|
def incr(att, count = 1)
|
638
|
-
|
1515
|
+
unless counters.include?(att)
|
1516
|
+
raise ArgumentError, "#{att.inspect} is not a counter."
|
1517
|
+
end
|
1518
|
+
|
639
1519
|
write_local(att, key.hincrby(att, count))
|
640
1520
|
end
|
641
1521
|
|
642
1522
|
# Decrement the counter denoted by :att.
|
643
1523
|
#
|
644
|
-
# @param
|
1524
|
+
# @param [Symbol] att Attribute to decrement.
|
1525
|
+
# @param [Fixnum] count An optional decrement step to use.
|
645
1526
|
def decr(att, count = 1)
|
646
1527
|
incr(att, -count)
|
647
1528
|
end
|
648
1529
|
|
649
1530
|
# Export the id and errors of the object. The `to_hash` takes the opposite
|
650
|
-
# approach of providing all the attributes and instead favors a
|
651
|
-
#
|
1531
|
+
# approach of providing all the attributes and instead favors a white
|
1532
|
+
# listed approach.
|
652
1533
|
#
|
653
1534
|
# @example
|
654
1535
|
#
|
@@ -684,26 +1565,62 @@ module Ohm
|
|
684
1565
|
attrs
|
685
1566
|
end
|
686
1567
|
|
1568
|
+
# Returns the JSON representation of the {#to_hash} for this object.
|
1569
|
+
# Defining a custom {#to_hash} method will also affect this and return
|
1570
|
+
# a corresponding JSON representation of whatever you have in your
|
1571
|
+
# {#to_hash}.
|
1572
|
+
#
|
1573
|
+
# @example
|
1574
|
+
# require "json"
|
1575
|
+
#
|
1576
|
+
# class Post < Ohm::Model
|
1577
|
+
# attribute :title
|
1578
|
+
#
|
1579
|
+
# def to_hash
|
1580
|
+
# super.merge(:title => title)
|
1581
|
+
# end
|
1582
|
+
# end
|
1583
|
+
#
|
1584
|
+
# p1 = Post.create(:title => "Delta Force")
|
1585
|
+
# p1.to_hash == { :id => "1", :title => "Delta Force" }
|
1586
|
+
# # => true
|
1587
|
+
#
|
1588
|
+
# p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}"
|
1589
|
+
# # => true
|
1590
|
+
#
|
1591
|
+
# @return [String] The JSON representation of this object defined in
|
1592
|
+
# terms of {#to_hash}.
|
687
1593
|
def to_json(*args)
|
688
1594
|
to_hash.to_json(*args)
|
689
1595
|
end
|
690
1596
|
|
1597
|
+
# Convenience wrapper for {Ohm::Model.attributes}.
|
691
1598
|
def attributes
|
692
1599
|
self.class.attributes
|
693
1600
|
end
|
694
1601
|
|
1602
|
+
# Convenience wrapper for {Ohm::Model.counters}.
|
695
1603
|
def counters
|
696
1604
|
self.class.counters
|
697
1605
|
end
|
698
1606
|
|
1607
|
+
# Convenience wrapper for {Ohm::Model.collections}.
|
699
1608
|
def collections
|
700
1609
|
self.class.collections
|
701
1610
|
end
|
702
1611
|
|
1612
|
+
# Convenience wrapper for {Ohm::Model.indices}.
|
703
1613
|
def indices
|
704
1614
|
self.class.indices
|
705
1615
|
end
|
706
1616
|
|
1617
|
+
# Implementation of equality checking. Equality is defined by two simple
|
1618
|
+
# rules:
|
1619
|
+
#
|
1620
|
+
# 1. They have the same class.
|
1621
|
+
# 2. They have the same key (_Redis_ key e.g. Post:1 == Post:1).
|
1622
|
+
#
|
1623
|
+
# @return [true, false] Whether or not the passed object is equal.
|
707
1624
|
def ==(other)
|
708
1625
|
other.kind_of?(self.class) && other.key == key
|
709
1626
|
rescue MissingID
|
@@ -711,11 +1628,38 @@ module Ohm
|
|
711
1628
|
end
|
712
1629
|
alias :eql? :==
|
713
1630
|
|
1631
|
+
# Allows you to safely use an instance of {Ohm::Model} as a key in a
|
1632
|
+
# Ruby hash without running into weird scenarios.
|
1633
|
+
#
|
1634
|
+
# @example
|
1635
|
+
#
|
1636
|
+
# class Post < Ohm::Model
|
1637
|
+
# end
|
1638
|
+
#
|
1639
|
+
# h = {}
|
1640
|
+
# p1 = Post.new
|
1641
|
+
# h[p1] = "Ruby"
|
1642
|
+
# h[p1] == "Ruby"
|
1643
|
+
# # => true
|
1644
|
+
#
|
1645
|
+
# p1.save
|
1646
|
+
# h[p1] == "Ruby"
|
1647
|
+
# # => false
|
1648
|
+
#
|
1649
|
+
# @return [Fixnum] An integer representing this object to be used
|
1650
|
+
# as the index for hashes in Ruby.
|
714
1651
|
def hash
|
715
1652
|
new? ? super : key.hash
|
716
1653
|
end
|
717
1654
|
|
718
|
-
# Lock the object before executing the block, and release it once the
|
1655
|
+
# Lock the object before executing the block, and release it once the
|
1656
|
+
# block is done.
|
1657
|
+
#
|
1658
|
+
# This is used during {#create} and {#save} to ensure that no race
|
1659
|
+
# conditions occur.
|
1660
|
+
#
|
1661
|
+
# @see http://code.google.com/p/redis/wiki/SetnxCommand SETNX in the
|
1662
|
+
# Redis Command Reference.
|
719
1663
|
def mutex
|
720
1664
|
lock!
|
721
1665
|
yield
|
@@ -724,6 +1668,11 @@ module Ohm
|
|
724
1668
|
unlock!
|
725
1669
|
end
|
726
1670
|
|
1671
|
+
# Returns everything, including {Ohm::Model.attributes attributes},
|
1672
|
+
# {Ohm::Model.collections collections}, {Ohm::Model.counters counters},
|
1673
|
+
# and the id of this object.
|
1674
|
+
#
|
1675
|
+
# Useful for debugging and for doing irb work.
|
727
1676
|
def inspect
|
728
1677
|
everything = (attributes + collections + counters).map do |att|
|
729
1678
|
value = begin
|
@@ -735,10 +1684,21 @@ module Ohm
|
|
735
1684
|
[att, value.inspect]
|
736
1685
|
end
|
737
1686
|
|
738
|
-
|
1687
|
+
sprintf("#<%s:%s %s>",
|
1688
|
+
self.class,
|
1689
|
+
new? ? "?" : id,
|
1690
|
+
everything.map {|e| e.join("=") }.join(" ")
|
1691
|
+
)
|
739
1692
|
end
|
740
1693
|
|
741
|
-
# Makes the model connect to a different Redis instance.
|
1694
|
+
# Makes the model connect to a different Redis instance. This is useful
|
1695
|
+
# for scaling a large application, where one model can be stored in a
|
1696
|
+
# different Redis instance, and some other groups of models can be
|
1697
|
+
# in another Redis instance.
|
1698
|
+
#
|
1699
|
+
# This approach of splitting models is a lot simpler than doing a
|
1700
|
+
# distributed *Redis* solution and may well be the right solution for
|
1701
|
+
# certain cases.
|
742
1702
|
#
|
743
1703
|
# @example
|
744
1704
|
#
|
@@ -753,16 +1713,37 @@ module Ohm
|
|
753
1713
|
# # definition:
|
754
1714
|
# Post.connect(:port => 6380, :db => 2)
|
755
1715
|
#
|
1716
|
+
# @see file:README.html#connecting Ohm.connect options documentation.
|
756
1717
|
def self.connect(*options)
|
757
1718
|
self.db = Ohm.connection(*options)
|
758
1719
|
end
|
759
1720
|
|
760
1721
|
protected
|
1722
|
+
attr_writer :id
|
761
1723
|
|
1724
|
+
# @return [Ohm::Key] A key scoped to the model which uses this object's
|
1725
|
+
# id.
|
1726
|
+
#
|
1727
|
+
# @see http://github.com/soveran/nest The Nest library.
|
762
1728
|
def key
|
763
1729
|
self.class.key[id]
|
764
1730
|
end
|
765
1731
|
|
1732
|
+
# Write all the attributes and counters of this object. The operation
|
1733
|
+
# is actually a 2-step process:
|
1734
|
+
#
|
1735
|
+
# 1. Delete the current key, e.g. Post:2.
|
1736
|
+
# 2. Set all of the new attributes (using HMSET).
|
1737
|
+
#
|
1738
|
+
# The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure
|
1739
|
+
# the atomicity of the write operation.
|
1740
|
+
#
|
1741
|
+
# @see http://code.google.com/p/redis/wiki/DelCommand DEL in the
|
1742
|
+
# Redis Command Reference.
|
1743
|
+
# @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the
|
1744
|
+
# Redis Command Reference.
|
1745
|
+
# @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
|
1746
|
+
# in the Redis Command Reference.
|
766
1747
|
def write
|
767
1748
|
unless (attributes + counters).empty?
|
768
1749
|
atts = (attributes + counters).inject([]) { |ret, att|
|
@@ -779,6 +1760,17 @@ module Ohm
|
|
779
1760
|
end
|
780
1761
|
end
|
781
1762
|
|
1763
|
+
# Write a single attribute both locally and remotely. It's very important
|
1764
|
+
# to know that this method skips validation checks, therefore you must
|
1765
|
+
# ensure data integrity and validity in your application code.
|
1766
|
+
#
|
1767
|
+
# @param [Symbol, String] att The name of the attribute to write.
|
1768
|
+
# @param [#to_s] value The value of the attribute to write.
|
1769
|
+
#
|
1770
|
+
# @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the
|
1771
|
+
# Redis Command Reference.
|
1772
|
+
# @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the
|
1773
|
+
# Redis Command Reference.
|
782
1774
|
def write_remote(att, value)
|
783
1775
|
write_local(att, value)
|
784
1776
|
|
@@ -789,6 +1781,11 @@ module Ohm
|
|
789
1781
|
end
|
790
1782
|
end
|
791
1783
|
|
1784
|
+
# Wraps any missing constants lazily in {Ohm::Model::Wrapper} delaying
|
1785
|
+
# the evaluation of constants until they are actually needed.
|
1786
|
+
#
|
1787
|
+
# @see Ohm::Model::Wrapper
|
1788
|
+
# @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation
|
792
1789
|
def self.const_missing(name)
|
793
1790
|
wrapper = Wrapper.new(name) { const_get(name) }
|
794
1791
|
|
@@ -812,6 +1809,7 @@ module Ohm
|
|
812
1809
|
Ohm.threaded[self] = connection
|
813
1810
|
end
|
814
1811
|
|
1812
|
+
# Allows you to do key manipulations scoped solely to your class.
|
815
1813
|
def self.key
|
816
1814
|
Key.new(self, db)
|
817
1815
|
end
|
@@ -820,6 +1818,26 @@ module Ohm
|
|
820
1818
|
key[:all].sismember(id)
|
821
1819
|
end
|
822
1820
|
|
1821
|
+
# The meat of the ID generation code for Ohm. For cases where you want to
|
1822
|
+
# customize ID generation (i.e. use GUIDs or Base62 ids) then you simply
|
1823
|
+
# override this method in your model.
|
1824
|
+
#
|
1825
|
+
# @example
|
1826
|
+
#
|
1827
|
+
# module UUID
|
1828
|
+
# def self.new
|
1829
|
+
# `uuidgen`.strip
|
1830
|
+
# end
|
1831
|
+
# end
|
1832
|
+
#
|
1833
|
+
# class Post < Ohm::Model
|
1834
|
+
#
|
1835
|
+
# private
|
1836
|
+
# def initialize_id
|
1837
|
+
# @id ||= UUID.new
|
1838
|
+
# end
|
1839
|
+
# end
|
1840
|
+
#
|
823
1841
|
def initialize_id
|
824
1842
|
@id ||= self.class.key[:id].incr.to_s
|
825
1843
|
end
|
@@ -876,14 +1894,29 @@ module Ohm
|
|
876
1894
|
key[:_indices].del
|
877
1895
|
end
|
878
1896
|
|
1897
|
+
# Get the value of a specific attribute. An important fact about
|
1898
|
+
# attributes in Ohm is that they are all loaded lazily.
|
1899
|
+
#
|
1900
|
+
# @param [Symbol] att The attribute you you want to get.
|
1901
|
+
# @return [String] The value of att.
|
879
1902
|
def read_local(att)
|
880
1903
|
@_attributes[att]
|
881
1904
|
end
|
882
1905
|
|
1906
|
+
# Write the value of an attribute locally, without persisting it.
|
1907
|
+
#
|
1908
|
+
# @param [Symbol] att The attribute you want to set.
|
1909
|
+
# @param [#to_s] value The value of the attribute you want to set.
|
883
1910
|
def write_local(att, value)
|
884
1911
|
@_attributes[att] = value
|
885
1912
|
end
|
886
1913
|
|
1914
|
+
# Used internally be the @_attributes hash to lazily load attributes
|
1915
|
+
# when you need them. You may also use this in your code if you know what
|
1916
|
+
# you are doing.
|
1917
|
+
#
|
1918
|
+
# @param [Symbol] att The attribute you you want to get.
|
1919
|
+
# @return [String] The value of att.
|
887
1920
|
def read_remote(att)
|
888
1921
|
unless new?
|
889
1922
|
value = key.hget(att)
|
@@ -893,23 +1926,48 @@ module Ohm
|
|
893
1926
|
end
|
894
1927
|
end
|
895
1928
|
|
1929
|
+
# Read attributes en masse locally.
|
896
1930
|
def read_locals(attrs)
|
897
1931
|
attrs.map do |att|
|
898
1932
|
send(att)
|
899
1933
|
end
|
900
1934
|
end
|
901
1935
|
|
1936
|
+
# Read attributes en masse remotely.
|
902
1937
|
def read_remotes(attrs)
|
903
1938
|
attrs.map do |att|
|
904
1939
|
read_remote(att)
|
905
1940
|
end
|
906
1941
|
end
|
907
1942
|
|
908
|
-
|
909
|
-
|
910
|
-
|
1943
|
+
# Get the index name for a specific index and value pair. The return value
|
1944
|
+
# is an instance of {Ohm::Key}, which you can readily do Redis operations
|
1945
|
+
# on.
|
1946
|
+
#
|
1947
|
+
# @example
|
1948
|
+
#
|
1949
|
+
# class Post < Ohm::Model
|
1950
|
+
# attribute :title
|
1951
|
+
# index :title
|
1952
|
+
# end
|
1953
|
+
#
|
1954
|
+
# post = Post.create(:title => "Foo")
|
1955
|
+
# key = Post.index_key_for(:title, "Foo")
|
1956
|
+
# key == "Post:title:Rm9v"
|
1957
|
+
# key.scard == 1
|
1958
|
+
# key.smembers == [post.id]
|
1959
|
+
# # => true
|
1960
|
+
#
|
1961
|
+
# @param [Symbol] name The name of the index.
|
1962
|
+
# @param [#to_s] value The value for the index.
|
1963
|
+
# @return [Ohm::Key] A {Ohm::Key key} which you can treat as a string,
|
1964
|
+
# but also do Redis operations on.
|
1965
|
+
def self.index_key_for(name, value)
|
1966
|
+
raise IndexNotFound, name unless indices.include?(name)
|
1967
|
+
key[name][encode(value)]
|
911
1968
|
end
|
912
1969
|
|
1970
|
+
# Thin wrapper around {Ohm::Model.index_key_for}.
|
913
1971
|
def index_key_for(att, value)
|
914
1972
|
self.class.index_key_for(att, value)
|
915
1973
|
end
|
@@ -935,8 +1993,9 @@ module Ohm
|
|
935
1993
|
key[:_lock].del
|
936
1994
|
end
|
937
1995
|
|
938
|
-
def lock_expired?
|
1996
|
+
def lock_expired?(timestamp)
|
939
1997
|
timestamp.to_f < Time.now.to_f
|
940
1998
|
end
|
941
1999
|
end
|
942
2000
|
end
|
2001
|
+
|