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

Chapter 14

Active Record Basics

Active Record is the object-relational mapping (ORM) layer supplied with Rails. In this chapter, we’ll look at the basics of Active Record—connecting to databases, mapping tables, and manipulating data. We’ll dig deeper into the more advanced stuff in the next chapter.

Active Record closely follows the standard ORM model: tables map to classes, rows to objects, and columns to object attributes. It differs from most other ORM libraries in the way it is configured. By using a sensible set of defaults, Active Record minimizes the amount of configuration that developers perform. To illustrate this, here’s a program that uses Active Record to wrap a table of orders in a MySQL database. After finding the order with a particular id, it modifies the purchaser’s name and saves the result back in the database, updating the original row.1

require "rubygems" require_gem "activerecord"

ActiveRecord::Base.establish_connection(:adapter => "mysql", :host => "localhost", :database => "railsdb")

class Order < ActiveRecord::Base end

order = Order.find(123) order.name = "Dave Thomas" order.save

That’s all there is to it—in this case no configuration information (apart from the database connection stuff) is required. Somehow Active Record figured out what we needed and got it right. Let’s have a look at how this works.

1The examples in this chapter connect to various MySQL databases on the machines we used while writing this book. You’ll need to adjust the connection parameters to get them to work with your database. We discuss connecting to a database in Section 14.4, Connecting to the Database, on page 199.

Prepared exclusively for Rida Al Barazi

TABLES AND CLASSES 191

14.1 Tables and Classes

When you create a subclass of ActiveRecord::Base, you’re creating something that wraps a database table. By default, Active Record assumes that the name of the table is the plural form of the name of the class. If the class name contains multiple capitalized words, the table name is assumed to have underscores between these words. Some irregular plurals are handled.

Class Name

Table Name

Class Name

Table Name

Order

orders

LineItem

line_items

TaxAgency

tax_agencies

Person

people

Batch

batches

Datum

data

Diagnosis

diagnoses

Quantity

quantities

 

 

 

 

These rules reflect DHH’s philosophy that class names should be singular while the names of tables should be plural. If you don’t like this behavior, you can disable it by setting a global flag in your configuration (the file environment.rb in the config directory).

ActiveRecord::Base.pluralize_table_names = false

The algorithm used to derive the plural form of a table name is fairly simplistic. It works in the majority of common cases, but if you have a class named Sheep, it’ll valiantly try to find a table named sheeps. The assumption that the table name and class names are related might also break down if you’re operating with a legacy schema,2 where the table names might otherwise force you to use strange or undesirable class names in your code. For this reason, Active Record allows you to override the default generation of a table name using the set_table_name directive.

class Sheep < ActiveRecord::Base

 

set_table_name "sheep"

# Not "sheeps"

end

 

class Order < ActiveRecord::Base

 

set_table_name "ord_rev99_x"

# Wrap a legacy table...

end

 

2The meaning of the word schema varies across the industry. We use it to mean the definition of tables and their interrelationships in the context of an application or suite of related applications. Basically, the schema is the database structure required by your code.

Prepared exclusively for Rida Al Barazi

Report erratum

COLUMNS AND ATTRIBUTES 192

David Says. . .

Where Are My Attributes?

The notion of a database administrator (DBA) as a separate role from programmer has led some developers to see strict boundaries between code and schema. Active Record blurs that distinction, and no other place is that more apparent than in the lack of explicit attribute definitions in the model.

But fear not. Practice has shown that it makes little difference whether you’re looking at a database schema, a separate XML mapping file, or inline attributes in the model. The composite view is similar to the separations already happening in the Model-View-Control pattern—just on a smaller scale.

Once the discomfort of treating the table schema as part of the model definition has dissipated, you’ll start to realize the benefits of keeping DRY. When you need to add an attribute to the model, you simply change the schema, which automatically retains your data (use alter instead of drop/create), and reload the application.

Taking the “build” step out of schema evolution makes it just as agile as the rest of the code. It becomes much easier to start with a small schema and extend and change it as needed.

14.2 Columns and Attributes

Active Record objects correspond to rows in a database table. The objects have attributes corresponding to the columns in the table. You probably noticed that our definition of class Order didn’t mention any of the columns in the orders table. That’s because Active Record determines them dynamically at runtime. Active Record reflects on the schema inside the database to configure the classes that wrap tables.3

Our orders table might have been created with the following SQL.

File 6

create table orders (

 

 

id

int

not null auto_increment,

 

name

varchar(100)

not null,

 

email

varchar(255)

not null,

3This isn’t strictly true, as a model may have attributes that aren’t part of the schema. We’ll discuss attributes in more depth in the next chapter, starting on page 272.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

 

COLUMNS AND ATTRIBUTES

193

 

address

text

not null,

 

 

pay_type

char(10)

not null,

 

 

shipped_at

datetime

null,

 

 

primary key (id)

 

 

 

 

);

 

 

 

 

We can create an Active Record class that wraps this table.

 

File 7

require 'rubygems'

 

 

 

 

require_gem 'activerecord'

 

 

 

# Connection code omitted...

 

 

 

class Order < ActiveRecord::Base

 

 

end

 

 

 

 

Once we’ve defined the Order class, we can interrogate it for information

 

 

about the attributes (columns) it contains. The code that follows uses the

 

 

columns( ) method, which returns an array of Column objects. From these,

 

 

we display just the name of each column in the orders table and dump

 

 

out the details for one particular column, shipped_at, by looking it up in a

 

 

hash. (This code uses the Ruby pp library to format objects nicely.)

 

File 7

require 'pp'

 

 

 

 

pp Order.columns.map { |col| col.name }

 

 

pp Order.columns_hash['shipped_at']

 

 

When we run this code, we get the following output.

 

 

["id", "name", "email", "address", "pay_type", "shipped_at"]

 

 

#<ActiveRecord::ConnectionAdapters::Column:0x10e4a50

 

 

@default=nil,

 

 

 

 

@limit=nil,

 

 

 

 

@name="shipped_at",

 

 

 

@type=:datetime>

 

 

 

Notice that Active Record determined the type of each column. In the

 

 

example, it has worked out that the shipped_at column is a datetime in the

 

 

database. It’ll hold values from this column in a Ruby Time object. We can

 

 

verify this by writing a string representation of a time into this attribute

 

 

and fetching the contents back out. You’ll find that they come back as a

 

 

Ruby Time.

 

 

 

File 7

order = Order.new

 

 

 

 

order.shipped_at = "2005-03-04 12:34"

 

 

pp order.shipped_at.class

 

 

 

pp order.shipped_at

 

 

This produces

Time

Fri Mar 04 12:34:00 CST 2005

Figure 14.1, on the following page, shows the mapping between SQL types and their Ruby representation. In general this mapping is intuitive. The

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

COLUMNS AND ATTRIBUTES

194

 

 

 

 

 

 

SQL Type

Ruby Class

SQL Type

Ruby Class

 

 

int, integer

Fixnum

float, double

Float

 

 

decimal, numeric

Float

char, varchar, string

String

 

 

interval, date

Date

datetime, time

Time

 

 

clob, blob, text

String

boolean

see text...

 

 

 

 

 

 

 

 

Figure 14.1: Mapping SQL Types to Ruby Types

only potential problem occurs with decimal columns. Schema designers use decimal columns to store numbers with a fixed number of decimal places—decimal columns are intended to be exact. Active Record maps these to objects of class Float. Although this will probably work for most applications, floating-point numbers are not exact, and rounding errors might occur if you perform a sequence of operations on attributes of this type. You might instead want to use integer columns and store currencies in units of pennies, cents, or whatever. Alternatively, you could use aggregations (described starting on page 247) to construct Money objects from separate database columns (dollars and cents, pounds and pence, or whatever).

Accessing Attributes

If a model object has an attribute named balance, you can access the attribute’s value using the indexing operator, passing it either a string or a symbol. Here we’ll use symbols.

account[:balance]

#=>

return current value

account[:balance] = 0.0

#=>

set value of balance

However, this is deprecated in normal code, as it considerably reduces your options should you want to change the underlying implementation of the attribute in the future. Instead, you should access values or model attributes using Ruby accessor methods.

account.balance #=> return current value account.balance = 0.0 #=> set value of balance

The value returned using these two techniques will be cast by Active Record to an appropriate Ruby type if possible (so, for example, if the database column is a timestamp, a Time object will be returned). If you want to get the raw value of an attribute, append _before_type_cast to the method form of its name, as shown in the following code.

Prepared exclusively for Rida Al Barazi

Report erratum

COLUMNS AND ATTRIBUTES 195

David Says. . .

Overriding Model Attributes

Here’s an example of the benefits of using accessors to get at the attributes of models. Our account model will raise an exception immediately when someone tries to set a balance below a minimum value.

class Account < ActiveRecord::Base def balance=(value)

raise BalanceTooLow if value < MINIMUM_LEVEL self[:balance] = value

end end

account.balance_before_type_cast

#=>

"123.4", a string

account.release_date_before_type_cast #=>

"20050301"

Finally, inside the code of the model itself, you can use the read_attribute( ) and write_attribute( ) private methods. These take the attribute name as a string parameter.

Boolean Attributes

Some databases support a boolean column type, others don’t. This makes it hard for Active Record to abstract booleans. For example, if the underlying database has no boolean type, some developers use a char(1) column containing “t” or “f” to represent true or false. Others use integer columns, where 0 is false and 1 is true. Even if the database supports boolean types directly (such as MySQL and its bool column type), they might just be stored as 0 or 1 internally.

The problem is that in Ruby the number 0 and the string “f” are both interpreted as true values in conditions.4 This means that if you use the value of the column directly, your code will interpret the column as true when you intended it to be false.

# DON'T DO THIS

user = Users.find_by_name("Dave") if user.superuser

grant_privileges end

4Ruby has a simple definition of truth. Any value that is not nil or the constant false is true.

Prepared exclusively for Rida Al Barazi

Report erratum

COLUMNS AND ATTRIBUTES 196

To query a column in a condition, you must append a question mark to the column’s name.

# INSTEAD, DO THIS

user = Users.find_by_name("Dave") if user.superuser?

grant_privileges end

This form of attribute accessor looks at the column’s value. It is interpreted as false only if it is the number zero; one of the strings "0", "f", "false", or "" (the empty string); a nil; or the constant false. Otherwise it is interpreted as true.

If you work with legacy schemas or have databases in languages other than English, the definition of truth in the previous paragraph may not hold. In these cases, you can override the built-in definition of the predicate methods. For example, in Dutch, the field might contain J or N (for Ja or Nee). In this case, you could write

class User < ActiveRecord::Base

def superuser? self.superuser == 'J'

end

# . . .

end

Storing Structured Data

It is sometimes convenient to store attributes containing arbitrary Ruby objects directly into database tables. One way that Active Record supports this is by serializing the Ruby object into a string (in YAML format) and storing that string in the database column corresponding to the attribute. In the schema, this column must be defined as type text.

Because Active Record will normally map a character or text column to a plain Ruby string, you need to tell Active Record to use serialization if you want to take advantage of this functionality. For example, we might want to record the last five purchases made by our customers. We’ll create a table containing a text column to hold this information.

File 6

create table purchases (

 

 

id

int

not null auto_increment,

 

name

varchar(100)

not null,

 

last_five

text,

 

 

primary key (id)

 

 

 

);

 

 

Prepared exclusively for Rida Al Barazi

Report erratum