ohm 0.1.5 → 1.0.0.alpha1
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/Rakefile +19 -1
- data/lib/ohm.rb +893 -1662
- data/lib/ohm/transaction.rb +129 -0
- data/test/association.rb +33 -0
- data/test/connection.rb +72 -0
- data/test/core.rb +26 -0
- data/test/counters.rb +67 -0
- data/test/extensibility.rb +48 -0
- data/test/filtering.rb +42 -0
- data/test/{hash_key_test.rb → hash_key.rb} +0 -0
- data/test/indices.rb +97 -0
- data/test/{json_test.rb → json.rb} +10 -3
- data/test/lua-save.rb +193 -0
- data/test/lua.rb +47 -0
- data/test/{model_test.rb → model.rb} +325 -439
- data/test/pipeline-performance.rb +65 -0
- data/test/transactions.rb +241 -0
- data/test/uniques.rb +87 -0
- data/test/{associations_test.rb → unused/associations_test.rb} +3 -3
- data/test/{pattern_test.rb → unused/pattern_test.rb} +0 -0
- data/test/{upgrade_script_test.rb → unused/upgrade_script_test.rb} +0 -0
- data/test/{wrapper_test.rb → unused/wrapper_test.rb} +0 -0
- data/test/{validations_test.rb → validations.rb} +20 -70
- metadata +44 -29
- data/lib/ohm/compat-1.8.6.rb +0 -39
- data/lib/ohm/key.rb +0 -35
- data/lib/ohm/pattern.rb +0 -37
- data/lib/ohm/validations.rb +0 -213
- data/lib/ohm/version.rb +0 -5
- data/test/connection_test.rb +0 -101
- data/test/errors_test.rb +0 -120
- data/test/indices_test.rb +0 -213
- data/test/mutex_test.rb +0 -84
data/Rakefile
CHANGED
@@ -27,7 +27,7 @@ end
|
|
27
27
|
task :test do
|
28
28
|
require File.expand_path("./test/helper", File.dirname(__FILE__))
|
29
29
|
|
30
|
-
Cutest.run(Dir["test
|
30
|
+
Cutest.run(Dir["test/*.rb"])
|
31
31
|
end
|
32
32
|
|
33
33
|
desc "Generate documentation"
|
@@ -90,3 +90,21 @@ namespace :examples do
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
+
task :paste_lua_inline do
|
94
|
+
def wrap(const, data)
|
95
|
+
ret = "#{const} = (<<-EOT).gsub(/^ {4}/, "")\n"
|
96
|
+
ret << data.gsub(/^/, " ")
|
97
|
+
ret << " EOT\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
save = File.read("./lua/save.lua")
|
101
|
+
del = File.read("./lua/delete.lua")
|
102
|
+
# save = "foo"
|
103
|
+
# del = "bar"
|
104
|
+
|
105
|
+
ohm = File.read("./lib/ohm.rb", encoding: "utf-8")
|
106
|
+
ohm.gsub!(/SAVE =(.*?)$(.*?)EOT/m, wrap("SAVE", save))
|
107
|
+
ohm.gsub!(/DELETE =(.*?)$(.*?)EOT/m, wrap("DELETE", del))
|
108
|
+
|
109
|
+
puts ohm
|
110
|
+
end
|
data/lib/ohm.rb
CHANGED
@@ -1,2000 +1,1231 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
-
require "base64"
|
4
|
-
require "redis"
|
5
3
|
require "nest"
|
6
|
-
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require File.join(File.dirname(__FILE__), "ohm", "key")
|
4
|
+
require "redis"
|
5
|
+
require "securerandom"
|
6
|
+
require "scrivener"
|
7
|
+
require "ohm/transaction"
|
11
8
|
|
12
9
|
module Ohm
|
13
10
|
|
14
|
-
#
|
15
|
-
#
|
11
|
+
# All of the known errors in Ohm can be traced back to one of these
|
12
|
+
# exceptions.
|
16
13
|
#
|
17
|
-
#
|
18
|
-
# commands would be one of the following:
|
14
|
+
# MissingID:
|
19
15
|
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# *#key* which should be used within your *Post* model.
|
16
|
+
# Comment.new.id # => Error
|
17
|
+
# Comment.new.key # => Error
|
23
18
|
#
|
24
|
-
#
|
25
|
-
# accessible, it is much cleaner and terse to use {Ohm::Model.key}.
|
19
|
+
# Solution: you need to save your model first.
|
26
20
|
#
|
27
|
-
#
|
21
|
+
# IndexNotFound:
|
28
22
|
#
|
29
|
-
#
|
30
|
-
# def comment_ids
|
31
|
-
# key[:comments].zrange(0, -1)
|
32
|
-
# end
|
23
|
+
# Comment.find(foo: "Bar") # => Error
|
33
24
|
#
|
34
|
-
#
|
35
|
-
# key[:comments].zadd(Time.now.to_i, id)
|
36
|
-
# end
|
25
|
+
# Solution: add an index with `Comment.index :foo`.
|
37
26
|
#
|
38
|
-
#
|
39
|
-
# # Let's use the db style here just to demonstrate.
|
40
|
-
# db.zrem key[:comments], id
|
41
|
-
# end
|
42
|
-
# end
|
27
|
+
# UniqueIndexViolation:
|
43
28
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# # => true
|
29
|
+
# Raised when trying to save an object with a `unique` index for
|
30
|
+
# which the value already exists.
|
47
31
|
#
|
48
|
-
#
|
49
|
-
#
|
32
|
+
# Solution: rescue `Ohm::UniqueIndexViolation` during save, but
|
33
|
+
# also, do some validations even before attempting to save.
|
50
34
|
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
#
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
35
|
+
class Error < StandardError; end
|
36
|
+
class MissingID < Error; end
|
37
|
+
class IndexNotFound < Error; end
|
38
|
+
class UniqueIndexViolation < Error; end
|
39
|
+
|
40
|
+
# Instead of monkey patching Kernel or trying to be clever, it's
|
41
|
+
# best to confine all the helper methods in a Utils module.
|
42
|
+
module Utils
|
43
|
+
|
44
|
+
# Used by: `attribute`, `counter`, `set`, `reference`,
|
45
|
+
# `collection`.
|
46
|
+
#
|
47
|
+
# Employed as a solution to avoid `NameError` problems when trying
|
48
|
+
# to load models referring to other models not yet loaded.
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
#
|
52
|
+
# class Comment < Ohm::Model
|
53
|
+
# reference :user, User # NameError undefined constant User.
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Instead of relying on some clever `const_missing` hack, we can
|
57
|
+
# simply use a Symbol.
|
58
|
+
#
|
59
|
+
# class Comment < Ohm::Model
|
60
|
+
# reference :user, :User
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
def self.const(context, name)
|
64
|
+
case name
|
65
|
+
when Symbol then context.const_get(name)
|
66
|
+
else name
|
67
|
+
end
|
68
|
+
end
|
64
69
|
end
|
65
70
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
class Connection
|
72
|
+
attr_accessor :context
|
73
|
+
attr_accessor :options
|
74
|
+
|
75
|
+
def initialize(context = :main, options = {})
|
76
|
+
@context = context
|
77
|
+
@options = options
|
78
|
+
end
|
79
|
+
|
80
|
+
def reset!
|
81
|
+
threaded[context] = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def start(options = {})
|
85
|
+
self.options = options
|
86
|
+
self.reset!
|
87
|
+
end
|
88
|
+
|
89
|
+
def redis
|
90
|
+
threaded[context] ||= Redis.connect(options)
|
91
|
+
end
|
92
|
+
|
93
|
+
def threaded
|
94
|
+
Thread.current[:ohm] ||= {}
|
95
|
+
end
|
76
96
|
end
|
77
97
|
|
78
|
-
|
79
|
-
|
80
|
-
Thread.current[:ohm] ||= {}
|
98
|
+
def self.conn
|
99
|
+
@conn ||= Connection.new
|
81
100
|
end
|
82
101
|
|
83
|
-
#
|
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.
|
102
|
+
# Stores the connection options for the Redis instance.
|
91
103
|
#
|
92
|
-
#
|
93
|
-
# be as follows:
|
104
|
+
# Examples:
|
94
105
|
#
|
95
|
-
#
|
96
|
-
# Ohm.connect
|
106
|
+
# Ohm.connect(port: 6380, db: 1, host: "10.0.1.1")
|
107
|
+
# Ohm.connect(url: "redis://10.0.1.1:6380/1")
|
97
108
|
#
|
98
|
-
#
|
99
|
-
# ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
|
100
|
-
# Ohm.connect
|
109
|
+
# All of the options are simply passed on to `Redis.connect`.
|
101
110
|
#
|
102
|
-
|
103
|
-
|
104
|
-
#
|
105
|
-
# @example Connect to a database in port 6380.
|
106
|
-
# Ohm.connect(:port => 6380)
|
107
|
-
def self.connect(*options)
|
108
|
-
self.redis = nil
|
109
|
-
@options = options
|
111
|
+
def self.connect(options = {})
|
112
|
+
conn.start(options)
|
110
113
|
end
|
111
114
|
|
112
|
-
#
|
115
|
+
# Use this if you want to do quick ad hoc redis commands against the
|
116
|
+
# defined Ohm connection.
|
113
117
|
#
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
@options = [] unless defined? @options
|
122
|
-
@options
|
118
|
+
# Examples:
|
119
|
+
#
|
120
|
+
# Ohm.redis.keys("User:*")
|
121
|
+
# Ohm.redis.set("foo", "bar")
|
122
|
+
#
|
123
|
+
def self.redis
|
124
|
+
conn.redis
|
123
125
|
end
|
124
126
|
|
125
|
-
#
|
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.
|
127
|
+
# Wrapper for Ohm.redis.flushdb.
|
130
128
|
def self.flush
|
131
129
|
redis.flushdb
|
132
130
|
end
|
133
131
|
|
134
|
-
#
|
135
|
-
|
136
|
-
|
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
|
155
|
-
class Model
|
156
|
-
|
157
|
-
# Wraps a model name for lazy evaluation.
|
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.
|
208
|
-
def initialize(name, &block)
|
209
|
-
@name = name
|
210
|
-
@caller = ::Kernel.caller[2]
|
211
|
-
@block = block
|
212
|
-
|
213
|
-
class << self
|
214
|
-
def method_missing(method_id, *args)
|
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
|
-
)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
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
|
251
|
-
def self.wrap(object)
|
252
|
-
object.class == self ? object : new(object.inspect) { object }
|
253
|
-
end
|
254
|
-
|
255
|
-
# Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
|
256
|
-
#
|
257
|
-
# @return [Class] The wrapped class.
|
258
|
-
def unwrap
|
259
|
-
@block.call
|
260
|
-
end
|
261
|
-
|
262
|
-
# Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
|
263
|
-
# to manually declare this.
|
264
|
-
#
|
265
|
-
# @return [Wrapper]
|
266
|
-
def class
|
267
|
-
Wrapper
|
268
|
-
end
|
132
|
+
# Defines most of the methods used by `Set` and `MultiSet`.
|
133
|
+
module Collection
|
134
|
+
include Enumerable
|
269
135
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
end
|
136
|
+
# Fetch the data from Redis in one go.
|
137
|
+
def to_a
|
138
|
+
fetch(ids)
|
274
139
|
end
|
275
140
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
class Collection
|
280
|
-
include Enumerable
|
281
|
-
|
282
|
-
# An instance of {Ohm::Key}.
|
283
|
-
attr :key
|
284
|
-
|
285
|
-
# A subclass of {Ohm::Model}.
|
286
|
-
attr :model
|
141
|
+
def each
|
142
|
+
to_a.each { |e| yield e }
|
143
|
+
end
|
287
144
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
@key = key
|
292
|
-
@model = model.unwrap
|
293
|
-
end
|
145
|
+
def empty?
|
146
|
+
size == 0
|
147
|
+
end
|
294
148
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
149
|
+
# Allows you to sort by any field in your model.
|
150
|
+
#
|
151
|
+
# Example:
|
152
|
+
#
|
153
|
+
# class User < Ohm::Model
|
154
|
+
# attribute :name
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# User.all.sort_by(:name, order: "ALPHA")
|
158
|
+
# User.all.sort_by(:name, order: "ALPHA DESC")
|
159
|
+
# User.all.sort_by(:name, order: "ALPHA DESC", limit: [0, 10])
|
160
|
+
#
|
161
|
+
# Note: This is slower compared to just doing `sort`, specifically
|
162
|
+
# because Redis has to read each individual hash in order to sort
|
163
|
+
# them.
|
164
|
+
#
|
165
|
+
def sort_by(att, options = {})
|
166
|
+
sort(options.merge(by: namespace["*->%s" % att]))
|
167
|
+
end
|
301
168
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
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 = {})
|
336
|
-
return [] unless key.exists
|
337
|
-
|
338
|
-
opts = options.dup
|
339
|
-
opts[:start] ||= 0
|
340
|
-
opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
|
341
|
-
|
342
|
-
key.sort(opts).map(&model)
|
169
|
+
# Allows you to sort your models using their IDs. This is much
|
170
|
+
# faster than `sort_by`. If you simply want to get records in
|
171
|
+
# ascending or descending order, then this is the best method to
|
172
|
+
# do that.
|
173
|
+
#
|
174
|
+
# Example:
|
175
|
+
#
|
176
|
+
# class User < Ohm::Model
|
177
|
+
# attribute :name
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# User.create(name: "John")
|
181
|
+
# User.create(name: "Jane")
|
182
|
+
#
|
183
|
+
# User.all.sort.map(&:id) == ["1", "2"]
|
184
|
+
# # => true
|
185
|
+
#
|
186
|
+
# User.all.sort(order: "ASC").map(&:id) == ["1", "2"]
|
187
|
+
# # => true
|
188
|
+
#
|
189
|
+
# User.all.sort(order: "DESC").map(&:id) == ["2", "1"]
|
190
|
+
# # => true
|
191
|
+
#
|
192
|
+
def sort(options = {})
|
193
|
+
if options.has_key?(:get)
|
194
|
+
options[:get] = namespace["*->%s" % options[:get]]
|
195
|
+
return execute { |key| key.sort(options) }
|
343
196
|
end
|
344
197
|
|
345
|
-
|
346
|
-
|
347
|
-
# @example Sorting elements by name:
|
348
|
-
#
|
349
|
-
# User.create :name => "B"
|
350
|
-
# User.create :name => "A"
|
351
|
-
#
|
352
|
-
# user = User.all.sort_by(:name, :order => "ALPHA").first
|
353
|
-
# user.name == "A"
|
354
|
-
# # => true
|
355
|
-
#
|
356
|
-
# @see file:README.html#sorting Sorting in the README.
|
357
|
-
def sort_by(att, options = {})
|
358
|
-
return [] unless key.exists
|
359
|
-
|
360
|
-
opts = options.dup
|
361
|
-
opts.merge!(:by => model.key["*->#{att}"])
|
362
|
-
|
363
|
-
if opts[:get]
|
364
|
-
key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
|
365
|
-
else
|
366
|
-
sort(opts)
|
367
|
-
end
|
368
|
-
end
|
198
|
+
fetch(execute { |key| key.sort(options) })
|
199
|
+
end
|
369
200
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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.
|
392
|
-
def clear
|
393
|
-
key.del
|
394
|
-
end
|
201
|
+
# Check if a model is included in this set.
|
202
|
+
#
|
203
|
+
# Example:
|
204
|
+
#
|
205
|
+
# u = User.create
|
206
|
+
#
|
207
|
+
# User.all.include?(u)
|
208
|
+
# # => true
|
209
|
+
#
|
210
|
+
# Note: Ohm simply checks that the model's ID is included in the
|
211
|
+
# set. It doesn't do any form of type checking.
|
212
|
+
#
|
213
|
+
def include?(model)
|
214
|
+
exists?(model.id)
|
215
|
+
end
|
395
216
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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.
|
422
|
-
def replace(models)
|
423
|
-
model.db.multi do
|
424
|
-
clear
|
425
|
-
models.each { |model| add(model) }
|
426
|
-
end
|
427
|
-
end
|
217
|
+
# Returns the total size of the set using SCARD.
|
218
|
+
def size
|
219
|
+
execute { |key| key.scard }
|
220
|
+
end
|
428
221
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
222
|
+
# Syntactic sugar for `sort_by` or `sort` when you only need the
|
223
|
+
# first element.
|
224
|
+
#
|
225
|
+
# Example:
|
226
|
+
#
|
227
|
+
# User.all.first ==
|
228
|
+
# User.all.sort(limit: [0, 1]).first
|
229
|
+
#
|
230
|
+
# User.all.first(by: :name, "ALPHA") ==
|
231
|
+
# User.all.sort_by(:name, order: "ALPHA", limit: [0, 1]).first
|
232
|
+
#
|
233
|
+
def first(options = {})
|
234
|
+
opts = options.dup
|
235
|
+
opts.merge!(limit: [0, 1])
|
433
236
|
|
434
|
-
|
435
|
-
|
436
|
-
|
237
|
+
if opts[:by]
|
238
|
+
sort_by(opts.delete(:by), opts).first
|
239
|
+
else
|
240
|
+
sort(opts).first
|
437
241
|
end
|
438
242
|
end
|
439
243
|
|
440
|
-
#
|
441
|
-
|
442
|
-
|
443
|
-
|
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.
|
472
|
-
def each(&block)
|
473
|
-
key.smembers.each { |id| block.call(model.to_proc[id]) }
|
474
|
-
end
|
244
|
+
# Grab all the elements of this set using SMEMBERS.
|
245
|
+
def ids
|
246
|
+
execute { |key| key.smembers }
|
247
|
+
end
|
475
248
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
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.
|
502
|
-
def [](id)
|
503
|
-
model[id] if key.sismember(id)
|
504
|
-
end
|
249
|
+
# Retrieve a specific element using an ID from this set.
|
250
|
+
#
|
251
|
+
# Example:
|
252
|
+
#
|
253
|
+
# # Let's say we got the ID 1 from a request parameter.
|
254
|
+
# id = 1
|
255
|
+
#
|
256
|
+
# # Retrieve the post if it's included in the user's posts.
|
257
|
+
# post = user.posts[id]
|
258
|
+
#
|
259
|
+
def [](id)
|
260
|
+
model[id] if exists?(id)
|
261
|
+
end
|
505
262
|
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
# @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis
|
511
|
-
# Command Reference.
|
512
|
-
def <<(model)
|
513
|
-
key.sadd(model.id)
|
514
|
-
end
|
515
|
-
alias add <<
|
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.
|
522
|
-
def size
|
523
|
-
key.scard
|
524
|
-
end
|
263
|
+
private
|
264
|
+
def exists?(id)
|
265
|
+
execute { |key| key.sismember(id) }
|
266
|
+
end
|
525
267
|
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
# @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis
|
530
|
-
# Command Reference.
|
531
|
-
def delete(member)
|
532
|
-
key.srem(member.id)
|
268
|
+
def fetch(ids)
|
269
|
+
arr = model.db.pipelined do
|
270
|
+
ids.each { |id| namespace[id].hgetall }
|
533
271
|
end
|
534
272
|
|
535
|
-
|
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.
|
560
|
-
def all
|
561
|
-
key.smembers.map(&model)
|
562
|
-
end
|
273
|
+
return [] if arr.nil?
|
563
274
|
|
564
|
-
|
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.
|
601
|
-
def find(options)
|
602
|
-
source = keys(options)
|
603
|
-
target = source.inject(key.volatile) { |chain, other| chain + other }
|
604
|
-
apply(:sinterstore, key, source, target)
|
275
|
+
arr.map.with_index do |atts, idx|
|
276
|
+
model.new(atts.update(id: ids[idx]))
|
605
277
|
end
|
278
|
+
end
|
279
|
+
end
|
606
280
|
|
607
|
-
|
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.
|
625
|
-
def except(options)
|
626
|
-
source = keys(options)
|
627
|
-
target = source.inject(key.volatile) { |chain, other| chain - other }
|
628
|
-
apply(:sdiffstore, key, source, target)
|
629
|
-
end
|
281
|
+
class Set < Struct.new(:key, :namespace, :model)
|
282
|
+
include Collection
|
630
283
|
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
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)
|
671
|
-
|
672
|
-
if opts[:by]
|
673
|
-
sort_by(opts.delete(:by), opts).first
|
674
|
-
else
|
675
|
-
sort(opts).first
|
676
|
-
end
|
677
|
-
end
|
284
|
+
# Add a model directly to the set.
|
285
|
+
#
|
286
|
+
# Example:
|
287
|
+
#
|
288
|
+
# user = User.create
|
289
|
+
# post = Post.create
|
290
|
+
#
|
291
|
+
# user.posts.add(post)
|
292
|
+
#
|
293
|
+
def add(model)
|
294
|
+
key.sadd(model.id)
|
295
|
+
end
|
678
296
|
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
key.sismember(model.id)
|
690
|
-
end
|
297
|
+
# Chain new fiters on an existing set.
|
298
|
+
#
|
299
|
+
# Example:
|
300
|
+
#
|
301
|
+
# set = User.find(name: "John")
|
302
|
+
# set.find(age: 30)
|
303
|
+
#
|
304
|
+
def find(dict)
|
305
|
+
keys = model.filters(dict)
|
306
|
+
keys.push(key)
|
691
307
|
|
692
|
-
|
693
|
-
|
694
|
-
end
|
308
|
+
MultiSet.new(keys, namespace, model)
|
309
|
+
end
|
695
310
|
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
311
|
+
# Replace all the existing elements of a set with a different
|
312
|
+
# collection of models. This happens atomically in a MULTI-EXEC
|
313
|
+
# block.
|
314
|
+
#
|
315
|
+
# Example:
|
316
|
+
#
|
317
|
+
# user = User.create
|
318
|
+
# p1 = Post.create
|
319
|
+
# user.posts.add(p1)
|
320
|
+
#
|
321
|
+
# p2, p3 = Post.create, Post.create
|
322
|
+
# user.posts.replace([p2, p3])
|
323
|
+
#
|
324
|
+
# user.posts.include?(p1)
|
325
|
+
# # => false
|
326
|
+
#
|
327
|
+
def replace(models)
|
328
|
+
ids = models.map { |model| model.id }
|
702
329
|
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
def keys(hash)
|
707
|
-
[].tap do |keys|
|
708
|
-
hash.each do |key, values|
|
709
|
-
values = [values] unless values.kind_of?(Array)
|
710
|
-
values.each do |v|
|
711
|
-
keys << model.index_key_for(key, v)
|
712
|
-
end
|
713
|
-
end
|
714
|
-
end
|
330
|
+
key.redis.multi do
|
331
|
+
key.del
|
332
|
+
ids.each { |id| key.sadd(id) }
|
715
333
|
end
|
716
334
|
end
|
717
335
|
|
718
|
-
|
719
|
-
|
720
|
-
|
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.
|
747
|
-
def find(options)
|
748
|
-
keys = keys(options)
|
749
|
-
return super(options) if keys.size > 1
|
750
|
-
|
751
|
-
Set.new(keys.first, Wrapper.wrap(model))
|
752
|
-
end
|
336
|
+
private
|
337
|
+
def execute
|
338
|
+
yield key
|
753
339
|
end
|
340
|
+
end
|
754
341
|
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
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.
|
786
|
-
def each(&block)
|
787
|
-
key.lrange(0, -1).each { |id| block.call(model.to_proc[id]) }
|
788
|
-
end
|
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.
|
808
|
-
def <<(model)
|
809
|
-
key.rpush(model.id)
|
810
|
-
end
|
811
|
-
alias push <<
|
812
|
-
|
813
|
-
# Returns the element at index, or returns a subarray starting at
|
814
|
-
# `start` and continuing for `length` elements, or returns a subarray
|
815
|
-
# specified by `range`. Negative indices count backward from the end
|
816
|
-
# of the array (-1 is the last element). Returns nil if the index
|
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.
|
845
|
-
def [](index, limit = nil)
|
846
|
-
case [index, limit]
|
847
|
-
when Pattern[Fixnum, Fixnum] then
|
848
|
-
key.lrange(index, limit).collect { |id| model.to_proc[id] }
|
849
|
-
when Pattern[Range, nil] then
|
850
|
-
key.lrange(index.first, index.last).collect { |id| model.to_proc[id] }
|
851
|
-
when Pattern[Fixnum, nil] then
|
852
|
-
model[key.lindex(index)]
|
853
|
-
end
|
854
|
-
end
|
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.
|
861
|
-
def first
|
862
|
-
self[0]
|
863
|
-
end
|
342
|
+
# Anytime you filter a set with more than one requirement, you
|
343
|
+
# internally use a `MultiSet`. `MutiSet` is a bit slower than just
|
344
|
+
# a `Set` because it has to `SINTERSTORE` all the keys prior to
|
345
|
+
# retrieving the members, size, etc.
|
346
|
+
#
|
347
|
+
# Example:
|
348
|
+
#
|
349
|
+
# User.all.kind_of?(Ohm::Set)
|
350
|
+
# # => true
|
351
|
+
#
|
352
|
+
# User.find(name: "John").kind_of?(Ohm::Set)
|
353
|
+
# # => true
|
354
|
+
#
|
355
|
+
# User.find(name: "John", age: 30).kind_of?(Ohm::MultiSet)
|
356
|
+
# # => true
|
357
|
+
#
|
358
|
+
class MultiSet < Struct.new(:keys, :namespace, :model)
|
359
|
+
include Collection
|
864
360
|
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
end
|
361
|
+
# Chain new fiters on an existing set.
|
362
|
+
#
|
363
|
+
# Example:
|
364
|
+
#
|
365
|
+
# set = User.find(name: "John", age: 30)
|
366
|
+
# set.find(status: 'pending')
|
367
|
+
#
|
368
|
+
def find(dict)
|
369
|
+
keys = model.filters(dict)
|
370
|
+
keys.push(*self.keys)
|
876
371
|
|
877
|
-
|
878
|
-
|
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.
|
885
|
-
def shift
|
886
|
-
model[key.lpop]
|
887
|
-
end
|
372
|
+
MultiSet.new(keys, namespace, model)
|
373
|
+
end
|
888
374
|
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
# @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH
|
894
|
-
# in Redis Command Reference.
|
895
|
-
def unshift(model)
|
896
|
-
key.lpush(model.id)
|
897
|
-
end
|
375
|
+
private
|
376
|
+
def execute
|
377
|
+
key = namespace[:temp][SecureRandom.uuid]
|
378
|
+
key.sinterstore(*keys)
|
898
379
|
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
def all
|
904
|
-
key.lrange(0, -1).map(&model)
|
380
|
+
begin
|
381
|
+
yield key
|
382
|
+
ensure
|
383
|
+
key.del
|
905
384
|
end
|
385
|
+
end
|
386
|
+
end
|
906
387
|
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
388
|
+
# The base class for all your models. In order to better understand
|
389
|
+
# it, here is a semi-realtime explanation of the details involved
|
390
|
+
# when creating a User instance.
|
391
|
+
#
|
392
|
+
# Example:
|
393
|
+
#
|
394
|
+
# class User < Ohm::Model
|
395
|
+
# attribute :name
|
396
|
+
# index :name
|
397
|
+
#
|
398
|
+
# attribute :email
|
399
|
+
# unique :email
|
400
|
+
#
|
401
|
+
# counter :points
|
402
|
+
#
|
403
|
+
# set :posts, :Post
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
# u = User.create(name: "John", email: "foo@bar.com")
|
407
|
+
# u.incr :points
|
408
|
+
# u.posts.add(Post.create)
|
409
|
+
#
|
410
|
+
# When you execute `User.create(...)`, you run the following Redis
|
411
|
+
# commands:
|
412
|
+
#
|
413
|
+
# # Generate an ID
|
414
|
+
# INCR User:id
|
415
|
+
#
|
416
|
+
# # Add the newly generated ID, (let's assume the ID is 1).
|
417
|
+
# SADD User:all 1
|
418
|
+
#
|
419
|
+
# # Store the unique index
|
420
|
+
# HSET User:uniques:email foo@bar.com 1
|
421
|
+
#
|
422
|
+
# # Store the name index
|
423
|
+
# SADD User:indices:name:John 1
|
424
|
+
#
|
425
|
+
# # Store the HASH
|
426
|
+
# HMSET User:1 name John email foo@bar.com
|
427
|
+
#
|
428
|
+
# Next we increment points:
|
429
|
+
#
|
430
|
+
# HINCR User:1:counters points 1
|
431
|
+
#
|
432
|
+
# And then we add a Post to the `posts` set.
|
433
|
+
# (For brevity, let's assume the Post created has an ID of 1).
|
434
|
+
#
|
435
|
+
# SADD User:1:posts 1
|
436
|
+
#
|
437
|
+
class Model
|
438
|
+
include Scrivener::Validations
|
916
439
|
|
917
|
-
|
918
|
-
|
919
|
-
|
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.
|
926
|
-
def include?(model)
|
927
|
-
key.lrange(0, -1).include?(model.id)
|
928
|
-
end
|
440
|
+
def self.conn
|
441
|
+
@conn ||= Connection.new(name, Ohm.conn.options)
|
442
|
+
end
|
929
443
|
|
930
|
-
|
931
|
-
|
932
|
-
|
444
|
+
def self.connect(options)
|
445
|
+
@key = nil
|
446
|
+
@lua = nil
|
447
|
+
conn.start(options)
|
933
448
|
end
|
934
449
|
|
935
|
-
|
936
|
-
|
937
|
-
# assertion contained within this module.
|
938
|
-
module Validations
|
939
|
-
include Ohm::Validations
|
940
|
-
|
941
|
-
# Validates that the attribute or array of attributes are unique. For
|
942
|
-
# this, an index of the same kind must exist.
|
943
|
-
#
|
944
|
-
# @overload assert_unique :name
|
945
|
-
# Validates that the name attribute is unique.
|
946
|
-
# @overload assert_unique [:street, :city]
|
947
|
-
# Validates that the :street and :city pair is unique.
|
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
|
953
|
-
end
|
450
|
+
def self.db
|
451
|
+
conn.redis
|
954
452
|
end
|
955
453
|
|
956
|
-
|
454
|
+
def self.lua
|
455
|
+
@lua ||= Lua.new(File.join(Dir.pwd, "lua"), db)
|
456
|
+
end
|
957
457
|
|
958
|
-
#
|
458
|
+
# The namespace for all the keys generated using this model.
|
959
459
|
#
|
960
|
-
#
|
961
|
-
# list :comments, Comment
|
962
|
-
# end
|
963
|
-
#
|
964
|
-
# class Comment < Ohm::Model
|
965
|
-
# end
|
460
|
+
# Example:
|
966
461
|
#
|
967
|
-
#
|
968
|
-
# begin
|
969
|
-
# Post.new.id
|
970
|
-
# rescue Exception => e
|
971
|
-
# ex = e
|
972
|
-
# end
|
462
|
+
# class User < Ohm::Model
|
973
463
|
#
|
974
|
-
#
|
464
|
+
# User.key == "User"
|
465
|
+
# User.key.kind_of?(String)
|
975
466
|
# # => true
|
976
467
|
#
|
977
|
-
#
|
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)
|
468
|
+
# User.key.kind_of?(Nest)
|
993
469
|
# # => true
|
994
470
|
#
|
995
|
-
#
|
996
|
-
#
|
471
|
+
# To find out more about Nest, see:
|
472
|
+
# http://github.com/soveran/nest
|
997
473
|
#
|
998
|
-
|
999
|
-
|
1000
|
-
# end
|
1001
|
-
class MissingID < Error
|
1002
|
-
def message
|
1003
|
-
"You tried to perform an operation that needs the model ID, " +
|
1004
|
-
"but it's not present."
|
1005
|
-
end
|
474
|
+
def self.key
|
475
|
+
@key ||= Nest.new(self.name, db)
|
1006
476
|
end
|
1007
477
|
|
1008
|
-
#
|
1009
|
-
# a key which you did not define as an index.
|
478
|
+
# Retrieve a record by ID.
|
1010
479
|
#
|
1011
|
-
#
|
1012
|
-
# attribute :title
|
1013
|
-
# end
|
480
|
+
# Example:
|
1014
481
|
#
|
1015
|
-
#
|
482
|
+
# u = User.create
|
483
|
+
# u == User[u.id]
|
484
|
+
# # => true
|
1016
485
|
#
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
#
|
1022
|
-
# end
|
486
|
+
def self.[](id)
|
487
|
+
new(id: id).load! if id && exists?(id)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Retrieve a set of models given an array of IDs.
|
1023
491
|
#
|
1024
|
-
#
|
1025
|
-
# # => true
|
492
|
+
# Example:
|
1026
493
|
#
|
1027
|
-
#
|
494
|
+
# ids = [1, 2, 3]
|
495
|
+
# ids.map(&User)
|
1028
496
|
#
|
1029
|
-
#
|
1030
|
-
#
|
1031
|
-
#
|
1032
|
-
#
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
end
|
1037
|
-
|
1038
|
-
def message
|
1039
|
-
"Index #{@att.inspect} not found."
|
1040
|
-
end
|
497
|
+
# Note: The use of this should be a last resort for your actual
|
498
|
+
# application runtime, or for simply debugging in your console. If
|
499
|
+
# you care about performance, you should pipeline your reads. For
|
500
|
+
# more information checkout the implementation of Ohm::Set#fetch.
|
501
|
+
#
|
502
|
+
def self.to_proc
|
503
|
+
lambda { |id| self[id] }
|
1041
504
|
end
|
1042
505
|
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
@@indices = Hash.new { |hash, key| hash[key] = [] }
|
1047
|
-
|
1048
|
-
def id
|
1049
|
-
@id or raise MissingID
|
506
|
+
# Check if the ID exists within <Model>:all.
|
507
|
+
def self.exists?(id)
|
508
|
+
key[:all].sismember(id)
|
1050
509
|
end
|
1051
510
|
|
1052
|
-
#
|
1053
|
-
# persisted by _Redis_ as a string. Any value stored here will be
|
1054
|
-
# retrieved in its string representation.
|
511
|
+
# Find values in `unique` indices.
|
1055
512
|
#
|
1056
|
-
#
|
1057
|
-
# Ohm::Typecast in Ohm::Contrib.
|
513
|
+
# Example:
|
1058
514
|
#
|
1059
|
-
#
|
1060
|
-
#
|
1061
|
-
|
1062
|
-
define_method(name) do
|
1063
|
-
read_local(name)
|
1064
|
-
end
|
1065
|
-
|
1066
|
-
define_method(:"#{name}=") do |value|
|
1067
|
-
write_local(name, value)
|
1068
|
-
end
|
1069
|
-
|
1070
|
-
attributes << name unless attributes.include?(name)
|
1071
|
-
end
|
1072
|
-
|
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.
|
515
|
+
# class User < Ohm::Model
|
516
|
+
# unique :email
|
517
|
+
# end
|
1075
518
|
#
|
1076
|
-
#
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
519
|
+
# u = User.create(email: "foo@bar.com")
|
520
|
+
# u == User.with(:email, "foo@bar.com")
|
521
|
+
# # => true
|
522
|
+
#
|
523
|
+
def self.with(att, val)
|
524
|
+
id = key[:uniques][att].hget(val)
|
525
|
+
id && self[id]
|
1083
526
|
end
|
1084
527
|
|
1085
|
-
#
|
1086
|
-
# the model instance is created, or if you assign an :id during object
|
1087
|
-
# construction.
|
528
|
+
# Find values in indexed fields.
|
1088
529
|
#
|
1089
|
-
#
|
530
|
+
# Example:
|
1090
531
|
#
|
1091
|
-
# class
|
1092
|
-
#
|
1093
|
-
# end
|
532
|
+
# class User < Ohm::Model
|
533
|
+
# attribute :email
|
1094
534
|
#
|
1095
|
-
#
|
535
|
+
# attribute :name
|
536
|
+
# index :name
|
537
|
+
#
|
538
|
+
# attribute :status
|
539
|
+
# index :status
|
540
|
+
#
|
541
|
+
# index :provider
|
542
|
+
# index :tag
|
543
|
+
#
|
544
|
+
# def provider
|
545
|
+
# email[/@(.*?).com/, 1]
|
546
|
+
# end
|
547
|
+
#
|
548
|
+
# def tag
|
549
|
+
# ["ruby", "python"]
|
550
|
+
# end
|
1096
551
|
# end
|
1097
552
|
#
|
1098
|
-
#
|
1099
|
-
#
|
1100
|
-
#
|
553
|
+
# u = User.create(name: "John", status: "pending", email: "foo@me.com")
|
554
|
+
# User.find(provider: "me", name: "John", status: "pending").include?(u)
|
555
|
+
# # => true
|
1101
556
|
#
|
1102
|
-
#
|
1103
|
-
#
|
1104
|
-
# post.comments << Comment.create
|
557
|
+
# User.find(tag: "ruby").include?(u)
|
558
|
+
# # => true
|
1105
559
|
#
|
1106
|
-
#
|
1107
|
-
#
|
1108
|
-
# post.comments << Comment.create
|
1109
|
-
# post.create
|
560
|
+
# User.find(tag: "python").include?(u)
|
561
|
+
# # => true
|
1110
562
|
#
|
1111
|
-
#
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
563
|
+
# User.find(tag: ["ruby", "python"]).include?(u)
|
564
|
+
# # => true
|
565
|
+
#
|
566
|
+
def self.find(dict)
|
567
|
+
keys = filters(dict)
|
568
|
+
|
569
|
+
if keys.size == 1
|
570
|
+
Ohm::Set.new(keys.first, key, self)
|
571
|
+
else
|
572
|
+
Ohm::MultiSet.new(keys, key, self)
|
573
|
+
end
|
1115
574
|
end
|
1116
575
|
|
1117
|
-
#
|
1118
|
-
#
|
1119
|
-
|
1120
|
-
|
1121
|
-
#
|
1122
|
-
# @param [Symbol] name Name of the set.
|
1123
|
-
def self.set(name, model)
|
1124
|
-
define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
|
1125
|
-
collections << name unless collections.include?(name)
|
576
|
+
# Index any method on your model. Once you index a method, you can
|
577
|
+
# use it in `find` statements.
|
578
|
+
def self.index(attribute)
|
579
|
+
indices << attribute unless indices.include?(attribute)
|
1126
580
|
end
|
1127
581
|
|
1128
|
-
#
|
582
|
+
# Create a unique index for any method on your model. Once you add
|
583
|
+
# a unique index, you can use it in `with` statements.
|
584
|
+
#
|
585
|
+
# Note: if there is a conflict while saving, an
|
586
|
+
# `Ohm::UniqueIndexViolation` violation is raised.
|
1129
587
|
#
|
1130
|
-
|
1131
|
-
|
588
|
+
def self.unique(attribute)
|
589
|
+
uniques << attribute unless uniques.include?(attribute)
|
590
|
+
end
|
591
|
+
|
592
|
+
# Declare an Ohm::Set with the given name.
|
1132
593
|
#
|
1133
|
-
#
|
594
|
+
# Example:
|
1134
595
|
#
|
1135
596
|
# class User < Ohm::Model
|
1136
|
-
#
|
1137
|
-
# index :email
|
597
|
+
# set :posts, :Post
|
1138
598
|
# end
|
1139
599
|
#
|
1140
|
-
#
|
1141
|
-
#
|
600
|
+
# u = User.create
|
601
|
+
# u.posts.empty?
|
602
|
+
# # => true
|
603
|
+
#
|
604
|
+
# Note: You can't use the set until you save the model. If you try
|
605
|
+
# to do it, you'll receive an Ohm::MissingID error.
|
1142
606
|
#
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
607
|
+
def self.set(name, model)
|
608
|
+
collections << name unless collections.include?(name)
|
609
|
+
|
610
|
+
define_method name do
|
611
|
+
model = Utils.const(self.class, model)
|
612
|
+
|
613
|
+
Ohm::Set.new(key[name], model.key, model)
|
614
|
+
end
|
1146
615
|
end
|
1147
616
|
|
1148
|
-
#
|
617
|
+
# A macro for defining a method which basically does a find.
|
1149
618
|
#
|
1150
|
-
#
|
619
|
+
# Example:
|
620
|
+
# class Post < Ohm::Model
|
621
|
+
# reference :user, :User
|
622
|
+
# end
|
1151
623
|
#
|
1152
|
-
# class
|
1153
|
-
#
|
1154
|
-
# reference :post, Post
|
624
|
+
# class User < Ohm::Model
|
625
|
+
# collection :posts, :Post
|
1155
626
|
# end
|
1156
627
|
#
|
1157
|
-
#
|
628
|
+
# # is the same as
|
1158
629
|
#
|
1159
|
-
#
|
630
|
+
# class User < Ohm::Model
|
631
|
+
# def posts
|
632
|
+
# Post.find(user_id: self.id)
|
633
|
+
# end
|
634
|
+
# end
|
1160
635
|
#
|
1161
|
-
|
1162
|
-
|
636
|
+
def self.collection(name, model, reference = to_reference)
|
637
|
+
define_method name do
|
638
|
+
model = Utils.const(self.class, model)
|
639
|
+
model.find(:"#{reference}_id" => id)
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
# A macro for defining an attribute, an index, and an accessor
|
644
|
+
# for a given model.
|
1163
645
|
#
|
1164
|
-
#
|
646
|
+
# Example:
|
1165
647
|
#
|
1166
|
-
#
|
1167
|
-
#
|
648
|
+
# class Post < Ohm::Model
|
649
|
+
# reference :user, :User
|
650
|
+
# end
|
1168
651
|
#
|
1169
|
-
#
|
652
|
+
# # It's the same as:
|
1170
653
|
#
|
1171
|
-
#
|
1172
|
-
#
|
654
|
+
# class Post < Ohm::Model
|
655
|
+
# attribute :user_id
|
656
|
+
# index :user_id
|
1173
657
|
#
|
1174
|
-
#
|
658
|
+
# def user
|
659
|
+
# @_memo[:user] ||= User[user_id]
|
660
|
+
# end
|
1175
661
|
#
|
1176
|
-
#
|
1177
|
-
#
|
662
|
+
# def user=(user)
|
663
|
+
# self.user_id = user.id
|
664
|
+
# @_memo[:user] = user
|
665
|
+
# end
|
666
|
+
#
|
667
|
+
# def user_id=(user_id)
|
668
|
+
# @_memo.delete(:user_id)
|
669
|
+
# self.user_id = user_id
|
670
|
+
# end
|
671
|
+
# end
|
1178
672
|
#
|
1179
|
-
# @see file:README.html#references References Explained.
|
1180
|
-
# @see Ohm::Model.collection
|
1181
673
|
def self.reference(name, model)
|
1182
|
-
model = Wrapper.wrap(model)
|
1183
|
-
|
1184
674
|
reader = :"#{name}_id"
|
1185
675
|
writer = :"#{name}_id="
|
1186
676
|
|
1187
|
-
attributes << reader unless attributes.include?(reader)
|
1188
|
-
|
1189
677
|
index reader
|
1190
678
|
|
1191
|
-
|
1192
|
-
|
679
|
+
define_method(reader) do
|
680
|
+
@attributes[reader]
|
1193
681
|
end
|
1194
682
|
|
1195
|
-
define_method(
|
683
|
+
define_method(writer) do |value|
|
1196
684
|
@_memo.delete(name)
|
1197
|
-
|
685
|
+
@attributes[reader] = value
|
1198
686
|
end
|
1199
687
|
|
1200
|
-
define_method(
|
1201
|
-
|
688
|
+
define_method(:"#{name}=") do |value|
|
689
|
+
@_memo.delete(name)
|
690
|
+
send(writer, value ? value.id : nil)
|
1202
691
|
end
|
1203
692
|
|
1204
|
-
define_method(
|
1205
|
-
@_memo
|
1206
|
-
|
693
|
+
define_method(name) do
|
694
|
+
@_memo[name] ||= begin
|
695
|
+
model = Utils.const(self.class, model)
|
696
|
+
model[send(reader)]
|
697
|
+
end
|
1207
698
|
end
|
1208
699
|
end
|
1209
700
|
|
1210
|
-
#
|
1211
|
-
#
|
701
|
+
# The bread and butter macro of all models. Basically declares
|
702
|
+
# persisted attributes. All attributes are stored on the Redis
|
703
|
+
# hash.
|
1212
704
|
#
|
1213
|
-
#
|
1214
|
-
#
|
1215
|
-
#
|
705
|
+
# Example:
|
706
|
+
# class User < Ohm::Model
|
707
|
+
# attribute :name
|
1216
708
|
# end
|
1217
709
|
#
|
1218
|
-
#
|
1219
|
-
# attribute :content
|
1220
|
-
# collection :comments, Comment
|
1221
|
-
# reference :author, Person
|
1222
|
-
# end
|
710
|
+
# # It's the same as:
|
1223
711
|
#
|
1224
|
-
# class
|
1225
|
-
#
|
712
|
+
# class User < Ohm::Model
|
713
|
+
# def name
|
714
|
+
# @attributes[:name]
|
715
|
+
# end
|
1226
716
|
#
|
1227
|
-
#
|
1228
|
-
#
|
1229
|
-
#
|
717
|
+
# def name=(name)
|
718
|
+
# @attributes[:name] = name
|
719
|
+
# end
|
1230
720
|
# end
|
1231
721
|
#
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
722
|
+
def self.attribute(name, cast = nil)
|
723
|
+
if cast
|
724
|
+
define_method(name) do
|
725
|
+
cast[@attributes[name]]
|
726
|
+
end
|
727
|
+
else
|
728
|
+
define_method(name) do
|
729
|
+
@attributes[name]
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
define_method(:"#{name}=") do |value|
|
734
|
+
@attributes[name] = value
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
# Declare a counter. All the counters are internally stored in
|
739
|
+
# a different Redis hash, independent from the one that stores
|
740
|
+
# the model attributes. Counters are updated with the `incr` and
|
741
|
+
# `decr` methods, which interact directly with Redis. Their value
|
742
|
+
# can't be assigned as with regular attributes.
|
1236
743
|
#
|
1237
|
-
#
|
1238
|
-
# # => "Indeed!"
|
744
|
+
# Example:
|
1239
745
|
#
|
1240
|
-
#
|
1241
|
-
#
|
746
|
+
# class User < Ohm::Model
|
747
|
+
# counter :points
|
748
|
+
# end
|
1242
749
|
#
|
1243
|
-
#
|
1244
|
-
#
|
1245
|
-
# collection directly.
|
750
|
+
# u = User.create
|
751
|
+
# u.incr :points
|
1246
752
|
#
|
1247
|
-
#
|
1248
|
-
#
|
1249
|
-
# @param model [Constant] Model where the reference is defined.
|
1250
|
-
# @param reference [Symbol] Reference as defined in the associated
|
1251
|
-
# model.
|
753
|
+
# Ohm.redis.hget "User:1:counters", "points"
|
754
|
+
# # => 1
|
1252
755
|
#
|
1253
|
-
#
|
1254
|
-
|
1255
|
-
model = Wrapper.wrap(model)
|
1256
|
-
define_method(name) {
|
1257
|
-
model.unwrap.find(:"#{reference}_id" => send(:id))
|
1258
|
-
}
|
1259
|
-
end
|
1260
|
-
|
1261
|
-
# Used by {Ohm::Model.collection} to infer the reference.
|
756
|
+
# Note: You can't use counters until you save the model. If you
|
757
|
+
# try to do it, you'll receive an Ohm::MissingID error.
|
1262
758
|
#
|
1263
|
-
|
1264
|
-
# format, separated by underscores and demodulized.
|
1265
|
-
def self.to_reference
|
1266
|
-
name.to_s.
|
1267
|
-
match(/^(?:.*::)*(.*)$/)[1].
|
1268
|
-
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
1269
|
-
downcase.to_sym
|
1270
|
-
end
|
1271
|
-
|
1272
|
-
# @private
|
1273
|
-
def self.define_memoized_method(name, &block)
|
759
|
+
def self.counter(name)
|
1274
760
|
define_method(name) do
|
1275
|
-
|
761
|
+
return 0 if new?
|
762
|
+
|
763
|
+
key[:counters].hget(name).to_i
|
1276
764
|
end
|
1277
765
|
end
|
1278
766
|
|
1279
|
-
#
|
1280
|
-
|
1281
|
-
|
1282
|
-
# @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does
|
1283
|
-
# not exist.
|
1284
|
-
def self.[](id)
|
1285
|
-
new(:id => id) if id && exists?(id)
|
767
|
+
# An Ohm::Set wrapper for Model.key[:all].
|
768
|
+
def self.all
|
769
|
+
Set.new(key[:all], key, self)
|
1286
770
|
end
|
1287
771
|
|
1288
|
-
#
|
1289
|
-
def self.
|
1290
|
-
|
772
|
+
# Syntactic sugar for Model.new(atts).save
|
773
|
+
def self.create(atts = {})
|
774
|
+
new(atts).save
|
1291
775
|
end
|
1292
776
|
|
1293
|
-
#
|
1294
|
-
# class.
|
777
|
+
# Manipulate the Redis hash of attributes directly.
|
1295
778
|
#
|
1296
|
-
#
|
779
|
+
# Example:
|
1297
780
|
#
|
1298
|
-
# class
|
781
|
+
# class User < Ohm::Model
|
782
|
+
# attribute :name
|
1299
783
|
# end
|
1300
784
|
#
|
1301
|
-
#
|
1302
|
-
#
|
1303
|
-
#
|
1304
|
-
# # => true
|
785
|
+
# u = User.create(name: "John")
|
786
|
+
# u.key.hget(:name)
|
787
|
+
# # => John
|
1305
788
|
#
|
1306
|
-
#
|
789
|
+
# For more details see
|
790
|
+
# http://github.com/soveran/nest
|
1307
791
|
#
|
1308
|
-
|
1309
|
-
|
1310
|
-
def self.all
|
1311
|
-
Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
|
1312
|
-
end
|
1313
|
-
|
1314
|
-
# All the defined attributes within a class.
|
1315
|
-
# @see Ohm::Model.attribute
|
1316
|
-
def self.attributes
|
1317
|
-
@@attributes[self]
|
1318
|
-
end
|
1319
|
-
|
1320
|
-
# All the defined counters within a class.
|
1321
|
-
# @see Ohm::Model.counter
|
1322
|
-
def self.counters
|
1323
|
-
@@counters[self]
|
792
|
+
def key
|
793
|
+
model.key[id]
|
1324
794
|
end
|
1325
795
|
|
1326
|
-
#
|
1327
|
-
# all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within
|
1328
|
-
# your class.
|
796
|
+
# Initialize a model using a dictionary of attributes.
|
1329
797
|
#
|
1330
|
-
#
|
1331
|
-
# class Post < Ohm::Model
|
1332
|
-
# set :authors, Author
|
1333
|
-
# list :comments, Comment
|
1334
|
-
# end
|
798
|
+
# Example:
|
1335
799
|
#
|
1336
|
-
#
|
1337
|
-
# # => true
|
800
|
+
# u = User.new(name: "John")
|
1338
801
|
#
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
end
|
1344
|
-
|
1345
|
-
# All the defined indices within a class.
|
1346
|
-
# @see Ohm::Model.index
|
1347
|
-
def self.indices
|
1348
|
-
@@indices[self]
|
802
|
+
def initialize(atts = {})
|
803
|
+
@attributes = {}
|
804
|
+
@_memo = {}
|
805
|
+
update_attributes(atts)
|
1349
806
|
end
|
1350
807
|
|
1351
|
-
#
|
808
|
+
# Access the ID used to store this model. The ID is used together
|
809
|
+
# with the name of the class in order to form the Redis key.
|
1352
810
|
#
|
1353
|
-
#
|
811
|
+
# Example:
|
1354
812
|
#
|
1355
|
-
# class
|
1356
|
-
# attribute :title
|
1357
|
-
# end
|
813
|
+
# class User < Ohm::Model; end
|
1358
814
|
#
|
1359
|
-
#
|
815
|
+
# u = User.create
|
816
|
+
# u.id
|
817
|
+
# # => 1
|
1360
818
|
#
|
1361
|
-
#
|
1362
|
-
#
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
819
|
+
# u.key
|
820
|
+
# # => User:1
|
821
|
+
#
|
822
|
+
def id
|
823
|
+
raise MissingID if not defined?(@id)
|
824
|
+
@id
|
1367
825
|
end
|
1368
826
|
|
1369
|
-
#
|
827
|
+
# Check for equality by doing the following assertions:
|
1370
828
|
#
|
1371
|
-
#
|
1372
|
-
#
|
1373
|
-
# event2 = Event.create day: "2009-09-09", author: "Benoit"
|
1374
|
-
# event3 = Event.create day: "2009-09-10", author: "Albert"
|
829
|
+
# 1. That the passed model is of the same type.
|
830
|
+
# 2. That they represent the same Redis key.
|
1375
831
|
#
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
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
|
-
|
1385
|
-
all.find(hash)
|
832
|
+
def ==(other)
|
833
|
+
other.kind_of?(model) && other.key == key
|
834
|
+
rescue MissingID
|
835
|
+
false
|
1386
836
|
end
|
1387
837
|
|
1388
|
-
#
|
1389
|
-
#
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
# @see Ohm::Model.index_key_for
|
1394
|
-
def self.encode(value)
|
1395
|
-
Base64.encode64(value.to_s).gsub("\n", "")
|
838
|
+
# Preload all the attributes of this model from Redis. Used
|
839
|
+
# internally by `Model::[]`.
|
840
|
+
def load!
|
841
|
+
update_attributes(key.hgetall) unless new?
|
842
|
+
return self
|
1396
843
|
end
|
1397
844
|
|
1398
|
-
#
|
1399
|
-
#
|
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
|
845
|
+
# Read an attribute remotly from Redis. Useful if you want to get
|
846
|
+
# the most recent value of the attribute and not rely on locally
|
847
|
+
# cached value.
|
1414
848
|
#
|
1415
|
-
#
|
1416
|
-
# p1 = Post.new(:title => "Redis", :user_id => user.id)
|
1417
|
-
# p1.save
|
849
|
+
# Example:
|
1418
850
|
#
|
1419
|
-
#
|
1420
|
-
# # => true
|
851
|
+
# User.create(name: "A")
|
1421
852
|
#
|
1422
|
-
#
|
1423
|
-
#
|
853
|
+
# Session 1 | Session 2
|
854
|
+
# --------------|------------------------
|
855
|
+
# u = User[1] | u = User[1]
|
856
|
+
# u.name = "B" |
|
857
|
+
# u.save |
|
858
|
+
# | u.name == "A"
|
859
|
+
# | u.get(:name) == "B"
|
1424
860
|
#
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
861
|
+
def get(att)
|
862
|
+
@attributes[att] = key.hget(att)
|
863
|
+
end
|
864
|
+
|
865
|
+
# Update an attribute value atomically. The best usecase for this
|
866
|
+
# is when you simply want to update one value.
|
1429
867
|
#
|
1430
|
-
#
|
1431
|
-
#
|
1432
|
-
# p2 == Post["ohm-redis-library"]
|
1433
|
-
# # => true
|
868
|
+
# Note: This method is dangerous because it doesn't update indices
|
869
|
+
# and uniques. Use it wisely. The safe equivalent is `update`.
|
1434
870
|
#
|
1435
|
-
|
1436
|
-
|
1437
|
-
@
|
1438
|
-
@_memo = {}
|
1439
|
-
@_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) }
|
1440
|
-
update_attributes(attrs)
|
871
|
+
def set(att, val)
|
872
|
+
val.to_s.empty? ? key.hdel(att) : key.hset(att, val)
|
873
|
+
@attributes[att] = val
|
1441
874
|
end
|
1442
875
|
|
1443
|
-
# @return [true, false] Whether or not this object has an id.
|
1444
876
|
def new?
|
1445
|
-
|
877
|
+
!defined?(@id)
|
1446
878
|
end
|
1447
879
|
|
1448
|
-
#
|
1449
|
-
|
1450
|
-
|
1451
|
-
# validation.
|
1452
|
-
def create
|
1453
|
-
return unless valid?
|
1454
|
-
initialize_id
|
1455
|
-
|
1456
|
-
mutex do
|
1457
|
-
create_model_membership
|
1458
|
-
write
|
1459
|
-
add_to_indices
|
1460
|
-
end
|
880
|
+
# Increment a counter atomically. Internally uses HINCRBY.
|
881
|
+
def incr(att, count = 1)
|
882
|
+
key[:counters].hincrby(att, count)
|
1461
883
|
end
|
1462
884
|
|
1463
|
-
#
|
1464
|
-
|
1465
|
-
|
1466
|
-
# validation.
|
1467
|
-
def save
|
1468
|
-
return create if new?
|
1469
|
-
return unless valid?
|
1470
|
-
|
1471
|
-
mutex do
|
1472
|
-
write
|
1473
|
-
update_indices
|
1474
|
-
end
|
885
|
+
# Decrement a counter atomically. Internally uses HINCRBY.
|
886
|
+
def decr(att, count = 1)
|
887
|
+
incr(att, -count)
|
1475
888
|
end
|
1476
889
|
|
1477
|
-
#
|
890
|
+
# Return a value that allows the use of models as hash keys.
|
1478
891
|
#
|
1479
|
-
#
|
1480
|
-
# version
|
1481
|
-
# @return [Ohm::Model, nil] The updated object or nil if it fails
|
1482
|
-
# validation.
|
1483
|
-
def update(attrs)
|
1484
|
-
update_attributes(attrs)
|
1485
|
-
save
|
1486
|
-
end
|
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.
|
892
|
+
# Example:
|
1491
893
|
#
|
1492
|
-
#
|
1493
|
-
def update_attributes(attrs)
|
1494
|
-
attrs.each do |key, value|
|
1495
|
-
send(:"#{key}=", value)
|
1496
|
-
end
|
1497
|
-
end
|
1498
|
-
|
1499
|
-
# Delete this object from the _Redis_ datastore, ensuring that all
|
1500
|
-
# indices, attributes, collections, etc are also deleted with it.
|
894
|
+
# h = {}
|
1501
895
|
#
|
1502
|
-
#
|
1503
|
-
def delete
|
1504
|
-
delete_from_indices
|
1505
|
-
delete_attributes(collections) unless collections.empty?
|
1506
|
-
delete_model_membership
|
1507
|
-
self
|
1508
|
-
end
|
1509
|
-
|
1510
|
-
# Increment the counter denoted by :att.
|
896
|
+
# u = User.new
|
1511
897
|
#
|
1512
|
-
#
|
1513
|
-
#
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
write_local(att, key.hincrby(att, count))
|
898
|
+
# h[:u] = u
|
899
|
+
# h[:u] == u
|
900
|
+
# # => true
|
901
|
+
#
|
902
|
+
def hash
|
903
|
+
new? ? super : key.hash
|
1520
904
|
end
|
905
|
+
alias :eql? :==
|
1521
906
|
|
1522
|
-
|
1523
|
-
|
1524
|
-
# @param [Symbol] att Attribute to decrement.
|
1525
|
-
# @param [Fixnum] count An optional decrement step to use.
|
1526
|
-
def decr(att, count = 1)
|
1527
|
-
incr(att, -count)
|
907
|
+
def attributes
|
908
|
+
@attributes
|
1528
909
|
end
|
1529
910
|
|
1530
|
-
# Export the
|
1531
|
-
#
|
1532
|
-
#
|
911
|
+
# Export the ID and the errors of the model. The approach of Ohm
|
912
|
+
# is to whitelist public attributes, as opposed to exporting each
|
913
|
+
# (possibly sensitive) attribute.
|
1533
914
|
#
|
1534
|
-
#
|
915
|
+
# Example:
|
1535
916
|
#
|
1536
|
-
#
|
1537
|
-
#
|
1538
|
-
#
|
917
|
+
# class User < Ohm::Model
|
918
|
+
# attribute :name
|
919
|
+
# end
|
1539
920
|
#
|
1540
|
-
#
|
1541
|
-
#
|
1542
|
-
#
|
1543
|
-
# person.valid?
|
1544
|
-
# # => false
|
921
|
+
# u = User.create(name: "John")
|
922
|
+
# u.to_hash
|
923
|
+
# # => { id: "1" }
|
1545
924
|
#
|
1546
|
-
#
|
1547
|
-
# # => true
|
925
|
+
# In order to add additional attributes, you can override `to_hash`:
|
1548
926
|
#
|
1549
|
-
#
|
927
|
+
# class User < Ohm::Model
|
928
|
+
# attribute :name
|
1550
929
|
#
|
1551
|
-
# class Person < Ohm::Model
|
1552
930
|
# def to_hash
|
1553
|
-
# super.merge(:
|
931
|
+
# super.merge(name: name)
|
1554
932
|
# end
|
1555
933
|
# end
|
1556
934
|
#
|
1557
|
-
#
|
1558
|
-
#
|
1559
|
-
#
|
1560
|
-
#
|
935
|
+
# u = User.create(name: "John")
|
936
|
+
# u.to_hash
|
937
|
+
# # => { id: "1", name: "John" }
|
938
|
+
#
|
1561
939
|
def to_hash
|
1562
940
|
attrs = {}
|
1563
941
|
attrs[:id] = id unless new?
|
1564
|
-
attrs[:errors] = errors
|
1565
|
-
|
942
|
+
attrs[:errors] = errors if errors.any?
|
943
|
+
|
944
|
+
return attrs
|
945
|
+
end
|
946
|
+
|
947
|
+
# Export a JSON representation of the model by encoding `to_hash`.
|
948
|
+
def to_json(*args)
|
949
|
+
to_hash.to_json(*args)
|
1566
950
|
end
|
1567
951
|
|
1568
|
-
#
|
1569
|
-
#
|
1570
|
-
# a corresponding JSON representation of whatever you have in your
|
1571
|
-
# {#to_hash}.
|
952
|
+
# Persist the model attributes and update indices and unique
|
953
|
+
# indices. The `counter`s and `set`s are not touched during save.
|
1572
954
|
#
|
1573
|
-
#
|
1574
|
-
#
|
955
|
+
# If the model is not valid, nil is returned. Otherwise, the
|
956
|
+
# persisted model is returned.
|
1575
957
|
#
|
1576
|
-
#
|
1577
|
-
# attribute :title
|
958
|
+
# Example:
|
1578
959
|
#
|
1579
|
-
#
|
1580
|
-
#
|
960
|
+
# class User < Ohm::Model
|
961
|
+
# attribute :name
|
962
|
+
#
|
963
|
+
# def validate
|
964
|
+
# assert_present :name
|
1581
965
|
# end
|
1582
966
|
# end
|
1583
967
|
#
|
1584
|
-
#
|
1585
|
-
#
|
1586
|
-
# # => true
|
968
|
+
# User.new(name: nil).save
|
969
|
+
# # => nil
|
1587
970
|
#
|
1588
|
-
#
|
971
|
+
# u = User.new(name: "John").save
|
972
|
+
# u.kind_of?(User)
|
1589
973
|
# # => true
|
1590
974
|
#
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
to_hash.to_json(*args)
|
975
|
+
def save(&block)
|
976
|
+
return if not valid?
|
977
|
+
save!(&block)
|
1595
978
|
end
|
1596
979
|
|
1597
|
-
#
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
980
|
+
# Saves the model without checking for validity. Refer to
|
981
|
+
# `Model#save` for more details.
|
982
|
+
def save!
|
983
|
+
transaction do |t|
|
984
|
+
t.watch(*_unique_keys)
|
985
|
+
t.watch(key) if not new?
|
1601
986
|
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
end
|
987
|
+
t.before do
|
988
|
+
_initialize_id if new?
|
989
|
+
end
|
1606
990
|
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
991
|
+
t.read do |store|
|
992
|
+
_verify_uniques
|
993
|
+
store.existing = key.hgetall
|
994
|
+
store.uniques = _read_index_type(:uniques)
|
995
|
+
store.indices = _read_index_type(:indices)
|
996
|
+
end
|
1611
997
|
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
998
|
+
t.write do |store|
|
999
|
+
model.key[:all].sadd(id)
|
1000
|
+
_delete_uniques(store.existing)
|
1001
|
+
_delete_indices(store.existing)
|
1002
|
+
_save
|
1003
|
+
_save_indices(store.indices)
|
1004
|
+
_save_uniques(store.uniques)
|
1005
|
+
end
|
1616
1006
|
|
1617
|
-
|
1618
|
-
|
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.
|
1624
|
-
def ==(other)
|
1625
|
-
other.kind_of?(self.class) && other.key == key
|
1626
|
-
rescue MissingID
|
1627
|
-
false
|
1628
|
-
end
|
1629
|
-
alias :eql? :==
|
1007
|
+
yield t if block_given?
|
1008
|
+
end
|
1630
1009
|
|
1631
|
-
|
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.
|
1651
|
-
def hash
|
1652
|
-
new? ? super : key.hash
|
1010
|
+
return self
|
1653
1011
|
end
|
1654
1012
|
|
1655
|
-
#
|
1656
|
-
# block is done.
|
1013
|
+
# Delete the model, including all the following keys:
|
1657
1014
|
#
|
1658
|
-
#
|
1659
|
-
#
|
1015
|
+
# - <Model>:<id>
|
1016
|
+
# - <Model>:<id>:counters
|
1017
|
+
# - <Model>:<id>:<set name>
|
1660
1018
|
#
|
1661
|
-
#
|
1662
|
-
# Redis Command Reference.
|
1663
|
-
def mutex
|
1664
|
-
lock!
|
1665
|
-
yield
|
1666
|
-
self
|
1667
|
-
ensure
|
1668
|
-
unlock!
|
1669
|
-
end
|
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.
|
1019
|
+
# If the model has uniques or indices, they're also cleaned up.
|
1674
1020
|
#
|
1675
|
-
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1680
|
-
rescue MissingID
|
1681
|
-
nil
|
1682
|
-
end
|
1021
|
+
def delete
|
1022
|
+
transaction do |t|
|
1023
|
+
t.read do |store|
|
1024
|
+
store.existing = key.hgetall
|
1025
|
+
end
|
1683
1026
|
|
1684
|
-
|
1685
|
-
|
1027
|
+
t.write do |store|
|
1028
|
+
_delete_uniques(store.existing)
|
1029
|
+
_delete_indices(store.existing)
|
1030
|
+
model.collections.each { |e| key[e].del }
|
1031
|
+
model.key[:all].srem(id)
|
1032
|
+
key[:counters].del
|
1033
|
+
key.del
|
1034
|
+
end
|
1686
1035
|
|
1687
|
-
|
1688
|
-
|
1689
|
-
new? ? "?" : id,
|
1690
|
-
everything.map {|e| e.join("=") }.join(" ")
|
1691
|
-
)
|
1036
|
+
yield t if block_given?
|
1037
|
+
end
|
1692
1038
|
end
|
1693
1039
|
|
1694
|
-
#
|
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.
|
1040
|
+
# Update the model attributes and call save.
|
1698
1041
|
#
|
1699
|
-
#
|
1700
|
-
# distributed *Redis* solution and may well be the right solution for
|
1701
|
-
# certain cases.
|
1042
|
+
# Example:
|
1702
1043
|
#
|
1703
|
-
#
|
1704
|
-
#
|
1705
|
-
# class Post < Ohm::Model
|
1706
|
-
# connect :port => 6380, :db => 2
|
1044
|
+
# User[1].update(name: "John")
|
1707
1045
|
#
|
1708
|
-
#
|
1709
|
-
# end
|
1046
|
+
# # It's the same as:
|
1710
1047
|
#
|
1711
|
-
#
|
1712
|
-
#
|
1713
|
-
#
|
1714
|
-
# Post.connect(:port => 6380, :db => 2)
|
1048
|
+
# u = User[1]
|
1049
|
+
# u.update_attributes(name: "John")
|
1050
|
+
# u.save
|
1715
1051
|
#
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
@options = options
|
1052
|
+
def update(attributes)
|
1053
|
+
update_attributes(attributes)
|
1054
|
+
save
|
1720
1055
|
end
|
1721
1056
|
|
1722
|
-
#
|
1723
|
-
|
1724
|
-
|
1725
|
-
# @see http://github.com/soveran/nest The Nest library.
|
1726
|
-
def key
|
1727
|
-
self.class.key[id]
|
1057
|
+
# Write the dictionary of key-value pairs to the model.
|
1058
|
+
def update_attributes(atts)
|
1059
|
+
atts.each { |att, val| send(:"#{att}=", val) }
|
1728
1060
|
end
|
1729
1061
|
|
1730
1062
|
protected
|
1731
|
-
|
1732
|
-
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
1736
|
-
#
|
1737
|
-
# 1. Delete the current key, e.g. Post:2.
|
1738
|
-
# 2. Set all of the new attributes (using HMSET).
|
1739
|
-
#
|
1740
|
-
# The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure
|
1741
|
-
# the atomicity of the write operation.
|
1742
|
-
#
|
1743
|
-
# @see http://code.google.com/p/redis/wiki/DelCommand DEL in the
|
1744
|
-
# Redis Command Reference.
|
1745
|
-
# @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the
|
1746
|
-
# Redis Command Reference.
|
1747
|
-
# @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
|
1748
|
-
# in the Redis Command Reference.
|
1749
|
-
def write
|
1750
|
-
unless (attributes + counters).empty?
|
1751
|
-
atts = (attributes + counters).inject([]) { |ret, att|
|
1752
|
-
value = send(att).to_s
|
1753
|
-
|
1754
|
-
ret.push(att, value) if not value.empty?
|
1755
|
-
ret
|
1756
|
-
}
|
1757
|
-
|
1758
|
-
db.multi do
|
1759
|
-
key.del
|
1760
|
-
key.hmset(*atts.flatten) if atts.any?
|
1761
|
-
end
|
1762
|
-
end
|
1063
|
+
def self.to_reference
|
1064
|
+
name.to_s.
|
1065
|
+
match(/^(?:.*::)*(.*)$/)[1].
|
1066
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
1067
|
+
downcase.to_sym
|
1763
1068
|
end
|
1764
1069
|
|
1765
|
-
|
1766
|
-
|
1767
|
-
|
1768
|
-
#
|
1769
|
-
# @param [Symbol, String] att The name of the attribute to write.
|
1770
|
-
# @param [#to_s] value The value of the attribute to write.
|
1771
|
-
#
|
1772
|
-
# @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the
|
1773
|
-
# Redis Command Reference.
|
1774
|
-
# @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the
|
1775
|
-
# Redis Command Reference.
|
1776
|
-
def write_remote(att, value)
|
1777
|
-
write_local(att, value)
|
1070
|
+
def self.indices
|
1071
|
+
@indices ||= []
|
1072
|
+
end
|
1778
1073
|
|
1779
|
-
|
1780
|
-
|
1781
|
-
else
|
1782
|
-
key.hset(att, value)
|
1783
|
-
end
|
1074
|
+
def self.uniques
|
1075
|
+
@uniques ||= []
|
1784
1076
|
end
|
1785
1077
|
|
1786
|
-
|
1787
|
-
|
1788
|
-
|
1789
|
-
# @see Ohm::Model::Wrapper
|
1790
|
-
# @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation
|
1791
|
-
def self.const_missing(name)
|
1792
|
-
wrapper = Wrapper.new(name) { const_get(name) }
|
1078
|
+
def self.collections
|
1079
|
+
@collections ||= []
|
1080
|
+
end
|
1793
1081
|
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
|
1082
|
+
def self.filters(dict)
|
1083
|
+
unless dict.kind_of?(Hash)
|
1084
|
+
raise ArgumentError,
|
1085
|
+
"You need to supply a hash with filters. " +
|
1086
|
+
"If you want to find by ID, use #{self}[id] instead."
|
1798
1087
|
end
|
1799
1088
|
|
1800
|
-
|
1089
|
+
dict.map { |k, v| toindices(k, v) }.flatten
|
1801
1090
|
end
|
1802
1091
|
|
1803
|
-
|
1804
|
-
|
1805
|
-
# Provides access to the Redis database. This is shared accross all models and instances.
|
1806
|
-
def self.db
|
1807
|
-
return Ohm.redis unless defined?(@options)
|
1092
|
+
def self.toindices(att, val)
|
1093
|
+
raise IndexNotFound unless indices.include?(att)
|
1808
1094
|
|
1809
|
-
|
1095
|
+
if val.kind_of?(Enumerable)
|
1096
|
+
val.map { |v| key[:indices][att][v] }
|
1097
|
+
else
|
1098
|
+
[key[:indices][att][val]]
|
1099
|
+
end
|
1810
1100
|
end
|
1811
1101
|
|
1812
|
-
|
1813
|
-
|
1814
|
-
Key.new(self, db)
|
1102
|
+
def self.new_id
|
1103
|
+
key[:id].incr
|
1815
1104
|
end
|
1816
1105
|
|
1817
|
-
|
1818
|
-
|
1106
|
+
attr_writer :id
|
1107
|
+
|
1108
|
+
def transaction
|
1109
|
+
txn = Transaction.new { |t| yield t }
|
1110
|
+
txn.commit(db)
|
1819
1111
|
end
|
1820
1112
|
|
1821
|
-
|
1822
|
-
|
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
|
-
#
|
1841
|
-
def initialize_id
|
1842
|
-
@id ||= self.class.key[:id].incr.to_s
|
1113
|
+
def model
|
1114
|
+
self.class
|
1843
1115
|
end
|
1844
1116
|
|
1845
1117
|
def db
|
1846
|
-
|
1118
|
+
model.db
|
1847
1119
|
end
|
1848
1120
|
|
1849
|
-
def
|
1850
|
-
|
1121
|
+
def _initialize_id
|
1122
|
+
@id = model.new_id.to_s
|
1851
1123
|
end
|
1852
1124
|
|
1853
|
-
def
|
1854
|
-
|
1125
|
+
def _skip_empty(atts)
|
1126
|
+
{}.tap do |ret|
|
1127
|
+
atts.each do |att, val|
|
1128
|
+
ret[att] = send(att).to_s unless val.to_s.empty?
|
1129
|
+
end
|
1130
|
+
end
|
1855
1131
|
end
|
1856
1132
|
|
1857
|
-
def
|
1858
|
-
key
|
1859
|
-
self.class.all.delete(self)
|
1133
|
+
def _unique_keys
|
1134
|
+
model.uniques.map { |att| model.key[:uniques][att] }
|
1860
1135
|
end
|
1861
1136
|
|
1862
|
-
def
|
1863
|
-
|
1864
|
-
|
1137
|
+
def _save
|
1138
|
+
key.del
|
1139
|
+
key.hmset(*_skip_empty(attributes).flatten)
|
1865
1140
|
end
|
1866
1141
|
|
1867
|
-
def
|
1868
|
-
|
1869
|
-
|
1870
|
-
send(att).each { |value| add_to_index(att, value) }
|
1142
|
+
def _verify_uniques
|
1143
|
+
if att = _detect_duplicate
|
1144
|
+
raise UniqueIndexViolation, "#{att} is not unique."
|
1871
1145
|
end
|
1872
1146
|
end
|
1873
1147
|
|
1874
|
-
def
|
1875
|
-
|
1876
|
-
|
1877
|
-
|
1878
|
-
|
1879
|
-
value.kind_of?(Enumerable) &&
|
1880
|
-
value.kind_of?(String) == false
|
1148
|
+
def _detect_duplicate
|
1149
|
+
model.uniques.detect do |att|
|
1150
|
+
id = model.key[:uniques][att].hget(send(att))
|
1151
|
+
id && id != self.id.to_s
|
1152
|
+
end
|
1881
1153
|
end
|
1882
1154
|
|
1883
|
-
def
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
1155
|
+
def _read_index_type(type)
|
1156
|
+
{}.tap do |ret|
|
1157
|
+
model.send(type).each do |att|
|
1158
|
+
ret[att] = send(att)
|
1159
|
+
end
|
1160
|
+
end
|
1887
1161
|
end
|
1888
1162
|
|
1889
|
-
def
|
1890
|
-
|
1891
|
-
|
1163
|
+
def _save_uniques(uniques)
|
1164
|
+
uniques.each do |att, val|
|
1165
|
+
model.key[:uniques][att].hset(val, id)
|
1892
1166
|
end
|
1893
|
-
|
1894
|
-
key[:_indices].del
|
1895
1167
|
end
|
1896
1168
|
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
# @return [String] The value of att.
|
1902
|
-
def read_local(att)
|
1903
|
-
@_attributes[att]
|
1169
|
+
def _delete_uniques(atts)
|
1170
|
+
model.uniques.each do |att|
|
1171
|
+
model.key[:uniques][att].hdel(atts[att.to_s])
|
1172
|
+
end
|
1904
1173
|
end
|
1905
1174
|
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
# @param [#to_s] value The value of the attribute you want to set.
|
1910
|
-
def write_local(att, value)
|
1911
|
-
@_attributes[att] = value
|
1912
|
-
end
|
1175
|
+
def _delete_indices(atts)
|
1176
|
+
model.indices.each do |att|
|
1177
|
+
val = atts[att.to_s]
|
1913
1178
|
|
1914
|
-
|
1915
|
-
|
1916
|
-
|
1917
|
-
#
|
1918
|
-
# @param [Symbol] att The attribute you you want to get.
|
1919
|
-
# @return [String] The value of att.
|
1920
|
-
def read_remote(att)
|
1921
|
-
unless new?
|
1922
|
-
value = key.hget(att)
|
1923
|
-
value.respond_to?(:force_encoding) ?
|
1924
|
-
value.force_encoding("UTF-8") :
|
1925
|
-
value
|
1179
|
+
if val
|
1180
|
+
model.key[:indices][att][val].srem(id)
|
1181
|
+
end
|
1926
1182
|
end
|
1927
1183
|
end
|
1928
1184
|
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1185
|
+
def _save_indices(indices)
|
1186
|
+
indices.each do |att, val|
|
1187
|
+
model.toindices(att, val).each do |index|
|
1188
|
+
index.sadd(id)
|
1189
|
+
end
|
1933
1190
|
end
|
1934
1191
|
end
|
1192
|
+
end
|
1935
1193
|
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
1940
|
-
|
1941
|
-
end
|
1194
|
+
class Lua
|
1195
|
+
attr :dir
|
1196
|
+
attr :redis
|
1197
|
+
attr :files
|
1198
|
+
attr :scripts
|
1942
1199
|
|
1943
|
-
|
1944
|
-
|
1945
|
-
|
1946
|
-
|
1947
|
-
|
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)]
|
1200
|
+
def initialize(dir, redis)
|
1201
|
+
@dir = dir
|
1202
|
+
@redis = redis
|
1203
|
+
@files = Hash.new { |h, cmd| h[cmd] = read(cmd) }
|
1204
|
+
@scripts = {}
|
1968
1205
|
end
|
1969
1206
|
|
1970
|
-
|
1971
|
-
|
1972
|
-
self.class.index_key_for(att, value)
|
1207
|
+
def run_file(file, options)
|
1208
|
+
run(files[file], options)
|
1973
1209
|
end
|
1974
1210
|
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
#
|
1979
|
-
# @see Model#mutex
|
1980
|
-
def lock!
|
1981
|
-
until key[:_lock].setnx(Time.now.to_f + 0.5)
|
1982
|
-
next unless timestamp = key[:_lock].get
|
1983
|
-
sleep(0.1) and next unless lock_expired?(timestamp)
|
1211
|
+
def run(script, options)
|
1212
|
+
keys = options[:keys]
|
1213
|
+
argv = options[:argv]
|
1984
1214
|
|
1985
|
-
|
1986
|
-
|
1215
|
+
begin
|
1216
|
+
redis.evalsha(sha(script), keys.size, *keys, *argv)
|
1217
|
+
rescue RuntimeError
|
1218
|
+
redis.eval(script, keys.size, *keys, *argv)
|
1987
1219
|
end
|
1988
1220
|
end
|
1989
1221
|
|
1990
|
-
|
1991
|
-
|
1992
|
-
|
1993
|
-
key[:_lock].del
|
1222
|
+
private
|
1223
|
+
def read(file)
|
1224
|
+
File.read("%s/%s.lua" % [dir, file])
|
1994
1225
|
end
|
1995
1226
|
|
1996
|
-
def
|
1997
|
-
|
1227
|
+
def sha(script)
|
1228
|
+
Digest::SHA1.hexdigest(script)
|
1998
1229
|
end
|
1999
1230
|
end
|
2000
1231
|
end
|