has_many_through new association for exisitng records

30 views
Skip to first unread message

fugee ohu

unread,
Feb 4, 2018, 2:30:45 PM2/4/18
to Ruby on Rails: Talk
In a has_many_through association where both records exist how do I create a new association
In this case Person has_many_pictures through person_picture and I'm trying to add an existing picture to an existing person like this:
@person.pictures << @picture
But this creates a new picture instead of adding the association

Walter Lee Davis

unread,
Feb 4, 2018, 4:55:08 PM2/4/18
to rubyonra...@googlegroups.com
I can't duplicate this finding here. Here's the console log:

2.4.2 :001 > p = Person.new name: 'Walter'
=> #<Person id: nil, name: "Walter", created_at: nil, updated_at: nil>
2.4.2 :002 > p.save
(0.2ms) begin transaction
SQL (2.8ms) INSERT INTO "people" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Walter"], ["created_at", "2018-02-04 21:47:31.000973"], ["updated_at", "2018-02-04 21:47:31.000973"]]
(1.3ms) commit transaction
=> true
2.4.2 :003 > c = Picture.new file: 'some file'
=> #<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>
2.4.2 :004 > c.save
(0.2ms) begin transaction
SQL (2.2ms) INSERT INTO "pictures" ("file", "created_at", "updated_at") VALUES (?, ?, ?) [["file", "some file"], ["created_at", "2018-02-04 21:47:54.717609"], ["updated_at", "2018-02-04 21:47:54.717609"]]
(1.4ms) commit transaction
=> true
2.4.2 :005 > p.pictures << c
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "person_pictures" ("person_id", "picture_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["person_id", 1], ["picture_id", 1], ["created_at", "2018-02-04 21:47:58.847298"], ["updated_at", "2018-02-04 21:47:58.847298"]]
(1.1ms) commit transaction
Picture Load (0.4ms) SELECT "pictures".* FROM "pictures" INNER JOIN "person_pictures" ON "pictures"."id" = "person_pictures"."picture_id" WHERE "person_pictures"."person_id" = ? LIMIT ? [["person_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Picture id: 1, file: "some file", created_at: "2018-02-04 21:47:54", updated_at: "2018-02-04 21:47:54">]>
2.4.2 :006 >

Here's the models:

class Picture < ApplicationRecord
has_many :person_pictures
has_many :people, through: :person_pictures
end

class Person < ApplicationRecord
has_many :person_pictures
has_many :pictures, through: :person_pictures
end

class PersonPicture < ApplicationRecord
belongs_to :person
belongs_to :picture
end

Whatever is happening on your app is not clear, but you can see that after you save the person and the picture, when you add that saved picture to the saved person's 'pictures' collection, the only record that gets created is a person_picture. Now if either the person or the picture was in the "new" state, that is to say, not saved yet, then I could imagine that it would be saved by ActiveRecord first in order to allow the person_picture record to be saved. Both IDs have to be known before the join object can be saved.

Walter
> --
> You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
> To post to this group, send email to rubyonra...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/741e9a16-483f-4fc6-a2e7-77706b9b250c%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Walter Lee Davis

unread,
Feb 4, 2018, 6:25:46 PM2/4/18
to rubyonra...@googlegroups.com
Even more interesting (to me, anyway) is what happens if you create (but don't save) any of these objects:

2.4.2 :006 > p = Person.new name: 'Walter'
=> #<Person id: nil, name: "Walter", created_at: nil, updated_at: nil>
2.4.2 :007 > c = Picture.new file: 'some file'
=> #<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>
2.4.2 :008 > p.pictures << c
=> #<ActiveRecord::Associations::CollectionProxy [#<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>]>
2.4.2 :009 > p.save
(1.9ms) begin transaction
SQL (23.1ms) INSERT INTO "people" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Walter"], ["created_at", "2018-02-04 23:23:15.342294"], ["updated_at", "2018-02-04 23:23:15.342294"]]
SQL (0.3ms) INSERT INTO "pictures" ("file", "created_at", "updated_at") VALUES (?, ?, ?) [["file", "some file"], ["created_at", "2018-02-04 23:23:15.374862"], ["updated_at", "2018-02-04 23:23:15.374862"]]
SQL (0.4ms) INSERT INTO "person_pictures" ("person_id", "picture_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["person_id", 2], ["picture_id", 2], ["created_at", "2018-02-04 23:23:15.376805"], ["updated_at", "2018-02-04 23:23:15.376805"]]
(1.3ms) commit transaction
=> true
2.4.2 :010 >

When you save one of them, all three are saved in order to preserve the entire set of relationships.

Walter
> To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/EDF36112-47D8-4749-B974-F55A2C57DBA6%40wdstudio.com.
Message has been deleted

fugee ohu

unread,
Feb 5, 2018, 1:13:09 AM2/5/18
to Ruby on Rails: Talk
Yea that is interesting, thanks for posting that

fugee ohu

unread,
Feb 5, 2018, 1:18:40 AM2/5/18
to Ruby on Rails: Talk


On Sunday, February 4, 2018 at 6:25:46 PM UTC-5, Walter Lee Davis wrote:
So if @person.pictures << @picture adds a picture to a person, how do you remove a picture from a person?

Walter Lee Davis

unread,
Feb 5, 2018, 11:46:24 AM2/5/18
to rubyonra...@googlegroups.com
You (effectively) delete the intermediate object. You could try one of two things:

@person.pictures -= [@picture]

If all the items are persisted, then that should delete the join object immediately. (Just tested, it did work here.)

@person.pictures = @person.pictures.to_a.reject{ |p| p == @picture }

(Long-hand way to do the same thing)

Now if you have a UI around this, what you would do is build an array of checkboxes with the picture IDs in them, and then use the helper method @person.picture_ids=(array of ids) that the association built for you. You don't need to do any of the above long-hand.

So in your controller, you would add picture_ids: [] to the end of your list of allowed attributes in the strong parameters. Next, you would create a checkbox for each attached image in your form:

<%- @person.pictures.each do |picture| %>
<%= check_box_tag 'person[picture_ids][]', picture.id, true %>
<%= image_tag picture.file_url %> (just guessing how your internals look, do something here to show a thumbnail)
<%- end %>

And that should do the whole thing for you.

Walter

fugee ohu

unread,
Feb 5, 2018, 12:44:51 PM2/5/18
to Ruby on Rails: Talk
Yea, that's pretty good thanks What about  the gem that let you use images in select lists I would think a lot of images is too many for a select list

fugee ohu

unread,
Feb 7, 2018, 12:01:04 PM2/7/18
to Ruby on Rails: Talk
I need to select from all pictures and if the picture's already associated with the person it would already be checked then I guess, something like that
Message has been deleted

Colin Law

unread,
Feb 7, 2018, 12:38:57 PM2/7/18
to Ruby on Rails: Talk
On 7 February 2018 at 17:19, fugee ohu <fuge...@gmail.com> wrote:
 
 I dunno why this doesn't produce any output

    <% @pictures=Picture.all %>

Model.all has been deprecated since rails 2.3.8.  Don't use it.
 
    <% @pictures.all do |picture| %>

this should be using each, not all

Colin
 
<% if @person %>
<% if picture.person_id == @person.id %>
<%= check_box_tag 'person[picture_ids][]', picture.id, true %>
<% else %>
   <%= check_box_tag 'person[picture_ids][]', picture.id %>
<% end %>
<%= image_tag picture.name.thumb %> 
<% end %> 
    <% end %>



fugee ohu

unread,
Feb 7, 2018, 12:51:51 PM2/7/18
to Ruby on Rails: Talk
I can use  <% if picture.person_id == @person.id %> ? This is a has_many_through relationship Person has_many :pictures, through: :person_pictures and also Picture has_many :people, through: :person_pictures What's the right syntax to test if the picture is already associated to the person when iterating through pictures Also if I can't use Model.all in a routine I can still use it to get @pictures=Picture.all right?
 

Colin Law

unread,
Feb 7, 2018, 1:01:02 PM2/7/18
to Ruby on Rails: Talk
Model.all is deprecated, don't use it.

If Picture has_many people then you can't use picture.person_id as one picture is associated with many people. so @picture.people is (effectively) an array of people. You will have determine whether the array includes that person.

Colin
 
 

--
You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-talk+unsubscribe@googlegroups.com.
To post to this group, send email to rubyonrails-talk@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/8d160a04-6951-4159-bfa7-8eee046cb3c4%40googlegroups.com.

fugee ohu

unread,
Feb 7, 2018, 1:43:31 PM2/7/18
to Ruby on Rails: Talk
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.


Thanks, yea that's what I meant How do I test for the inclusion of picture in @person.pictures If I can't use Model.all how do I select all pictures from the pictures table?

Walter Lee Davis

unread,
Feb 7, 2018, 2:34:06 PM2/7/18
to rubyonra...@googlegroups.com
I would start by setting up @pictures in the controller, not the view.

@pictures = Picture.order :name

Then in the view, you could use that to create your field of checkboxes:

<%- @pictures.each do |picture| %>

Use the presence of that picture's ID in the @person's picture_ids array to set or not set the checked attribute
<%= check_box_tag 'person[picture_ids][]', picture.id, @person.picture_ids.include?(picture.id) %>
<%= do_something_to_show_a_thumbnail_here %>

<%- end %>

What you will end up doing is mutating the picture_ids array in this form. As long as you have allowed that (and defined it as requiring an array) in your whitelist, then the association will have built the necessary accessor methods and this will all just work.

Walter

>
> --
> You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
> To post to this group, send email to rubyonra...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/45dcd144-63d7-47c8-b92f-e842dd4d0bdf%40googlegroups.com.
Message has been deleted

fugee ohu

unread,
Feb 8, 2018, 9:55:30 AM2/8/18
to Ruby on Rails: Talk
Since I'm not creating a new picture, only adding existing pictures to a person  I put an update action in the people controller but it tries to create a new picture and fails with validation :name can't be blank

def add_pictures_update
           if (params[:person][:person_id])
               @person=Person.find(params[:person][:person_id])
               @pictures = Picture.find(params[:person][:picture_ids])
               @person.pictures << @pictures
               respond_to do |format|
                 if @person.update
                            format.html {  redirect_to pictures_path(person_id: params[:picture][:person_id]), notice: 'Person picture updated.' }
                            format.json { render :show, status: :created, location: @picture }



Walter Lee Davis

unread,
Feb 8, 2018, 11:10:29 AM2/8/18
to rubyonra...@googlegroups.com

> On Feb 8, 2018, at 8:28 AM, fugee ohu <fuge...@gmail.com> wrote:
>
> I was having trouble hiding and showing elements on the form so I created a separate action in the pictures controller to handle the submission of the form that selects pictures to add to the person from existing pictures in the database It attempts to create a new picture I guess because validation fails :name cannot be blank
>
> def add_from_pictures_create
> if (params[:picture][:person_id])
> @person=Person.find(params[:picture][:person_id])
> @pictures = Picture.find(params[:person][:picture_ids])

# these are now new instances -- with different object IDs -- than the ones you want to compare them with

> @person.pictures << @pictures

# assigning these in this way means that you are adding an array to an array. Trouble is, you are adding duplicate instances in the second array to an array which may already contain the ones you want to add.

> respond_to do |format|
> if @person.save
> format.html { redirect_to pictures_path(person_id: params[:picture][:person_id]), notice: 'Person picture updated.' }
> format.json { render :show, status: :created, location: @picture }

I would avoid this entire approach. Think about this in the abstract: you are editing and updating the @person, manipulating the picture_ids attribute on that instance. Your form is built on the person. This form should be submitted to the PersonController#update method. If you have (as I have said many times in this thread) whitelisted the picture_ids attribute, then simply calling @person.save will persist that attribute -- it is already going to be in the person_params strong parameters hash.

Here's a clue: when you find yourself writing something like this in a controller:

> @person=Person.find(params[:picture][:person_id])

...just walk away and have a think about your life.

Walter

fugee ohu

unread,
Feb 8, 2018, 11:36:47 AM2/8/18
to Ruby on Rails: Talk
I created an update action in the persons controller Valitadation was failing with :name can't be blank so I assumed @person.save was trying to create a new picture That's why I moved the action from the pictures controller to the persons controller and changed the action to @person.update instead of @person.save Did you already understand that?

fugee ohu

unread,
Feb 8, 2018, 12:01:08 PM2/8/18
to Ruby on Rails: Talk
You wanted me to use patch or post to achieve @person.pictures << @pictures and then I would wanna use if @person.save or if @person.update Please clarify that Thanks in advance  I took out the stuff left over from when this routine was in the pictures controller and now rely on the callback set_person 

Walter Lee Davis

unread,
Feb 8, 2018, 12:23:43 PM2/8/18
to rubyonra...@googlegroups.com
That validation was probably on the person, not the picture, unless you added validates_associated to the Person class.

Look at this: https://github.com/walterdavis/fugee/blob/master/app/controllers/people_controller.rb#L74

and this:

https://github.com/walterdavis/fugee/blob/master/app/views/people/_form.html.erb#L25

The rest is scaffolded, there's nothing mysterious here.

Clone this to your machine, run it in rails server.

Go to localhost:3000/pictures and add some pictures (just file names).

Go to localhost:3000/people, and add some people.

See how you can choose pictures for each person? See how the association is saved and updated? Watch in the console as the record is saved or updated from the web.

Walter

fugee ohu

unread,
Feb 8, 2018, 12:37:38 PM2/8/18
to Ruby on Rails: Talk
I'll do that today thanks In the meanwhile, the validtion's in picture.rb

fugee ohu

unread,
Feb 9, 2018, 5:01:41 PM2/9/18
to Ruby on Rails: Talk
Do  I have to create routes like "get '/people/:id/addresses' => 'addresses#index', as: 'person_addresses' I was thinking maybe rails already creates those routes from the associations and if I make them explicit maybe I'll mess up the routes 

Walter Lee Davis

unread,
Feb 10, 2018, 9:32:09 AM2/10/18
to rubyonra...@googlegroups.com
Rails would make those if you had nested routes:

resources :people do
resources :addresses
end

That will require you to do some additional changes in your addresses controller -- knowing that you will always have a person_id attribute running around in all actions, for example.

Read the relevant Rails guide (routing, I think).

Walter


Reply all
Reply to author
Forward
0 new messages