Reputation: 3564
I have a rails project similar to a blog with posts that have set of images and one featured image. The image set was a pretty straight forward HABTM relationship, as several posts can share the same image and one post can have many images, but the featured image has been a bit more troubling.
Every post should have one and only one featured image and one image can be the featured image on several posts, so my first thought was just to reverse the relationship and let images has_many
posts and posts belong_to
images, but that seems problematic in a lot of different ways. First, it's not very semantic. Second, the post controller needs extra code to accept a value for image_id, as Post.new didn't seem to want to accept image_id as an attribute.
My second thought --and this is the one I'm going with so far-- was to use a HABTM relationship on both with a limit: 1
specifier on the the post's has_and_belongs_to_many :featured_images
and a unique: true
on t.belongs_to :post
in the migration. This solution works, but it seems hack-ish. Also, it means that I have to access the featured picture like this post.featured_images.first
rather than post.featured_image
. Worse, I can't help but think that this would hurt database performance as it has to access three tables instead of two and it has to search for the post id in the many-to-many table, rather than identifying immeadiately via the id column.
So, is this the right way to do this or is there something better? Does rails have anything like a has_one
, belongs_to_many
relationship?
Upvotes: 0
Views: 182
Reputation: 2232
Since this is a case where you have a "has and belongs to many" relationship but you want to store extra information about the relationship itself (the fact that an image is "featured" for a post), I would try a has_many :through
arrangement instead. Something like this:
class Post < ActiveRecord::Base
has_many :post_images, inverse_of: :post
has_many :images, through: :post_images
has_one :featured_post_image, class_name: PostImage,
inverse_of: :post, conditions: { is_featured: true }
has_one :featured_image, through: :featured_post_image
accepts_nested_attributes_for :post_images, allow_destroy: true
attr_accessible :post_images_attributes
end
class PostImage < ActiveRecord::Base
belongs_to :post
belongs_to :image
attr_accessible :image_id
end
class Image < ActiveRecord::Base
has_many :post_images
has_many :posts, through: :post_images
end
Unfortunately, adding validations to ensure that a post can never have more than one featured image is trickier than it looks. You can put a validation on Post
, but that won't save you if some other part of your app creates PostImages directly without touching their associated posts. If anyone else reading this has some insight into this problem, I'd love to hear it.
Upvotes: 1
Reputation: 1379
why do not try something like that (without HABTM, just has_many):
class Image < ActiveRecord::Base
belongs_to :post
attr_accessible :featured
after_commit :reset_featured, if: :persisted?
protected
# garant that featured will be only one
def reset_featured
Image.where('id <> ?', self.id).update_all(featured: false) if self.featured
end
end
class Post < ActiveRecord::Base
has_many :images, conditions: { featured: false }
has_one :featured_image, class_name: 'Image', conditions: { featured: true }
end
Upvotes: 3