README.md in zermelo-1.1.0 vs README.md in zermelo-1.2.0
- old
+ new
@@ -36,15 +36,15 @@
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.
### Class ids
-Include **zermelo**'s Record module in the class you want to persist data from:
+Include **zermelo**'s `Zermelo::Records::Redis` module in the class you want to persist data from:
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
end
```
and then create and save an instance of that class:
@@ -65,11 +65,11 @@
A data record without any actual data isn't very useful, so let's add a few simple data fields to the Post model:
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
define_attributes :title => :string,
:score => :integer,
:timestamp => :timestamp,
:published => :boolean
end
@@ -94,12 +94,11 @@
```ruby
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}
```
-Zermelo supports the following simple attribute types, and automatically
-validates that the values are of the correct class, casting if possible:
+Zermelo supports the following simple attribute types, and automatically validates that the values are of the correct class, casting if possible:
| Type | Ruby class | Notes |
|------------|-------------------------------|-------|
| :string | String | |
| :integer | Integer | |
@@ -114,20 +113,20 @@
So if we add tags to the Post data definition:
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
define_attributes :title => :string,
:score => :integer,
:timestamp => :timestamp,
:published => :boolean,
:tags => :set
end
```
-and then create another
+and then create another `Post` instance:
```ruby
post = Post.new(:id => 1, :tags => Set.new(['database', 'ORM']))
post.save
```
@@ -137,35 +136,34 @@
```
SADD post:1:attrs:tags 'database' 'ORM'
SADD post::attrs:ids 1
```
-Zermelo supports the following complex attribute types, and automatically
-validates that the values are of the correct class, casting if possible:
+Zermelo supports the following complex attribute types, and automatically validates that the values are of the correct class, casting if possible:
-| Type | Ruby class | Notes |
-|------------|---------------|---------------------------------------------------------|
-| :list | Enumerable | Stored as a Redis [LIST](http://redis.io/commands#list) |
-| :set | Array or Set | Stored as a Redis [SET](http://redis.io/commands#set) |
-| :hash | Hash | Stored as a Redis [HASH](http://redis.io/commands#hash) |
+| Type | Ruby class | Notes |
+|-------------|---------------|---------------------------------------------------------|
+| :list | Enumerable | Stored as a Redis [LIST](http://redis.io/commands#list) |
+| :set | Array or Set | Stored as a Redis [SET](http://redis.io/commands#set) |
+| :hash | Hash | Stored as a Redis [HASH](http://redis.io/commands#hash) |
+| :sorted_set | Enumerable | Stored as a Redis [ZSET](http://redis.io/commands#zset) |
-Structure data members must be primitives that will cast OK to and from Redis via the
-driver, thus String, Integer and Float.
+Structure data members must be primitives that will cast OK to and from Redis via the driver, thus String, Integer and Float.
-Redis [sorted sets](http://redis.io/commands#sorted_set) are only supported through associations, for which see later on.
+Redis [sorted sets](http://redis.io/commands#sorted_set) are also supported through **zermelo**'s associations (recommended due to the fact that queries can be constructed against them).
### Validations
All of the [validations](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html) offered by ActiveModel are available in **zermelo** objects.
So an attribute which should be present:
```ruby
class Post
- include Zermelo::Record
- define_attributes :title => :string,
- :score => :integer
+ include Zermelo::Records::Redis
+ define_attributes :title => :string,
+ :score => :integer
validates :title, :presence => true
end
```
but isn't:
@@ -200,19 +198,19 @@
**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:
```ruby
class Author
- include Zermelo::Record
+ include Zermelo::Records::Redis
end
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
end
class Comment
- include Zermelo::Record
+ include Zermelo::Records::Redis
end
Author.lock(Post, Comment) do
# ... complicated data operations ...
end
@@ -222,11 +220,11 @@
Assuming a saved `Post` instance has been created:
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
define_attributes :title => :string,
:score => :integer,
:timestamp => :timestamp,
:published => :boolean
end
@@ -294,51 +292,62 @@
**Zermelo** supports multiple association types, which are named similarly to those provided by ActiveRecord:
|Name | Type | Redis data structure | Notes |
|---------------------------|---------------------------|----------------------|-------|
| `has_many` | one-to-many | [SET](http://redis.io/commands#set) | |
-| `has_sorted_set` | one-to-many | [ZSET](http://redis.io/commands#sorted_set) | |
+| `has_sorted_set` | one-to-many | [ZSET](http://redis.io/commands#sorted_set) | Arguments: `:key` (required), `:order` (optional, `:asc` or `:desc`) |
| `has_one` | one-to-one | [HASH](http://redis.io/commands#hash) | |
| `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 |
| `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. |
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
has_many :comments, :class_name => 'Comment', :inverse_of => :post
end
class Comment
- include Zermelo::Record
+ include Zermelo::Records::Redis
belongs_to :post, :class_name => 'Post', :inverse_of => :comments
end
```
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.
Records are added and removed from their parent one-to-many or many-to-many associations like so:
```ruby
post.comments.add(comment) # or post.comments << comment
+post.comments.remove(comment)
```
-Associations' `.add` can also take more than one argument:
+Associations' `.add`/`.remove` can also take more than one argument:
```ruby
post.comments.add(comment1, comment2, comment3)
+post.comments.remove(comment1, comment2, comment3)
```
+If you only have ids available, you don't need to `.load` the respective objects, you can instead use `.add_ids`/`.remove_ids`:
+
+```ruby
+post.comments.add_ids("comment_id")
+post.comments.remove_ids("comment_id")
+post.comments.add_ids("comment1_id", "comment2_id", "comment3_id")
+post.comments.remove_ids("comment1_id", "comment2_id", "comment3_id")
+```
+
`has_one` associations are simply set with an `=` method on the association:
```ruby
class User
- include Zermelo::Record
+ include Zermelo::Records::Redis
has_one :preferences, :class_name => 'Preferences', :inverse_of => :user
end
class Preferences
- include Zermelo::Record
+ include Zermelo::Records::Redis
belongs_to :user, :class_name => 'User', :inverse_of => :preferences
end
user = User.new
user.save
@@ -346,14 +355,20 @@
prefs.save
user.preferences = prefs
```
+and cleared by assigning the association to nil:
+
+```ruby
+user.preferences = nil
+```
+
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.
```ruby
-post = Post.new(:id => 'a')
+post = Post.new(:id => 'a')
post.save
comment1 = Comment.new(:id => '1')
comment1.save
comment2 = Comment.new(:id => '2')
comment2.save
@@ -362,45 +377,41 @@
p Comment.ids # == [1, 2]
post.comments << comment1
p post.comments.ids # == [1]
```
-`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:
+`.associated_ids_for` is somewhat of a special case; it uses the simplest queries possible to get the ids of the associated records of a set of records, e.g. for the data directly above:
```ruby
Post.associated_ids_for(:comments) # => {'a' => ['1']}
-post_b = Post.new(:id => 'b')
+post_b = Post.new(:id => 'b')
post_b.save
post_b.comments << comment2
comment3 = Comment.new(:id => '3')
comment3.save
post.comments << comment3
Post.associated_ids_for(:comments) # => {'a' => ['1', '3'], 'b' => ['2']}
-Post.intersect(:id => 'a').associated_ids_for(:comments) # => {'a' => ['1', '3']}
```
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.
```ruby
Comment.associated_ids_for(:post) # => {'1' => 'a', '2' => 'b', '3' => 'a'}
Comment.associated_ids_for(:post, :inversed => true) # => {'a' => ['1', '3'], 'b' => ['2']}
-
-Comment.intersect(:id => ['1', '2']).associated_ids_for(:post) # => {'1' => 'a', '2' => 'b'}
-Comment.intersect(:id => ['1', '2']).associated_ids_for(:post, :inversed => true) # => {'a' => ['1'], 'b' => ['2']}
```
### Class data indexing
Simple instance attributes, as defined above, can be indexed by value (and those indices can be queried).
Using the code from the instance attributes section, and adding indexing:
```ruby
class Post
- include Zermelo::Record
+ include Zermelo::Records::Redis
define_attributes :title => :string,
:score => :integer,
:timestamp => :timestamp,
:published => :boolean
@@ -434,19 +445,16 @@
`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.
| Name | Input | Output | Arguments | Options |
|-----------------|-----------------------|--------------|---------------------------------------|------------------------------------------|
-| intersect | `set` or `sorted_set` | `set` | Query hash | |
-| union | `set` or `sorted_set` | `set` | Query hash | |
-| diff | `set` or `sorted_set` | `set` | Query hash | |
-| intersect_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
-| union_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
-| diff_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
+| intersect | `set` / `sorted_set` | (as input) | Query hash | |
+| union | `set` / `sorted_set` | (as input) | Query hash | |
+| diff | `set` / `sorted_set` | (as input) | Query hash | |
| sort | `set` or `sorted_set` | `list` | keys (Symbol or Array of Symbols) | :limit (`Integer`), :offset (`Integer`) |
-| offset | `list` | `list` | amount (`Integer`) | |
-| limit | `list` | `list` | amount (`Integer`) | |
+| offset | `list` / `sorted_set` | `list` | amount (`Integer`) | :limit (`Integer`) |
+| page | `list` / `sorted_set` | `list` | page_number (`Integer`) | :per_page (`Integer`) |
These queries can be applied against all instances of a class, or against associations belonging to an instance, e.g.
```ruby
post.comments.intersect(:title => 'Interesting')
@@ -479,24 +487,40 @@
DEL comment::tmp:fe8dd59e4a1197f62d19c8aa942c4ff9
```
(where the name of the temporary Redis `SET` will of course change every time)
-The current implementation of the filtering is somewhat ad-hoc, and has these limitations:
+---
-* no conversion of `list`s back into `set`s is allowed
-* `sort`/`offset`/`limit` can only be used once in a filter chain
+`has_sorted_set` queries can take exact values, or a range bounded in no, one or both directions. (Regular Ruby `Range` objects can't be used as they don't easily support timestamps, so there's a `Zermelo::Filters::IndexRange` class which can be used as a query value instead.)
-I plan to fix these as soon as I possibly can.
+```ruby
+class Comment
+ include Zermelo::Records::Redis
+ define_attributes :created_at => :timestamp
+end
+t = Time.now
+
+comment1 = Comment.new(:id => '1', :created_at => t - 120)
+comment1.save
+comment2 = Comment.new(:id => '2', :created_at => t - 60)
+comment2.save
+
+range = Zermelo::Filters::IndexRange.new(t - 90, t, :by_score => true)
+Comment.ids # ['1', '2']
+Comment.intersect(:created_at => range).ids # ['2']
+
+```
+
### Future
Some possible changes:
-* pluggable key naming strategies
* pluggable id generation strategies
+* pluggable key naming strategies
* instrumentation for benchmarking etc.
-* multiple data backends; there's currently an experimental InfluxDB backend, and more are planned.
+* multiple data backends; there's currently an experimental InfluxDB 0.8 backend (waiting for the Ruby driver to update for 0.9 support).
## License
Zermelo is released under the MIT license: