Question

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?

Was it helpful?

Solution 2

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.

OTHER TIPS

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top