redis-orm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +189 -0
- data/Rakefile +1 -0
- data/lib/redis-orm.rb +32 -0
- data/lib/redis/actions.rb +21 -0
- data/lib/redis/actions/creating.rb +31 -0
- data/lib/redis/actions/destroying.rb +39 -0
- data/lib/redis/actions/finding.rb +26 -0
- data/lib/redis/actions/saving.rb +47 -0
- data/lib/redis/actions/timestamps.rb +19 -0
- data/lib/redis/actions/updating.rb +18 -0
- data/lib/redis/orm.rb +59 -0
- data/lib/redis/orm/attributes.rb +65 -0
- data/lib/redis/orm/version.rb +14 -0
- data/lib/redis/relations.rb +22 -0
- data/lib/redis/relations/belongs_to.rb +51 -0
- data/lib/redis/relations/has_many.rb +60 -0
- data/lib/redis/relations/has_one.rb +51 -0
- data/lib/redis/validations.rb +9 -0
- data/lib/redis/validations/uniqueness.rb +41 -0
- data/redis-orm.gemspec +25 -0
- data/spec/redis/actions/creating_spec.rb +32 -0
- data/spec/redis/actions/destroying_spec.rb +27 -0
- data/spec/redis/actions/finding_spec.rb +26 -0
- data/spec/redis/actions/timestamps_spec.rb +11 -0
- data/spec/redis/actions/updating_spec.rb +32 -0
- data/spec/redis/orm_spec.rb +56 -0
- data/spec/redis/relations/belongs_to_spec.rb +54 -0
- data/spec/redis/relations/has_many_spec.rb +33 -0
- data/spec/redis/relations/has_one_spec.rb +23 -0
- data/spec/redis/relations/interop_spec.rb +45 -0
- data/spec/redis/validations/uniqueness_spec.rb +32 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/active_model_lint.rb +20 -0
- data/spec/support/redis_helper.rb +27 -0
- data/test.rb +23 -0
- metadata +136 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# redis-orm
|
2
|
+
|
3
|
+
Easy-to-use object relational mapping for Redis.
|
4
|
+
|
5
|
+
## Disclaimer
|
6
|
+
|
7
|
+
The gem is intended to be similar to ActiveRecord; however, it's not identical. Most notably, a lot of options that ActiveRecord gives you are currently missing from `redis-orm`. This is mostly because I'm just one guy, and I have other projects I'd like to make some headway on. As time goes on, expect `redis-orm` to become more and more ActiveRecord-like. The end goal is to produce a Redis library that is a drop-in replacement for ActiveRecord in a Rails 3 project (but that is also usable outside of Rails).
|
8
|
+
|
9
|
+
And by the way, I'm totally accepting pull requests. Hint, hint.
|
10
|
+
|
11
|
+
## Requiring
|
12
|
+
|
13
|
+
Add it to your Gemfile if you're using one. If necessary, require the redis-orm libraries like so:
|
14
|
+
|
15
|
+
require 'redis-orm'
|
16
|
+
|
17
|
+
# -or-
|
18
|
+
|
19
|
+
require 'redis/orm'
|
20
|
+
|
21
|
+
...whichever one strikes your fancy.
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Inherit from `Redis::ORM` as if you were inheriting from `ActiveRecord::Base`:
|
26
|
+
|
27
|
+
class User < Redis::ORM
|
28
|
+
# more code here!
|
29
|
+
end
|
30
|
+
|
31
|
+
At this point your Accounts can be created, saved, and so forth. It might be helpful to define attributes on the model, however, or else the model will be quite useless indeed!
|
32
|
+
|
33
|
+
### attributes
|
34
|
+
|
35
|
+
Instead of defining them in a schema file or set of migrations, `redis-orm` defines attributes directly within the model:
|
36
|
+
|
37
|
+
class User < Redis::ORM
|
38
|
+
attribute :login
|
39
|
+
attribute :password
|
40
|
+
end
|
41
|
+
|
42
|
+
Attributes can have any value that can be serialized. The default serializer is `Marshal`, but more on that later.
|
43
|
+
|
44
|
+
Optionally, you can give attributes a default value. If not specified, the default value is implicitly `nil`.
|
45
|
+
|
46
|
+
class Post < Redis::ORM
|
47
|
+
attribute :subject, "(No subject.)"
|
48
|
+
attribute :body
|
49
|
+
end
|
50
|
+
|
51
|
+
Like `ActiveRecord`, `redis-orm` defines `created_at` and `updated_at` attributes for you automatically, so you don't need to worry about these. Additionally, the `id` field is created and managed for you.
|
52
|
+
|
53
|
+
### relationships
|
54
|
+
|
55
|
+
There are 3 core relationships defined by `redis-orm`: `belongs_to`, `has_one` and `has_many`. They function essentially similar to relations of the same name in ActiveRecord, but it's important to keep in mind that they are specifically designed for Redis, and they have some minor differences.
|
56
|
+
|
57
|
+
class User < Redis::ORM
|
58
|
+
attribute :login
|
59
|
+
attribute :password
|
60
|
+
has_many :posts
|
61
|
+
has_one :profile
|
62
|
+
end
|
63
|
+
|
64
|
+
class Post < Redis::ORM
|
65
|
+
attribute :subject, "(no subject)"
|
66
|
+
attribute :body
|
67
|
+
belongs_to :user
|
68
|
+
end
|
69
|
+
|
70
|
+
class Profile < Redis::ORM
|
71
|
+
belongs_to :user
|
72
|
+
end
|
73
|
+
|
74
|
+
#### inference
|
75
|
+
|
76
|
+
Unlike `ActiveRecord`, `redis-orm` does _not_ infer class names from the relation name. You can give any value you like to the relations. During look-up, class names are retrieved from the object's ID, which (as mentioned) is already maintained for you. So in most cases, you should not have to care about the object's class at all. The only caveat is, all related objects _must_ inherit from `Redis::ORM` so that they can be looked up and deserialized properly.
|
77
|
+
|
78
|
+
### validations
|
79
|
+
|
80
|
+
The `Redis::ORM` base class automatically pulls in `ActiveModel::Validations`, so you should look at those. In addition, a Redis-specific `validates_uniqueness_of` validation has been added, and can be used thusly:
|
81
|
+
|
82
|
+
class User < Redis::ORM
|
83
|
+
attribute :login
|
84
|
+
|
85
|
+
validates_uniqueness_of :login
|
86
|
+
end
|
87
|
+
|
88
|
+
## Actions
|
89
|
+
|
90
|
+
Now we get to discuss things you can actually _do_ with your Redis-based models!
|
91
|
+
|
92
|
+
### creating and saving records
|
93
|
+
|
94
|
+
You can instantiate a new record by simply calling _new_, just like any Ruby object. Supply an optional hash of attributes:
|
95
|
+
|
96
|
+
new_user = User.new() #=> a user with no attributes
|
97
|
+
new_user = User.new(:login => "Colin") #=> a user named Colin, but with no password
|
98
|
+
new_user = User.new(:profile => Profile.new) #=> a user with a profile (it has_one, remember?)
|
99
|
+
|
100
|
+
So far, none of these records have been saved. Let's do that now:
|
101
|
+
|
102
|
+
if new_user.save
|
103
|
+
# save successful, do something
|
104
|
+
else
|
105
|
+
# save failed, let's get the error messages
|
106
|
+
puts new_user.errors.full_messages
|
107
|
+
end
|
108
|
+
|
109
|
+
The `save` method returns `true` if the save was successful, `false` otherwise. If the save failed, the model's `errors` object will have been populated.
|
110
|
+
|
111
|
+
The `errors` object comes straight from `ActiveModel`, so using it is identical to that of `ActiveRecord`.
|
112
|
+
|
113
|
+
You can call `save!` instead of `save` if you prefer an actual exception to be raised if the record couldn't be saved. The exception will include the full error messages, so you will know if the save failed due to validations.
|
114
|
+
|
115
|
+
You can also make use of the ActiveRecord-esque `create` method:
|
116
|
+
|
117
|
+
new_user = User.create(:login => 'Colin')
|
118
|
+
|
119
|
+
The model instance will be returned whether the record was successfully saved or not. If you would prefer an error to be raised upon failing validations, you can call `create!` instead.
|
120
|
+
|
121
|
+
### finding records
|
122
|
+
|
123
|
+
Finding an existing record is done like so:
|
124
|
+
|
125
|
+
user = User.find('id_of_user')
|
126
|
+
|
127
|
+
If the record could not be found, `nil` will be returned.
|
128
|
+
|
129
|
+
Dynamic `find_by_*` methods are not supported at this time.
|
130
|
+
|
131
|
+
### destroying records
|
132
|
+
|
133
|
+
Destroying records is easy:
|
134
|
+
|
135
|
+
user = User.find('id_of_user')
|
136
|
+
user.destroy
|
137
|
+
|
138
|
+
To destroy all records of a given type, call `destroy_all` on the model class:
|
139
|
+
|
140
|
+
User.destroy_all
|
141
|
+
|
142
|
+
## Callbacks
|
143
|
+
|
144
|
+
The following callbacks are supported, and are used just like their `ActiveRecord` counterparts:
|
145
|
+
|
146
|
+
* before_initialize
|
147
|
+
* after_initialize
|
148
|
+
* around_initialize
|
149
|
+
* before_save
|
150
|
+
* after_save
|
151
|
+
* around_save
|
152
|
+
* before_create
|
153
|
+
* after_create
|
154
|
+
* around_create
|
155
|
+
* before_update
|
156
|
+
* after_update
|
157
|
+
* around_update
|
158
|
+
* before_destroy
|
159
|
+
* after_destroy
|
160
|
+
* around_destroy
|
161
|
+
|
162
|
+
In addition, you can register behavior to happen _during_ some actions, specifically saving and destroying:
|
163
|
+
|
164
|
+
class User < Redis::ORM
|
165
|
+
within_save_block :save_extra_stuff
|
166
|
+
within_destroy_block :delete_extra_stuff
|
167
|
+
|
168
|
+
def save_extra_stuff
|
169
|
+
# hardcore low-level database action
|
170
|
+
end
|
171
|
+
|
172
|
+
def delete_extra_stuff
|
173
|
+
# crazy low-level database madness
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
You can use these callbacks to perform commands directly upon the Redis database connection, and they will be rolled into the corresponding save and destroy transaction blocks. This way, if the saving or destroying of the model should fail at any point during the save or destroy process respectively, the entire transaction will be rolled back and you can rest assured that you haven't needlessly altered the database beyond recognition.
|
178
|
+
|
179
|
+
## Configuration
|
180
|
+
|
181
|
+
You can set the host and port for Redis:
|
182
|
+
|
183
|
+
Redis.host = 'localhost'
|
184
|
+
Redis.port = 3200
|
185
|
+
|
186
|
+
If you already have an active connection, however, these changes will not take effect. You'll need to replace the connection directly:
|
187
|
+
|
188
|
+
Redis.connection = Redis.new(:host => 'localhost', :port => 3000)
|
189
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/redis-orm.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
autoload :ORM, 'redis/orm'
|
5
|
+
|
6
|
+
@@connection = nil
|
7
|
+
class << self
|
8
|
+
def connection
|
9
|
+
@@connection ||= Redis.new(:host => host, :port => port)
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection=(redis)
|
13
|
+
@@connection = redis
|
14
|
+
end
|
15
|
+
|
16
|
+
def host
|
17
|
+
@host ||= 'localhost'
|
18
|
+
end
|
19
|
+
|
20
|
+
def port
|
21
|
+
@port ||= 6379
|
22
|
+
end
|
23
|
+
|
24
|
+
def host=(a)
|
25
|
+
@host = a
|
26
|
+
end
|
27
|
+
|
28
|
+
def port=(a)
|
29
|
+
@port = a
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Redis
|
2
|
+
module Actions
|
3
|
+
autoload :Finding, "redis/actions/finding"
|
4
|
+
autoload :Destroying, "redis/actions/destroying"
|
5
|
+
autoload :Saving, "redis/actions/saving"
|
6
|
+
autoload :Updating, "redis/actions/updating"
|
7
|
+
autoload :Creating, "redis/actions/creating"
|
8
|
+
autoload :Timestamps, "redis/actions/timestamps"
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
include Redis::Actions::Finding
|
13
|
+
include Redis::Actions::Destroying
|
14
|
+
include Redis::Actions::Saving
|
15
|
+
include Redis::Actions::Updating
|
16
|
+
include Redis::Actions::Creating
|
17
|
+
include Redis::Actions::Timestamps
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Redis::Actions::Creating
|
2
|
+
def self.included(base)
|
3
|
+
base.send :extend, Redis::Actions::Creating::ClassMethods
|
4
|
+
base.define_model_callbacks :create
|
5
|
+
base.around_save :create_if_new
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_if_new
|
9
|
+
if new_record?
|
10
|
+
run_callbacks(:create) do
|
11
|
+
yield
|
12
|
+
end
|
13
|
+
else
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def create(attributes = {})
|
20
|
+
record = new(attributes)
|
21
|
+
record.save
|
22
|
+
record
|
23
|
+
end
|
24
|
+
|
25
|
+
def create!(attributes = {})
|
26
|
+
record = new(attributes)
|
27
|
+
record.save!
|
28
|
+
record
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Redis::Actions::Destroying
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
extend Redis::Actions::Destroying::ClassMethods
|
5
|
+
define_model_callbacks :destroy
|
6
|
+
class_inheritable_array :within_destroy_blocks
|
7
|
+
self.within_destroy_blocks ||= []
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroy
|
12
|
+
unless new_record?
|
13
|
+
# run_callbacks(:before_destroy)
|
14
|
+
run_callbacks(:destroy) do
|
15
|
+
transaction do
|
16
|
+
connection.del key
|
17
|
+
within_destroy_blocks.each do |method_name_or_block|
|
18
|
+
if method_name_or_block.kind_of?(String) || method_name_or_block.kind_of?(Symbol)
|
19
|
+
send method_name_or_block
|
20
|
+
else
|
21
|
+
method_name_or_block.call self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def destroy_all
|
31
|
+
all.each { |orm| orm.destroy }
|
32
|
+
end
|
33
|
+
|
34
|
+
def within_destroy_block(method_name = nil, &block)
|
35
|
+
within_destroy_blocks << method_name if method_name
|
36
|
+
within_destroy_blocks << block if block_given?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Redis::Actions::Finding
|
2
|
+
def self.included(base)
|
3
|
+
base.send :extend, Redis::ORM::Finding::ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def find(key)
|
8
|
+
data = connection.get(key)
|
9
|
+
if data
|
10
|
+
klass_name = key.split(/\//)[0]
|
11
|
+
klass = (klass_name.camelize.constantize rescue self)
|
12
|
+
instance = klass.new(serializer.load(data))
|
13
|
+
instance.set_unchanged!
|
14
|
+
instance
|
15
|
+
else
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def all
|
21
|
+
connection.hgetall(File.join(model_name, "keys")).collect do |key|
|
22
|
+
find(key.first)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Redis::Actions::Saving
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
define_model_callbacks :save
|
5
|
+
class_inheritable_array :within_save_blocks
|
6
|
+
self.within_save_blocks ||= []
|
7
|
+
extend Redis::Actions::Saving::ClassMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def save
|
12
|
+
if valid?
|
13
|
+
define_key if key.nil?
|
14
|
+
run_callbacks(:save) do
|
15
|
+
transaction do
|
16
|
+
within_save_blocks.each do |block_or_method|
|
17
|
+
if block_or_method.kind_of?(String) || block_or_method.kind_of?(Symbol)
|
18
|
+
send block_or_method
|
19
|
+
else
|
20
|
+
block_or_method.call self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
connection.set(key, serializer.dump(attributes))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
set_unchanged!
|
27
|
+
true
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def save!
|
34
|
+
raise "Record was not saved: #{errors.full_messages}" unless save
|
35
|
+
end
|
36
|
+
|
37
|
+
def define_key
|
38
|
+
self.key = File.join(model_name, connection.incr("__uniq__").to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
def within_save_block(method_name = nil, &block)
|
43
|
+
within_save_blocks << method_name if method_name
|
44
|
+
within_save_blocks << block if block_given?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Redis::Actions::Timestamps
|
2
|
+
def updated_at_timestamp
|
3
|
+
self.updated_at = Time.now
|
4
|
+
end
|
5
|
+
|
6
|
+
def created_at_timestamp
|
7
|
+
self.created_at ||= Time.now
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.instance_eval do
|
12
|
+
attribute :created_at
|
13
|
+
attribute :updated_at
|
14
|
+
|
15
|
+
before_save :updated_at_timestamp
|
16
|
+
before_create :created_at_timestamp
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Redis::Actions::Updating
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
base.define_model_callbacks :update
|
5
|
+
around_save :update_if_not_new
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def update_if_not_new
|
10
|
+
if new_record?
|
11
|
+
yield
|
12
|
+
else
|
13
|
+
run_callbacks(:update) do
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|