Rails: How to group by nested association? - ruby-on-rails

I have a Follow model with user_id and track_id. The Track model has an artist_id field.
What I want to do is count which artists have the most followers, but since users follow "tracks" and not "artists", I need to figure out how to do a count through the tracks.
So, what I was thinking was to do some sort of group by on a nested association. i.e. Group the Follow records by "track -> artist_id", somehow.
Then I could count the number of users for each.
Is that even possible? Is there more info that would be useful here?

You need to use has_many :through to establish the Artist <-> Tracks <-> User relationship.
class Artist < ApplicationRecord
has_many :tracks
has_many :users, through: :tracks
end
class Follow < ApplicationRecord
belongs_to :user
belongs_to :track
end
class Track < ApplicationRecord
belongs_to :artist
has_and_belongs_to_many :users, join_table: :follows
end
class User < ApplicationRecord
has_and_belongs_to_many :tracks
has_many :artists, through: :tracks, join_table: :follows
end
Then Rails can take care of the joins between Artist and User.
Artist.includes(:users).group(:id).count("users.id")

Related

Extracting data using rails query from a join table

I have users table, books table and books_users join table. In the users_controller.rb I am trying extract the users who have filtered_books. Please help me to resolve that problem.
user.rb
has_many :books_users, dependent: :destroy
has_and_belongs_to_many :books, join_table: :books_users
book.rb
has_and_belongs_to_many :users
books_user.rb
belongs_to :user
belongs_to :book
users_controller.rb
def filter_users
#filtered_books = Fiction.find(params[:ID]).books
#users = **I want only those users who have filtered_books**
end
has_and_belongs_to_many does not actually use a join model. What you are looking for is has_many through:
class User < ApplicationRecord
has_many :book_users
has_many :books, through: :book_users
end
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
end
class BookUser < ApplicationRecord
belongs_to :book
belongs_to :user
end
If you want to add categories to books you would do it by adding a Category model and another join table. Not by creating a Fiction model which will just create a crazy amount of code duplication if you want multiple categories.
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
has_many :book_categories
has_many :categories, through: :book_categories
end
class BookCategory < ApplicationRecord
belongs_to :book
belongs_to :category
end
class Category < ApplicationRecord
has_many :book_categories
has_many :books, through: :book_categories
end
If you want to query for users that follow a certain book you can do it by using an inner join with a condition on books:
User.joins(:books)
.where(books: { title: 'Lord Of The Rings' })
If you want to get books that have a certain category:
Book.joins(:categories)
.where(categories: { name: 'Fiction' })
Then for the grand finale - to query users with a relation to at least one book that's categorized with "Fiction" you would do:
User.joins(books: :categories)
.where(categories: { name: 'Fiction' })
# or if you have an id
User.joins(books: :categories)
.where(categories: { id: params[:category_id] })
You can also add an indirect association that lets you go straight from categories to users:
class Category < ApplicationRecord
# ...
has_many :users, though: :books
end
category = Category.includes(:users)
.find(params[:id])
users = category.users
See:
The has_many :through Association
Joining nested assocations.
Specifying Conditions on Joined Tables
From looking at the code i am assuming that Book model has fiction_id as well because of the has_many association shown in this line Fiction.find(params[:ID]).books. There could be two approaches achieve this. First one could be that you use #filtered_books variable and extract users from it like #filtered_books.collect {|b| b.users}.flatten to extract all the users. Second approach could be through associations using fiction_id which could be something like User.joins(:books).where(books: {id: #filtererd_books.pluck(:id)})

Creating a custom validation across two HMT associations

I have two HMT associations. The first is between my Artist and Album models, basically signifying that multiple artists can belong to an album. The other association is setup between my Artist and Track models via TrackFeature. The purpose of this association is add a featured artist to a track.
I'd like to setup a validation that none of the possible album artists cannot be featured artists on any tracks pertaining to that particular album. I know that this validation would need to be on the join model. The problem is that what I have now doesn't seem to work. What do I need to change in order for this to successfully validate?
class Artist < ActiveRecord::Base
has_many :album_artists
has_many :albums, through: :album_artists
has_many :track_features
has_many :tracks, through: :track_features
end
class Album < ActiveRecord::Base
has_many :album_artists
has_many :artists, through: :album_artists
end
class AlbumArtist < ActiveRecord::Base
belongs_to :album
belongs_to :artist
end
class Track < ActiveRecord::Base
has_many :track_features
has_many :featured_artists, through: :track_features
belongs_to :album
end
class TrackFeature < ActiveRecord::Base
belongs_to :track
belongs_to :featured_artist, class_name: "Artist", foreign_key: :artist_id
validate :track_artist
private
def track_artist
if track.album.artists.exists?(name: artist.name)
track.errors[:base] << "Album artist cannot be a featured artist"
end
end
end

Rails 4 has_many :through association with :source

I was following the answer laid out at the link below to set up a many_to_many relationships on my Rails 4 app. (New to rails, here.)
Implement "Add to favorites" in Rails 3 & 4
I have Users and Exercises, and I want users to be able to have Favorite Exercises. I created a join table called FavoriteExercise with user_id and exercise_id as columns. I've got it populating, and it seems to be working fine, but I'm not able to use it to call directly to my favorites. 
Meaning, I want to type:
user.favorite = #list of exercises that have been favorited
I get this error when I try to load that list in my browser:
SQLite3::SQLException: no such column: exercises.favorite_exercise_id:
SELECT "exercises".* FROM "exercises" INNER JOIN "favorite_exercises"
ON "exercises"."favorite_exercise_id" = "favorite_exercises"."id"
WHERE > "favorite_exercises"."user_id" = ?
My models:
class User < ActiveRecord::Base
has_many :workouts
has_many :exercises
has_many :favorite_exercises
has_many :favorites, through: :favorite_exercises, source: :exercises
class Exercise < ActiveRecord::Base
belongs_to :user
has_many :workouts, :through => :exercises_workouts
has_many :favorites
has_many :favorited_by, through: :favorite_exercises, source: :exercises
class FavoriteExercise < ActiveRecord::Base
has_many :exercises
has_many :users
I just tried switching FavoriteExercise to 'belongs_to' instead of 'has_many, because it seems maybe that's the way that should go? but then I get this error:
uninitialized constant User::Exercises
Just trying to figure out how to set up the tables and associations so I can call .favorites on a user and get all their favorites.
If you want the list of exercises of the user and at the same, the list of favorite exercise of the user, then I think your join table should just be users_exercises wherein it will list all the exercises by the users. To list the favorite exercises, just add a boolean field indicating if the exercise is a user favorite and add a :scope to get all the favorite exercises.
So in your migration file:
users_exercises should have user_id, exercise_id, is_favorite
Then in your model:
class User < ActiveRecord::Base
has_many :workouts
has_many :users_exercises
has_many :exercises, through: :users_exercises
scope :favorite_exercises, -> {
joins(:users_exercises).
where("users_exercises.is_favorite = ?", true)
}
class Exercise < ActiveRecord::Base
has_many :workouts, :through => :exercises_workouts
has_many :users_exercises
has_many :users, through: :users_exercises
class UsersExercise < ActiveRecord::Base
belongs_to :exercise
belongs_to :user
You just need to simplify your model logic as follow:
class User < ActiveRecord::Base
has_many :workouts
has_many :exercises
has_many :favorite_exercises
has_many :favorites, through: :favorite_exercises, class_name: "Exercise"
class Exercise < ActiveRecord::Base
belongs_to :user
has_many :workouts, :through => :exercises_workouts
has_many :favorite_exercises
has_many :favorited_by, through: :favorite_exercises, class_name: "User"
class FavoriteExercise < ActiveRecord::Base
belongs_to :favorited_by
belongs_to :favorite
Then you can call user.excercises or excercise.users in your User/Excercise instance.
user.excercises = #list of exercises that have been favorited
Is that the many-to-many relationship you want?

Rails belongs_to_many

I'm a beginner in Rails and I have a problem with ActiveRecords associations.
I'm creating simple car rental service and I made the following associations:
class Client < ActiveRecord::Base
has_many :rentals
has_many :bookings
has_many :cars, :through => :rentals
has_many :cars, :through => :bookings
end
class Rental < ActiveRecord::Base
belongs_to :client, dependent: :destroy
has_one :car
end
class Booking < ActiveRecord::Base
belongs_to :client, dependent: :destroy
has_one :car
end
What I need is to have a car belonging to many bookings and rentals while every booking and rental can have only one car assigned.
class Car < ActiveRecord::Base
# belongs_to_many :bookings
# belongs_to_many :rentals
end
How should I do that?
If a car can have many bookings/rentals, but a booking/rental can only have one car, you're looking at a classic belongs_to/has_many situation. It looks like you're being tripped up by the distinction between belongs_to and has_one -- it's not a grammatical one, but a matter of where the foreign key column is located in your database.
belongs_to: "I am related to exactly one of these, and I have the foreign key."
has_one: "I am related to exactly one of these, and it has the foreign key."
has_many: "I am related to many of these, and they have the foreign key."
Note that has_one and has_many both imply there's a belongs_to on the other model, since that's the only option where "this" model has the foreign key. Note also that this means has_one should only be used when you have a one-to-one relationship, not a one-to-many.
Taking this into consideration, I would replace the has_one :car with belongs_to :car in both your Rental and Booking models, and place has_many :bookings and has_many :rentals in your Car model. Also ensure that your rentals and bookings tables have a car_id column; there should be no rental- or booking-related columns in your cars table.
Yes, there is a "belongs_to_many" in Rails, sort of. It's a little more work and you can't use generators with it. It's called a polymorphic association.
Even though you could make a car have many bookings & rentals, you could associate the car by making it belong to a polymorph such as rentable_vehicle. Your code would look like this
class Car < ActiveRecord::Base
belongs_to :rentable_vehicle, polymorphic: true
end
class Rental < ActiveRecord::Base
belongs_to :client, dependent: :destroy
has_many :cars, as: :rentable_vehicle
end
class Booking < ActiveRecord::Base
belongs_to :client, dependent: :destroy
has_many :cars, as: :rentable_vehicle
end
You can't do belongs_to_many. The closest you can really get is has_and_belongs_to_many, but I'm not sure that's what you want here - unless you can have multiple cars per rental/booking. Check out the guide for a full explanation.
I'd change it up like this:
class Rental < ActiveRecord::Base
belongs_to :client, dependent: :destroy
belongs_to :car
end
class Booking < ActiveRecord::Base
belongs_to :client, dependent: :destroy
belongs_to :car
end
class Car < ActiveRecord::Base
has_many :bookings
has_many :rentals
end
Also, I don't know how your rentals relate to bookings, but my immediate thought is that there should be some relationship between the two, because you probably can't have a rental without booking it, right?

Rails: ignoring duplicates in an nested association

I have models User, Team, Document. There's a many-to-many relationship between Users and Teams, and a many-to-many relationship between Teams and Documents, using join tables called TeamMembership and TeamDocument respectively.
The relationships in my models look like this:
class Document < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :teams, through: :team_documents
end
class User < ActiveRecord::Base
has_many :team_memberships, dependent: :destroy, foreign_key: :member_id
has_many :teams, through: :team_memberships
has_many :documents, through: :teams
end
class TeamDocument < ActiveRecord::Base
belongs_to :team
belongs_to :document
end
class TeamMembership < ActiveRecord::Base
belongs_to :team
belongs_to :member, class_name: "User"
end
class Team < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :documents, through: :team_documents
has_many :team_memberships, dependent: :destroy
has_many :members, through: :team_memberships
end
The idea is that users can belong to many teams, a document can be associated with many teams, and users will only have access to documents that "belong" to at least one team that the user is a member of.
Here's the question: I can use User#documents to retrieve a list of all the documents that this user is allowed to view. But this will return duplicates if a document is viewable by more than one team which the user is a member of. How can I avoid this?
I know I can remove the duplicates after the fact with #user.documents.uniq, but as I will never want to include the duplicates in any case, is there a way I can just make #documents not include duplicates every time?
I don't have nested has_many :through like yours to test it, but I suspect using uniq option on your user association would help :
class User < ActiveRecord::Base
has_many :documents, through: :teams, uniq: true
end
You can add a default_scope on Document model:
class Document < ActiveRecord::Base
default_scope group: { documents: :id }

Resources