zermelo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +512 -0
- data/Rakefile +1 -0
- data/lib/zermelo/associations/association_data.rb +24 -0
- data/lib/zermelo/associations/belongs_to.rb +115 -0
- data/lib/zermelo/associations/class_methods.rb +244 -0
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
- data/lib/zermelo/associations/has_many.rb +120 -0
- data/lib/zermelo/associations/has_one.rb +109 -0
- data/lib/zermelo/associations/has_sorted_set.rb +124 -0
- data/lib/zermelo/associations/index.rb +50 -0
- data/lib/zermelo/associations/index_data.rb +18 -0
- data/lib/zermelo/associations/unique_index.rb +44 -0
- data/lib/zermelo/backends/base.rb +115 -0
- data/lib/zermelo/backends/influxdb_backend.rb +178 -0
- data/lib/zermelo/backends/redis_backend.rb +281 -0
- data/lib/zermelo/filters/base.rb +235 -0
- data/lib/zermelo/filters/influxdb_filter.rb +162 -0
- data/lib/zermelo/filters/redis_filter.rb +558 -0
- data/lib/zermelo/filters/steps/base_step.rb +22 -0
- data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/diff_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
- data/lib/zermelo/filters/steps/limit_step.rb +17 -0
- data/lib/zermelo/filters/steps/offset_step.rb +17 -0
- data/lib/zermelo/filters/steps/sort_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_step.rb +17 -0
- data/lib/zermelo/locks/no_lock.rb +16 -0
- data/lib/zermelo/locks/redis_lock.rb +221 -0
- data/lib/zermelo/records/base.rb +62 -0
- data/lib/zermelo/records/class_methods.rb +127 -0
- data/lib/zermelo/records/collection.rb +14 -0
- data/lib/zermelo/records/errors.rb +24 -0
- data/lib/zermelo/records/influxdb_record.rb +35 -0
- data/lib/zermelo/records/instance_methods.rb +224 -0
- data/lib/zermelo/records/key.rb +19 -0
- data/lib/zermelo/records/redis_record.rb +27 -0
- data/lib/zermelo/records/type_validator.rb +20 -0
- data/lib/zermelo/version.rb +3 -0
- data/lib/zermelo.rb +102 -0
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
- data/spec/lib/zermelo/associations/index_spec.rb +6 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
- data/spec/lib/zermelo/records/key_spec.rb +6 -0
- data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
- data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
- data/spec/lib/zermelo/version_spec.rb +6 -0
- data/spec/lib/zermelo_spec.rb +6 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/profile_all_formatter.rb +44 -0
- data/spec/support/uncolored_doc_formatter.rb +74 -0
- data/zermelo.gemspec +30 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a3d59a8d79a91b5703b7b9b40760f6dd25e668dd
|
4
|
+
data.tar.gz: 5f92bcd3c18207b37f2220aee647aa3cc08d1d36
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dff50d1c6b883cb73f33dbeaf379d7a8cc25f87bb55eec340850b0362be0f34857d39f2bbc6584203d0bd6acdbd8d2725cd5e910a89c3fb88e4d650d755a16e4
|
7
|
+
data.tar.gz: c6e64def6eaa3fe7212fd78eb5221dba0e32babc74d7a04711134e411a0f69f10598d4617383b2d6051efa7ac54cfd0cd706d43c1748a4e29c6ab0b9f4d1d88e
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
--color
|
2
|
+
--format progress
|
3
|
+
--format html
|
4
|
+
--out tmp/spec.html
|
5
|
+
--require ./spec/support/uncolored_doc_formatter.rb
|
6
|
+
--format UncoloredDocFormatter
|
7
|
+
--out tmp/spec_doc.txt
|
8
|
+
--require ./spec/support/profile_all_formatter.rb
|
9
|
+
--format ProfileAllFormatter
|
10
|
+
--out tmp/spec_profile.txt
|
data/.travis.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- '2.0'
|
4
|
+
- '2.1'
|
5
|
+
- '2.2'
|
6
|
+
script: "bundle exec rspec"
|
7
|
+
services:
|
8
|
+
- redis-server
|
9
|
+
before_install:
|
10
|
+
- gem --version
|
11
|
+
- wget http://s3.amazonaws.com/influxdb/influxdb_0.8.8_amd64.deb
|
12
|
+
- sudo dpkg -i influxdb_0.8.8_amd64.deb
|
13
|
+
- sudo /etc/init.d/influxdb start
|
14
|
+
- sleep 10
|
15
|
+
- 'curl -X POST ''http://localhost:8086/db?u=root&p=root'' -d ''{"name": "zermelo_test"}'''
|
16
|
+
- 'curl -X POST ''http://localhost:8086/db/zermelo_test/users?u=root&p=root'' -d
|
17
|
+
''{"name": "zermelo", "password": "zermelo"}'''
|
18
|
+
- 'curl -X POST ''http://localhost:8086/db/zermelo_test/users/zermelo?u=root&p=root''
|
19
|
+
-d ''{"admin": true}'''
|
20
|
+
notifications:
|
21
|
+
hipchat:
|
22
|
+
template:
|
23
|
+
- '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}
|
24
|
+
(<a href="%{build_url}">Details</a>/<a href="%{compare_url}">Change view</a>)'
|
25
|
+
format: html
|
26
|
+
rooms:
|
27
|
+
secure: GrQkFR0osJal/ciXSMydKYoQFzNwSxJCtWcaZtUgxEjba+xYbNEmT/RiRpq0MhGTAn5DUEcqHKENC0qVOxiBp8WPkCzcDLjmDzTpci1QDelB0faORfG8/71JpkrOoSvWzqg0QU3H4OgQaROE9mq3MdjYml6bH3M1ZtWSArX257Y=
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
if RUBY_VERSION.split('.')[0] == '1' && RUBY_VERSION.split('.')[1] == '8'
|
4
|
+
gemspec :name => 'zermelo-ruby1.8'
|
5
|
+
else
|
6
|
+
gemspec :name => 'zermelo'
|
7
|
+
end
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'influxdb'
|
11
|
+
gem 'redis'
|
12
|
+
gem 'rspec', '>= 3.0.0'
|
13
|
+
gem 'simplecov', :require => false
|
14
|
+
|
15
|
+
if RUBY_VERSION.split('.')[0] == '1' && RUBY_VERSION.split('.')[1] == '8'
|
16
|
+
gem 'timecop', '0.6.1'
|
17
|
+
else
|
18
|
+
gem 'timecop'
|
19
|
+
end
|
20
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Ali Graham
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,512 @@
|
|
1
|
+
# zermelo
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/flapjack/zermelo.png)](https://travis-ci.org/flapjack/zermelo)
|
4
|
+
|
5
|
+
Zermelo is an [ActiveModel](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/)-based [Object-Relational Mapper](http://en.wikipedia.org/wiki/Object-relational_mapping) for [Redis](http://redis.io/), written in [Ruby](http://www.ruby-lang.org/).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'zermelo', :github => 'flapjack/zermelo', :branch => 'master'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install zermelo
|
20
|
+
|
21
|
+
## Requirements
|
22
|
+
|
23
|
+
* Redis 2.4.0 or higher (as it uses the multiple arguments provided in 2.4 for some commands). This could probaby be lowered to 2.0 with some branching for backwards compatibility.
|
24
|
+
|
25
|
+
* Ruby 1.8.7 or higher.
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Initialisation
|
30
|
+
|
31
|
+
Firstly, you'll need to set up **zermelo**'s Redis access, e.g.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Zermelo.redis = Redis.new(:host => '127.0.0.1', :db => 8)
|
35
|
+
```
|
36
|
+
|
37
|
+
You can optionally set `Zermelo.logger` to an instance of a Ruby `Logger` class, or something with a compatible interface, and Zermelo will log the method calls (and arguments) being made to the Redis driver.
|
38
|
+
|
39
|
+
### Class ids
|
40
|
+
|
41
|
+
Include **zermelo**'s Record module in the class you want to persist data from:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class Post
|
45
|
+
include Zermelo:Record
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
and then create and save an instance of that class:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
post = Post.new(:id => 'abcde')
|
53
|
+
post.save
|
54
|
+
```
|
55
|
+
|
56
|
+
Behind the scenes, this will run the following Redis command:
|
57
|
+
|
58
|
+
```
|
59
|
+
SADD post::attrs:ids 'abcde'
|
60
|
+
```
|
61
|
+
|
62
|
+
(along with a few others which we'll discuss shortly).
|
63
|
+
|
64
|
+
### Simple instance attributes
|
65
|
+
|
66
|
+
A data record without any actual data isn't very useful, so let's add a few simple data fields to the Post model:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class Post
|
70
|
+
include Zermelo:Record
|
71
|
+
define_attributes :title => :string,
|
72
|
+
:score => :integer,
|
73
|
+
:timestamp => :timestamp,
|
74
|
+
:published => :boolean
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
and create and save an instance of that model class:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
post = Post.new(:title => 'Introduction to Zermelo',
|
82
|
+
:score => 100, :timestamp => Time.parse('Jan 1 2000'), :published => false)
|
83
|
+
post.save
|
84
|
+
```
|
85
|
+
|
86
|
+
An `:id => :string` attribute is implicitly defined, but in this case no id was passed, so **zermelo** generates a UUID:
|
87
|
+
|
88
|
+
```
|
89
|
+
HMSET post:03c839ac-24af-432e-aa58-fd1d4bf73f24:attrs title 'Introduction to Zermelo' score 100 timestamp 1384473626.36478 published 'false'
|
90
|
+
SADD post::attrs:ids 03c839ac-24af-432e-aa58-fd1d4bf73f24
|
91
|
+
```
|
92
|
+
|
93
|
+
which can then be verified by inspection of the object's attributes, e.g.:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
post.attributes.inpsect # == {:id => '03c839ac-24af-432e-aa58-fd1d4bf73f24', :title => 'Introduction to Zermelo', :score => 100, :timestamp => '2000-01-01 00:00:00 UTC', :published => false}
|
97
|
+
```
|
98
|
+
|
99
|
+
Zermelo supports the following simple attribute types, and automatically
|
100
|
+
validates that the values are of the correct class, casting if possible:
|
101
|
+
|
102
|
+
| Type | Ruby class | Notes |
|
103
|
+
|------------|-------------------------------|-------|
|
104
|
+
| :string | String | |
|
105
|
+
| :integer | Integer | |
|
106
|
+
| :float | Float | |
|
107
|
+
| :id | String | |
|
108
|
+
| :timestamp | Integer or Time or DateTime | Stored as a float value |
|
109
|
+
| :boolean | TrueClass or FalseClass | Stored as string 'true' or 'false' |
|
110
|
+
|
111
|
+
### Complex instance attributes
|
112
|
+
|
113
|
+
**Zermelo** also provides mappings for the compound data structures supported by Redis.
|
114
|
+
|
115
|
+
So if we add tags to the Post data definition:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class Post
|
119
|
+
include Zermelo:Record
|
120
|
+
define_attributes :title => :string,
|
121
|
+
:score => :integer,
|
122
|
+
:timestamp => :timestamp,
|
123
|
+
:published => :boolean,
|
124
|
+
:tags => :set
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
and then create another
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
post = Post.new(:id => 1, :tags => Set.new(['database', 'ORM']))
|
132
|
+
post.save
|
133
|
+
```
|
134
|
+
|
135
|
+
which would run the following Redis commands:
|
136
|
+
|
137
|
+
```
|
138
|
+
SADD post:1:attrs:tags 'database' 'ORM'
|
139
|
+
SADD post::attrs:ids 1
|
140
|
+
```
|
141
|
+
|
142
|
+
Zermelo supports the following complex attribute types, and automatically
|
143
|
+
validates that the values are of the correct class, casting if possible:
|
144
|
+
|
145
|
+
| Type | Ruby class | Notes |
|
146
|
+
|------------|---------------|---------------------------------------------------------|
|
147
|
+
| :list | Enumerable | Stored as a Redis [LIST](http://redis.io/commands#list) |
|
148
|
+
| :set | Array or Set | Stored as a Redis [SET](http://redis.io/commands#set) |
|
149
|
+
| :hash | Hash | Stored as a Redis [HASH](http://redis.io/commands#hash) |
|
150
|
+
|
151
|
+
Structure data members must be primitives that will cast OK to and from Redis via the
|
152
|
+
driver, thus String, Integer and Float.
|
153
|
+
|
154
|
+
Redis [sorted sets](http://redis.io/commands#sorted_set) are only supported through associations, for which see later on.
|
155
|
+
|
156
|
+
### Validations
|
157
|
+
|
158
|
+
All of the [validations](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html) offered by ActiveModel are available in **zermelo** objects.
|
159
|
+
|
160
|
+
So an attribute which should be present:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class Post
|
164
|
+
include Zermelo:Record
|
165
|
+
define_attributes :title => :string,
|
166
|
+
:score => :integer
|
167
|
+
validates :title, :presence => true
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
but isn't:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
post = Post.new(:score => 85)
|
175
|
+
post.valid? # == false
|
176
|
+
|
177
|
+
post.errors.full_messages # == ["Title can't be blank"]
|
178
|
+
post.save # calls valid? before saving, fails and returns false
|
179
|
+
```
|
180
|
+
|
181
|
+
produces the results you would expect.
|
182
|
+
|
183
|
+
### Callbacks
|
184
|
+
|
185
|
+
ActiveModel's [lifecycle callbacks](http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html) are also supported, and **zermelo** uses similar invocations to ActiveRecord's:
|
186
|
+
|
187
|
+
```
|
188
|
+
before_create, around_create, after_create,
|
189
|
+
before_update, around_update, after_update,
|
190
|
+
before_destroy, around_destroy, after_destroy
|
191
|
+
```
|
192
|
+
|
193
|
+
As noted in the linked documentation, you'll need to `yield` from within an `around_*` callback, or the original action won't be carried out.
|
194
|
+
|
195
|
+
### Detecting changes
|
196
|
+
|
197
|
+
Another feature added by ActiveModel is the ability to detect changed data in record instances using [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html).
|
198
|
+
|
199
|
+
### Locking around changes
|
200
|
+
|
201
|
+
**Zermelo** will lock operations to ensure that changes are applied consistently. The locking code is based on [redis-lock](https://github.com/mlanett/redis-lock), but has been extended and customised to allow **zermelo** to lock more than one class at a time. Record saving and destroying is implicitly locked, while if you want to carry out complex queries or changes without worring about what else may be changing data at the same time, you can use the `lock` class method as follows:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
class Author
|
205
|
+
include Zermelo:Record
|
206
|
+
end
|
207
|
+
|
208
|
+
class Post
|
209
|
+
include Zermelo:Record
|
210
|
+
end
|
211
|
+
|
212
|
+
class Comment
|
213
|
+
include Zermelo:Record
|
214
|
+
end
|
215
|
+
|
216
|
+
Author.lock(Post, Comment) do
|
217
|
+
# ... complicated data operations ...
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
### Loading data
|
222
|
+
|
223
|
+
Assuming a saved `Post` instance has been created:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class Post
|
227
|
+
include Zermelo:Record
|
228
|
+
define_attributes :title => :string,
|
229
|
+
:score => :integer,
|
230
|
+
:timestamp => :timestamp,
|
231
|
+
:published => :boolean
|
232
|
+
end
|
233
|
+
|
234
|
+
post = Post.new(:id => '1234', :title => 'Introduction to Zermelo',
|
235
|
+
:score => 100, :timestamp => Time.parse('Jan 1 2000')), :published => false)
|
236
|
+
post.save
|
237
|
+
```
|
238
|
+
|
239
|
+
which executes the following Redis calls:
|
240
|
+
|
241
|
+
```
|
242
|
+
HMSET post:1234:attrs title 'Introduction to Zermelo' score 100 timestamp 1384473626.36478 published 'false'
|
243
|
+
SADD post::attrs:ids 1234
|
244
|
+
```
|
245
|
+
|
246
|
+
This data can be loaded into a fresh `Post` instance using the `find_by_id(ID)` class method:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
same_post = Post.find_by_id('1234')
|
250
|
+
same_post.attributes # == {:id => '1234', :score => 100, :timestamp => '2000-01-01 00:00:00 UTC', :published => false}
|
251
|
+
```
|
252
|
+
|
253
|
+
You can load more than one record using the `find_by_ids(ID, ID, ...)` class method (returns an array), and raise exceptions if records matching the ids are not found using `find_by_id!(ID)` and `find_by_ids!(ID, ID, ...)`.
|
254
|
+
|
255
|
+
### Class methods
|
256
|
+
|
257
|
+
Classes that include `Zermelo::Record` have the following class methods made available to them.
|
258
|
+
|
259
|
+
|Name | Arguments | Returns |
|
260
|
+
|-------------------------|---------------|---------|
|
261
|
+
|`all` | | Returns an Array of all the records stored for this class |
|
262
|
+
|`each` | | Yields all records to the provided block, returns the same Array as .all(): [Array#each](http://ruby-doc.org/core-2.1.2/Array.html#method-i-each) |
|
263
|
+
|`collect` / `map` | | Yields all records to the provided block, returns an Array with the values returned from the block: [Array#collect](http://ruby-doc.org/core-2.1.2/Array.html#method-i-collect) |
|
264
|
+
|`select` / `find_all` | | Yields all records to the provided block, returns an Array with each record where the block returned true: [Array#select](http://ruby-doc.org/core-2.1.2/Array.html#method-i-select) |
|
265
|
+
|`reject` | | Yields all records to the provided block, returns an Array with each record where the block returned false: [Array#reject](http://ruby-doc.org/core-2.1.2/Array.html#method-i-reject) |
|
266
|
+
|`ids` | | Returns an Array with the ids of all stored records |
|
267
|
+
|`count` | | Returns an Integer count of the number of stored records |
|
268
|
+
|`empty?` | | Returns true if no records are stored, false otherwise |
|
269
|
+
|`destroy_all` | | Removes all stored records |
|
270
|
+
|`exists?` | ID | Returns true if the record with the id is present, false if not |
|
271
|
+
|`find_by_id` | ID | Returns the instantiated record for the id, or nil if not present |
|
272
|
+
|`find_by_ids` | ID, ID, ... | Returns an Array of instantiated records for the ids, with nils if the respective record is not present |
|
273
|
+
|`find_by_id!` | ID | Returns the instantiated record for the id, or raises a Zermelo::Records::RecordNotFound exception if not present |
|
274
|
+
|`find_by_ids!` | ID, ID, ... | Returns an Array of instantiated records for the ids, or raises a Zermelo::Records::RecordsNotFound exception if any are not present |
|
275
|
+
|`associated_ids_for` | association | (Defined in the `Associations` section below) |
|
276
|
+
|
277
|
+
### Instance methods
|
278
|
+
|
279
|
+
Instances of classes including `Zermelo::Record` have the following methods:
|
280
|
+
|
281
|
+
|Name | Arguments | Returns |
|
282
|
+
|---------------------|---------------|---------|
|
283
|
+
|`persisted?` | | returns true if the record has been saved, false if not |
|
284
|
+
|`load` | ID | loads the record with the provided ID, discarding current state |
|
285
|
+
|`refresh` | | refreshes the record from saved data, discarding current changes |
|
286
|
+
|`save` | | returns false if validations fail, true and saves data if valid |
|
287
|
+
|`update_attributes` | HASH | mass assignment of attribute accessors, calls `save()` after attribute changes have been applied |
|
288
|
+
|`destroy` | | removes the saved data for the record |
|
289
|
+
|
290
|
+
Instances also have attribute accessors and the various methods included from the ActiveModel classes mentioned earlier.
|
291
|
+
|
292
|
+
### Associations
|
293
|
+
|
294
|
+
**Zermelo** supports multiple association types, which are named similarly to those provided by ActiveRecord:
|
295
|
+
|
296
|
+
|Name | Type | Redis data structure | Notes |
|
297
|
+
|---------------------------|---------------------------|----------------------|-------|
|
298
|
+
| `has_many` | one-to-many | [SET](http://redis.io/commands#set) | |
|
299
|
+
| `has_sorted_set` | one-to-many | [ZSET](http://redis.io/commands#sorted_set) | |
|
300
|
+
| `has_one` | one-to-one | [HASH](http://redis.io/commands#hash) | |
|
301
|
+
| `belongs_to` | many-to-one or one-to-one | [HASH](http://redis.io/commands#hash) or [STRING](http://redis.io/commands#string) | Inverse of any of the above three |
|
302
|
+
| `has_and_belongs_to_many` | many-to-many | 2 [SET](http://redis.io/commands#set)s | Mirrored by an inverse HaBtM association on the other side. |
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
class Post
|
306
|
+
include Zermelo:Record
|
307
|
+
has_many :comments, :class_name => 'Comment', :inverse_of => :post
|
308
|
+
end
|
309
|
+
|
310
|
+
class Comment
|
311
|
+
include Zermelo:Record
|
312
|
+
belongs_to :post, :class_name => 'Post', :inverse_of => :comments
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
Class names of the associated class are used, instead of a reference to the class itself, to avoid circular dependencies being established. The inverse association is provided in order that multiple associations between the same two classes can be created.
|
317
|
+
|
318
|
+
Records are added and removed from their parent one-to-many or many-to-many associations like so:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
post.comments.add(comment) # or post.comments << comment
|
322
|
+
```
|
323
|
+
|
324
|
+
Associations' `.add` can also take more than one argument:
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
post.comments.add(comment1, comment2, comment3)
|
328
|
+
```
|
329
|
+
|
330
|
+
`has_one` associations are simply set with an `=` method on the association:
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
class User
|
334
|
+
include Zermelo:Record
|
335
|
+
has_one :preferences, :class_name => 'Preferences', :inverse_of => :user
|
336
|
+
end
|
337
|
+
|
338
|
+
class Preferences
|
339
|
+
include Zermelo:Record
|
340
|
+
belongs_to :user, :class_name => 'User', :inverse_of => :preferences
|
341
|
+
end
|
342
|
+
|
343
|
+
user = User.new
|
344
|
+
user.save
|
345
|
+
prefs = Preferences.new
|
346
|
+
prefs.save
|
347
|
+
|
348
|
+
user.preferences = prefs
|
349
|
+
```
|
350
|
+
|
351
|
+
The class methods defined above can be applied to associations references as well, so the resulting data will be filtered by the data relationships applying in the association, e.g.
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
post = Post.new(:id => 'a')
|
355
|
+
post.save
|
356
|
+
comment1 = Comment.new(:id => '1')
|
357
|
+
comment1.save
|
358
|
+
comment2 = Comment.new(:id => '2')
|
359
|
+
comment2.save
|
360
|
+
|
361
|
+
p post.comments.ids # == []
|
362
|
+
p Comment.ids # == [1, 2]
|
363
|
+
post.comments << comment1
|
364
|
+
p post.comments.ids # == [1]
|
365
|
+
```
|
366
|
+
|
367
|
+
`associated_ids_for` is somewhat of a special case; it uses the smallest/simplest queries possible to get the ids of the associated records of a set of records, e.g. for the data directly above:
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
Post.associated_ids_for(:comments) # => {'a' => ['1']}
|
371
|
+
|
372
|
+
post_b = Post.new(:id => 'b')
|
373
|
+
post_b.save
|
374
|
+
post_b.comments << comment2
|
375
|
+
comment3 = Comment.new(:id => '3')
|
376
|
+
comment3.save
|
377
|
+
post.comments << comment3
|
378
|
+
|
379
|
+
Post.associated_ids_for(:comments) # => {'a' => ['1', '3'], 'b' => ['2']}
|
380
|
+
Post.intersect(:id => 'a').associated_ids_for(:comments) # => {'a' => ['1', '3']}
|
381
|
+
```
|
382
|
+
|
383
|
+
For `belongs to` associations, you may pass an extra option to `associated_ids_for`, `:inversed => true`, and you'll get the data back as if it were applied from the inverse side; however the data will only cover that used as the query root. Again, assuming the data from the last two code blocks, e.g.
|
384
|
+
|
385
|
+
```ruby
|
386
|
+
Comment.associated_ids_for(:post) # => {'1' => 'a', '2' => 'b', '3' => 'a'}
|
387
|
+
Comment.associated_ids_for(:post, :inversed => true) # => {'a' => ['1', '3'], 'b' => ['2']}
|
388
|
+
|
389
|
+
Comment.intersect(:id => ['1', '2']).associated_ids_for(:post) # => {'1' => 'a', '2' => 'b'}
|
390
|
+
Comment.intersect(:id => ['1', '2']).associated_ids_for(:post, :inversed => true) # => {'a' => ['1'], 'b' => ['2']}
|
391
|
+
```
|
392
|
+
|
393
|
+
### Class data indexing
|
394
|
+
|
395
|
+
Simple instance attributes, as defined above, can be indexed by value (and those indices can be queried).
|
396
|
+
|
397
|
+
Using the code from the instance attributes section, and adding indexing:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
class Post
|
401
|
+
include Zermelo:Record
|
402
|
+
define_attributes :title => :string,
|
403
|
+
:score => :integer,
|
404
|
+
:timestamp => :timestamp,
|
405
|
+
:published => :boolean
|
406
|
+
|
407
|
+
unique_index_by :title
|
408
|
+
index_by :published
|
409
|
+
|
410
|
+
validates :title, :presence => true
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
when we again create and save our instance of that model class:
|
415
|
+
|
416
|
+
```ruby
|
417
|
+
post = Post.new(:title => 'Introduction to Zermelo',
|
418
|
+
:score => 100, :timestamp => Time.parse('Jan 1 2000'), :published => false)
|
419
|
+
post.save
|
420
|
+
```
|
421
|
+
|
422
|
+
some extra class-level data is saved, in order that it is able to be queried later:
|
423
|
+
|
424
|
+
```
|
425
|
+
HMSET post:03c839ac-24af-432e-aa58-fd1d4bf73f24:attrs title 'Introduction to Zermelo' score 100 timestamp 1384473626.36478 published 'false'
|
426
|
+
SADD post::attrs:ids 03c839ac-24af-432e-aa58-fd1d4bf73f24
|
427
|
+
HSET post::indices:by_title 'Introduction to Zermelo' 03c839ac-24af-432e-aa58-fd1d4bf73f24
|
428
|
+
SADD post::indices:by_published:boolean:false 03c839ac-24af-432e-aa58-fd1d4bf73f24
|
429
|
+
```
|
430
|
+
|
431
|
+
|
432
|
+
### Queries against these indices
|
433
|
+
|
434
|
+
`Zermelo` will construct Redis queries for you based on higher-level data expressions. Only those properties that are indexed can be queried against, as well as `:id` -- this ensures that most operations are carried out purely within Redis against collections of id values.
|
435
|
+
|
436
|
+
|
437
|
+
| Name | Input | Output | Arguments | Options |
|
438
|
+
|-----------------|-----------------------|--------------|---------------------------------------|------------------------------------------|
|
439
|
+
| intersect | `set` or `sorted_set` | `set` | Query hash | |
|
440
|
+
| union | `set` or `sorted_set` | `set` | Query hash | |
|
441
|
+
| diff | `set` or `sorted_set` | `set` | Query hash | |
|
442
|
+
| intersect_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
|
443
|
+
| union_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
|
444
|
+
| diff_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
|
445
|
+
| sort | `set` or `sorted_set` | `list` | keys (Symbol or Array of Symbols) | :limit (`Integer`), :offset (`Integer`) |
|
446
|
+
| offset | `list` | `list` | amount (`Integer`) | |
|
447
|
+
| limit | `list` | `list` | amount (`Integer`) | |
|
448
|
+
|
449
|
+
These queries can be applied against all instances of a class, or against associations belonging to an instance, e.g.
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
post.comments.intersect(:title => 'Interesting')
|
453
|
+
Comment.intersect(:title => 'Interesting')
|
454
|
+
```
|
455
|
+
|
456
|
+
are both valid, and the `Comment` instances returned by the first query would be contained in those returned by the second.
|
457
|
+
|
458
|
+
The chained queries are only executed when the results are invoked (lazy evaluation) by the addition of one of the class methods listed above; e.g.
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
Comment.intersect(:title => 'Interesting').all # -> [Comment, Comment, ...]
|
462
|
+
Comment.intersect(:title => 'Interesting', :promoted => true).count # -> Integer
|
463
|
+
```
|
464
|
+
|
465
|
+
Assuming one `Comment` record exists, the first of these (`.all`) will execute the Redis commands
|
466
|
+
|
467
|
+
```
|
468
|
+
SINTER comment::attrs:ids comment::indices:by_title:string:Interesting
|
469
|
+
HGET comment:ca9e427d-4d81-47f8-bcfe-bb614d40528c:attrs title
|
470
|
+
```
|
471
|
+
|
472
|
+
with the result being an Array with one member, a Comment record with `{:id => 'ca9e427d-4d81-47f8-bcfe-bb614d40528c', :title => 'Interesting'}`
|
473
|
+
|
474
|
+
and the second (`.count`) will execute these Redis commands.
|
475
|
+
|
476
|
+
```
|
477
|
+
SINTERSTORE comment::tmp:fe8dd59e4a1197f62d19c8aa942c4ff9 comment::indices:by_title:string:Interesting comment::indices:by_promoted:boolean:true
|
478
|
+
SCARD comment::tmp:fe8dd59e4a1197f62d19c8aa942c4ff9
|
479
|
+
DEL comment::tmp:fe8dd59e4a1197f62d19c8aa942c4ff9
|
480
|
+
```
|
481
|
+
|
482
|
+
(where the name of the temporary Redis `SET` will of course change every time)
|
483
|
+
|
484
|
+
The current implementation of the filtering is somewhat ad-hoc, and has these limitations:
|
485
|
+
|
486
|
+
* no conversion of `list`s back into `set`s is allowed
|
487
|
+
* `sort`/`offset`/`limit` can only be used once in a filter chain
|
488
|
+
|
489
|
+
I plan to fix these as soon as I possibly can.
|
490
|
+
|
491
|
+
### Future
|
492
|
+
|
493
|
+
Some possible changes:
|
494
|
+
|
495
|
+
* pluggable key naming strategies
|
496
|
+
* pluggable id generation strategies
|
497
|
+
* instrumentation for benchmarking etc.
|
498
|
+
* multiple data backends; there's currently an experimental InfluxDB backend, and more are planned.
|
499
|
+
|
500
|
+
## License
|
501
|
+
|
502
|
+
Zermelo is released under the MIT license:
|
503
|
+
|
504
|
+
www.opensource.org/licenses/MIT
|
505
|
+
|
506
|
+
## Contributing
|
507
|
+
|
508
|
+
1. Fork it
|
509
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
510
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
511
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
512
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Zermelo
|
2
|
+
module Associations
|
3
|
+
class AssociationData
|
4
|
+
attr_writer :data_klass_name, :related_klass_names
|
5
|
+
attr_accessor :name, :type_klass, :inverse, :sort_key, :callbacks
|
6
|
+
|
7
|
+
def initialize(opts = {})
|
8
|
+
[:name, :type_klass, :inverse, :sort_key, :callbacks, :data_klass_name,
|
9
|
+
:related_klass_names].each do |a|
|
10
|
+
|
11
|
+
send("#{a}=".to_sym, opts[a])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def data_klass
|
16
|
+
@data_klass ||= @data_klass_name.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def related_klasses
|
20
|
+
@related_klasses ||= (@related_klass_names || []).map(&:constantize)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|