2.6. Complex
Classes
For Photo Share, we've built an object model in
which one table relates to one class. Sometimes, you'll want to map
more sophisticated object models to a database table. The two most
common scenarios for doing so are inheritance and composition. Let's look at how you'd
handle each mapping with Active Record. These examples are not part
of our Photo Share application, but the problems are common enough
that we will show them to you here.
2.6.1.
Inheritance
Active Record supports inheritance relationships
using a strategy called single-table
inheritance, shown in
"#rubyrails-chp-2-fig-3">Figure 2-3. With this kind of
inheritance mapping, all descendents of a common class use the same
table. For example, a photographer is a person with a camera. With
single-table inheritance, all columns for both Person and
Photographer go into the same table. Consider this
table:
CREATE TABLE people (
id INT AUTO_INCREMENT NOT NULL,
type VARCHAR(20),
name VARCHAR(20),
email VARCHAR(30),
camera VARCHAR(20),
PRIMARY KEY (id)
);
A query against Person will return
people and photographers. Active Record doesn't
need to build any special support to handle a query against a
superclass. Subclasses are more difficult. In order to allow a
query returning only Photographers, Active Record must
have some way to determine the type of an object for an individual
row. Active Record uses the type field for this
purpose.
Now, we need classes, which are trivial:
class Photographer < Person
end
class Person < ActiveRecord::Base
end
We declare Photographer as a subclass
of Person. Active Record will manage the type
attribute and everything else. You'll be able to access the
camera property from Photographer. We don't need
these classes for Photo Share, so we'll delete them.
You've probably noticed that Active Record's
implementation of inheritance is not true inheritance because all
items in an inheritance tree have the same attributes. In our
example, all people have cameras even if they are not
photographers. In practice, that limitation is not severe. A parent
can ignore attributes introduced by subclasses. This strategy is a
compromise. You get slightly better performance (because fewer
tables means fewer joins) and simplicity at the cost of muddying
the abstraction a little.
|
Normally, only Active Record needs to set the
type attribute. Be careful when you need to manage
type yourself. You can't say person.type because
type is a class method on Object. If you need to
see the value of the type field, use
person[:type] instead.
|
|
2.6.2.
Composition
If you want to extend a Person class
with Address, you can use a has_one relationship,
or you can use composition.
Composition works well when you want to use a pervasive type like
address or currency across many Active Record models. You'll use
composed_of for this type of relationship, as shown in
"#rubyrails-chp-2-fig-4">Figure
2-4.
Let's look at a Person that is
composed_of an Address. In a composition
relationship, there's a main class (Person) and one or
more component classes (Address). Each component class
explicitly references one or more database columns. Start with a
table that's defined like this:
CREATE TABLE people (
id INT AUTO_INCREMENT NOT NULL,
type VARCHAR(20),
name VARCHAR(20),
email VARCHAR(30),
street_address VARCHAR(30),
city VARCHAR(30),
state VARCHAR(20),
zip INTEGER(5),
camera VARCHAR(20),
PRIMARY KEY (id)
);
You then map the table onto two different
classes. First, create a Person class with a
composed_of relationship:
class Person < ActiveRecord::Base
composed_of :address, :class_name => "Address",
:mapping => [[:street_address, :street_address],
[:city, :city],
[:state, :State],
[:zip, :zip]]
end
If the first parameter for composed_of
and the name of the component class are the same, Active Record can
infer the name of the component class. Otherwise, you can override
it with a :class_name modifier. For example, you can use
composed_of person_address class_name => "Address".
Next, create an Address class:
class Address
def initialize(street_address, city, state, zip)
@street_address = street_address
@city = city
@state = state
@zip = zip
end
attr_reader :street_address, :city, :state, :zip
end
Address is the component class. For
each database column that the component represents, the component
class must have an attribute and a parameter in the
initialize method:
>> elvis=Person.new
>> elvis.name="Elvis Presley"
>> elvis.email= "elvis@graceland.com"
>> address=Address.new("3734 Elvis Presley Blvd", "Memphis", "Tennessee", 38118)
>> elvis.address=address
>> elvis.save
>> puts elvis.address.street_address
3734 Elvis Presley Blvd
Though street_address, city,
state, and zip are columns on the people
table, you don't use those attributes on any Person object
directly. Instead, access these attributes through the
address attribute on Person. Table 2-3 shows the attributes
added by a composed_of relationship.
Table 2-3. Metaprogramming for
composed_of :class
|
Attributes
|
Description
|
|
<class>
|
The component class
(person.address)
|
|
<class>_<attribute>
|
Attributes for the component class
(person.address_zip)
|
|