Reputation: 2942
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)?
Upvotes: 41
Views: 24396
Reputation: 308
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.
Upvotes: 1
Reputation: 19331
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
Upvotes: 100
Reputation: 57974
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
Upvotes: 2
Reputation: 4201
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
Upvotes: 20
Reputation: 1
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.
Upvotes: -2
Reputation: 1067
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
Upvotes: 2
Reputation: 2610
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 ;)
Upvotes: 1
Reputation: 3042
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
Upvotes: 7
Reputation: 2610
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
Upvotes: 39