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: