redis_orm 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/Manifest +23 -0
- data/README.md +332 -0
- data/Rakefile +32 -0
- data/lib/redis_orm.rb +20 -0
- data/lib/redis_orm/active_model_behavior.rb +20 -0
- data/lib/redis_orm/associations/belongs_to.rb +48 -0
- data/lib/redis_orm/associations/has_many.rb +54 -0
- data/lib/redis_orm/associations/has_many_proxy.rb +118 -0
- data/lib/redis_orm/associations/has_one.rb +52 -0
- data/lib/redis_orm/redis_orm.rb +502 -0
- data/redis_orm.gemspec +42 -0
- data/test/associations_test.rb +226 -0
- data/test/basic_functionality_test.rb +166 -0
- data/test/callbacks_test.rb +140 -0
- data/test/changes_array_test.rb +47 -0
- data/test/dynamic_finders_test.rb +89 -0
- data/test/exceptions_test.rb +63 -0
- data/test/has_one_has_many_test.rb +75 -0
- data/test/indices_test.rb +76 -0
- data/test/options_test.rb +186 -0
- data/test/redis.conf +417 -0
- data/test/validations_test.rb +49 -0
- metadata +138 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 by Dmitrii Samoilov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
LICENSE
|
2
|
+
Manifest
|
3
|
+
README.md
|
4
|
+
Rakefile
|
5
|
+
lib/redis_orm.rb
|
6
|
+
lib/redis_orm/active_model_behavior.rb
|
7
|
+
lib/redis_orm/associations/belongs_to.rb
|
8
|
+
lib/redis_orm/associations/has_many.rb
|
9
|
+
lib/redis_orm/associations/has_many_proxy.rb
|
10
|
+
lib/redis_orm/associations/has_one.rb
|
11
|
+
lib/redis_orm/redis_orm.rb
|
12
|
+
redis_orm.gemspec
|
13
|
+
test/associations_test.rb
|
14
|
+
test/basic_functionality_test.rb
|
15
|
+
test/callbacks_test.rb
|
16
|
+
test/changes_array_test.rb
|
17
|
+
test/dynamic_finders_test.rb
|
18
|
+
test/exceptions_test.rb
|
19
|
+
test/has_one_has_many_test.rb
|
20
|
+
test/indices_test.rb
|
21
|
+
test/options_test.rb
|
22
|
+
test/redis.conf
|
23
|
+
test/validations_test.rb
|
data/README.md
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
RedisOrm supposed to be *almost* drop-in replacement of ActiveRecord. It's based on the [Redis](http://redis.io) advanced key-value store and is work in progress.
|
2
|
+
|
3
|
+
Here's the standard model definition:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class User < RedisOrm::Base
|
7
|
+
property :first_name, String
|
8
|
+
property :last_name, String
|
9
|
+
property :created_at, Time
|
10
|
+
property :modified_at, Time
|
11
|
+
|
12
|
+
index :last_name
|
13
|
+
index [:first_name, :last_name]
|
14
|
+
|
15
|
+
has_many :photos
|
16
|
+
has_one :profile
|
17
|
+
|
18
|
+
after_create :create_mailboxes
|
19
|
+
|
20
|
+
def create_mailboxes
|
21
|
+
# ...
|
22
|
+
end
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
## Defining a model and specifing properties
|
27
|
+
|
28
|
+
To specify properties for your model you should use the following syntax:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class User < RedisOrm::Base
|
32
|
+
property :first_name, String
|
33
|
+
property :last_name, String
|
34
|
+
property :created_at, Time
|
35
|
+
property :modified_at, Time
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
Following property types are supported:
|
40
|
+
|
41
|
+
* **Integer**
|
42
|
+
|
43
|
+
* **String**
|
44
|
+
|
45
|
+
* **Float**
|
46
|
+
|
47
|
+
* **RedisOrm::Boolean**
|
48
|
+
there is no Boolean class in Ruby so it's a special class to specify TrueClass or FalseClass objects
|
49
|
+
|
50
|
+
* **Time**
|
51
|
+
|
52
|
+
The value of the
|
53
|
+
Property definition supports following options:
|
54
|
+
|
55
|
+
* **:default**
|
56
|
+
|
57
|
+
The default value of the attribute when it's getting saved w/o any.
|
58
|
+
|
59
|
+
## Searching records by the value
|
60
|
+
|
61
|
+
Usually it's done via specifing an index and using dynamic finders. For example:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class User < RedisOrm::Base
|
65
|
+
property :name, String
|
66
|
+
|
67
|
+
index :name
|
68
|
+
end
|
69
|
+
|
70
|
+
User.create :name => "germaninthetown"
|
71
|
+
User.find_by_name "germaninthetown" # => found 1 record
|
72
|
+
User.find_all_by_name "germaninthetown" # => array with 1 record
|
73
|
+
```
|
74
|
+
|
75
|
+
Dynamic finders work mostly the way they work in ActiveRecord. The only difference is if you didn't specified index or compaund index on the attributes you are searching on the exception will be raised.
|
76
|
+
|
77
|
+
## Options for #find/#all
|
78
|
+
|
79
|
+
For example we associate 2 photos with the album
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
@album.photos << @photo2
|
83
|
+
@album.photos << @photo1
|
84
|
+
```
|
85
|
+
|
86
|
+
To extract all or part of the associated records by far you could use 3 options (#find is an alias for #all in has_many proxy):
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
@album.photos.all(:limit => 0, :offset => 0).should == []
|
90
|
+
@album.photos.all(:limit => 1, :offset => 0).size.should == 1
|
91
|
+
@album.photos.all(:limit => 2, :offset => 0)
|
92
|
+
@album.photos.find(:order => "asc")
|
93
|
+
|
94
|
+
Photo.all(:order => "asc", :limit => 5)
|
95
|
+
Photo.all(:order => "desc", :limit => 10, :offset => 50)
|
96
|
+
```
|
97
|
+
|
98
|
+
## Indices
|
99
|
+
|
100
|
+
Indices are used in a different way then they are used in relational databases.
|
101
|
+
|
102
|
+
You could add index to any attribute of the model (it also could be compound):
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class User < RedisOrm::Base
|
106
|
+
property :first_name, String
|
107
|
+
property :last_name, String
|
108
|
+
|
109
|
+
index :first_name
|
110
|
+
index [:first_name, :last_name]
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
With index defined for the property (or number of properties) the id of the saved object is stored in the special sorted set, so it could be found later. For example with defined User model for the above code:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
user = User.new :first_name => "Robert", :last_name => "Pirsig"
|
118
|
+
user.save
|
119
|
+
|
120
|
+
# 2 redis keys are created "user:first_name:Robert" and "user:first_name:Robert:last_name:Pirsig" so we could search things like this:
|
121
|
+
|
122
|
+
User.find_by_first_name("Robert") # => user
|
123
|
+
User.find_all_by_first_name("Robert") # => [user]
|
124
|
+
User.find_by_first_name_and_last_name("Robert", "Pirsig") # => user
|
125
|
+
User.find_all_by_first_name_and_last_name("Chris", "Pirsig") # => []
|
126
|
+
```
|
127
|
+
|
128
|
+
Index definition supports following options:
|
129
|
+
|
130
|
+
* **:unique** Boolean default: false
|
131
|
+
|
132
|
+
## Associations
|
133
|
+
|
134
|
+
RedisOrm provides 3 association types:
|
135
|
+
|
136
|
+
* has_one
|
137
|
+
|
138
|
+
* has_many
|
139
|
+
|
140
|
+
* belongs_to
|
141
|
+
|
142
|
+
HABTM association could be emulated with 2 has_many declarations in related models.
|
143
|
+
|
144
|
+
### has_many/belongs_to associations
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class Article < RedisOrm::Base
|
148
|
+
property :title, String
|
149
|
+
has_many :comments
|
150
|
+
end
|
151
|
+
|
152
|
+
class Comment < RedisOrm::Base
|
153
|
+
property :body, String
|
154
|
+
belongs_to :article
|
155
|
+
end
|
156
|
+
|
157
|
+
article = Article.create :title => "DHH drops OpenID support on 37signals"
|
158
|
+
comment1 = Comment.create :body => "test"
|
159
|
+
comment2 = Comment.create :body => "test #2"
|
160
|
+
|
161
|
+
article.comments << [comment1, comment2]
|
162
|
+
|
163
|
+
# or rewrite associations
|
164
|
+
article.comments = [comment1, comment2]
|
165
|
+
|
166
|
+
article.comments # => [comment1, comment2]
|
167
|
+
comment1.article # => article
|
168
|
+
comment2.article # => article
|
169
|
+
```
|
170
|
+
|
171
|
+
Backlinks are automatically created.
|
172
|
+
|
173
|
+
### has_one/belongs_to associations
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class User < RedisOrm::Base
|
177
|
+
property :name, String
|
178
|
+
has_one :profile
|
179
|
+
end
|
180
|
+
|
181
|
+
class Profile < RedisOrm::Base
|
182
|
+
property :age, Integer
|
183
|
+
|
184
|
+
validates_presence_of :age
|
185
|
+
belongs_to :user
|
186
|
+
end
|
187
|
+
|
188
|
+
user = User.create :name => "Haruki Murakami"
|
189
|
+
profile = Profile.create :age => 26
|
190
|
+
user.profile = profile
|
191
|
+
|
192
|
+
user.profile # => profile
|
193
|
+
profile.user # => user
|
194
|
+
```
|
195
|
+
|
196
|
+
Backlink is automatically created.
|
197
|
+
|
198
|
+
### has_many/has_many associations (HABTM)
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
class Article < RedisOrm::Base
|
202
|
+
property :title, String
|
203
|
+
has_many :categories
|
204
|
+
end
|
205
|
+
|
206
|
+
class Category < RedisOrm::Base
|
207
|
+
property :name, String
|
208
|
+
has_many :articles
|
209
|
+
end
|
210
|
+
|
211
|
+
article = Article.create :title => "DHH drops OpenID support on 37signals"
|
212
|
+
|
213
|
+
cat1 = Category.create :name => "Nature"
|
214
|
+
cat2 = Category.create :name => "Art"
|
215
|
+
cat3 = Category.create :name => "Web"
|
216
|
+
|
217
|
+
article.categories << [cat1, cat2, cat3]
|
218
|
+
|
219
|
+
article.categories # => [cat1, cat2, cat3]
|
220
|
+
cat1.articles # => [article]
|
221
|
+
cat2.articles # => [article]
|
222
|
+
cat3.articles # => [article]
|
223
|
+
```
|
224
|
+
|
225
|
+
Backlinks are automatically created.
|
226
|
+
|
227
|
+
### self-referencing association
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class User < RedisOrm::Base
|
231
|
+
property :name, String
|
232
|
+
index :name
|
233
|
+
has_many :users, :as => :friends
|
234
|
+
end
|
235
|
+
|
236
|
+
me = User.create :name => "german"
|
237
|
+
friend1 = User.create :name => "friend1"
|
238
|
+
friend2 = User.create :name => "friend2"
|
239
|
+
|
240
|
+
me.friends << [friend1, friend2]
|
241
|
+
|
242
|
+
me.friends # => [friend1, friend2]
|
243
|
+
friend1.friends # => []
|
244
|
+
friend2.friends # => []
|
245
|
+
```
|
246
|
+
|
247
|
+
As an exception if *:as* option for the association is provided the backlinks are not established.
|
248
|
+
|
249
|
+
For more examples please check test/associations_test.rb
|
250
|
+
|
251
|
+
## Validation
|
252
|
+
|
253
|
+
RedisOrm includes ActiveModel::Validations. So all well-known functions are already in. An example from test/validations_test.rb:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
class Photo < RedisOrm::Base
|
257
|
+
property :image, String
|
258
|
+
|
259
|
+
validates_presence_of :image
|
260
|
+
validates_length_of :image, :in => 7..32
|
261
|
+
validates_format_of :image, :with => /\w*\.(gif|jpe?g|png)/
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
265
|
+
## Callbacks
|
266
|
+
|
267
|
+
RedisOrm provides 6 standard callbacks:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
after_save :callback
|
271
|
+
before_save :callback
|
272
|
+
after_create :callback
|
273
|
+
before_create :callback
|
274
|
+
after_destroy :callback
|
275
|
+
before_destroy :callback
|
276
|
+
```
|
277
|
+
|
278
|
+
They are implemented differently than in ActiveModel though work as expected:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class Comment < RedisOrm::Base
|
282
|
+
property :text, String
|
283
|
+
|
284
|
+
belongs_to :user
|
285
|
+
|
286
|
+
before_save :trim_whitespaces
|
287
|
+
|
288
|
+
def trim_whitespaces
|
289
|
+
self.text = self.text.strip
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## Saving records
|
295
|
+
|
296
|
+
When saving object standard ActiveModel's #valid? method is invoked at first. Then appropriate callbacks are run. Then new Hash in Redis is created with keys/values equal to the properties/values of the saving object.
|
297
|
+
|
298
|
+
The object's id is stored in "model_name:ids" sorted set with Time.now.to_f as a score. So records are ordered by created_at time by default.
|
299
|
+
|
300
|
+
## Tests
|
301
|
+
|
302
|
+
Though I a fan of the Test::Unit all tests are based on RSpec. And the only reason I did it is before(:all) and after(:all) hooks. So I could spawn/kill redis-server's process:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
describe "check callbacks" do
|
306
|
+
before(:all) do
|
307
|
+
path_to_conf = File.dirname(File.expand_path(__FILE__)) + "/redis.conf"
|
308
|
+
$redis_pid = spawn 'redis-server ' + path_to_conf, :out => "/dev/null"
|
309
|
+
sleep(0.3) # must be some delay otherwise "Connection refused - Unable to connect to Redis"
|
310
|
+
path_to_socket = File.dirname(File.expand_path(__FILE__)) + "/../redis.sock"
|
311
|
+
$redis = Redis.new(:host => 'localhost', :path => path_to_socket)
|
312
|
+
end
|
313
|
+
|
314
|
+
before(:each) do
|
315
|
+
$redis.flushall if $redis
|
316
|
+
end
|
317
|
+
|
318
|
+
after(:each) do
|
319
|
+
$redis.flushall if $redis
|
320
|
+
end
|
321
|
+
|
322
|
+
after(:all) do
|
323
|
+
Process.kill 9, $redis_pid.to_i if $redis_pid
|
324
|
+
end
|
325
|
+
|
326
|
+
# it "should ..." do
|
327
|
+
# ...
|
328
|
+
# end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
Copyright © 2011 Dmitrii Samoilov, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require 'echoe'
|
5
|
+
|
6
|
+
Echoe.new('redis_orm', '0.1') do |p|
|
7
|
+
p.description = "ORM for Redis advanced key-value storage"
|
8
|
+
p.url = "https://github.com/german/redis_orm"
|
9
|
+
p.author = "Dmitrii Samoilov"
|
10
|
+
p.email = "germaninthetown@gmail.com"
|
11
|
+
p.dependencies = ["activesupport >=3.0.0", "activemodel >=3.0.0", "redis >=2.2.0"]
|
12
|
+
p.development_dependencies = ["rspec >=2.5.0"]
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
#require 'rake/testtask'
|
17
|
+
#$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
|
18
|
+
|
19
|
+
=begin
|
20
|
+
desc 'Test the redis_orm gem.'
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
#t.libs << 'lib'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = true
|
25
|
+
end
|
26
|
+
=end
|
27
|
+
|
28
|
+
task :test do |t|
|
29
|
+
Dir['test/**/*_test.rb'].each do |file|
|
30
|
+
puts `ruby -I./lib #{file}`
|
31
|
+
end
|
32
|
+
end
|
data/lib/redis_orm.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'redis'
|
3
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'active_model_behavior')
|
4
|
+
|
5
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'belongs_to')
|
6
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_many_proxy')
|
7
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_many')
|
8
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_one')
|
9
|
+
|
10
|
+
class String
|
11
|
+
def i18n_key
|
12
|
+
self.to_s.tableize
|
13
|
+
end
|
14
|
+
|
15
|
+
def human
|
16
|
+
self.to_s.humanize
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'redis_orm')
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveModelBehavior
|
2
|
+
module ClassMethods
|
3
|
+
def model_name
|
4
|
+
#@_model_name ||= ActiveModel::Name.new(self).to_s.downcase
|
5
|
+
@_model_name ||= ActiveModel::Name.new(self).to_s.tableize.singularize
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def model_name
|
11
|
+
#@_model_name ||= ActiveModel::Name.new(self.class).to_s.downcase
|
12
|
+
@_model_name ||= ActiveModel::Name.new(self.class).to_s.tableize.singularize
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.send(:include, InstanceMethods)
|
18
|
+
base.extend ClassMethods
|
19
|
+
end
|
20
|
+
end
|