More than one has_many :through? - ruby-on-rails

So. I have users and movies. Users have watched some movies and not others. I want to express this relationship something like this:
Note:
Not sure if it matters; but movies don't have to be connected to a user; they can exist independently (i.e. Movie 1 has no relationship to User 2). Users can also exist independently; they don't have to have watched or unwatched movies (not pictured here, but you get the idea)
One movie can be watched by one user but unwatched by another (grey vs. black connections)
My initial reaction is that this a has_many :through relationship, something like:
/models/user.rb:
def User
has_many :movies, :through => :unwatched_movies
has_many :movies, :through => :watched_movies
end
/models/movie.rb:
def Movie
has_many :users, :through => :unwatched_movies
has_many :users, :through => :watched_movies
end
But first of all, that code definitely doesn't work...
I want to be able to query for, say, u.unwatched_movies (where u is an instance of User, which doesn't seem to jive with the above.
I have a feeling this has something to do with :source or :as... but I'm feeling a little lost. Am I right in thinking that this is a 3-level hierarchy, where I need models for User, UnwatchedMovieList/WatchedMovieList, and Movie? This question feels very close but I can't seem to make it work in this context.
Any help on how to write these models and migrations would be super helpful. Thank you!

You're trying to create a relationship of omission - "unmatched movies". Which isn't a good idea, you should build up a history of movies watch (which is watched_movies) but then for unwatched you would want to find all movies minus watched movies. Then stick it in a function in User, like so:
def unwatched_movies
Movie.where("id NOT IN ?", self.watched_movies.collect(&:movie_id))
end

Here is my solution
Create these models
class User < ActiveRecord::Base
has_many :user_movies
# Use a block to add extensions
has_many :movies, through: :user_movies, source: 'movie' do
# this is an extension
def watched
where('user_movies.watched = ?', true)
end
def unwatched
where('user_movies.watched = ?', false)
end
end
end
class Movie < ActiveRecord::Base
has_many :user_movies
has_many :watchers, through: :user_movies, source: :user do
# users who is an effective watcher
def watchers
where('user_movies.watched = ?', true)
end
# users how marked it but did not watch it yet
def markers
where('user_movies.watched = ?', false)
end
end
end
class UserMovie < ActiveRecord::Base
belongs_to :user
belongs_to :movie
end
class CreateUserMovies < ActiveRecord::Migration
def change
create_table :user_movies do |t|
t.belongs_to :user, index: true
t.belongs_to :movie, index: true
t.boolean :watched, default: false, null: false
t.timestamps null: false
end
add_foreign_key :user_movies, :users
add_foreign_key :user_movies, :movies
end
end
then for queries
#user = User.first
#user.movies.watched
#user.movies.unwatched
#movie = Movie.first
#movie.watchers.watchers
#movie.watchers.markers

The following set of associations should cover your use case of being able to explicitly mark movies watched and unwatched. It makes use of a join table called user_movies that simply contains the following fields: user_id, movie_id, and watched
class User
has_many :unwatched_user_movies, -> { where(watched: false) }, class_name: 'UserMovie'
has_many :unwatched_movies, through: :unwatched_user_movies, class_name: 'Movie'
has_many :watched_user_movies, -> { where(watched: true) }, class_name: 'UserMovie'
has_many :watched_movies, through: :watched_user_movies, class_name: 'Movie'
end
class UserMovie
belongs_to :movie
belongs_to :user
end
class Movie
has_many :user_movies
end

Related

How do I incorporate a "matching feature" in rails activerecord relationship

I am currently working on a project that is similar to dating apps like Tinder. A user (named Owner in my program) swipes on other owners and if they both swipe on each other it creates a "match". I have been looking into finding a solution to this such as friend requests similar to Facebook friends. I am seeing people using a "confirmed" column that is boolean defaulted to false and changing it to true but I cannot figure out the logic for this. Any advice on how to accomplish this would be appreciated. My only experience in this has been following or followers which doesn't require mutual requests to accomplish.
Owner class: (the user)
class Owner < ApplicationRecord
has_many :matches
has_many :friends, :through => :matches
end
Match class:
class Match < ApplicationRecord
belongs_to :owner
belongs_to :friend, :class_name => "Owner"
end
Thank you for any help! Self joins have been a complicated topic for me to understand.
You can add more fields to your join table. You could add something like owner_accepted and friend_accepted. Although I think that just one accepted field is enough.
Example solution:
class AddAcceptedToMatches < ActiveRecord::Migration[6.0]
def change
add_column :matches, :accepted, :boolean, default: false
end
end
class Owner < ApplicationRecord
has_many :matches
has_many :friends, :through => :matches
def send_request_to(friend)
friends << friend
end
def accept_request_from(owner)
matches.find_by(owner_id: owner.id).accept
end
def is_friends_with?(stranger)
match = matches.find_by(friend_id: stranger.id)
return false unless match
match.accepted?
end
class Match < ApplicationRecord
belongs_to :owner
belongs_to :friend, :class_name => "Owner"
def accept
update(accepted: true)
end
end
then you can do something like:
owner = Owner.new
friend = Owner.new
owner.send_request_to(friend)
owner.is_friends_with?(friend)
# false
friend.accept_request_from(owner)
owner.is_friends_with?(friend)
# true

How to get associated polymorphic objects in rails 5?

In my Post model, I have
has_many :followers
In my Follower model, I have
belongs_to :model
belongs_to :owner,polymorphic: true
In my User and Admin devise models, I have
has_many :followers,as: :owner
Requirement: I want to have something like Post.owners and it should return me a list of all users and/or admins that are following this post.
I'm not sure, but I think that AR doesn't provide a way to load polymorphic associations in just one query. But you can use:
post = Post.find(1)
post_followers = post.followers.includes(:owner)
post_owners = post_followers.map(&:owner)
The solution you're looking for is polymorphic has many through. You can add these lines in your model of User and Admin.
has_many :followers
has_many :posts, through: :followers, source: :owner, source_type: 'Owner'
I think you want something like this:
class Post < ApplicationRecord
belongs_to :owner, polymorphic: true
end
class User < ApplicationRecord
has_many :posts, as: :owner
end
class Follower < ApplicationRecord
has_many :posts, as: :owner
end
From an instance of your User you can then retrieve their posts with #user.posts
The same goes for your Follower, #follower.posts
If you want to get to the parent of your post instance, you can do so via #post.owner. To make this work, however, we need to set up the schema correctly by declaring both a foreign key column and a type column in the model that declares the polymorphic interface using the references form:
class CreatePosts < ActiveRecord::Migration[5.0]
def change
create_table :posts do |t|
# your attribs here
t.references :owner, polymorphic: true, index: true
end
end
end

Loose associations in ActiveRecord

I'm new to Ruby on Rails and I'm trying to build a relationship between the classes Club, Sponsor and Match.
The relationhip has to be like:
One Club has zero to many Sponsors
One Sponsor has zero to many Matches
One Match has zero to many Sponsors
My models look like this
class Match < ApplicationRecord
belongs_to :team
has_many :matchplayers
has_many :players, through: :matchplayers
has_many :sponsors
end
class Club < ApplicationRecord
has_many :teams
has_many :sponsors
accepts_nested_attributes_for :teams, :reject_if => :all_blank, :allow_destroy => true
end
class Sponsor < ApplicationRecord
belongs_to :club
end
and my migrations file for the Sponsor model looks like this:
class CreateSponsors < ActiveRecord::Migration[5.1]
def change
create_table :sponsors do |t|
t.text :name
t.text :url
t.text :imgUrl
t.references :club, foreign_key: true
t.timestamps
end
add_reference :matches, :sponsor, index: true
add_foreign_key :matches, :sponsor
end
end
I have no problems retrieving sponsors for each club instance but I'm having trouble retrieving the sponsors associated with each match.
In my matches_controller.rb I have this
def show
#match = Match.find(params[:id])
render :json => #match.to_json(:include => [:players, :sponsors])
end
But when I try to run it the script fails. with the error message "no such column: sponsors.match_id" as the script tries to run the following SQL statement
SELECT "sponsors".* FROM "sponsors" WHERE "sponsors"."match_id" = ?
What I'd really like it to do would be to run the following statement
SELECT "sponsors".*
FROM "sponsors"
LEFT JOIN "matches"
ON "matches"."sponsor_id" = "sponsors"."id"
WHERE "matches"."id" = ?
And placing the resulting array into the output JSON's "sponsors" attribute.
I have been looking into the different Active Record association types and I feel like the type of association I need for this task is looser than the ones described in the documentation.
You need many-to-many relationship. In rails where are 2 ways to do this. You can read about this here. In general you will need to add has_and_belongs_to_many to Sponsor and to Match. And create 'join-model' which will contain match_id + sponsor_id. In this way ActiveRecord will be able to create suitable SQL query due to 'join-table'.

many to many polymorphic association

I'm not sure how to create this, I'd like to create a many-to-many polymorphic association.
I have a question model, which belongs to a company.
Now the question can has_many users, groups, or company. Depending on how you assign it.
I'd like to be able to assign the question to one / several users, or one / several groups, or the company it belongs to.
How do I go about setting this up?
In this case I would add a Assignment model which acts as an intersection between questions and the entities which are assigned to it.
Create the table
Lets run a generator to create the needed files:
rails g model assignment question:belongs_to assignee_id:integer assignee_type:string
Then let's open up the created migration file (db/migrations/...__create_assignments.rb):
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.integer :assignee_id
t.string :assignee_type
t.belongs_to :question, index: true, foreign_key: true
t.index [:assignee_id, :assignee_type]
t.timestamps null: false
end
end
end
If you're paying attention here you can see that we add a foreign key for question_id but not assignee_id. That's because the database does not know which table assignee_id points to and cannot enforce referential integrity*. We also add a compound index for [:assignee_id, :assignee_type] as they always will be queried together.
Setting up the relationship
class Assignment < ActiveRecord::Base
belongs_to :question
belongs_to :assignee, polymorphic: true
end
The polymorpic: true option tells ActiveRecord to look at the assignee_type column to decide which table to load assignee from.
class User < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Company < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
Unfortunately one of the caveats of polymorphic relationships is that you cannot eager load the polymorphic assignee relationship. Or declare a has_many :assignees, though: :assignments.
One workaround is:
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
def assignees
assignments.map(&:assignee)
end
end
But this can result in very inefficient SQL queries since each assignee will be loaded in a query!
Instead you can do something like this:
class Question < ActiveRecord::Base
has_many :assignments
# creates a relationship for each assignee type
['Company', 'Group', 'User'].each do |type|
has_many "#{type.downcase}_assignees".to_sym,
through: :assignments,
source: :assignee,
source_type: type
end
def assignees
(company_assignees + group_assignees + user_assignees)
end
end
Which will only cause one query per assignee type which is a big improvement.

What would be an efficient way to set up this relationship?

I'm trying to create a web application to organize a user's TV interests, to do this, I need to store data of three types: Shows, Seasons, and Episodes.
I would like to query my data like this: Show.find(1).season(2).episode.each. This should return each episode of the second season of the show with the id 1. How can I set my model up to a achieve this?
I've tried having values of season_id and show_id on the episodes, but its unable to find the episodes belonging to each season.
Define relationship in mode,
Show
has_many :seasons
Season
has_many :episodes
belongs_to :show
Episode
belongs_to :season
Then you can call like this,
Show.find(1).seasons.first.episodes.each {}
Maybe it's a good idea to read through the guides. Assuming that your entity relationships looking like this:
You can implement this with activerecord easily. The models would look like this:
require 'active_record'
class Show < ActiveRecord::Base
has_many :seasons
end
class Season < ActiveRecord::Base
belongs_to :show
has_many :episodes
end
class Episode < ActiveRecord::Base
belongs_to :season
end
Your migrations could look like:
require 'active_record'
class CreateShows < ActiveRecord::Migration
def change
create_table :shows do |t|
t.timestamps
end
end
end
class CreateSeasons < ActiveRecord::Migration
def change
create_table :seasons do |t|
t.references :show, :null => false
t.timestamps
end
end
end
class CreateEpisodes < ActiveRecord::Migration
def change
create_table :episodes do |t|
t.references :season, :null => false
t.timestamps
end
end
end
Put some data into your database and query them with:
Show.find(1).seasons.first.episodes.each{ |e| puts e.title }
The answers above are great; I'd take it a step further and use has_many's :through option in the Show model and has_one :through on the Episode model:
# Show
has_many :seasons
has_many :episodes, through: :seasons
# Season
belongs_to :show
has_many :episodes
# Episode
belongs_to :season
has_one :show, through: :season
This lets you make calls like this:
Show.first.episodes
Episode.first.show
... and will also allow you to write some query-minimizing scopes, and write delegate methods that simplifying finding related information.
# Episode
delegate :name, to: :show, prefix: :show
Episode.first.show_name # => Episode.first.show.name

Resources