Rubernate

{Dynamic Persistence
for Dynamic Language}

Testing

Rubernate v. 0.1.5 has been testes with DBI 0.1.0.

Unit test suit includes 76 tests (233 assertions) of common tests and 39 tests (336 assertions) for each database. So full set of tests consists of 205 tests(1268 assertions).

Database Structure

It is essential to be familiar with database structure for effective work with Rubernate. Not only for query building but also for performance tuning. Database initialization script for Oracle looks like the following:

  CREATE TABLE R_OBJECTS (
      OBJECT_PK       NUMBER(20)      PRIMARY KEY,
      OBJECT_CLASS    VARCHAR2(100)   NOT NULL) 

  CREATE TABLE R_PARAMS (
      OBJECT_PK       NUMBER(20)    NOT NULL,
      NAME            VARCHAR2(100) NOT NULL,
      FLAGS           NUMBER(5)     NOT NULL,
      INT_VALUE       NUMBER(20),
      FLT_VALUE       FLOAT,
      STR_VALUE       VARCHAR2(1000),
      DAT_VALUE       DATE,
      REF_VALUE       NUMBER(20),
    CONSTRAINT R_PARAM_FK FOREIGN KEY (OBJECT_PK) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE,
    CONSTRAINT R_REF_FK   FOREIGN KEY (REF_VALUE) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE)

  CREATE INDEX R_O_PK_CLASS ON R_OBJECTS (OBJECT_PK, OBJECT_CLASS)
  CREATE INDEX R_P_PK_NAME  ON R_PARAMS  (OBJECT_PK, NAME)
  
  CREATE SEQUENCE R_PK_SEQUENCE START WITH 1001 INCREMENT BY 1  
  

With this set of indexes, query like this

  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 = ?
  

has quite good plan. Note that most important index here is R_O_PK_CLASS. Therefore it worth specifying object classes in all queries to avoid bad execution plans. Feel free to change indexes on your database.

To better understaning database structure consider the following example:

  require 'rubernate'

  class Holder
    persistent :items
  end

  class Item
    persistent :number
    def initialize number
      self.number = number
    end
  end

  Rubernate.config :oracle, 'dbi:OCI8:sid', 'login', 'password'
  Rubernate.session do
    o = Holder.new.attach
    o.items = {
      'first'  => Item.new(1).attach,
      'second' => Item.new(2).attach,
      'third'  => Item.new(3).attach
    }
  end

After this script has been executed, database will have state like this:

Performance of Collections

Performance of collections is another critical issue. Let's consider simple example: ex4_lazy.rb.

  class P
    persistent :prop
    
    def initialize prop = nil
      self.prop = prop
    end

    # Calculates sum of values in collection.
    # prop.inject(0) {|sum, o| sum + o.prop} 
    def sum 
      sum = 0
      for o in prop
        sum += o.prop
      end
      sum
    end
  end

Class P has one persistent attribute prop. Method sum treats this attribute as array of objects of class P, and sum values of attribute prop of those objects. They must be of type Numeric.

  pk = nil
  # Create test data
  Rubernate.session do
    o  = P.new.attach
    pk = o.primary_key

    o.prop = []  
    for i in 1..500
      o.prop << P.new(i).attach
    end
  end

Now we have object whose prop contains array of 500 objects.

  require 'benchmark'

  Rubernate.session do
    o = Rubernate.find_by_pk pk   # Load object    

    puts "o.prop.size = #{o.prop.size}"   
    
    puts Benchmark.measure { puts "o.sum = #{o.sum}"}
  end

Benchmark produces 1.046000 0.047000 1.093000 ( 1.765000) on my Oracle database. The simple iteration over array taks about a second. The question is not why does it take so long. but why does it take any time at all. The reason is that, find_by_pk and find_by_query do not load objects that are held by collections. When we access the objects from collection they load lazily. In result all of 500 objects loads separately, see logs/ex4_pre-load.log. There is one obvious solution of this proplem - pre-load objects.

  puts "\nThe same but with objects pre-loading..."
  Rubernate.session do
    o = Rubernate.find_by_pk pk  # Load object
    puts "o.prop.size = #{o.prop.size}"

    puts Benchmark.measure {
      # Pre-load objects
      Rubernate.find_by_query "select ref_value from 
                               r_params where object_pk = ?", [pk]
      puts "o.sum = #{o.sum}"
    }
  end

The result now is 0.109000 0.000000 0.109000 ( 0.578000). It's much better by time but pre-loading code looks very ugly. Perhaps this issue will be solved somehow in future release.

Peers

Peers are objects that holds attribute values of persistent objects. Each persistent objects has one peer. Peer is just hash that maps name of attribute to its value. It can be achieved via method __peer on persistent object.

  require 'rubernate'

  class C; persistent :p1, :p2 end

  o = C.new
  o.p1, o.p2 = 3.1415, 'Pi'

  puts o.__peer[:p1], o.__peer[:p2]

Inspite of __peer is public method, it is implementation detail and you are not supposed to use it. Peers are also responsible for tracking changes. They are able to track changes of both simple attributes (Integer, Float, String, Date, Time, Reference to Persistent Object) and collections (Arrays and Hashes) of References. References to persistent object is stored in database in column ref_value in table r_params. In runtime it is ordinary object and behave as ordinary object so you can invoke any method on it. But it may have no peer. That will be loaded lazily when you try to access persistent attribute of object.


Lazy Loading

Since ver.1.6. lazy loading facility has been improved largely. There are several lazy loading strategies now, that can be chosen via property Rubernate.settings[:lazy_load] = [strategy]. Where the [strategy] can be one of the following:


Now we can get rid of ugly pre-loading code described above.

  Rubernate.settings[:lazy_load] = :collection
  Rubernate.session do |rt|
    o = Rubernate.find_by_pk pk  # Load object
    puts "o.prop.size = #{o.prop.size}"
    puts Benchmark.measure {puts "o.sum = #{o.sum}"}
  end