How do you create a controller & view to create a list of ob


#1

I’m trying to figure out how to design a view and controller to work
with a
simple collection. I have a Foo that has many Bars, so here’s what I
did:

$ ruby script/generate model Foo
$ ruby script/generate model Bar
(Uncomment t.column, :name: :string in foo and bar migrations)
(Edit Foo.rb and Bar.rb, add has_many :bars to Foo, belongs_to :foo to
Bar)
$ rake db:migrate
$ ruby script/generate scaffold Foo

At this point you can create/edit foos, but no bars. So what I want to
be
able to do it create 5 bars at the same time I create a foo. So the
form
would look like this:

Name: []
Bar #1 Name: [
]
Bar #2 Name: []
Bar #3 Name: [
]
Bar #4 Name: []
Bar #5 Name: [
]

So, trying to follow the example from
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html, I
added this to app/views/foos/_form.rhtml:

<% 5.times do |i| %>


<%= text_field 'bar[]', 'name' %>

<% end %>

But I get this error:

You have a nil object when you didn’t expect it!
You might have expected an instance of ActiveRecord::Base.
The error occured while evaluating nil.id_before_type_cast

Extracted source (around line #9):

6:
7: <% 5.times do |i| %>
8:



9: <%= text_field ‘bar[]’, ‘name’ %>


10: <% end %>
11:
12:

Hmmm… so I guess I need a bar array that is initialized with Bar
objects?
So I add this to the new method in the FooController:

@bars = []
5.times do |i|
  @bars.push Bar.new
end

But then I get this error:

undefined method `id_before_type_cast’ for #Array:0x3925ee8

Extracted source (around line #9):

6:
7: <% 5.times do |i| %>
8:



9: <%= text_field ‘bars[]’, ‘name’ %>


10: <% end %>
11:
12:

So I’m stuck, how are you supposed to do this?


#2

Thanks to some help from people in #rubyonrails, I’ve made some progress
on
this. Handling an object graph is definately a weakness of Ruby on
Rails
when compared to other web frameworks, such as Webwork. First of all,
for
any non-Java Railers, Webwork is a Java MVC framework. When working
with an
object graph, you use Object Graph Navigation Language (OGNL) to specify
the
properties in the forms. For example, lets say you want to have a form
that
has a User object that has a property that is a Collection of Addresses.
You first define you model objects:

public class User {
private List addresses = new ArrayList();
}

public class Address {
private String street;
private String city;
//etc.
}

Yes, you need to make getters and setters, there is no attr for Java :slight_smile:

Then, create an Action, which is Webwork terminology for a Controller.
Then
you define the class properties for the Action (again, can’t just do it
on
the fly, this is a staticly typed language we’re dealing with)

public class UsersAction {
private User user = new User();
}

So, there is a little more work here setting this up, because you’re
dealing
with Java instead of Ruby, but here’s where Java/Webwork wins out over
Ruby
on Rails. In your view, you might have this:

Then when you submit your form, Webwork’s type conversion takes care of
things just like you’d want. The city property of the 3rd element (zero
based index) of the user’s addresses is set to that value of these
field.

As far as I can tell, you can’t do this with Ruby on Rails. The problem
lies in the fact that RoR converts the params into a Hash of Hashes. So
sticking with the example above, you’d like to have a form like this:

<%= text_field ‘user[address]’, ‘name’, “index” => i %>

But that doesn’t work. The only way around it seems to be to create an
addresses Array in the controller, then putting it back into the user on
submit. Then, in the controller, you’d have to do this:

def new
@user = User.new
@addresses = []
5.times {|i| @addresses.push Address.new}
end

And that assumes you know exactly how many Addresses that you will have.
Any way, then when you submit it, you need to put it back together:

def create
@user = User.new(params[:user])
@user.addresses = params[:user].values.collect {|a| Address.new(a)}
end

I’ve got this simple example to work, put I’m having problems getting it
work with a more complex example. So, somebody prove me wrong. Tell me
there’s a better way to handle an object graph with Ruby On Rails.


#3

I’m still having no luck with trying to create an object with n child
objects through a form. I feel like I’m close though, hoping someone
can
help me figure it out. What basically happens is that when my form is
submitted to the controller, I have a Foo object and it has a bars
property. Everything is getting populated, the bars property has 5 Bar
objects, but each Bar object’s foo_id is nil. That makes sense, because
the
Foo itself hasn’t been created yet, so there is no id. My hope would be
that AR would save the Foo to the DB first, then populate the foo_id of
each
Bar, then save each Bar. But instead I get 5 “Bars is Invalid” errors,
one
for each Bar in foo.bars. Has anybody been able to create an object
with
child objects using RoR? If so, what am I doing wrong? Here’s my code:

#Pretty standard model classes
class Foo < ActiveRecord::Base
has_many :bars
end

class Bar < ActiveRecord::Base
belongs_to :foo
validates_presence_of :foo_id
end

#Controller with just the relevant methods shown here, new and create
#I am doing something a little funky here
#The form submits to /foos/new instead of /foos/create
#The new method calls create if the request is a post
#This allows me to easily show the form again if there is a problem
#without explicitly rendering the template or calling the new method
class FoosController < ApplicationController

def new
if request.post?
create
else
@foo = Foo.new
@bars = []
5.times {|i| @bars.push Bar.new} #There’s probably a cleaner way
to do
this
end
end

def create
@foo = Foo.new(params[:foo])
@foo.bars = params[:bar].values.collect {|bar| Bar.new(bar)}
if @foo.save
flash[:notice] = ‘Foo was successfully created.’
redirect_to :action => ‘list’
end
@bars = @foo.bars
end

end

#The form partial for foo _form.rhtml:

Name
<%= text_field 'foo', 'name' %>

<% @bars.each_index do |i| %>

Bar #<%=i%>:
<%= text_field 'bar', 'name', "index" => i %>

<% end %>

#4

Could you do:

foo = Foo.new
foo.save

if foo.errors.empty?
params[:bars].each do |bar|
foo.bars.create(bar)
end
end

I think the trouble with any of these methods is error handling.
Thinking about it, you would have to check that all the bars were valid
first. Could get kinda complicated.

When i’ve had to do this, i’ve created the parent then the children as
above. For error checking i check the creation of each child and make a
custom hash of errors. That’s as hacky as hell though and was more of a
necessity than a well thought out solution :0)

Steve

Paul B. wrote:

Alright, I’ve boiled this problem down to a specific AR thing, using
script/console instead of the forms and controllers:

C:\ruby\workspace\eval>ruby script/console
Loading development environment.

foo = Foo.new
=> #<Foo:0x3e3c1e0 @attributes={“name”=>nil}, @new_record=true>

foo.bars = []
=> []

foo.bars << Bar.new
=> [#<Bar:0x3e27330 @attributes={“name”=>nil, “foo_id”=>nil},
@new_record=true>]

foo.save!
ActiveRecord::RecordInvalid: Validation failed: Bars is invalid
from c:/ruby/ruby-
1.8.4_16/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:736:in`save!’
from (irb):4

So I guess AR doesn’t know how to save a new record that has child
records?
Is this the only way to do it:

C:\ruby\workspace\eval>ruby script/console
Loading development environment.

foo = Foo.new
=> #<Foo:0x3e3c1e0 @attributes={“name”=>nil}, @new_record=true>

foo.save
=> true

foo.bars << Bar.new
=> [#<Bar:0x3e250b0 @errors=#<ActiveRecord::Errors:0x3e22878 @errors={},
@base=#<Bar:0x3e250b0 …>>, @attributes={“name”=>nil, “foo_id”=>4,
“id”=>11}, @new_record=false>]

foo.save
=> true

If that’s the case, I guess I should probably create a
foo.save_with_barsmethod to encapsulate this. Is this how other
people are handling saving a
record that has_many other records?


#5

So now I’m trying to create a method that copies the bars into a
temporary
array, saves the Foo, then adds the bars back to the Foo, and save the
foo
again. Seems to be the only way this is going to work. In the porcess,
I’ve run across something about Ruby that is weird (probably only weird
because I’m used to the Java way of things). Here’s my method:

def save_new
logger.debug “self.bars = #{self.bars.inspect}”
tmp = self.bars
self.bars = []
logger.debug “tmp = #{tmp.inspect}”
logger.debug “self.bars = #{self.bars.inspect}”
end

Ignore the fact that this does nothing for now. Being a Java programmer
here, the way I read what this code does is:

Assign the variable tmp to point to the object that self.bars points to
Assign the variable self.bars to point to a new object, an empty array

So I would expect the variable tmp to still be pointing to the original
array that self.bars points to. But I guess that’s not the case with
Ruby?
When you run it, it prints this:

self.bars = [#<Bar:0x3ce0888 …
tmp = []
self.bars = []

So I assume tmp is pointing to the variable (or maybe the word reference
is
more appropriate?) self.bars, not what self.bars points to. And once I
make
self.bars point to something else, tmp then in turn points to that. So
in
otherwords, tmp is a reference to a reference, and self.bars is a
reference
to an Array. Just looking for someone to confirm what I’m finding and
that
I actually understand what is going on.


#6

Alright, I’ve boiled this problem down to a specific AR thing, using
script/console instead of the forms and controllers:

C:\ruby\workspace\eval>ruby script/console
Loading development environment.

foo = Foo.new
=> #<Foo:0x3e3c1e0 @attributes={“name”=>nil}, @new_record=true>

foo.bars = []
=> []

foo.bars << Bar.new
=> [#<Bar:0x3e27330 @attributes={“name”=>nil, “foo_id”=>nil},
@new_record=true>]

foo.save!
ActiveRecord::RecordInvalid: Validation failed: Bars is invalid
from c:/ruby/ruby-
1.8.4_16/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:736:in`save!’
from (irb):4

So I guess AR doesn’t know how to save a new record that has child
records?
Is this the only way to do it:

C:\ruby\workspace\eval>ruby script/console
Loading development environment.

foo = Foo.new
=> #<Foo:0x3e3c1e0 @attributes={“name”=>nil}, @new_record=true>

foo.save
=> true

foo.bars << Bar.new
=> [#<Bar:0x3e250b0 @errors=#<ActiveRecord::Errors:0x3e22878 @errors={},
@base=#<Bar:0x3e250b0 …>>, @attributes={“name”=>nil, “foo_id”=>4,
“id”=>11}, @new_record=false>]

foo.save
=> true

If that’s the case, I guess I should probably create a
foo.save_with_barsmethod to encapsulate this. Is this how other
people are handling saving a
record that has_many other records?


#7

So when you assign self.bars = [] you are changing the object that
self.bars is pointing at and therefor the tmp var is pointing at the
same
object.

Yeah, that’s the fundamental difference between Ruby and Java. In Java,
self.bars = [] has no effect at all on the object that self.bars was
originally pointing to. You are just making the reference self.bars
point
at a new object, not changing the object that self.bars points to.

Anyway, I’ll give your controller code a try. That makes sense, why
would I
put the bars into foo, only to take them out temporarily and then add
them
back in. My thinking though was more along the lines of the separation
between the controller and the model. The controller just wants the
whole
object graph to be saved, so delagate it to the model. But that just
isn’t
the way it works, because like you said, AR doesn’t populate the foo_id
of
each bar. Thanks for your help, I’ll let you know how it turns out.


#8

Paul-

You are correct. In ruby variables are kind of like pointers to

objects. When you assign tmp = self.bars they both now point to the
same object. So when you assign self.bars = [] you are changing the
object that self.bars is pointing at and therefor the tmp var is
pointing at the same object. You can get around this by doing tmp =
self.bars.dup and then continue on with what you are doing.

But I don't understand why you are trying to set self.bars to be an

empty array? Lets look at how you can accomplish this in your
controller. Since you have set validates_presence_of :foo_id in your
comments model you have to either use create to make a new foo or
foo.save before you can add comments to it. This is because there is
no foo_id for the bars to use until you save foo to the db and it
gets an id

One liner :wink:

@foo = Foo.create(:name => ‘First Foo!’).bars << params[:bars].map
{ |bar| Bar.create(bar) }

#OR with error handling
if @foo = Foo.new(:name => ‘First Foo!’).save
@foo.bars << params[:bars].map { |bar| Bar.new(bar) }
if @foo.bars.each {|bar| bar.valid?}
@foo.save
else
# bars have errors
end
else

foo has errors

end

Depending on what you are trying to do you might want to look at

validates_associated instead of valiodates_presence_of :foo_id.

Cheers-
-Ezra