I've been trying to figure out how to associate my models on a project I've been working on for a while, and I've come here for help a couple times before, but I never got a satisfactory answer. I have two models: Post and Image. Every post has several images attached to it and posts can share images, so a HABTM relationship made sense for that, like this:
class Post < ActiveRecord::Base
has_and_belongs_to_many :images
end
class Image < ActiveRecord::Base
has_and_belongs_to_many :posts
end
The problem now is that I want each post to have a single 'featured image.' How do I do this? The first thought that comes to mind is a simple has_one :featured_image on the post and belongs_to :post_featured_on on the image, but the problem with that is that the same image can be featured on multiple posts.
So the next idea I came up with is to reverse the relationship: belongs_to :featured_image on the post and has_many :posts_featured_on on the image. The problem with that is that it isn't very semantic and rails doesn't seem to want to let me set a post's featured image from its form, like this in the controller: Post.new(:featured_image => Image.find(params[:image_id]))
So the next idea suggested to me was a second HABTM relationship, like so: has_and_belongs_to_many :featured_images. There's an obvious problem with this, it's plural. I tried putting unique: true on the post_id column in the migration, but that didn't help the fact that I kept having to do this in my code: post.featured_images.first which can be very frustrating.
The last idea I tried was a has_many :posts, through: :attachment and has_one :featured_posts, through: :attachment in place of the original HABTM, but these just seems unnecessarily cumbersome, and rails doesn't seem to want to let me assign the images on the fly this way like Post.new(:featured_image => Image.find(params[:image_id])).
Is there any good way to do this? Have I done something wrong in my previous attempts? Shouldn't this just be a simple foreign key on the post table? Why does it have to be so difficult?
I like your second idea just fine. The full blown approach is to use a transaction model, such as #depa suggested. The transaction model is great when you want to store additional attributes such as when the Image was made featured for a given post (and perhaps when it was not, as well). But, whether you build that transaction object as well or not, you can just cache the featured image on the post object for quick access. Try just doing this:
class Post < ActiveRecord::Base
has_and_belongs_to_many :images
belongs_to :featured_image, class_name: 'Image'
end
class Image < ActiveRecord::Base
has_and_belongs_to_many :posts
# Purposefully not defining an inverse relationship back to Post.
# You can if you need or want it but you may not.
end
Then, in the controller I'd recommend:
#post = Post.find_by_id(params[:id])
#post.featured_image = Image.find(params[:image_id])
#post.save
You probably didn't have success with this before because of not having attr_accessible :featured_image_id on the Post model. And/or because you were using the wrong attribute name. (It should have been Post.new(featured_image_id: Image.find(params[:image_id])).) Either way, it's good to keep the code a little more object-oriented than all that. The way I laid it out above, you don't have to think about the column name in the database, and can just think about the objects you're dealing with. I.e., just assign the Image to the Post's feature_image reference. Keeping this in mind, I prefer to not set foreign keys as attr_accessible when possible.
You can do what you want using a has_one :through association.
class Post < ActiveRecord::Base
has_and_belongs_to_many :images
has_one :featured_image,
:through => :feature,
:class_name => 'Image'
has_one :feature
end
class Image < ActiveRecord::Base
has_and_belongs_to_many :images
has_many :featured_images,
:through => :features,
:class_name => 'Image',
:foreign_key => :featured_image_id
has_many :features
end
class Feature < ActiveRecord::Base
belongs_to :featured_image,
:class_name => 'Image'
belongs_to :post
end
Related
I have the following model structure:
class Test < ActiveRecord::Base
has_many :questions
end
class Question < ActiveRecord::Base
belongs_to :test
belongs_to :answer, class_name: 'Choice'
has_many :choices, dependent: :destroy
end
class Choice < ActiveRecord::Base
belongs_to :question
has_one :question_answered, dependent: :nullify, class_name: 'Question', foreign_key: 'answer_id'
end
Now I wanted to create a single form where the user could create/save the tests params and edit all the questions and their choices, and also select which one is the correct answer.
The problem arrives when the user is creating a new question. None of the choices have an id and, therefore, I don't know how to define which will be the correct answer without having to write extra code on my controller (only using accepts_nested_attributes_for).
What I did is: Before saving the nested attributes of Test (but saving test before), I fetch from params all the questions and choices that don't have an id and save them. After that I update the answer_id params for all the questions.
This solutions is working right now but I don't think it's the most elegant one. Knowing Rails and its awesomeness, I know there is a better way of doing this. What do you guys suggest?
I've found good answers here, here, and here but I'm having trouble generalizing that to what I'm after.
I have multiple categories, that will be curated and selectable. So, users will be able to select cat1, cat2, and cat3, but not type a custom category.
A category can have many posts, a post can have many categories.
A post can have many comments.
A user can have many posts, and many comments.
For the post/category relationship, I'm thinking this will work, but the user/post/comment relationship is where I'm scratching my head...
# app/models/category.rb
class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
end
# app/models/post.rb
class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
belongs_to :user
has_many :comments
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Does this look close? Do I need any foreign keys anywhere to handle all this? Thanks in advance, I'm sure this is simple and I'm missing something obvious in my understanding.
And then I have to worry about how to write the tests for all this! That's for another day though...
EDIT: I should point out, I haven't started this yet. Just trying to map it out before I start, so it should simplify things, fewer migrations, etc.
EDIT AGAIN: Implemented suggested changes so far. Thanks!
why not start with the specs first? is a good practice on rails with all the power you have with rspec
Your Item should be called Post, why Item? is there any reason? if you want to call it "Item" you need to specify that on the associations
belongs_to :post, class_name: 'Item'
but you are better with Post instead of Item
A comment belongs to a user so the the user has_many :comments, you don't need the ", through: :posts" part
has_many :category_posts
has_many :posts, :through => :category_posts #or would has_and_belongs_to_many work better?
this depends on you, you need extra behavior on the CategoriesPosts? (Categories, in plural) if not, just use has_and_belongs_to_many
Really, i would suggest you start with the specs, you will end up with the implementations without thinking it too much and then you already have it tested and then you can add more specs and refactor it. Read something about TDD and BDD, it's hard at first but it's really good when you get it.
The only change I think I would make to this, other than actually naming Item Post, would be on your user model:
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
You don't need a through association there. You could add other scoped comments to be something like comments_on_my_posts, through: :posts, class_name: "Comment", but for the above association on comments, it should be direct (commenter <=> comment).
I currently have a model named Image. This model is tied to an Article.
class Article < ActiveRecord::Base
has_many: images
end
The Image table contains an article_id, so an image is associated with an article.
However, I would like to use my images together with other models as well. Therefore I imagine changing article_id into something like owner_id and add a new attribute called owner_model to the Image model.
That way other models can also have many images.
Any idea if this is possible to achieve gracefully using ActiveRecord and how to go about it?
On a sidenote, I am using CarrierWave for images.
You can use polymorphic associations.
The rails guide has a neat example that goes with your question.
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
Have you looked into polymorphic associations? Ryan has a video up on it: http://railscasts.com/episodes/154-polymorphic-association
It will allow you to associate different classes with your images.
What you are looking for is called polymorphic associations ( http://guides.rubyonrails.org/association_basics.html#polymorphic-associations ).
I have a Comments model, and I also have a Video, and Photo model. Now, I want for my Video and Photo models to have_many comments, but that means my Comment model will have to have a belongs to :video and a belongs_to :model (as well as foreign keys for each model in the database). Now say I create a Post model in that same application and I want it to have many comments, that would mean I would have to add belongs_to :post to my Comment class. In rails is there a better way to implement a Comment model when there are many other models that are going to have an association with it, or is this just how it is done? Any advice would be much appreciated.
You're looking for polymorphic associations.
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Video < ActiveRecord::Base
has_many :comments, :as => :commentable
end
You also have to make some changes to your migrations, see the linked documentation for more information.
I'm working on an application that allows users to associate images with specific events. Events are owned by a user. The easy solution would of course be:
class Image < ActiveRecord::Base
belongs_to :event
end
class Event < ActiveRecord::Base
has_many :images
belongs_to :user
end
class User < ActiveRecord::Base
has_many :events
has_many :images, :through => :events
end
Except! There is one problem. I also want my users to be able to own images directly, without an intermediary event. If I use polymorphic association "conventionally" here, then obviously user.images won't work properly. Should I just hold my nose, use an :as => :event_images to disambiguate, and define user.all_images if that need ever comes up? Should I have all images owned directly by users and optionally associated with events somehow? (With, of course, a validation to ensure consistency... but that seems code-smelly.) Is there a third solution that is prettier than either of these?
I often find it useful to forget the ActiveRecord DSL and work with the data model directly when defining complex relationships. Once the data model is correct you can then map it into model statements.
It is not quite clear from your question if Images can be owned by a User or an Event, of if they can be owned by both at the same time. That issue will determine the data model you use.
If an Image can be owned by both, the Image table will need a reference to both a user_id and an event_id, which may need to be nullable depending on your use-case (user or event being optional relationships). If the Image can only be owned by one, you could set up some sort of polymorphic ownerable relationship that maps owners to the right owner table (owner_id, owner_type etc etc).
Assuming it can belong to both:
class Image < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
class Event < ActiveRecord::Base
has_many :images
belongs_to :user
end
class User < ActiveRecord::Base
has_many :events
has_many :images
has_many :event_images, :through => :events, :class_name => "Image"
end