Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
28
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

SINGLE TABLE INHERITANCE 253

The correct way to change the value of the columns associated with a composite attribute is to assign a new composite object to that attribute.

customer = Customer.find(123) old_name = customer.name

customer.name = Name.new(old_name.first, old_name.initials, "Smith") customer.save

15.3 Single Table Inheritance

When we program with objects and classes, we sometimes use inheritance to express the relationship between abstractions. Our application might deal with people in various roles: customers, employees, managers, and so on. All roles will have some properties in common and other properties that are role specific. We might model this by saying that class Employee and class Customer are both subclasses of class Person and that Manager is in turn a subclass of Employee. The subclasses inherit the properties and responsibilities of their parent class.

In the relational database world, we don’t have the concept of inheritance: relationships are expressed primarily in terms of associations. However, we may need to store an object-oriented object model inside a relational database. There are many ways of mapping one into the other. Possibly the simplest is a scheme called single table inheritance. In it, we map all the classes in the inheritance hierarchy into a single database table. This table contains a column for each of the attributes of all the classes in the hierarchy. It additionally includes a column, by convention called type, that identifies which particular class of object is represented by any particular row. This is illustrated in Figure 15.3, on page 255.

Using single table inheritance in Active Record is straightforward. Define the inheritance hierarchy you need in your model classes, and ensure that the table corresponding to the base class of the hierarchy contains a column for each of the attributes of all the classes in that hierarchy. The table must additionally include a type column, used to discriminate the class of the corresponding model objects.

When defining the table, remember that the attributes of subclasses will be present only in the table rows corresponding to those subclasses; an employee doesn’t have a balance attribute, for example. As a result, you must define the table to allow null values for any column that doesn’t appear in all subclasses. The following is the DDL for the table illustrated in Figure 15.3, on page 255.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

 

SINGLE TABLE INHERITANCE

254

File 6

create table people (

 

 

 

id

int

not null auto_increment,

 

 

type

varchar(20)

not null,

 

 

/* common attributes */

 

 

 

name

varchar(100)

not null,

 

 

email

varchar(100)

not null,

 

 

/* attributes for type=Customer */

 

 

balance

decimal(10,2),

 

 

/* attributes for type=Employee */

 

 

reports_to

int,

 

 

 

dept

int,

 

 

 

/* attributes for type=Manager */

 

 

/* -- none -- */

 

 

 

 

constraint fk_reports_to foreign key (reports_to) references people(id),

 

 

primary key (id)

 

 

 

 

);

 

 

 

 

We can define our hierarchy of model objects.

 

File 15

class Person < ActiveRecord::Base

 

 

end

 

 

 

 

class Customer < Person

 

 

 

end

 

 

 

 

class Employee < Person

 

 

 

end

 

 

 

 

class Manager < Employee

 

 

 

end

 

 

 

 

Then we create a couple of rows and read them back.

 

File 15

Manager.create(:name => 'Bob', :email => "bob@some.add",

 

 

:dept => 12, :reports_to => nil)

 

Customer.create(:name => 'Sally', :email => "sally@other.add", :balance => 123.45)

person = Person.find(:first)

puts person.class

#=> Manager

puts person.name

#=> Bob

puts person.dept

#=> 12

person = Person.find_by_name("Sally")

puts person.class

#=> Customer

puts person.email

#=> sally@other.add

puts person.balance

#=> 123.45

Notice how we ask the base class, Person, to find a row, but the class of the object returned is Manager in one instance and Customer in the next; Active Record determined the type by examining the type column of the row and created the appropriate object.

There’s one fairly obvious constraint when using single table inheritance. Two subclasses can’t have attributes with the same name but with different types, as the two attributes would map to the same column in the underlying schema.

Prepared exclusively for Rida Al Barazi

Report erratum

SINGLE TABLE INHERITANCE 255

 

 

 

 

 

Person

 

 

 

 

 

 

class Person < ActiveRecord::Base

 

 

 

 

name

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

# ...

 

 

 

 

 

 

 

 

email

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

end

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class Customer < Person

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

# ...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

end

 

 

 

Customer

 

 

 

Employee

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

balance

 

 

 

reports_to

 

 

 

class Employee < Person

 

 

 

 

 

 

 

 

dept

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

# ...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

end

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class Manager < Employee

 

 

 

 

 

 

 

 

 

Manager

 

 

 

 

 

 

 

 

 

 

 

 

 

# ...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

end

 

 

 

people

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

id

type

name

email

 

balance

reports_to

dept

 

 

1

Customer

John Doe

john@doe.com

 

78.29

 

 

 

 

 

2

Manager

Wilma Flint

wilma@here.com

 

 

 

 

23

 

 

3

Customer

Bert Public

b@public.net

 

12.45

 

 

 

 

 

4

Employee

Barney Rub

barney@here.com

 

 

2

 

23

 

 

5

Employee

Betty Rub

betty@here.com

 

 

2

 

23

 

 

6

Customer

Ira Buyer

ira9652@aol.com

 

-66.75

 

 

 

 

 

7

Employee

Dino Dogg

dino@dig.prg

 

 

2

 

23

 

Figure 15.3: Single Table Inheritance: A Hierarchy of Four Classes Mapped into One Table

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 256

David Says. . .

Won’t Subclasses Share All the Attributes in STI?

Yes, but it’s not as big of a problem as you think it would be. As long as the subclasses are more similar than not, you can safely ignore the reports_to attribute when dealing with a customer. You simply just don’t use that attribute.

We’re trading the purity of the customer model for speed (selecting just from the people table is much faster than fetching from a join of people and customers tables) and for ease of implementation.

This works in a lot of cases, but not all. It doesn’t work too well for abstract relationships with very little overlap between the subclasses. For example, a content management system could declare a Content base class and have subclasses such as Article, Image, Page, and so forth. But these subclasses are likely to be wildly different, which will lead to an overly large base table as it has to encompass all the attributes from all the subclasses. In this case, it would be better to use associations and define a ContentMetadata class that all the concrete content classes could do a has_one( ) with.

There’s also a less obvious constraint. The attribute type is also the name of a built-in Ruby method, so accessing it directly to set or change the type of a row may result in stange Ruby messages. Instead, access it implicitly by creating objects of the appropriate class, or access it via the model object’s indexing interface, using something such as

person[:type] = 'Manager'

15.4 Validation

Active Record can validate the contents of a model object. This validation can be performed automatically when an object is saved. You can also programmatically request validation of the current state of a model.

As we mentioned in the previous chapter, Active Record distinguishes between models that correspond to an existing row in the database and those that don’t. The latter are called new records (the new_record?( ) method will return true for them). When you call the save( ) method, Active Record will perform an SQL insert operation for new records and an update for existing ones.

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 257

This distinction is reflected in Active Record’s validation workflow—you can specify validations that are performed on all save operations and other validations that are performed only on creates or updates.

At the lowest level you specify validations by implementing one or more of the methods validate( ), validate_on_create( ), and validate_on_update( ). The validate( ) method is invoked on every save operation. One of the other two is invoked depending on whether the record is new or whether it was previously read from the database.

You can also run validation at any time without saving the model object to the database by calling the valid?( ) method. This invokes the same two validation methods that would be invoked if save( ) had been called.

For example, the following code ensures that the user name column is always set to something valid and that the name is unique for new User objects. (We’ll see later how these types of constraints can be specified more simply.)

class User < ActiveRecord::Base

def validate

unless name && name =~ /^\w+$/ errors.add(:name, "is missing or invalid")

end end

def validate_on_create

if self.find_by_name(name) errors.add(:name, "is already being used")

end end

end

When a validate method finds a problem, it adds a message to the list of errors for this model object using errors.add( ). The first parameter is the name of the offending attribute, and the second is an error message. If you need to add an error message that applies to the model object as a whole, use the add_to_base( ) method instead. (Note that this code uses the support method blank?( ), which returns true if its receiver is nil or an empty string.)

def validate

if name.blank? && email.blank?

errors.add_to_base("You must specify a name or an email address") end

end

As we’ll see on page 353, Rails views can use this list of errors when displaying forms to end users—the fields that have errors will be automatically highlighted, and it’s easy to add a pretty box with an error list to the top of the form.

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 258

You can programmatically get the errors for a particular attribute using errors.on(:name) (aliased to errors[:name]), and you can clear the full list of errors using errors.clear( ). If you look at the RDoc for ActiveRecord::Errors, you’ll find a number of other methods. Most of these have been superseded by higher-level validation helper methods.

Validation Helpers

Some validations are common: this attribute must not be empty, that other attribute must be between 18 and 65, and so on. Active Record has a set of standard helper methods that will add these validations to your model. Each is a class-level method, and all have names that start validates_. Each method takes a list of attribute names optionally followed by a hash of configuration options for the validation.

For example, we could have written the previous validation as

class User < ActiveRecord::Base

validates_format_of :name,

 

 

:with

=>

/^\w+$/,

:message =>

"is missing or invalid"

validates_uniqueness_of :name,

 

:on

 

=> :create,

:message => "is already being used"

end

The majority of the validates_ methods accept :on and :message options. The :on option determines when the validation is applied and takes one of the values :save (the default), :create, or :update. The :message parameter can be used to override the generated error message.

When validation fails, the helpers add an error object to the Active Record model object. This will be associated with the field being validated. After validation, you can access the list of errors by looking at the errors attribute of the model object. When Active Record is used as part of a Rails application, this checking is often done in two steps.

1.The controller attempts to save an Active Record object, but the save fails because of validation problems (returning false). The controller redisplays the form containing the bad data.

2.The view template uses the error_messages_for( ) method to display the error list for the model object, and the user has the opportunity to fix the fields.

We cover the interactions of forms and models in Section 17.8, Error Handling and Model Objects, on page 353.

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 259

Here’s a list of the validation helpers you can use in model objects.

validates_acceptance_of

Validates that a checkbox has been checked.

validates_acceptance_of attr... [ options... ]

Many forms have a checkbox that users must select in order to accept some terms or conditions. This validation simply verifies that this box has been checked by validating that the value of the attribute is the string 1. The attribute itself doesn’t have to be stored in the database (although there’s nothing to stop you storing it if you want to record the confirmation explicitly).

class Order < ActiveRecord::Base validates_acceptance_of :terms,

:message => "Please accept the terms to proceed"

end

Options:

 

:message

text Default is “must be accepted.”

:on

:save, :create, or :update

validates_associated

Performs validation on associated objects.

validates_associated name... [ options... ]

Performs validation on the given attributes, which are assumed to be Active Record models. For each attribute where the associated validation fails, a single message will be added to the errors for that attribute (that is, the individual detailed reasons for failure will not appear in this model’s errors).

Be careful not to include a validates_associated( ) call in models that refer to each other: the first will try to validate the second, which in turn will validate the first, and so on, until you run out of stack.

class Order < ActiveRecord::Base has_many :line_items belongs_to :user

validates_associated :line_items,

:message => "are messed up" validates_associated :user

end

Options:

 

:message

text Default is “is invalid.”

:on

:save, :create, or :update

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 260

validates_confirmation_of

Validates that a field and its doppelgänger have the same content.

validates_confirmation_of attr... [ options... ]

Many forms require a user to enter some piece of information twice, the second copy acting as a confirmation that the first was not mistyped. If you use the naming convention that the second field has the name of the attribute with _confirmation appended, you can use validates_confirmation_of( ) to check that the two fields have the same value. The second field need not be stored in the database.

For example, a view might contain

<%= password_field "user", "password" %><br />

<%= password_field "user", "password_confirmation" %><br />

Within the User model, you can validate that the two passwords are the same using

class User < ActiveRecord::Base validates_confirmation_of :password

end

Options:

 

:message

text Default is “doesn’t match confirmation.”

:on

:save, :create, or :update

validates_each

Validates one or more attributes using a block.

validates_each attr... [ options... ] { |model, attr, value| ... }

Invokes the block for each attribute (skipping those that are nil if :allow_nil is true). Passes in the model being validated, the name of the attribute, and the attribute’s value. As the following example shows, the block should add to the model’s error list if a validation fails.

class User < ActiveRecord::Base

validates_each :name, :email do |model, attr, value| if value =~ /groucho|harpo|chico/i

model.errors.add(attr, "You can't be serious, #{value}")

end end

end

Options:

 

:allow_nil boolean

If :allow_nil is true, attributes with values of nil will not be passed into

 

the block. By default they will.

:on

:save, :create, or :update

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 261

validates_exclusion_of

Validates that attributes are not in a set of values.

validates_exclusion_of attr..., :in => enum [ options... ]

Validates that none of the attributes occurs in enum (any object that supports the include?( ) predicate).

class User < ActiveRecord::Base validates_exclusion_of :genre,

:in => %w{ polka twostep foxtrot }, :message => "no wild music allowed"

validates_exclusion_of :age,

 

 

:in => 13..19,

 

 

:message => "cannot be a teenager"

end

 

 

Options:

 

 

:allow_nil

 

enum is not checked if an attribute is nil and the :allow_nil

 

 

option is true.

:in (or :within)

enumerable

An enumerable object.

:message

text

Default is “is not included in the list.”

:on

 

:save, :create, or :update

validates_format_of

Validates attributes against a pattern.

validates_format_of attr..., :with => regexp [ options... ]

Validates each of the attributes by matching its value against regexp.

class User < ActiveRecord::Base

validates_format_of :length, :with => /^\d+(in|cm)/ end

Options:

 

:message

text Default is “is invalid.”

:on

:save, :create, or :update

:with

The regular expression used to validate the attributes.

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 262

validates_inclusion_of

Validates that attributes belong to a set of values.

validates_inclusion_of attr..., :in => enum [ options... ]

Validates that the value of each of the attributes occurs in enum (any object that supports the include?( ) predicate).

class User < ActiveRecord::Base validates_inclusion_of :gender,

:in => %w{ male female },

:message => "should be 'male' or 'female'" validates_inclusion_of :age,

 

 

:in => 0..130,

 

 

:message => "should be between 0 and 130"

end

 

 

Options:

 

 

:allow_nil

 

enum is not checked if an attribute is nil and the :allow_nil

 

 

option is true.

:in (or :within)

enumerable

An enumerable object.

:message

text

Default is “is not included in the list.”

:on

 

:save, :create, or :update

validates_length_of

Validates the length of attribute values.

validates_length_of attr..., [ options... ]

Validates that the length of the value of each of the attributes meets some constraint: at least a given length, at most a given length, between two lengths, or exactly a given length. Rather than having a single :message option, this validator allows separate messages for different validation failures, although :message may still be used. In all options, the lengths may not be negative.

class User < ActiveRecord::Base validates_length_of :name, :maximum => 50 validates_length_of :password, :in => 6..20 validates_length_of :address, :minimum => 10,

:message => "seems too short"

end

Prepared exclusively for Rida Al Barazi

Report erratum

VALIDATION 263

Options (for validates_length_of):

:in (or :within)

range

The length of value must be in range.

:is

integer

Value must be integer characters long.

:minimum

integer

Value may not be less than the integer characters long.

:maximum

integer

Value may not be greater than integer characters long.

:message

text

The default message depends on the test being performed. The

 

 

message may contain a single %d sequence, which will be

 

 

replaced by the maximum, minimum, or exact length required.

:on

 

:save, :create, or :update

:too_long

text

A synonym for :message when :maximum is being used.

:too_short

text

A synonym for :message when :minimum is being used.

:wrong_length

text

A synonym for :message when :is is being used.

validates_numericality_of

Validates that attributes are valid numbers.

validates_numericality_of attr... [ options... ]

Validates that each of the attributes is a valid number. With the :only_integer option, the attributes must consist of an optional sign followed by one or more digits. Without the option (or if the option is not true), any floating-point format accepted by the Ruby Float( ) method is allowed.

class User < ActiveRecord::Base validates_numericality_of :height_in_meters validates_numericality_of :age, :only_integer => true

end

Options:

 

:message

text Default is “is not a number.”

:on

:save, :create, or :update

:only_integer

If true, the attributes must be strings that contain an optional sign

 

followed only by digits.

validates_presence_of

Validates that attributes are not empty.

validates_presence_of attr... [ options... ]

Validates that each of the attributes is neither nil nor empty.

class User < ActiveRecord::Base validates_presence_of :name, :address

end

Options:

 

:message

text Default is “can’t be empty.”

:on

:save, :create, or :update

Prepared exclusively for Rida Al Barazi

Report erratum