rails, accepts_nested and has_many :through is creating duplicate entries - ruby-on-rails

I have a store model that has many products with a has_many :through relationship.
I have this working with accepts_nested_attributes, but the result is that rails is making duplicate associates.
I don't have anything special going on it is a very simple app.
Any ideas on why duplicates associates are getting created?

look at answer : how to avoid duplicates in a has_many :through relationship? here :
add :uniq => true to the has_many :through
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end

This is a confirmed bug in Rails, with a fix set to be included in 2.3.6.
https://rails.lighthouseapp.com/projects/8994/tickets/3575-multiple-join-records-when-using-nested_attributes-in-habtm

Related

after_destroy not called for linked table

I have these models
class User < ActiveRecord::Base
has_many :user_functions, :dependent => :destroy
has_many :functions, :through => :user_functions
accepts_nested_attributes_for :functions, allow_destroy: true
Model of the linked table:
class UserFunction < ActiveRecord::Base
belongs_to :user, inverse_of: :user_functions
belongs_to :function, inverse_of: :user_functions
after_destroy :unplan_items
after_create :plan_items
and of course the model of function but this is like user...
Now when I do the following in my tests:
#user.functions = [#functions]
#user.save
expect(#user.planned_items.count).to eq(1)
#user.functions = []
#user.save
I notice the callback after_destroy isn't called. Why is this and how can I avoid this. There are certain steps that need to be done every time a UserFunction is destroyed...
I believe this has to do with: https://github.com/rails/rails/issues/7618 (I'm using rails 4.2.5 though). The after_create is working perfect though...
Currently rails uses :delete_all as default strategy of has_many_through. It only calls :destroy_all when we explicitly specify dependent: :destroy on the association.
The docs mention advice to use has_many :through if you need callbacks:
See the suggestion here: http://guides.rubyonrails.org/association_basics.html
You should use has_many :through if you need validations, callbacks,
or extra attributes on the join model.
So there currently is an inconsistency between after_create which does do the callback and after_destroy.
This is mentioned in these two issues posted on GitHub:
https://github.com/rails/rails/issues/7618
https://github.com/rails/rails/issues/27099
The fix for now is to explicitly put :dependent => :destroy on the :through part. This will make sure the callback are used.
class User < ActiveRecord::Base
has_many :user_functions
has_many :functions, :through => :user_functions, :dependent => :destroy
accepts_nested_attributes_for :functions, allow_destroy: true
For anyone reading this 2021+
Change This
has_many :object_tags, :as => :taggable, :dependent => :destroy
has_many :tags, :through => :object_tags
To This
has_many :object_tags
has_many :tags, :through => :object_tags, :dependent => :destroy

How do I collect all the topics that are owned by the members of the groups I am a member of?

This may be tough for me to explain, so if it's not clear just let me know so I can edit as needed!
I have the following example:
class User < ActiveRecord::Base
has_many :topics
has_many :memberships
end
class Topic < ActiveRecord::Base
belongs_to :user
end
#join model between User and Group
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships, :source => :user
has_many :topics, :through => :members
end
The problem I'm having is that I am trying to create a feed (#feed_topics) of all topics that are owned by all the members of the groups I am a member of, and I'm driving myself a little nuts.
Should I try to make this happen using associations, or make an instance method in my User model that has some ActiveRecord/SQL to union all the groups' members' topics into one ActiveRecord::Relation object?
My goal is to write current_user.feed_topics in my controller's action.
Sorry for not explaining earlier! The idea was to utilize 'Nested has_many_through's in order to get to your feed topics. This concept is documented here under the heading 'Nested Associations': http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html.
Let me know if this still is unclear (or if it doesn't work).
class User < ActiveRecord::Base
has_many :topics
has_many :memberships
has_many :groups, :through => :membership
has_many :group_members, :through => :groups, :source => :member
has_many :feed_topics, :through => :group_members, :source => :topic
end
So far these are the final versions of the models from the original question (topic and membership did not change):
class User < ActiveRecord::Base
has_many :topics
has_many :memberships
has_many :groups, :through => :memberships
has_many :feed_topics, :through => :groups, :source => :member_topics
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships, :source => :user
has_many :member_topics, :through => :members, :source => :topics
end
I am testing right now by adding more groups and members to see if it pulls in all the other members' topics of other groups.
EDIT: things seem to be working ok.
EDIT2: one little problem I had was seeing duplicate topics because a member was in multiple groups. I learned about :uniq => true and it saved the day.

:has_many relationship with :through on another relationship with :through

My setup is as follows:
class User < ActiveRecord::Base
has_many :owners, :dependent => :destroy
has_many :properties, :through => :owners
end
class Owner < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class Property < ActiveRecord::Base
has_many :owners, :dependent => :destroy
has_many :users, :through => :owners
has_many :datafiles, :dependent => :destroy
end
class Datafile < ActiveRecord::Base
belongs_to :property
end
Now I'd like to be able to do #user.datafiles.
I tried has_many :datafiles, :through => :properties, :source => :datafiles but there appears to be a problem with a :through on something that's already went to a :through. So how would I go about to try and manage what I'm trying to do here?
Thank you in advance.
2 approaches;
1>
class User < AR
has_many :owners, :dependent => :destroy
has_many :properties, :through => :owners
has_many datafiles
end
class Datafile < AR
belongs_to :user
belongs_to :property
end
Your requirement of user.datafiles should be fulfilled with this.
If you want a nested has_many through, you'll need to use a plugin which is the 2nd approach.
2>
You can find it here.
The plugin works out of the box and does the job.
How about something like:
#user.rb
def datafiles
Property.find(:all, :joins => :owners, :conditions => ['owners.user_id = self.id'], :include => :datafile).collect(&:datafile)

Nested Through Has Many Associations

I am having some trouble creating a nested association and I can't quite spot what's going on:
class Package < ActiveRecord::Base
has_many :selections, :class_name => "PackageSelection"
has_many :channels, :through => :selections
has_many :categories, :through => :channels, :source => ?????
end
class Channel < ActiveRecord::Base
belongs_to :category
has_many :package_selections
has_many :packages, :through => :package_selections
end
class Category < ActiveRecord::Base
has_many :channels
end
I am trying to figure out how to build the association to categories. Any advice?
It seems like this is impossible in Rails Core (currently 2.3) but someone has written a plugin to allow it

how to avoid duplicates in a has_many :through relationship?

How can I achieve the following? I have two models (blogs and readers) and a JOIN table that will allow me to have an N:M relationship between them:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What I want to do now, is add readers to different blogs. The condition, though, is that I can only add a reader to a blog ONCE. So there mustn't be any duplicates (same readerID, same blogID) in the BlogsReaders table. How can I achieve this?
The second question is, how do I get a list of blog that the readers isn't subscribed to already (e.g. to fill a drop-down select list, which can then be used to add the reader to another blog)?
Simpler solution that's built into Rails:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Note adding the :uniq => true option to the has_many call.
Also you might want to consider has_and_belongs_to_many between Blog and Reader, unless you have some other attributes you'd like to have on the join model (which you don't, currently). That method also has a :uniq opiton.
Note that this doesn't prevent you from creating the entries in the table, but it does ensure that when you query the collection you get only one of each object.
Update
In Rails 4 the way to do it is via a scope block. The Above changes to.
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { uniq }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Update for Rails 5
The use of uniq in the scope block will cause an error NoMethodError: undefined method 'extensions' for []:Array. Use distinct instead :
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
This should take care of your first question:
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates_uniqueness_of :reader_id, :scope => :blog_id
end
The Rails 5.1 way
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What about:
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blog_ids])
Rails takes care of the collection of ids for us with association methods! :)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
The answer at this link shows how to override the "<<" method to achieve what you are looking for without raising exceptions or creating a separate method: Rails idiom to avoid duplicates in has_many :through
The top answer currently says to use uniq in the proc:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
This however kicks the relation into an array and can break things that are expecting to perform operations on a relation, not an array.
If you use distinct it keeps it as a relation:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
I'm thinking someone will come along with a better answer than this.
the_reader = Reader.find(:first, :include => :blogs)
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])
[edit]
Please see Josh's answer below. It's the way to go. (I knew there was a better way out there ;)
I do the following for Rails 6
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates :blog_id, uniqueness: { scope: :reader_id }
end
Don't forget to create database constraint to prevent violations of a uniqueness.
Easiest way is to serialize the relationship into an array:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
serialize :reader_ids, Array
end
Then when assigning values to readers, you apply them as
blog.reader_ids = [1,2,3,4]
When assigning relationships this way, duplicates are automatically removed.

Resources