- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •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 A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
TABLES AND CLASSES 284
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.
17.1Tables 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 change it 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 |
|
If you don’t like methods called set_xxx, there’s also a more direct form.
class Sheep < ActiveRecord::Base self.table_name = "sheep"
end
17.2Columns 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.2
2. This isn’t strictly true, because 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 380.
Report erratum
COLUMNS AND ATTRIBUTES 285
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 create a new migration 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.
In the Depot application, our orders table is defined by the following migration.
Download depot_r/db/migrate/005_create_orders.rb
def self.up
create_table :orders do |t| t.column :name, :string t.column :address, :text t.column :email, :string
t.column :pay_type, :string, :limit => 10 end
We’ve already written an Order model class as part of the Depot application. Let’s use the handy-dandy script/console command to play with it. First, we’ll ask for a list of column names.
depot> ruby script/console
Loading development environment. >> Order.column_names
=> ["id", "name", "address", "email", "pay_type"]
Then we’ll ask for the details of the pay_type column.
>> Order.columns_hash["pay_type"]
=> #<ActiveRecord::ConnectionAdapters::MysqlColumn:0x23d8b5c @sql_type="varchar(10)", @default=nil, @name="pay_type",
Report erratum
COLUMNS AND ATTRIBUTES 286
SQL Type |
Ruby Class |
SQL Type |
Ruby Class |
|
|
|
|
int, integer |
Fixnum |
float, double |
Float |
|
|
|
|
decimal, numeric |
BigDecimal¹ |
char, varchar, string |
String |
|
|
|
|
interval, date |
Date |
datetime, time |
Time |
|
|
|
|
clob, blob, text |
String |
boolean |
see text |
|
|
|
|
¹ Decimal and numeric columns are mapped to integers when their scale is 0
Figure 17.1: Mapping SQL Types to Ruby Types
@number=false, @limit=10, @text=true, @type=:string, @null=true, @primary=false>
Notice that Active Record has gleaned a fair amount of information about the pay_type column. It knows that it’s a string of at most 10 characters, it has no default value, it isn’t the primary key, and it may contain a null value. This information was obtained by asking the underlying database the first time we tried to use the Order class.
Figure 17.1 shows the mapping between SQL types and their Ruby representation. Decimal columns are slightly tricky: if the schema specifies columns with no decimal places, they are mapped to integers; otherwise they are mapped to Ruby BigDecimal objects, ensuring that no precision is lost.
Accessing Rows and Attributes
Active Record classes correspond to tables in a database. Instances of a class correspond to the individual rows in a database table. Calling Order.find(1), for instance, returns an instance of an Order class containing the data in the row with the primary key of 1.
The attributes of an Active Record instance generally correspond to the data in the corresponding row of the database table. For example, our orders table might contain the following data.
depot> |
mysql -u root |
depot_development |
|
|
|||
mysql> |
select * from |
orders limit 1; |
|
|
|||
+---- |
+------------- |
|
+------------- |
|
+----------------------- |
+---------- |
+ |
| id | |
name |
| |
address |
| pay_type | |
|||
+---- |
+------------- |
|
+------------- |
|
+----------------------- |
+---------- |
+ |
| |
1 | |
Dave Thomas |
| |
123 Main St | customer@pragprog.com |
| check |
| |
|
+---- |
+------------- |
|
+------------- |
|
+----------------------- |
+---------- |
+ |
1 |
row in set (0.00 |
sec) |
|
|
|
Report erratum
COLUMNS AND ATTRIBUTES 287
If we fetched this row into an Active Record object, that object would have five attributes. The id attribute would be 1 (a Fixnum), the name attribute the string
"Dave Thomas", and so on.
You access these attributes using accessor methods. Rails automatically constructs both attribute readers and attribute writers when it reflects on the schema.
o = Order.find(1) |
|
puts o.name |
#=> "Dave Thomas" |
o.name = "Fred Smith" |
# set the name |
Setting the value of an attribute does not change anything in the database— you must save the object for this change to become permanent.
The value returned by the attribute readers is cast by Active Record to an appropriate Ruby type if possible (so, for example, if the database column is a time stamp, a Time object will be returned). If you want to get the raw value of an attribute, append _before_type_cast to its name, as shown in the following code.
account.balance_before_type_cast |
#=> "123.4", a string |
account.release_date_before_type_cast #=> "20050301"
Inside the code of the model, 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, and others don’t. This makes it hard for Active Record to create an abstraction for 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.3 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
3. Ruby has a simple definition of truth. Any value that is not nil or the constant false is true.
Report erratum