| Path: | README |
| Last Update: | Sun Feb 22 15:57:06 CET 2004 |
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:
class Firm < ActiveRecord::Base has_many :clients has_one :account belong_to :conglomorate end
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
class Person < ActiveRecord::Base
def before_destroy # is called just before Person#destroy
CreditCard.find(credit_card_id).destroy
end
end
class CommentObserver < ActiveRecord::Observer
def after_create(comment) # is called just after Comment#save
NotificationService.send_email("david@loudthinking.com", comment)
end
end
class Company < ActiveRecord::Base; end class Firm < Company; end class Client < Company; end class PriorityClient < Client; end
ActiveRecord::Base.establish_dbi_connection(uri, username, pass) ActiveRecord::Base.establish_mysql_connection(host, table, username, pass)
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
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
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
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).
Convention over Configuration:
Admit the Database:
The latest version of Active Record can be found at
Documentation can be found at
You can install Active Record with the following command.
% [sudo] ruby install.rb
from its distribution directory.
Active Record is released under the same license as Ruby.
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.