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

Acts As 354

18.6Acts As

We’ve seen how 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.4

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.

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.

Download e1/ar/acts_as_list.rb

create_table :parents, :force => true do |t| end

create_table :children, :force => true do |t| t.column :parent_id, :integer

t.column :name, :string t.column :position, :integer

end

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.

4. Rails 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 still has some serious problems that prevent us from verifying its use with working code.

Report erratum

Acts As 355

Download e1/ar/acts_as_list.rb

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, establishing the connection with the parent. We also have an acts_as_list declaration. We qualify this with a :scope option, specifying that the list is per parent record. 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.

Download e1/ar/acts_as_list.rb

parent = Parent.new

%w{ One Two Three Four}.each do |name| parent.children.create(:name => name)

end parent.save

We’ll write a method to let us examine the contents of the list. There’s a subtlety here—notice that we pass true to the children association. That forces it to be reloaded every time we access it. That’s because 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 reload forces them to be brought into memory.

Download e1/ar/acts_as_list.rb

def display_children(parent)

puts parent.children(true).map {|child| child.name }.join(", ") end

And finally we’ll play around with our list. The comments show the output produced by display_children.

Download e1/ar/acts_as_list.rb

 

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

Report erratum

Acts As 356

parent.children[0].move_lower

 

display_children(parent)

#=> Two, One, Three, Four

parent.children[2].move_to_top

 

display_children(parent)

#=> Three, Two, One, Four

parent.children[2].destroy

 

display_children(parent)

#=> Three, Two, Four

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 means 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 into the same table, linking child rows to their parent row. This is illustrated in Figure 18.4, on the following page.

To show how trees work, let’s create a simple category table, where each toplevel category may have subcategories and each subcategory may have additional levels of subcategories. Note the foreign key pointing back into the same table.

Download e1/ar/acts_as_tree.rb

create_table :categories, :force => true do |t| t.column :name, :string

t.column :parent_id, :integer end

Report erratum

Acts As 357

 

1

 

5

3

2

4

6

7

8

 

9

categories

id

parent_id

rest

_of_data

1

null

 

...

 

 

 

 

2

1

...

 

 

 

3

1

...

 

 

 

4

3

...

5

1

...

 

 

 

6

3

...

 

 

 

7

2

...

 

 

 

8

6

...

 

 

 

9

6

...

 

 

 

 

Figure 18.4: Representing a Tree Using Parent Links in a Table

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.

Download e1/ar/acts_as_tree.rb

class Category < ActiveRecord::Base acts_as_tree :order => "name"

end

Normally you’d have some end-user functionality to create and maintain the category hierarchy. Here, we’ll just create it using code. Note how we manipulate the children of any node using the children attribute.

Download e1/ar/acts_as_tree.rb

 

 

root

= Category.create(:name => "Books")

fiction

=

root.children.create(:name

=>

"Fiction")

non_fiction

=

root.children.create(:name

=>

"Non Fiction")

non_fiction.children.create(:name => "Computers") non_fiction.children.create(:name => "Science") non_fiction.children.create(:name => "Art History")

fiction.children.create(:name => "Mystery") fiction.children.create(:name => "Romance") fiction.children.create(:name => "Science Fiction")

Now that we’re all set up, we can play with the tree structure. We’ll use the same display_children method we wrote for the acts as list code. The listing appears on the next page.

Report erratum