README

Path: README
Last Update: Sun Feb 22 15:57:06 CET 2004

Active Record — Less painful object-relational mapping

Active Record implements the object-relational mapping (ORM) pattern of the same name as described by Martin Fowler, which connects a single business object (or inheritance hierarchy) to a single table. Fowler describes it as follows:

  "An object that wraps a row in a database table or view, encapsulates
       the database access, and adds domain logic on that data."

This differs from data-mapper solutions, like Hibernate for Java, by foregoing the inconvenience of operating everything through a service interface at the expence of marrying the business object to the ORM solution.

So instead of (Hibernate example):

   long pkId = 1234;
   DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
   // something interesting involving a cat...
   sess.save(cat);
   sess.flush(); // force the SQL INSERT

Active Record lets you:

   pkId = 1234
   cat = Cat.find(pkId)
   # something even more interesting involving a the same cat...
   cat.save

Although this is a fundemental difference in approach, it’s the least of the simplifications that Active Record brings.

A short rundown of the major features:

  • Associations between objects controlled by simple meta-programming macros.
     class Firm < ActiveRecord::Base
       has_many  :clients
       has_one   :account
       belong_to :conglomorate
     end
    
  • Validation rules that can differ for new or existing objects.
     class Post < ActiveRecord::Base
       def validate # validates on both creates and updates
         errors.add_on_empty "title"
       end
    
       def validate_on_update
         errors.add_on_empty "password"
       end
     end
    
  • Lifecycle callbacks on everything (instantiation, saving, destroying, validating, etc).
     class Person < ActiveRecord::Base
       def before_destroy # is called just before Person#destroy
         CreditCard.find(credit_card_id).destroy
       end
     end
    
  • Lifecycle observers
     class CommentObserver < ActiveRecord::Observer
       def after_create(comment) # is called just after Comment#save
         NotificationService.send_email("david@loudthinking.com", comment)
       end
     end
    
  • Inheritance hierarchies
     class Company < ActiveRecord::Base; end
     class Firm < Company; end
     class Client < Company; end
     class PriorityClient < Client; end
    
  • Database abstraction through simple adapters
     ActiveRecord::Base.establish_dbi_connection(uri, username, pass)
     ActiveRecord::Base.establish_mysql_connection(host, table, username, pass)
    
  • Logging support for Log4r and Logger

Simple example (1/2): Defining tables and classes

Data definitions are specified only in the database. Active Record queries the database for the column names (that then serves to determine which attributes are valid) on regular objects instantiation through the new constructor and relies on the column names in the rows with the finders.

   # CREATE TABLE companies (
   #   id int(11) unsigned NOT NULL auto_increment,
   #   client_of int(11),
   #   name varchar(255),
   #   type varchar(100),
   #   PRIMARY KEY  (id)
   # )

Active Record automatically links the "Company" object to the "companies" table

   class Company < ActiveRecord::Base
     has_many :people, :class_name => "Person"
   end

   class Firm < Company
     has_many :clients

     def people_with_all_clients
      clients.inject([]) { |people, client| people + client.people }
     end
   end

The foreign_key is only necessary because we didn’t use "firm_id" in the data definition

   class Client < Company
     belong_to :firm, :foreign_key => "client_of"
   end

   # CREATE TABLE people (
   #   id int(11) unsigned NOT NULL auto_increment,
   #   name text,
   #   company_id text,
   #   PRIMARY KEY  (id)
   # )

Active Record can’t guess the table name itself from exceptions like these, so we help it…

   class Person < ActiveRecord
     belong_to :company

     def table_name() "people" end
   end

Simple example (2/2): Using the domain

Picking a database connection for all the active records

   ActiveRecord::Base.establish_mysql_connection("host", "user", "pass", "table")

Create some fixtures

   firm = Firm.new("name" => "Next Angle")
   # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
   firm.save

   client = Client.new("name" => "37signals", "client_of" => firm.id)
   # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
   client.save

Lots of different finders

   # SQL: SELECT * FROM companies WHERE id = 1
   next_angle = Company.find(1)

   # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
   next_angle = Firm.find(1)

   # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
   next_angle = Company.find_first "name = 'Next Angle'"

   next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first

The supertype, Company, will return subtype instances

   Firm === next_angle

All the dynamic methods added by the has_many macro

  next_angle.has_clients?  # true
  next_angle.clients_count # total number of clients
  all_clients = next_angle.clients

Constrained finds makes access security easier when ID comes from a web-app

   # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
   thirty_seven_signals = next_angle.find_in_clients(2)

Bi-directional associations thanks to the "belong_to" macro

   thirty_seven_signals.has_firm? # true
   thirty_seven_signals.firm?(next_angle) # true

Examples

Active Record ships with a bunch of examples that should give you a good feel for operating usage. Be sure to edit the examples/shared_setup.rb file for your own database before running the examples.

It’s also highly recommended to have a look at the unit tests. Read more in RUNNING_UNIT_TESTS.html

Database support

Active Record ships with connection adapters for DBI and MySQL/Ruby (compatible with Ruby/MySQL), but I expect to add other adapters shortly. The adapters are less than 100 lines of code fulfilling the interface specified by ActiveRecord::ConnectionAdapters::AbstractAdapter. Writing a new adapter should be a small task.

Note: The DBI adapter currently calls a MySQL-specific function called "last_id". I’ll add some logic to make it call the appropriate function for the other databases that is supported by DBI shortly. (If you know exactly which for a DBI-supported DB, please send me a note).

Philosophy

Convention over Configuration:

  • No XML-files
  • Lots of reflection
  • Some degree of "magic"

Admit the Database:

  • Lets you drop down to SQL for odd cases and performance
  • Doesn‘t attempt to duplicate or replace data definitions

Download

The latest version of Active Record can be found at

Documentation can be found at

Installation

You can install Active Record with the following command.

  % [sudo] ruby install.rb

from its distribution directory.

License

Active Record is released under the same license as Ruby.

Support

The Active Record homepage is activerecord.rubyforge.org. You can find the Active Record RubyForge page at rubyforge.org/projects/activerecord. And as Jim from Rake says:

   Feel free to submit commits or feature requests.  If you send a patch,
   remember to update the corresponding unit tests.  If fact, I prefer
   new feature to be submitted in the form of new unit tests.

For other information, feel free to ask on the ruby-talk mailing list (which is mirrored to comp.lang.ruby) or contact david@loudthinking.com.

[Validate]