Accessible Associations {.document-title} 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 {.contents-heading} - contents {:toc} >> 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') >> gem 'will_paginate', '~> 2' >> 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) ActiveRecord::Migration.class_eval(up) ActiveRecord::Base.send(:subclasses).each { |model| model.reset_column_information } [up, down] end {.hidden} # 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](/api_tag_defs/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](/manual/viewhints). See the [Rails rdoc](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) 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](/api_tag_defs/input-many) returns an array of hashes, [select-many](/api_tag_defs/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](/api_tag_defs/input-many), [input-all](/api_tag_defs/input-all), [select-many](/api_tag_defs/select-many) and [check-many](/api_tag_defs/check-many) all require accessible associations. `input-many` may even be used in a nested fashion:
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*
# 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](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms) or [Ryan Daigle's blog](http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes). 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.