Ricordami: store and query Ruby objects using Redis
Ricordami ("Remember me" in Italian) is an attempt at providing a simple
interface to build Ruby objects that can be validated, persisted and
queried in a Redis data structure server.
NOTE: This gem is in active development and is
not ready for use yet.
What Does It Look Like?
require "ricordami"
Ricordami::configure do |config|
config.redis_host = "127.0.0.1"
config.redis_port = 6379
config.redis_db = 15
end
Ricordami.redis.flushdb
class Singer
include Ricordami::Model
model_can :be_validated, :have_relationships
attribute :name
validates_presence_of :name
validates_uniqueness_of :name
references_many :songs
end
class Song
include Ricordami::Model
model_can :be_queried, :have_relationships
attribute :title
attribute :year, :indexed => :value
index :unique => :title, :get_by => true
referenced_in :singer
end
serge = Singer.create :name => "Gainsbourg"
jetaime = serge.songs.create :title => "Je T'Aime Moi Non Plus", :year => "1967"
jetaime.year = "1968"
p :changes, jetaime.changes # => {:year => ["1967", "1968"]}
jetaime.save
["La Javanaise", "Melody Nelson", "Love On The Beat"].each do |name|
serge.songs.create :title => name, :year => "1962"
end
Song.get_by_title("Melody Nelson").update_attributes(:year => "1971")
Song.get_by_title("Love On The Beat").update_attributes(:year => "1984")
p :count, Song.count # => 4
p :all, Song.all.map(&:title)
p :where, Song.where(:year => "1971").map(&:title) # => "Melody Nelson"
How To Install?
Ricordami is tested against the following versions of Ruby:
- MRI 1.9.2
- Ruby Enterprise 1.8.7
- Rubinius 1.2.2
and Redis 2.2.x.
Install using bundler:
In your Gemfile file:
gem "ricordami"
And just run:
$ bundle
Directly with Rubygems:
$ gem install ricordami
Features
Here is a quick description for each main feature.
Configuration
Ricordami can be configured in two ways. With values stored in the
source code:
Ricordami::Model.configure do |config|
config.redis_host = "redis.lab"
config.redis_port = 6379
config.redis_db = 0
config.thread_safe = true
end
Or using a hash:
Ricordami.configure do |config|
values = YAML.load(File.expand_path("../../config.yml"))
config.from_hash(values)
end
Declare A Model
You just need to require "ricordami" and include the
Ricordami::Model module into the model class. You can also include
additional features using the class method #model_can.
class Asset
include Ricordami::Model
model_can :be_validated,
:be_queried,
:have_relationships
end
A model class has the following methods:
- #get - lookup an instance by id (i.e.: Singer.get(2))
- #[] - alias for #get (i.e.: Singer[2])
- #all - return all existing instances
- #count - return the number of existing instances
and a model instance the following expected instance methods:
- #save
- #delete
- #update_attributes
- #reload
Both class and instances have a shortcut access to the redis object
(that uses the redis gem).
Declare Attributes
The model state is stored in attributes. Those attributes can be indexed
in order to query the models later on, or enforce the unicity of certain
attributes. Each model gets a default attribute id that is a unique
sequence set automatically when the model is saved into Redis. It is
possible to override this attribute by redeclaring it with different
options.
An attribute is declared using the class method #attribute and takes
the following options:
- :default - that's the value the attribute will take when it is not
specified - it can be a value or a Proc (or any object responding to
#call) that will return the value
- :initial - similar to :default but rather than used when the
model is instanciated, it is used when it is persisted to Redis
- :read_only - this attribute can be set only once, after that you
are certified it will not change
- :indexed* - this attribute will be indexed as unique to enforce
unicity (:indexed => :unique) or as value (:indexed => :value)
to allow querying the model (using where/and/any/not)
- :type - attribute type is a string by default (:string) but can
also be an integer (:integer) or a float (:float)
Example:
class Person
include Ricordami::Model
attribute :name, :default => "First name, Last name"
attribute :sex, :indexed => :value
attribute :age, :type => :integer
end
zhanna = Person.create(:name => "Zhanna", :sex => "Female", :age => 29)
p :id, zhanna.id # => "1"
p :[], Person["1"].name # => "Zhanna"
p :get, Person.get("1").name # => "Zhanna"
Methods:
- save: persists the model to Redis (attributes and indices added
in one atomic operation)
- update_attributes: update the value of the attributes passed and
saves the model to Redis
- delete: deletes the model from Redis (attributes and indices are
removed in one atomic operation)
Declare Indices
It is also possible to declare an index using the class method
#index to add index specific options, or conditionnaly index an
attribute dynamically. The only option currently supported is :get_by
which is used for unique indices in order to request generating
a get_by_xxx class method used to fetch a model instance by its
unique value.
Example:
class Person
include Ricordami::Model
attribute :name
index :name => :unique, :get_by => true
end
zhanna = Person.get_by_name("Zhanna")
Validation Rules
Ricordami relies on the validation capabilities offered by Active Model,
so you can refer to Rails documentation pages for
ActiveModel::Validations and
ActiveModel::Validations::HelperMethods.
Note: when using the #validates_uniqueness_of macro, Ricordami
automatically adds a value index to the column it it is not done
already.
Example:
class Singer
include Ricordami::Model
model_can :be_validated
attribute :username
attribute :email
attribute :first_name
attribute :last_name
attribute :deceased, :default => "false", :indexed => :value
validates_presence_of :username, :email, :deceased
validates_uniqueness_of :username
validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
:allow_blank => true, :message => "is not a valid email"
validates_inclusion_of :deceased, :in => ["true", "false"]
end
Relationships
Ricordami handles two kind of relationships: one to one and one to many.
You declare a referrer model to have many of a referenced model using
the class method #references_many. It gives the referrer instances
access to an instance method of the plural name of the reference. This
method can be used to fetch the list of reference objects, build or
create a new one, or query the list (see next section for querying).
You declare the referenced method using the class method
#referenced_in, which creates one method of the name of the referrer
to fetch it. It also creates two other methods #build_xxx and
#create_xxx where xxx is the referrer name. Finally it declares a
new attribute xxx_id where xxx is the name or the alias of the
referrer.
Finally you can setup a one to one relationship using
#references_one and #referenced_in. #references_one gives
the referrer access to the same type of methods than #referenced_in.
Better go with an example to make it all clear:
class Singer
include Ricordami::Model
model_can :have_relationships
attribute :name
references_many :songs
end
class Song
include Ricordami::Model
model_can :have_relationships
attribute :title
referenced_in :singer
end
bashung = Singer.create(:name => "Alain Bashung")
bashung.songs # => []
osez = bashung.songs.build(:title => "Osez Josephine")
osez.save
gaby = bashung.songs.create(:title => "Vertiges de l'Amour")
p :songs, bashung.songs.map(&:title) # => ["Osez Josephine", "Vertiges de l'Amour"]
p :singer_id, gaby.singer_id == bashung.id # => true
padam = Song.create(:title => "Padam")
p :padam, padam
benjamin = padam.build_singer(:name => "Benjamin Biolay")
p :benjamin, benjamin
p :songs, benjamin.songs.map(&:title) # => "Padam"
The class methods #references_many, #references_one and
#referenced_by can take the following options:
- :as - used to give a different name to the other party in the
relationship
- :alias - used to give a differnt name of itself to the other party
in the relationship - there must be a mapping: if A references_many
B as Ben and B is referenced_in A as Al, references_many must have
an alias Al and referenced_in must have an alias Ben.
- :dependent - only used for :references_one and :references_many
relationships - it is possible to set to :nullify so all dependents
get their referrer id set to nil when the referrer is deleted, or to
:delete to have them all deleted instead when the referrer is
deleted
Basic Queries
It is possible to create basic queries and sort the result list of
models. Please note that the queries currently available are quite
limited but might be enhanced in the future. Currently any kind of
querying more advanced than what is described here would have to be
implemented using directly the Redis gem and Redis native commands.
The querying feature adds the following class methods that can be
chained together:
- #when/#and: pass a hash of equalities, the result will be the
list of items that matches ALL the parameter equalities at once
- #any: pass a hash of equalities, the result will be the list of
items that matches ANY of the parameter equalities
- #not: pass a hash of equalities, the result will be the list of
items that matches NONE of the parameter equalities
- #sort: sorts the result based on the attribute passed, using the
default ascending alphanumeric order (:inc_alpha) - the other
possible orders are: :desc_num, :desc_alpha, :asc_num and :asc
:desc_alpha
- #first, #last, #rand and #all can be called on any sort
query result to fetch the desired result
The methods #and (and alias #when), #any and #not create
intermediate Redis sets
Example: we have a tenant model that represent user accounts on a
telephony service application. A tenant has many phone calls that are
made on the platform. Each phone call that goes through the platform is
made from a phone number called the ANI (calling number), to another
phone number called the DNIS (number called). Each call can be using the
Plain Old Telephone Service (pots) or Voice Over IP (voip), and lasts
for a number of seconds. And finally each call goes through the network
of an operator among AT&T, Qwest and Level3.
class Tenant
include Ricordami::Model
model_can :be_queried, :be_validated, :have_relationships
attribute :name, :read_only => true
index :unique => :name, :get_by => true
references_many :calls, :alias => :owner, :dependent => :delete
validates_presence_of :name
validates_uniqueness_of :name
end
class Call
include Ricordami::Model
model_can :be_queried, :be_validated, :have_relationships
attribute :ani, :indexed => :value
attribute :dnis, :indexed => :value
attribute :call_type, :indexed => :value
attribute :network, :indexed => :value
attribute :seconds, :type => :integer
referenced_in :tenant, :as => :owner
validates_presence_of :call_type, :seconds, :owner_id
validates_inclusion_of :call_type, :in => ["pots", "voip"]
validates_inclusion_of :network, :in => ["att", "qwest", "level3"]
end
# ...create tenant and calls...
# What is the total number of seconds of the phone calls made from the phone number 650 123 4567?
seconds = Call.where(:ani => "6501234567").inject(0) { |sum, call| sum + call.seconds }
puts " => seconds = #{seconds}"
# What are the VoIP calls that didn't go through Level3 network?
calls = Call.where(:call_type => "voip").not(:network => "level3")
puts " => #{calls.inspect}"
# What are the calls for tenant "mycompany" that went through AT&T's network or originated from ANI 408 123 4567? but were not VoIP calls?
mycompany = Tenant.get_by_name("mycompany")
calls = mycompany.calls.any(:ani => "4081234567", :network => "att").not(:call_type => "voip")
puts " => #{calls.count} calls"
puts " => first page of 10: #{calls.paginate(:page => 1, :per_page => 10).inspect}"
How To Run Specs
$ bundle exec rspec spec
$ rake rspec
$ bundle exec autotest
Multiple Ruby Versions
Infinity test is like autotest for testing with several versions of Ruby
rather than just one. It requires using rvm to install and manage
multiple Ruby versions.
First you need to install the ruby versions (only install those that are
missing of course). For each version we create a new gemset which
basically acts as a gem sandbox that won't affect the other work you do
on the same machine.
Ruby Enterprise:
$ rvm install ree-1.8.7-2011.03 # install if necessary
$ rvm use ree-1.8.7-2011.03
$ rvm gemset create ricordami
$ rvm gemset use ricordami
$ gem install bundler --no-ri --no-rdoc
$ bundle
Rubinius:
$ rvm install rbx-1.2.2 # install if necessary
$ rvm use rbx-1.2.2
$ rvm gemset create ricordami
$ rvm gemset use ricordami
$ gem install bundler --no-ri --no-rdoc
$ bundle
MRI 1.9.2:
$ rvm install 1.9.2-p180 # install if necessary
$ rvm use 1.9.2-p180
$ rvm gemset create ricordami
$ rvm gemset use ricordami
$ gem install bundler --no-ri --no-rdoc
$ bundle
Run the infinity test:
$ bundle exec infinity_test
Why Ricordami?
Ricordami's design goal is to find the best trade off between speed and
features. Its syntax goal is to be close enough to ORMs such as Active
Record or Mongoid, so the learning curve stays pretty small.
Ricordami is NOT an attempt at competing with full featured ORMs such as
Active Record or Data Mapper for relational databases, or Mongoid or
Mongo Mapper for MongoDB.
I started Ricordami because I needed to scale and distribute an
event based application accross many servers. I decided to use
the REST-like API micro framework Grape to structure the API, and chose
Redis to externalize and hold the application state. I needed a library
to structure the data layer and didn't find any library that would work
for me. If I would have searched a bit more I would have found Ohm
(http://ohm.keyvalue.org/) and the story would have stopped here.
Thanks
First of all thanks to Salvatore Sanfilippo (@antirez)
for Redis. Redis is sucn an amazing application, it makes you want to
write things for it just for the fun of playing with it.
Also I might not have started Ricordami without the amazing work done
and shared by the Rails team, especially DHH, Yehuda Katz and Carl Huda.
ActiveSupport and ActiveModel are just amazingly flexible and so easy to
build on. Also I might never have heard of great resources like
Grape and
Infinity Test without
the podcasts Ruby5,
ChangeLog and The Ruby Show.
License
Released under the MIT License. See the MIT-LICENSE file for further
details.
Copyright
Copyright (c) 2011 Mathieu Lajugie