- •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
|
WHEN THINGS GET SAVED |
358 |
Download e1/ar/acts_as_tree.rb |
|
|
display_children(root) |
# Fiction, Non Fiction |
|
sub_category = root.children.first |
|
|
puts sub_category.children.size |
#=> 3 |
|
display_children(sub_category) |
#=> Mystery, Romance, Science Fiction |
|
non_fiction = root.children.find(:first, :conditions => "name = 'Non Fiction'")
display_children(non_fiction) |
#=> |
Art History, Computers, Science |
puts non_fiction.parent.name |
#=> |
Books |
The various methods we use to manipulate the children should look familar: they’re the same as those provided by has_many. In fact, if we look at the implementation of acts_as_tree, we’ll see that all it does is establish both a belongs_to and a has_many attribute, each pointing back into the same table. It’s as if we’d written
class Category < ActiveRecord::Base
belongs_to |
:parent, |
|
|
:class_name |
=> "Category" |
has_many |
:children, |
|
|
:class_name |
=> "Category", |
|
:foreign_key => "parent_id", |
|
|
:order |
=> "name", |
|
:dependent |
=> :destroy |
end
If you need to optimize the performance of children.size, you can use a counter cache (just as you can with has_many). Add the option :counter_cache => true to the acts_as_tree declaration, and add the column catgories_count to your table.
18.7When Things Get Saved
Let’s look again at invoices and orders.
Download e1/ar/one_to_one.rb
class Order < ActiveRecord::Base has_one :invoice
end
class Invoice < ActiveRecord::Base belongs_to :order
end
You can associate an invoice with an order from either side of the relationship: you can tell an order that it has an invoice associated with it, or you can tell the invoice that it’s associated with an order. The two are almost equivalent. The difference is in the way they save (or don’t save) objects to the database.
Report erratum
WHEN THINGS GET SAVED 359
David Says. . .
Why Things in Associations Get Saved When They Do
It might seem inconsistent that assigning an order to the invoice will not save the association immediately, but the reverse will. This is because the invoices table is the only one that holds the information about the relationship. Hence, when you associate orders and invoices, it’s always the invoice rows that hold the information. When you assign an order to an invoice, you can easily make this part of a larger update to the invoice row that might also include the billing date. It’s therefore possible to fold what would otherwise have been two database updates into one. In an ORM, it’s generally the rule that fewer database calls is better.
When an order object has an invoice assigned to it, it still needs to update the invoice row. So, there’s no additional benefit in postponing that association until the order is saved. In fact, it would take considerably more software to do so. And Rails is all about less software.
If you assign an object to a has_one association in an existing object, that associated object will be automatically saved.
order = Order.find(some_id)
an_invoice = Invoice.new(... |
) |
order.invoice = an_invoice |
# invoice gets saved |
If instead you assign a new object to a belongs_to association, it will never be automatically saved.
order = Order.new(...) |
|
|
an_invoice.order = order |
# |
Order will not be saved here |
an_invoice.save |
# |
both the invoice and the order get saved |
Finally, there’s a danger here. If the child row cannot be saved (for example, because it fails validation), Active Record will not complain—you’ll get no indication that the row was not added to the database. For this reason, we strongly recommend that instead of the previous code, you write
invoice = Invoice.new
# fill in the invoice invoice.save! an_order.invoice = invoice
The save! method throws an exception on failure, so at least you’ll know that something went wrong.
Report erratum
PRELOADING CHILD ROWS 360
Saving and Collections
The rules for when objects get saved when collections are involved (that is, when you have a model containing a has_many or has_and_belongs_to_many declaration) are basically the same.
•If the parent object exists in the database, then adding a child object to a collection automatically saves that child. If the parent is not in the database, then the child is held in memory and is saved once the parent has been saved.
•If the saving of a child object fails, the method used to add that child to the collection returns false.
As with has_one, assigning an object to the belongs_to side of an association does not save it.
18.8Preloading Child Rows
Normally Active Record will defer loading child rows from the database until you reference them. For example, drawing from the example in the RDoc, assume that a blogging application had a model that looked like this.
class Post < ActiveRecord::Base belongs_to :author
has_many |
:comments, :order => 'created_on DESC' |
end
If we iterate over the posts, accessing both the author and the comment attributes, we’ll use one SQL query to return the n rows in the posts table and n queries each to get rows from the authors and comments tables, a total of 2n+1 queries.
for post in Post.find(:all)
puts "Post: |
#{post.title}" |
|
puts |
"Written by: |
#{post.author.name}" |
puts |
"Last comment on: #{post.comments.first.created_on}" |
end
This performance problem is sometimes fixed using the :include option to the find method. It lists the associations that are to be preloaded when the find is performed. Active Record does this in a fairly smart way, such that the whole wad of data (for both the main table and all associated tables) is fetched in a single SQL query. If there are 100 posts, the following code will eliminate 100 queries compared with the previous example.
for post in Post.find(:all, :include => :author)
puts "Post: |
#{post.title}" |
|
puts |
"Written by: |
#{post.author.name}" |
puts |
"Last comment on: #{post.comments.first.created_on}" |
end
Report erratum
COUNTERS 361
And this example will bring it all down to just one query.
for post in Post.find(:all, :include => [:author, :comments])
puts "Post: |
#{post.title}" |
|
puts |
"Written by: |
#{post.author.name}" |
puts |
"Last comment on: #{post.comments.first.created_on}" |
end
This preloading is not guaranteed to improve performance.5 Under the covers, it joins all the tables in the query together and so can end up returning a lot of data to be converted into Active Record objects. And if your application doesn’t use the extra information, you’ve incurred a cost for no benefit. You might also have problems if the parent table contains a large number of rows— compared with the row-by-row lazy loading of data, the preloading technique will consume a lot more server memory.
If you use :include, you’ll need to disambiguate all column names used in other parameters to find—prefix each with the name of the table that contains it. In the following example, the title column in the condition needs the table name prefix for the query to succeed.
for post in Post.find(:all, :conditions => "posts.title like '%ruby%'", :include => [:author, :comments])
# ...
end
18.9Counters
The has_many relationship defines an attribute that is a collection. It seems reasonable to be able to ask for the size of this collection: how many line items does this order have? And indeed you’ll find that the aggregation has a size method that returns the number of objects in the association. This method goes to the database and performs a select count(*) on the child table, counting the number of rows where the foreign key references the parent table row.
This works and is reliable. However, if you’re writing a site where you frequently need to know the counts of child items, this extra SQL might be an overhead you’d rather avoid. Active Record can help using a technique called counter caching. In the belongs_to declaration in the child model you can ask Active Record to maintain a count of the number of associated children in the parent table rows. This count will be automatically maintained—if you add a child row, the count in the parent row will be incremented, and if you delete a child row, it will be decremented.
To activate this feature, you need to take two simple steps. First, add the option :counter_cache to the belongs_to declaration in the child table.
5. In fact, it might not work at all! If your database doesn’t support left outer joins, you can’t use the feature. Oracle 8 users, for instance, will need to upgrade to version 9 to use preloading.
Report erratum
COUNTERS 362
Download e1/ar/counters.rb
class LineItem < ActiveRecord::Base belongs_to :product, :counter_cache => true
end
Second, in the definition of the parent table (products in this example) you need to add an integer column whose name is the name of the child table with _count appended.
Download e1/ar/counters.rb
create_table :products, :force => true do |t| t.column :title, :string
t.column :description, :text
# ...
t.column :line_items_count, :integer, :default => 0 end
There’s an important point in this DDL. The column must be declared with a default value of zero (or you must do the equivalent and set the value to zero when parent rows are created). If this isn’t done, you’ll end up with null values for the count regardless of the number of child rows.
Once you’ve taken these steps, you’ll find that the counter column in the parent row automatically tracks the number of child rows.
There is an issue with counter caching. The count is maintained by the object that contains the collection and is updated correctly if entries are added via that object. However, you can also associate children with a parent by setting the link directly in the child. In this case the counter doesn’t get updated.
The following shows the wrong way to add items to an association. Here we link the child to the parent manually. Notice how the size attribute is incorrect until we force the parent class to refresh the collection.
Download e1/ar/counters.rb
product = Product.create(:title => "Programming Ruby", :description => " ... ")
line_item = LineItem.new line_item.product = product line_item.save
puts |
"In memory size |
= |
#{product.line_items.size}" |
#=> |
0 |
puts |
"Refreshed size |
= |
#{product.line_items(:refresh).size}" |
#=> |
1 |
The correct approach is to add the child to the parent.
Download e1/ar/counters.rb
product = Product.create(:title => "Programming Ruby", :description => " ... ")
product.line_items.create
puts |
"In memory size |
= |
#{product.line_items.size}" |
#=> |
1 |
puts |
"Refreshed size |
= |
#{product.line_items(:refresh).size}" |
#=> |
1 |
Report erratum