Accessible Associations
This chapter describes Hobo’s support for nested models in forms. This is mostly technical background – beginners should not have to read more than the introduction.
Contents
>> require 'rubygems'
>> require 'active_support'
>> require 'active_record'
>> require 'action_pack'
>> require 'action_view'
>> require 'action_controller'
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
>> mysql_user = 'root'; mysql_password = ''
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
>> mysql_database = "hobofields_doctest"
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
>> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
:database => mysql_database,
:host => "localhost",
:username => mysql_user,
:password => mysql_password)
>> $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '../../hobofields/lib')
>> $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '../../hobosupport/lib')
>> $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '../../hobo/lib')
>> require 'will_paginate'
>> require 'will_paginate/finder'
>> require 'hobosupport'
>> require 'hobofields'
>> require 'hobo'
>> Hobo::Model.enable
>> HoboFields.enable
>> ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(STDOUT)
>>
def migrate(renames={})
up, down = HoboFields::MigrationGenerator.run(renames)
puts up
ActiveRecord::Migration.class_eval(up)
ActiveRecord::Base.send(:subclasses).each { |model| model.reset_column_information }
[up, down]
end
Introduction
Using multi-model forms in Hobo is very straightforward:
has_many :posts, :accessible => true
Once you’ve done that, the default forms that Hobo builds will use the input-many tag.
:accessible => true works for has_many, has_many :through and belongs_to, but does not work for has_one or has_and_belongs_to_many.
It’s quite common to also add the :dependent => :destroy flag to accessible associations. This also used to trigger magic in Hobo, but this additional magic has been removed and replaced with View Hints. See the Rails rdoc for more information on :dependent => :destroy.
Model Support
We’ll use rubydoctest to provide our examples for this section. Here are the models:
>>
class Foo < ActiveRecord::Base
hobo_model
fields { name :string }
has_many :bars, :accessible => true
end
>>
class Bar < ActiveRecord::Base
hobo_model
fields { name :string }
belongs_to :foo
end
>> migrate
The :accessible => true option patches in Hobo::AccessibleAssociations to your ActiveRecord model. It modifies the bars= writer function to support assigning an array of records, an array of hashes, an array of ids, or an empty string.
Assigning an array of records
The whole array must be assigned – any records that are not assigned are deleted from your association.
>> bar1 = Bar.new(:name => "bar1")
>> bar2 = Bar.new(:name => "bar2")
>> foo = Foo.new(:name => "foo1")
>> foo.bars = [bar1, bar2]
>> foo.bars.*.name
=> ["bar1", "bar2"]
>> foo.save!
>> foo.bars = [bar2]
>> foo.bars.*.name
=> ["bar2"]
>> foo.save!
>> bar2.foo.name
=> "foo1"
>> bar1.reload
>> bar1.foo
=> nil
If :dependent => :destroy had been set on has_many :bars, bar1 would now be deleted from the database. Since it hasn’t, it still exists in the database but has become orphaned.
Assigning an array of hashes
Assigning an array of hashes maps nicely with how Rails deconstructs your URI encoded query string. For example, your form can return
foo[bars][0][name]=bar1&foo[bars][0][name]=bar2
which Rails will decode into your params hash as
>> params = {"foo" => {"bars" => [ {"name" => "bar3"}, {"name" => "bar4"}]}}
With Hobo’s accessible associations, the params hash may be directly assigned.
>> foo.attributes = params["foo"]
>> foo.bars.*.name
=> ["bar3", "bar4"]
>> foo.save!
Because these parameters did not include an ID, Hobo created new bar models. If you include an ID, Hobo looks up the existing record in the database and modifies it with the parameters assigned.
>> params = {"foo" => {"bars" => [ {:name => "bar3_mod", :id => "#{foo.bars[0].id}"}]}}
>> old_bar3_id = foo.bars[0].id
>> foo.attributes = params["foo"]
>> foo.save!
>> foo.bars.*.name
=> ["bar3_mod"]
>> foo.bars[0].id == old_bar3_id
=> true
Assigning an array of IDs
While input-many returns an array of hashes, select-many returns an array of ids. These ids must have an ”@” prepended.
>> params = {"foo" => {"bars" => ["@#{bar1.id}", "@#{bar2.id}"]}}
>> foo.attributes = params["foo"]
>> foo.save!
>> foo.bars.*.name
=> ["bar1", "bar2"]
Assigning an empty string
You can remove all elements from the association by assigning an empty array:
>> foo.bars = []
>> foo.bars
=> []
However, there is no way to format a URI query string to make Rails construct an empty array in its params hash, so Hobo adds a useful shortcut to it’s accessible associations:
>> foo.bars = ""
>> foo.bars
=> []
View Support
The Rapid tags input-many, input-all, select-many and check-many all require accessible associations.
input-many may even be used in a nested fashion:
<form>
<field-list:>
<foos-view:>
<input-many>
<field-list:>
<bars-view:>
<input-many>
</input-many>
</bars-view:>
</field-list:>
</input-many>
</foos-view:>
</field-list:>
</form>
You do not need to use the accessible association tags – standard inputs acquire the correct name for use with accessible associations when called from the appropriate context. Here’s an example form that will work with the example given above in Model Support
<form>
<field-list:>
<bars-view:>
<repeat>
<input:name/>
</repeat>
</bars-view:>
<field-list:>
</form>
Controller Support
No special code is required in your controllers to support accessible associations, even if you aren’t using a Hobo controller.
Validations
Validations simply work as you’d expect. The only thing to note is that validation errors in a child object will cause the parent object to receive an error message of ”…” on the association.
Transactions
Hobo’s accessible associations do not do any explicit saves so any new child objects are not saved until the parent object is saved. Rails wraps this save in a transaction, so any save is an all or nothing deal even though parents and children are saved via different SQL statements.
Rails 2.3 nested models
Rails 2.3 includes a functionality similar to Hobo’s accessible associations. The Hobo version is based on an early version of the Rails functionality, but unfortunately the Rails version changed significantly between the time that the Hobo version was released and when Rails 2.3 was released.
For more information on Rails 2.3’s accept_nested_attributes_for, see the Ruby on Rails blog or Ryan Daigle’s blog.
The two versions use a different model: in Hobo the whole array is assigned, allowing add, delete or update via a single mechanism. In rails, there’s a different mechanism for adding or deleting objects from the association, and there’s no method for update.
Each version have their pluses and minuses. The Hobo version is conceptually simpler, but it starts to get unwieldy if there are a large number of elements in the association.
Both mechanisms are compatible and may be enabled simultaneously.
It’s certainly possible that Rapid >=1.1 will acquire tags that will require accept_nested_attributes_for. However, it’s unlikely that Hobo will drop support for accessible associations unless ActiveRecord itself changes significantly.