Please explain the has_many, through: source: Rails Association - ruby-on-rails

I've found a bunch of articles, stackoverflow answers and rails documentation about 'source:', but none of it explains this association in a way I can understand it. I need the most simplified explanation of this way of associating, if possible.
My example is this:
Album:
has_many :reviews, :dependent => :destroy
has_many :reviewers, through: :reviews, source: :user
belongs_to :user
Review:
belongs_to :album, optional: true
belongs_to :user
User:
has_many :reviews
has_many :reviewed_albums, through: :reviews, source: :album
has_many :albums
The rest of the code does not mention "reviewers" or "reviewed_albums", so that is the part I understand the least.
Are those names completely irrelevant?

TL;DR
source is standing for the table that this association is referring to, since we give it a different name than just .users because we already have the belongs_to :user association.
Long explanation
I think it's easiest with this little picture which is basically the database schema for the models you posted above.
We have albums, that belong to users, meaning that a user is basically someone who creates an album. We also have reviews and they belong to albums, meaning an album can be reviewed. And a review is made by a user so that's why a review belongs to a user.
Now associations in rails is a way to create methods that can be called on a database record to find its associated record.
We could get a user from an album or all the reviews a user made for example.
album = Album.find(1)
album.user # => returns the creator of the album
user = User.first
user.reviews # => returns all the reviews a user made
Now there is even more connections between those models than the ones mentioned above.
Let's look at album first:
# album.rb
has_many :reviews, :dependent => :destroy
belongs_to :user
has_many :reviewers, through: :reviews, source: :user
An album belongs to one user who created it. It has many reviews. And, if we we follow the line from albums to reviews and then further along to the users table, we see that we can also access the users that gave the reviews.
So we would want to do something like
album.reviews.users
Meaning: give me all the users that left a review for this album. Now this line of code wouldn't work - because album.reviews returns an array (an ActiveRecord::Relation object to be exact) and we cannot just call .users on this.
But we can have another association
has_many :reviewers, through: :reviews, source: :user
And here we're calling it reviewers to not get confused with the method/association .user that refers to the creator. Normally, Rails would refer the database table name from the name of the association. Since we're giving a different name here, we have to explicitly give the name of the DB table we're referring to and that is the users table.
So this is what this line is about - we create another association, we don't want the direct line (see image) between album and user, we want the users that left a review on this album, so we go through the reviews table and then we have to give the name (source) of the table so Rails knows in which table to look.
And this will finally allow us to write code like this:
album = Album.first
album.user # => creator of the album
album.reviewers # => all users that have left a review for this album
Hope that helps! Let me know if you have any more questions.
Maybe you can explain the other association with source in the users model in the comments.

Related

rails join table issue with different roles (owner, non-owner)

In my app users can create products so at the moment User has_many :products and Product belongs_to :user. Now I want the product creator product.user to be able to invite other users to join the product, but I wanna keep the creator the only one who can edit the product.
One of the setups I've got in my mind is this, but I guess it wouldn't work, since I don't know how to distinguish between created and "joined-by-invitation" products when calling user.products.
User
has_many :products, through: :product_membership
has_many :product_memberships
has_many :products # this is the line I currently have but think it wouldn't
# work with the new setup
Product
has_many :users, through: :product_membership
has_many :product_memberships
belongs_to :user # I also have this currently but I'd keep the user_id on the product
# table so I could call product.user and get the creator.
ProductUsers
belongs_to :user
belongs_to :product
Invitation
belongs_to :product
belongs_to :sender, class: "User"
belongs_to :recipient, class: "User"
To work around this issue I can think of 2 solutions:
Getting rid of the User has_many :products line that I currently have and simply adding an instance method to the user model:
def owned_products
Product.where("user_id = ?", self.id)
end
My problem with this that I guess it doesn't follow the convention.
Getting rid of the User has_many :products line that I currently have and adding a boolean column to the 'ProductUsers' called is_owner?. I haven't tried this before so I'm not sure how this would work out.
What is the best solution to solve this issue? If none of these then pls let me know what you recommend. I don't wanna run into some issues later on because of my db schema is screwed up.
You could add an admin or creator attribute to the ProductUsers table, and set it to false by default, and set it to true for the creator.
EDIT: this is what you called is_owner?
This seems to be a fairly good solution to me, and would easily allow you to find the creator.
product.product_memberships.where(is_owner?: true)
should give you the creator

Validate uniqueness through join model in rails

I have a has_many :through association setup between two tables (Post and Category). The reason I'm using has_many :through instead of HABTM is that I want to do some validation on the join table (PostCategory).
So I have 4 models in use here:
User:
has_many :posts
has_many :categories
Post:
belongs_to :user
has_many :post_categories
has_many :categories, :through => :post_categories
Category:
belongs_to :user
has_many :post_categories
has_many :posts, :through => :post_categories
PostCategory:
belongs_to :post
belongs_to :category
Basically what I want is: Users can create posts, users can also create their own categories. A user can then categorize posts (not just their posts, any posts). A post can be categorized by many different users (in different ways potentially), and a category could contain many different posts (A user could categorize N posts under a specific category of theirs).
Here's where it gets a little bit tricky for me (I'm a Rails noob).
A post can ONLY belong to ONE category for a given user. That is, a post CANNOT belong to more than ONE category for any user.
What I want to be able to do is create a validation for this. I haven't been able to figure out how.
I've tried things like (inside PostCategory)
validates_uniqueness_of :post_id, :scope => :category_id
But I realize this isn't correct. This would just make sure that a post belongs to 1 category, which means that after one user categorizes the post, no other user could.
Really what I'm looking for is how to validate this in my PostCategory model (or anywhere else for that matter). I'm also not against changing my db schema if that would make things easier (I just felt that this schema was pretty straight forward).
Any ideas?
The simpliest way is to add user_id to PostCategory and to validate uniqueness of post_id with user_id scope.
Another way is to create custom validation which checks using sql if category owner has added category to that post.
Option 1 : use a before_save. In it, do a SQL look up to make sure a post with a similar category for your user doesn't exist (take care that on edit, you'll have to make sure you don't look-up for the current Post that is already in the DB)
Option 2 : custom validators :
http://guides.rubyonrails.org/v3.2.13/active_record_validations_callbacks.html#custom-validators
Never used them, but sounds like it can do what you want

How to associate descendant models to a parent

I have the following models:
Account
has_many :libraries
Library
has_many :topics
belongs_to :account
Topic
has_many :functions
belongs_to :library
Function
has_one :example
belongs_to :topic
Example
belongs_to :function
I would like to be able to able to do things such as:
some_account.libraries
some_account.topics
some_account.functions
some_account.examples
In addition, I would like to be able to assign an account to a descendant, i.e
some_example.account = some_account
some_function.account = some_account
some_topic.account = some_account
some_library.account = some_account
To give some context:
I am letting a user (Account) create each Library, Topic, Function, Example. record separately. Then a user is free to change how the records are associated: Change the topic of a Function, move a Topic to a different Library, add an example to a function, and so on.
To my understanding no matter what record is created, I would need to assign it to a user (account) so that I can have a list of each Model records that a user has created, as well as prevent other users from seeing stuff that doesn't belong to them
Although I might be overcomplicating, I really don't know :(
Thanks in advance.
Just put
belongs_to :account
on each entity a user can make... and add a foreign key, and
Account
has_many :libraries
has_many :topics
has_many :functions
has_many :examples
(Note: I use the hobo_fields gem to make migrations easier)
That way.. if they change which functions are in which topics etc.. you can't loose who created it.
If you want to make sure users cannot add their topics to someone else's library just put validation on the record to prevent it.

Rails 3 set parameter of belongs_to association

I have a rails app that is tracking social data. The users are going to be able to create groups and add pages(ie. facebook fan pages) to their groups by the page's social id. Since users could potentially be adding the page as someone else, I have it set up so that there is only one page per social id. I also have a pivot table called Catgorizations that links of the pages to the groups and users.
My model relationships are set up as follows:
User
has_many :groups
Group
belongs_to :user
has_many :categorizations
has_many :pages, :through => :categorizations
Page
has_many :categorizations
has_many :groups, :through => :categorizations
Categorization
belongs_to :group
belongs_to :page
Now when I create a new page and it saves, it is creating a new categorization. The problem I'm running into is that in the Categorization I need to set the user_id manually. I've tried:
#page.categorizations.user_id
But I get an undefined method user_id error. I may be approaching this from the wrong direction but how would I go about setting the user_id of a categorization through the page object?
Also if it matters, I'm using devise to handle my user management stuff.
Any help would be greatly appreciated.
Thanks
What you're tring to do is to access something several levels deep in a 'chained' set of relationships.
In order to access an instance of the User model, given a page, you need to get its categorizations, pick one (how?), get its group, then see what the user_id is of that group.
Conceptually a page will actually have many users that might be in charge of categorizing it.
To get something to happen you could arbitrarily pick the first categorization and then do something with its user:
cat = #page.categorizations.first
user = cat.group.user
I don't know what you mean about 'setting' the user id for the categorization - it doesn't have a user, so I don't know what you'll then want to do with that information, sorry!
From your description and your model, only Pages have a User, not Categorization, which is why you get the error that it doesn't exist (it's not in your database).
Also, you are missing the opposite association on Page:
Page
belongs_to :user
This allows you to get back to User from Page:
#page.user.id

Rails using a join model attribute in a condition for a find

I'm using a :has_many, :through association to link two models, User and Place
It looks like this -
In User:
has_many :user_places
has_many :places, :through=>:user_places
In Place:
has_many :user_places
has_many :users, :through=>:user_places
In User_Place
belongs_to :user
belongs_to :place
belongs_to :place_status
On that last one note the place_status.
I want to write a find that returns all places associated to a user with a particular place_status_id.
Place_Status_id is on the join model, user_place.
So basically I want
User.places.where(:place_status_id=>1)
(in rails 3)
but i get an error with that because place_status_id isnt on the place model.
Any ideas? Thanks all.
I believe you can do your find this way
#user.places.joins(:user_places).where(:user_places => {:place_status_id => 1})
I've never used Rails 3, so I'm sorry if there's any errors.

Resources