Rubernate
Dynamic Persistence
for Dynamic Language

Rubernate User Guide

Table of Content

Introduction
Installation
Getting Started
Persisten Objects
  Database Structure
  Dynamic Persistence
  Arrays
  Hashes
  References
  Supported Types
  Drawbacks
Module Rubernate
Query Language
Class Runtime
Callbacks
TODO: ...

Intorduction

This guide shortly introduce you to main concepts of Rubernate. You'll see how Rubernate differs from traditional ORMs, how it can be used to manage peristence of Ruby objects and which advantages and disadvantages it has. It's very simple to use you can create and modify persistent classes without bother about database structure.

Installation

Rubernate depends on Ruby/DBI and Log4r so you should have them installed. Also you should have appropriate DBI driver for MySQL or Oracle.
Rubernate Gem can be got from project page. To install Rubernate use:

  
  gem install Rubernate-0.1.2.gem
  
Then you should create and initialize database that you want to use in your project. Rubernate creates two additional tables in your database. Fill free to change settings of these tables - add/remove indices and etc.

Data base can be initialized by following way

  
  require 'rubernate'

  Rubernate.config :mysql|:oracle|:pg, db_url , db_user, db_password

  Rubernate.init_db
  
This program prints full initialization script and tries to run it. You can get this script here.

Getting Started

It is better to start witch example and take a look how Rubernate differs from usual ORMs. We'll use a common example - a Person persistent class.

  
  require 'rubernate'  
   
  class Person
    persistent :name, :surname
    
    def initialize name, surname
      self.name = name
      self.surname = surname
    end
  end
  
That's all. Persistent class is created. You can get and use it.

Let's look what really happened:

  
  persistent :name, :surname
  
This line adds two attributes named 'name' and 'surname' to class Person (like attr_accessor) and makes them and whole class persistent. This means that you can store instances of this class and its subclasses to database and load them back. Moreover persistent adds several useful methods to class on which it is invoked. The most important of them are:
  • primary_key - read only property indicates primary key of object in database.
  • attach - attaches new object to session, primary key will be generated and image of that object will be stored in database.
  • remove! - removes object from database.

Let's create instance of class Person in database.
But first we should setup connection to database. It can be done using Rubernate.config. This method is invoked once per application to configure Rubernate. It accepts following parameters:

  1. Type of database, only :mysql and :oracle are supported now.
  2. Url of database
  3. User name or nil if authentication is disabled
  4. Password or nil if authentication is disabled
Rubernate uses Log4r so we can use it to track events happens in runtime. The easiest way to setup logger is - by hand in code. Next example demonstrates it.
  
  Rubernate.config :mysql, 'dbi:Mysql:rubernate_db:localhost', nil, nil
  
  Rubernate::Log.level = Log4r::DEBUG
  Rubernate::Log.add Log4r::FileOutputter.new('file', 
      :filename => $0.sub('.rb', '.log'),
      :trunc => true)       
  
  Rubernate.session do
    homer = Person.new('Homer', 'Simpson').attach    
    puts "I'm in database now, my primary key is #{homer.primary_key}"
  end
  
Done. Homer has been stored in database as attach has been invoked. Method Rubernate.session manages transaction. Module Rubernate will be described later.

Now we can load Homer by two ways. If we remember his primary key we can use Rubernate.find_by_pk, else we can use Rubernate.find_by_query and find object by name.

  
  Rubernate.session do
    homers = Rubernate.find_by_query 'Select :o; Where o.name.str == :name',
        :name => 'Homer'
    puts 'No objects have been found' if homers.empty?
    puts "Homer has been found #{homers[0].primary_key}." if homers.size == 1
    puts "There are #{homers.size} persons with name Homer" if homers.size > 0
  end
  
You can get full text of this example here. Rubernates query language will be explained later in this guide.

Persistent Objects

Note, that have created persistent class in previous example, we don't ever remembered about databases tables and other SQL stuff. This level of simplicity is achieved because of special databases structure used by rubernate.

Database Structure

All objects managed by rubernate are stored only in two tables r_objects and r_params which look like following

  
  r_objects       # This table hold records about all objects.      
    object_pk     # Objects primary key
    object_class  # Objects class
  
  
  r_params        # This table holds objects attributes values.    
    object_pk     # Objects primary key
    flags         # Parameters flags
    name          # Parameters name
    int_value     # Integer value
    flt_value     # Float value
    str_value     # String value
    dat_value     # Date and Time values
    ref_value     # Reference to other persistent object
  

Dynamic Persistence

Let's find out which advantages and disadvantages gives such approach to object persistense. An obvious advantage is great flexibility. Developing your persistent classes you are not longer restricted by database structure. You can even completely forget about it, just use function persistent if you want to make some attribute persistent.

Attributes made by persistent behave like ordinary attributes made by attr_accessor with one exception there is no private variable @attr_name created. You shoud access these attributes only by methods self.attr_name and self.attr_name=. Using persistent classes managed by ORMs, you can assign to attributes only values agreed with types of appropriate tables columns. Rubernate don't impose such strong constraint. Feel free to assign to persistent attributes values of any of supported types. It's fairly consistent with dynamic nature of Ruby language. Lets consider some examples illustrates this. We'll add new persistent attribute family to class Person.

  
  class Person
    persistent :name, :surname, :family
    
    def initialize name, surname
      self.name = name
      self.surname = surname
    end  
  end
  

Arrays

Attribute family has been added, but we have still not decided how to represent family. The simplest way is to use Array.
  
  homers_pk = nil
  
  Rubernate.session do
    homer = Person.new('Homer', 'Simpson').attach 
    
    homer.family = [
      Person.new('Marge', 'Simpson').attach,
      Person.new('Burt', 'Simpson').attach,
      Person.new('Liza', 'Simpson').attach   ]

    homers_pk = homer.primary_key
  end
  
We have created three new Persons and assign array with them it to Homers family property. Array will be successfully stored to database. But it istoo rough to representfamily as Array because it is hard to recognize who is who.

Hashes

Hash appears to be more appropriate structure.
   
  Rubernate.session do
    homer = Rubernate.find_by_pk  homers_pk 
    
    homer.family = {
      'wife'     => homer.family[0], # Marge
      'son'      => homer.family[1], # Burt
      'daughter' => homer.family[2]  # Liza
    } 
  end
  

References

Hash is more convenient. But the best way is to create new class to represent family.
   
  class Family
    persistent :name, :father, :mother, :sons, :daughters  
    def initialize name
      self.name = name
    end  
    # Returns all children of family
    def children
      sons + daughters
    end 
    def to_s
      "Father: #{father}\nMother: #{mother}\nChildren: #{children.join(', ')}"
    end
  end
  
It is not thought-out but for example it well enough.
  
  Rubernate.session do
    homer = Rubernate.find_by_pk homers_pk
    
    simpsons = Family.new('Simpsons').attach
    simpsons.father = homer
    simpsons.mother = homer.family['wife']
    simpsons.sons = [homer.family['son']] 
    simpsons.daughters = [homer.family['daughter']]

    puts simpsons
    
    homer.family = simpsons
  end   
  
If you want to all family members to be removed along with Family object use callback method on_remove. It can be something like this
  
  class Family
    # Removes all members along with family object
    def on_remove
      father.remove! if father
      mother.remove! if mother
      sons.each {|son| son.remove!} if sons
      daughters.each {|dt| dt.remove!} if daughters
    end
  end
  
Complete test of this example can be got here.

Supported Types

This table summarizes supported types and the way they are stored in database.
TypeField of r_params
Integer  int_value
Float  flt_value
String  str_value
Date  dat_value (converted during loading/storing)
Time  dat_value
Reference to Persistent Object  ref_value
Array of References  ref_value, index is stored in int_value
Hash of References,
with key of type:
Integer, String, Date or Time
 rev_value, key is stored in appropriate column

Drawbacks

There are some drawback in the way Rubernate stores objects.

  • The most important of them is performance lack. TODO: As well as Ruby :) ...
  • Queries complexity. In comparsion with ordinary tables, SQL queries to Rubernates tables can be quite complex. For example query that finds all objects of class Person with property name equal 'Homer' can looks like following
      
      select o_.* from r_objects o_
        left outer join r_params o_name on (o_.object_pk = o_name.object_pk)
        where o_name.name = 'name' and
          o_.object_class = 'Person' and
          o_name.str_value = 'Homer'
      
    However such queries can be much more flexible then queries to usual flat tables. They allow you, for example, to query all objects of certain class or its subclasses. Rubernates Query Language described later simplifies queries. See how previous example was built here.
  • Weak reference integrity. TODO: ...
TODO...

Module Rubernate

Rubernate is main module of the library. It contains several useful methods most of them are available both as instance and as module methods. So you can include this module into your classes to access them. All persistent classes inclued this module by default.

Rubernate.config

This method performs configuration of Rubernate. It accepts four parameters

  1. db - type of databaes (:mysql or :oracle).
  2. url - url of database.
  3. user - databases user name
  4. password - databases users password

Rubernate.init_db

Prints database initialization sql script and tries to execute it. If database has already been initialized nothing happens with it. Else all necessary tables will be created and rubernte will be able to work properly. You can get sql script printed by this method, make any changes you need (add/change indexes, database engine for mysql and etc.) and run agains database by hand. Rubernate will still work with it.

  
  Rubernate.config :mysql, 'dbi:Mysql:rubernate_db:localhost', 'db_user', 'db_password'
  Rubernate.init_db
  

Rubernate.session

This method creates new database transaction and object of type Rubernate::Runtime that manages that transaction. Returns created Runime object or pass it to block if given. All changes of persistent objects made in session boundaries will be stored to database. You should manually commit or rollback session by invoking corresponding method on Runtime object. If you use block, session do it for you unless exception rises.

Examples:

  
  runtime = Rubernate.session 
  # Do smth...
  runtime.commit # Commit transaction
  
or
  
  Rubernate.session do |runtime|
  # Do smth...
  end # Commit transaction
  
Rolling back.
  
  begin
    Rubernate.session do
      raise 'I want to rollback transaction'
    end
  rescue
    puts 'Transaction rolledback'
  end    
  

Rubernate.find_by_pk

Finds object by primary key. Raises Rubernate::ObjectNotFoundException if there is no one. This method is both module method and instance method.

Rubernate.find_by_query

Finds objects by query. This method accepts two kind of queries:
  • Rubernate Query Language. In this case the second argument (if ther is one) must be a Hash that maps query parameter marker (Symbol) to query parameter value of any of supported types.
      
      persons = Rubernate.find_by_query 'Select :p; Where p.klass == Person, 
                  p.name.str == :name, p.surname.str == :surname', 
            :name => 'John',
            :surname => 'Smith'
      
    You can get this example here.
  • Database native SQL. In this case second argument shoud be an array of query parameters. There is only one restriction imposed on SQL queries passed to this method - it must return resulting objects primary keys in first column of result set. Other columns except first won't be read.
      
      persons = Rubernate.find_by_query "select p_.* from r_objects p_
            left outer join r_params p_name on (p_.object_pk = p_name.object_pk)
            left outer join r_params p_surname on (p_.object_pk = p_surname.object_pk)
          where p_name.name = 'name' and
              p_surname.name = 'surname' and
              p_.object_class = 'Person' and
              p_name.str_value = ? and
              p_surname.str_value = ?", ['John', 'Smith']  
      
    You can get working script here.

Rubernate.dbh

This method retruns DBI::DatabaseHandle used in this session.

Query Language

As we have seen writing native sql queries to Rubernate database is too tedious. So there is query language built in. This language is just Ruby tailored to build SQL queries. Ordinary query on this language looks like following

  
  Select(:o, :f)
  Where ( And(Eq(o.family.ref, f.pk), 
              Eq(f.name.str, :name)))
  
Or in short form
  
  Select :o, :f; Where o.family.ref == f.pk, f.name.str == :name
  
This query finds all objects 'o' whose attribute 'family' refers to other object 'p' whose attribute 'name' has certain value marked as ':name'. It will be translated to following sql
  select o_.* from 
      r_objects f_ left outer join r_params f_name   on (f_.object_pk = f_name.object_pk),
      r_objects o_ left outer join r_params o_family on (o_.object_pk = o_family.object_pk)
    where 
      o_family.name = 'family' and
      f_name.name   = 'name' and
      o_family.ref_value = f_.object_pk and
      f_name.str_value = ?
  
All in this query are plain Ruby functions.
  • Select - accepts any number of Symbols that will be treated as virtual tables of objects. This means that they will appear in FROM sql clause. The first of them will be result of query i.e. it will appear in SELECT sql clause (see previous example). This function also creates corresponding methods in context of query so you can access them in query.
  • Where - accepts any number of expressions that will be treated as constraints and will be listed in WHERE sql clause through the AND operator.
  • [obj_name].[attr_name] - generates left outer join from r_object to r_param and appends condition on field 'name' of table r_params. E.g. o.family will be translated to join and constraint like follow
         
      select *...
        from r_objects o_ left outer join r_params o_family on (o_.object_pk = o_family.object_pk)
        where o_family.name = 'family'  # constraing on attribute name
      
  • [obj_name].[attr_name].[field] - access to certain field of r_params, [field] can be:
    pk - correspond to r_params.object_pk
    name - r_params.name
    flags - r_params.flags
    int - r_params.int_value
    float - r_params.flt_value
    str - r_params.str_value
    time - r_params.dat_value
    date - r_params.dat_value
    ref - r_params.ref_value
    These functions are presented in class Rubernate::Queryes::RParam. For example: o.family.ref will be translated to
      
      ... o_family.name = 'family' and o_family.ref_value
      
  • And - accepsts any number of expression and orders them through AND.
  • Or - the same as And exept it builds expresson with OR.
  • Eq or '==' - accepts two arguments and creates sql equality '=' clause. So o.family.ref == f.pk translates to
      
      ... o_family.ref_value = f_.object_pk
      
These and other functions can be found in module Rubernate::Queries::Operations and in class Rubernate::Queries::Query.

Class Runtime

Runtime is central class of the library. Instances of this class are created and associated witch each session. It holds objects loaded during session, and other session-related data. It's abstract class so actual working with database is implemented in subclasses.
  • begin()
  • - Begins session and starts transaction. This method is invoked by Rubernate.
  • commit()
  • - Stores all changes and commits transction.
  • rollback()
  • - Stores all changes and commits transction.
  • attach(object)
  • - Attaches new object to session and persists it in database. This method is actually invoked when you call attach on persistent object.
  • remove(object)
  • - Removes object. This method is actually invoked when you call remove! on persistent object.
  • find_by_pk(pk)
  • - Finds object by primary key. Raises ObjectNotFoundException if object was not found. There is also shortcut method in module Rubernate.find_by_pk
  • find_by_query(query, params)
  • - Finds objects by query (SQL/Query Language).
  • flush_modified()
  • - Stores all modified objects to database. This method is invoked before transaction commit and before each invokation of find_by_query(). You can invoke this method manually if you want to store all changes in your objects to database immediately.

    Callbacks

    TODO

    Copyright (C) 2006 Andrey Ryabov