- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
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, |
|
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