- •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 15
More Active Record
15.1 Acts As
We’ve seen how the has_one, has_many, and has_and_belongs_to_many allow us to represent the standard relational database structures of one-to-one, one-to-many, and many-to-many mappings. But sometimes we need to build more on top of these basics.
For example, an order may have a list of invoice items. So far, we’ve represented these successfully using has_many. But as our application grows, it’s possible that we might need to add more list-like behavior to the line items, letting us place line items in a certain order and move line items around in that ordering.
Or perhaps we want to manage our product categories in a tree-like data structure, where categories have subcategories, and those subcategories in turn have their own subcategories.
Active Record comes with support for adding this functionality on top of the existing has_ relationships. It calls this support acts as, because it makes a model object act as if it were something else.1
Acts As List
Use the acts_as_list declaration in a child to give that child list-like behavior from the parent’s point of view. The parent will be able to traverse children, move children around in the list, and remove a child from the list.
1Rails ships with three acts as extensions: acts_as_list, acts_as_tree, and acts_as_nested_set. I’ve chosen to document just the first two of these; as this book was being finalized, the nested set variant had some serious problems that prevent us from verifying its use with working code.
Prepared exclusively for Rida Al Barazi
Acts As 244
Lists are implemented by assigning each child a position number. This means that the child table must have a column to record this. If we call that column position, Rails will use it automatically. If not, we’ll need to tell it the name. For our example, we’ll create a new child table (called children) along with a parent table.
File 6 |
create table parents ( |
|
|
|
id |
int |
not null auto_increment, |
|
primary key (id) |
|
|
|
); |
|
|
|
create table children ( |
|
|
|
id |
int |
not null auto_increment, |
|
parent_id |
int |
not null, |
|
name |
varchar(20), |
|
|
position |
int, |
|
|
constraint fk_parent foreign key (parent_id) references parents(id), |
||
|
primary key (id) |
|
|
|
); |
|
|
|
Next we’ll create the model classes. Note that in the Parent class we order |
|
our children based on the value in the position column. This ensures that |
|
the array fetched from the database is in the correct list order. |
File 1 |
class Parent < ActiveRecord::Base |
|
has_many :children, :order => :position |
|
end |
|
class Child < ActiveRecord::Base |
|
belongs_to :parent |
|
acts_as_list :scope => :parent_id |
|
end |
|
In the Child class, we have the conventional belongs_to declaration, estab- |
|
lishing the connection with the parent. We also have an acts_as_list decla- |
|
ration. We qualify this with a :scope option, specifying that the list is per |
|
parent. Without this scope operator, there’d be one global list for all the |
|
entries in the children table. |
|
Now we can set up some test data: we’ll create four children for a particular |
|
parent, calling them One, Two, Three, and Four. |
File 1 |
parent = Parent.new |
|
%w{ One Two Three Four}.each do |name| |
|
parent.children.create(:name => name) |
|
end |
|
parent.save |
|
We’ll write a simple method to let us examine the contents of the list. |
File 1 |
def display_children(parent) |
puts parent.children.map {|child| child.name }.join(", ") end
Prepared exclusively for Rida Al Barazi
Report erratum
Acts As |
245 |
And finally we’ll play around with our list. The comments show the output produced by display_children( ).
File 1 |
display_children(parent) |
#=> One, Two, Three, Four |
|
puts parent.children[0].first? |
#=> true |
|
two = parent.children[1] |
|
|
puts two.lower_item.name |
#=> Three |
|
puts two.higher_item.name |
#=> One |
|
parent.children[0].move_lower |
|
|
parent.reload |
|
|
display_children(parent) |
#=> Two, One, Three, Four |
|
parent.children[2].move_to_top |
|
|
parent.reload |
|
|
display_children(parent) |
#=> Three, Two, One, Four |
|
parent.children[2].destroy |
|
|
parent.reload |
|
|
display_children(parent) |
#=> Three, Two, Four |
Note how we had to call reload( ) on the parent. The various move_ methods update the child items in the database, but because they operate on the children directly, the parent will not know about the change immediately.
The list library uses the terminology lower and higher to refer to the relative positions of elements. Higher means closer to the front of the list, lower closer to the end. The top of the list is therefore the same as the front, and the bottom of the list is the end. The methods move_higher( ), move_lower( ), move_to_bottom( ), and move_to_top( ) move a particular item around in the list, automatically adjusting the position of the other elements.
higher_item( ) and lower_item( ) return the next and previous elements from the current one, and first?( ) and last?( ) return true if the current element is at the front or end of the list.
Newly created children are automatically added to the end of the list. When a child row is destroyed, the children after it in the list are moved up to fill the gap.
Acts As Tree
Active Record provides support for organizing the rows of a table into a hierarchical, or tree, structure. This is useful for creating structures where entries have subentries, and those subentries may have their own subentries. Category listings often have this structure, as do descriptions of permissions, directory listings, and so on.
This tree-like structure is achieved by adding a single column (by default called parent_id) to the table. This column is a foreign key reference back
Prepared exclusively for Rida Al Barazi
Report erratum
Acts As 246
|
1 |
|
5 |
3 |
2 |
4 |
6 |
7 |
|
8 |
9 |
categories
id |
parent_id |
. . . |
1 |
null |
. . . |
2 |
1 |
. . . |
3 |
1 |
. . . |
4 |
3 |
. . . |
5 |
1 |
. . . |
6 |
3 |
. . . |
7 |
2 |
. . . |
8 |
6 |
. . . |
9 |
6 |
. . . |
Figure 15.1: Representing a Tree Using Parent Links in a Table
into the same table, linking child rows to their parent row. This is illustrated in Figure 15.1 .
To show how trees work, let’s create a simple category table, where each top-level cateory may have subcategories, and each subcategory may have additional levels of subcategories. Note the foreign key pointing back into the same table.
File 6 |
create table categories ( |
|
|
|
id |
int |
not null auto_increment, |
|
name |
varchar(100) |
not null, |
|
parent_id |
int, |
|
|
constraint fk_category |
foreign key (parent_id) references categories(id), |
|
|
primary key (id) |
|
|
|
); |
|
|
|
The corresponding model uses the method with the tribal name acts_as_tree |
|
to specify the relationship. The :order parameter means that when we look |
|
at the children of a particular node, we’ll see them arranged by their name |
|
column. |
File 2 |
class Category < ActiveRecord::Base |
|
acts_as_tree :order => "name" |
|
end |
Prepared exclusively for Rida Al Barazi
Report erratum