Marcin Simonides
2006-Aug-12 18:33 UTC
[Rails] Collection assignment to a has_many :through
I''m working on a simple photo gallery in rails, it seems to be a good project for a newbie. I have photos and categories, many-to-many association. It worked well with HABTM. Then I decided that it would be good to be able to change order of the photos so that thumbnail pages would look less chaotic. So I created a Layout model which is a join model (or whatever it is called) that replaces HABTM relation. It has photo_id, category_id and position. Photo has many layouts and many categories through layouts. Category is analogous. In this way I obtained a structure that works as the one before with one exception: there is no Photo.categories= method when :through is used. After hours of googling (no-one gives examples of creating/editing data for such relationship, the documentation just says that collection=objects method exists) I gave up and wrote my own accessor: def categories=(collection) self.layouts.clear collection.find_all do |category| layout = self.layouts.build(:category => category) self.layouts << layout end end Some questions arise however: 1. Is there other possibility, more "standard", that I could use to replace categories in photo with those from user form? 2. If not, then what do you think about this solution (I''m new to both Rails and Ruby)? 3. Is my abstraction correct? Maybe there is no collection= method because it the proper solution of my problem doesn''t need it? Here are some more details: Each photo can have many categories. Each category has many photos. There is only one layout per category. When user edits a photo (s)he selects categories in which it should be present. List of selected categories is assigned into photo.categories. This operation should also create a layout that binds this photo with selected categories - regular many-to-many relationship. My solution doesn''t take the layout, ie. the acts_as_list part, into account. Maybe when I start implementing it, the whole situation changes. -- Posted via http://www.ruby-forum.com/.
Josh Susser
2006-Aug-12 20:38 UTC
[Rails] Re: Collection assignment to a has_many :through
Marcin Simonides wrote:> I have photos and categories, many-to-many association. It worked well > with HABTM. Then I decided that it would be good to be able to change > order of the photos so that thumbnail pages would look less chaotic. > So I created a Layout model which is a join model (or whatever it is > called) that replaces HABTM relation. It has photo_id, category_id and > position. Photo has many layouts and many categories through layouts. > Category is analogous. > > In this way I obtained a structure that works as the one before with one > exception: there is no Photo.categories= method when :through is used. > > After hours of googling (no-one gives examples of creating/editing data > for such relationship, the documentation just says that > collection=objects method exists) I gave up and wrote my own accessor: > > def categories=(collection) > self.layouts.clear > collection.find_all do |category| > layout = self.layouts.build(:category => category) > self.layouts << layout > end > end > > Some questions arise however: > 1. Is there other possibility, more "standard", that I could use to > replace categories in photo with those from user form? > 2. If not, then what do you think about this solution (I''m new to both > Rails and Ruby)? > 3. Is my abstraction correct? Maybe there is no collection= method > because it the proper solution of my problem doesn''t need it? Here are > some more details: > > Each photo can have many categories. > Each category has many photos. > There is only one layout per category. > When user edits a photo (s)he selects categories in which it should be > present. List of selected categories is assigned into photo.categories. > This operation should also create a layout that binds this photo with > selected categories - regular many-to-many relationship. > > My solution doesn''t take the layout, ie. the acts_as_list part, into > account. Maybe when I start implementing it, the whole situation > changes.Read my explanation of what''s going on here: http://blog.hasmanythrough.com/articles/2006/04/17/join-models-not-proxy-collections Your accessor is a step in the right direction, but not exactly how I would do it. You''ll find it has problems with keeping the database in sync with the in-memory collections. Here is a tighter solution that should do just what you want. # Photo.rb def categories=(collection) Layout.set_categories_for_photo(self, collection) layouts.reset categories.reset end # Layout.rb: def set_categories_for_photo(photo, categories) old_categories = photo.categories delete_from photo, (old_categories - categories) add_to photo, (categories - old_categories) end def delete_from(photo, categories) unless categories.empty? delete_all [''photo_id = ? and category_id in (?)'', photo.id, categories.collect { |c| c.id }] end end def add_to(photo, categories) unless categories.empty? self.transaction do categories.each do |category| next if photo.categories.include? category create! :photo => photo, :category => category end end end end -- Josh Susser http://blog.hasmanythrough.com -- Posted via http://www.ruby-forum.com/.
Marcin Simonides
2006-Aug-13 19:56 UTC
[Rails] Re: Collection assignment to a has_many :through
Josh Susser wrote:> Marcin Simonides wrote:[...]>> def categories=(collection) >> self.layouts.clear >> collection.find_all do |category| >> layout = self.layouts.build(:category => category) >> self.layouts << layout >> end >> end >>[...]> Read my explanation of what''s going on here: > http://blog.hasmanythrough.com/articles/2006/04/17/join-models-not-proxy-collectionsI didn''t try using << operator, lucky me :)> Your accessor is a step in the right direction, but not exactly how I > would do it. You''ll find it has problems with keeping the database in > sync with the in-memory collections. Here is a tighter solution that > should do just what you want.Thanks. I''ve pasted your solution into my program. But despite reading the code a few times I don''t understand, why is it better than mine in terms of synchronization with database (mine is incorrect, because it removes and then reinserts elements if they aren''t supposed to be changed, which causes loss of information about position, yours is correct in this regard). I was planning on using transactions if I were to stick to my solution, but I don''t think this would have anything to do with writing data to db or not, just race conditions. Also, why did you use resets here:> def categories=(collection) > Layout.set_categories_for_photo(self, collection) > layouts.reset > categories.reset > end>From what I''ve seen in the docs (or rather in the code) it clears errorconditions on Sybase connection adapter (if it''s the right reset I''ve been looking at). Sorry for those nagging questions, but I feel uneasy when I have a piece of code in my application that I don''t fully understand. -- Marcin Simonides -- Posted via http://www.ruby-forum.com/.
Josh Susser
2006-Aug-13 23:10 UTC
[Rails] Re: Collection assignment to a has_many :through
Marcin Simonides wrote:> Josh Susser wrote: >> Marcin Simonides wrote: > [...] >>> def categories=(collection) >>> self.layouts.clear >>> collection.find_all do |category| >>> layout = self.layouts.build(:category => category) >>> self.layouts << layout >>> end >>> end >>> > [...] >> Read my explanation of what''s going on here: >> http://blog.hasmanythrough.com/articles/2006/04/17/join-models-not-proxy-collections > > I didn''t try using << operator, lucky me :) > >> Your accessor is a step in the right direction, but not exactly how I >> would do it. You''ll find it has problems with keeping the database in >> sync with the in-memory collections. Here is a tighter solution that >> should do just what you want. > > Thanks. I''ve pasted your solution into my program. But despite reading > the code a few times I don''t understand, why is it better than mine in > terms of synchronization with database (mine is incorrect, because it > removes and then reinserts elements if they aren''t supposed to be > changed, which causes loss of information about position, yours is > correct in this regard).The main synchronization issue has to do with the ActiveRecord association caches. has_many keeps an array of associated objects in memory. The << method lets you add to that array, but also updates the foreign key of the associated object and saves it to the database. You don''t get that with has_many :through, so you need to do a bit more work to make sure the changes you are making to your Ruby objects get written to the database. That''s the key part of what my code does.> I was planning on using transactions if I were to stick to my solution, > but I don''t think this would have anything to do with writing data to db > or not, just race conditions. > > Also, why did you use resets here: > >> def categories=(collection) >> Layout.set_categories_for_photo(self, collection) >> layouts.reset >> categories.reset >> end > > From what I''ve seen in the docs (or rather in the code) it clears error > conditions on Sybase connection adapter (if it''s the right reset I''ve > been looking at).reset() is a method on the association proxy that clears the in-memory cache collection of associated objects. Or more simply, "layouts.reset" clears the layouts collection cache. Since you just modified the join model, you need to clear the cache so that the next time you access the association, it will get the fresh data from the database. Otherwise you''ll have sync problems and will wonder where your objects went. -- Josh Susser http://blog.hasmanythrough.com -- Posted via http://www.ruby-forum.com/.
Marcin Simonides
2006-Aug-14 21:23 UTC
[Rails] Re: Collection assignment to a has_many :through
Josh Susser wrote:> Marcin Simonides wrote: >> Josh Susser wrote: >>> Marcin Simonides wrote: >> [...] >>>> def categories=(collection) >>>> self.layouts.clear >>>> collection.find_all do |category| >>>> layout = self.layouts.build(:category => category) >>>> self.layouts << layout >>>> end >>>> end >>>>[...]> The main synchronization issue has to do with the ActiveRecord > association caches. has_many keeps an array of associated objects in > memory. The << method lets you add to that array, but also updates the > foreign key of the associated object and saves it to the database. You > don''t get that with has_many :through, so you need to do a bit more work > to make sure the changes you are making to your Ruby objects get written > to the database. That''s the key part of what my code does.Ok, but going back to my code above: when Photo''s layouts are inserted with << then the cache for Photo.layouts and Photo.categories should be ok, right? Or perhaps not, because updating and saving layouts with << doesn''t update categories? Just to remind, the relations for Photo are this: has_many :layouts has_many :categories :through => layouts Or maybe it''s more complicated and I should really dig into documentation and some books? :) -- Marcin Simonides -- Posted via http://www.ruby-forum.com/.
Marcin Simonides wrote:> > Ok, but going back to my code above: when Photo''s layouts are inserted > with << then the cache for Photo.layouts and Photo.categories should be > ok, right? Or perhaps not, because updating and saving layouts with << > doesn''t update categories? > > Just to remind, the relations for Photo are this: > has_many :layouts > has_many :categories :through => layouts > > Or maybe it''s more complicated and I should really dig into > documentation and some books? :) > -- > Marcin SimonidesDid you ever get this fully worked out? I''m trying to do something similar and could use any tips/advice you have. thanx ;) -- Posted via http://www.ruby-forum.com/. --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk-unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en -~----------~----~----~----~------~----~------~--~---
skwasha wrote:> Did you ever get this fully worked out? I''m trying to do something > similar and could use any tips/advice you have. thanx ;)Yes, the code Josh Susser included earlier in this thread works fine. Please provide some more information on what you''re trying to do. -- Marcin Simonides -- Posted via http://www.ruby-forum.com/. --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk-unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en -~----------~----~----~----~------~----~------~--~---
Damien Le berrigaud
2007-Nov-02 12:47 UTC
Re: Collection assignment to a has_many :through
I made some "improvements" to this code for personnal purpose, you may find it useful, it is available there: http://www.webdrivenblog.com/2007/10/28/assigning-a-collection-to-has_many-through -- Posted via http://www.ruby-forum.com/. --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk-unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en -~----------~----~----~----~------~----~------~--~---