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

 

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