SkillAgentSearch skills...

Zermelo

Ruby ActiveModel-based ORM for Redis

Install / Use

/learn @flapjack/Zermelo
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

zermelo

Build Status

Zermelo is an ActiveModel-based Object-Relational Mapper for Redis, written in Ruby.

Installation

Add this line to your application's Gemfile:

gem 'zermelo', :github => 'flapjack/zermelo', :branch => 'master'

And then execute:

$ bundle

Or install it yourself as:

$ gem install zermelo

Requirements

  • 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.

  • Ruby 1.8.7 or higher.

Usage

Initialisation

Firstly, you'll need to set up zermelo's Redis access, e.g.

Zermelo.redis = Redis.new(:host => '127.0.0.1', :db => 8)

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 Zermelo::Records::Redis module in the class you want to persist data from:

class Post
  include Zermelo::Records::Redis
end

and then create and save an instance of that class:

post = Post.new(:id => 'abcde')
post.save

Behind the scenes, this will run the following Redis command:

SADD post::attrs:ids 'abcde'

(along with a few others which we'll discuss shortly).

Simple instance attributes

A data record without any actual data isn't very useful, so let's add a few simple data fields to the Post model:

class Post
  include Zermelo::Records::Redis
  define_attributes :title     => :string,
                    :score     => :integer,
                    :timestamp => :timestamp,
                    :published => :boolean
end

and create and save an instance of that model class:

post = Post.new(:title => 'Introduction to Zermelo',
  :score => 100, :timestamp => Time.parse('Jan 1 2000'), :published => false)
post.save

An :id => :string attribute is implicitly defined, but in this case no id was passed, so zermelo generates a UUID:

HMSET post:03c839ac-24af-432e-aa58-fd1d4bf73f24:attrs title 'Introduction to Zermelo' score 100 timestamp 1384473626.36478 published 'false'
SADD post::attrs:ids 03c839ac-24af-432e-aa58-fd1d4bf73f24

which can then be verified by inspection of the object's attributes, e.g.:

post.attributes.inspect # == {: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:

| Type | Ruby class | Notes | |------------|-------------------------------|-------| | :string | String | | | :integer | Integer | | | :float | Float | | | :id | String | | | :timestamp | Integer or Time or DateTime | Stored as a float value | | :boolean | TrueClass or FalseClass | Stored as string 'true' or 'false' |

Complex instance attributes

Zermelo also provides mappings for the compound data structures supported by Redis.

So if we add tags to the Post data definition:

class Post
  include Zermelo::Records::Redis
  define_attributes :title     => :string,
                    :score     => :integer,
                    :timestamp => :timestamp,
                    :published => :boolean,
                    :tags      => :set
end

and then create another Post instance:

post = Post.new(:id => 1, :tags => Set.new(['database', 'ORM']))
post.save

which would run the following Redis commands:

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:

| Type | Ruby class | Notes | |-------------|---------------|---------------------------------------------------------| | :list | Enumerable | Stored as a Redis LIST | | :set | Array or Set | Stored as a Redis SET | | :hash | Hash | Stored as a Redis HASH | | :sorted_set | Enumerable | Stored as a Redis ZSET |

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 are also supported through zermelo's associations (recommended due to the fact that queries can be constructed against them).

Validations

All of the validations offered by ActiveModel are available in zermelo objects.

So an attribute which should be present:

class Post
  include Zermelo::Records::Redis
  define_attributes :title    => :string,
                    :score    => :integer
  validates :title, :presence => true
end

but isn't:

post = Post.new(:score => 85)
post.valid? # == false

post.errors.full_messages # == ["Title can't be blank"]
post.save # calls valid? before saving, fails and returns false

produces the results you would expect.

Callbacks

ActiveModel's lifecycle callbacks are also supported, and zermelo uses similar invocations to ActiveRecord's:

before_create,  around_create,  after_create,
before_update,  around_update,  after_update,
before_destroy, around_destroy, after_destroy

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.

Detecting changes

Another feature added by ActiveModel is the ability to detect changed data in record instances using ActiveModel::Dirty.

Locking around changes

Zermelo will lock operations to ensure that changes are applied consistently. The locking code is based on 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:

class Author
  include Zermelo::Records::Redis
end

class Post
  include Zermelo::Records::Redis
end

class Comment
  include Zermelo::Records::Redis
end

Author.lock(Post, Comment) do
  # ... complicated data operations ...
end

Loading data

Assuming a saved Post instance has been created:

class Post
  include Zermelo::Records::Redis
  define_attributes :title     => :string,
                    :score     => :integer,
                    :timestamp => :timestamp,
                    :published => :boolean
end

post = Post.new(:id => '1234', :title => 'Introduction to Zermelo',
  :score => 100, :timestamp => Time.parse('Jan 1 2000')), :published => false)
post.save

which executes the following Redis calls:

HMSET post:1234:attrs title 'Introduction to Zermelo' score 100 timestamp 1384473626.36478 published 'false'
SADD post::attrs:ids 1234

This data can be loaded into a fresh Post instance using the find_by_id(ID) class method:

same_post = Post.find_by_id('1234')
same_post.attributes # == {:id => '1234', :score => 100, :timestamp => '2000-01-01 00:00:00 UTC', :published => false}

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, ...).

Class methods

Classes that include Zermelo::Record have the following class methods made available to them.

|Name | Arguments | Returns | |-------------------------|---------------|---------| |all | | Returns a Set of all the records stored for this class | |each | | Yields all records to the provided block, returns the same Set as .all(): Enumerable#each | |collect / map | | Yields all records to the provided block, returns an Array with the values returned from the block: Enumerable#collect | |select / find_all | | Yields all records to the provided block, returns an Array with each record where the block returned true: Enumerable#select | |reject | | Yields all records to the provided block, returns an Array with each record where the block returned false: Enumerable#reject | |ids | | Returns a Set with the ids of all stored records | |count | | Returns an Integer count of the number of stored records | |empty? | | Returns true if no records are stored, false otherwise | |destroy_all

Related Skills

View on GitHub
GitHub Stars7
CategoryDevelopment
Updated1y ago
Forks1

Languages

Ruby

Security Score

70/100

Audited on May 15, 2024

No findings