Simple way to persist Ruby objects into the Redis data structure server.
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
autotest
examples
lib
spec
test
.gitignore
.infinity_test
.rspec
.rvmrc
CHANGELOG.md
Gemfile
Gemfile.lock
ISSUES.md
MIT-LICENSE
README.md
Rakefile
TODO.md
ricordami.gemspec

README.md

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.

$ for ruby in 1.8.7-p334 rbx-1.2.3 ree-1.8.7-2011.03 jruby-1.6.0 1.9.2-p180
  do
    echo "-*- $ruby -*-"
    rvm use $ruby@ricordami --create
    gem install bundler --no-ri --no-rdoc
    bundle
  done

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