Reverse query on a has_many :through - sql

Scenario:
Team
has_many :players, dependent: :destroy
has_many :users, through: :players
Player
belongs_to :team
belongs_to :user
User
So, let's say that i have 4 teams with different users:
Team 1
User 1, User 2
Team 2
User 2, User 3
Team 3
User 1
Team 4
User 2, User 4, User 5
Now, suppose i have the id of two users, (User 1, User 5), and i want to know if there is any team which consists of ONLY these two players. Let's say i have a team that consists of users 1, 2 and 5. The query should not bring this team.
How can i use ActiveRecord semantics in my favor to do this? It is easy to get all players from a team, but i couldn't find a way to do the opposite.

UPDATE: AH! I got it in pure SQL:
users = User.first(2)
Team.joins(:users).group('teams.id').having('SUM( CASE WHEN users.id in (?) THEN 1 ELSE -1 END ) = ?', users, users.count)
Try it and let me know if it works for you (working here: http://sqlfiddle.com/#!17/bb2a9/8 and the same example but with 3 players: http://sqlfiddle.com/#!17/bb2a9/10)
This is not optimized on the DB level as it uses a lot of ruby/rails code, but can you try it?
users = User.first(2)
# find teams with that exact number of players associated
teams = Team.joins(:users).group('teams.id').having('COUNT(users.id) = ?', users.count)
# find players referencing to those teams with other users than the ones specified
players_to_ignore = Player.where(team_id: teams).where('user_id NOT IN (?)', users)
# get Teams where associated players id is not in the previous list
Team.where(id: teams).joins(:players).where('players.id NOT IN (?)', players_to_ignore)

You can use two where clauses:
One for getting all the Teams having exactly two users.
Team.joins(:users).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")
Second for having all Teams with users 1 and 5.
Team.joins(:users).where('users.id in (?)', [1,5]).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")
Intersection of these two should give you what you need.
So to combine it all:
Team.where(id: Team.joins(:users).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")).where(id: Team.joins(:users).where('users.id in (?)', [1,5]).group("teams.id").having("count('distinct users.id') = 2").select("teams.id"))

Team.join(:users).where('users.id in (?)', [1,5]).
select { |team| team.users.map(&:id).sort == [1,5] }
Previous answer (For pre edited question)
Will this works for you?
Team.join(:users).where('users.id in (?)', [1,5])
You can do the same on user model by
# user.rb
has_many :teams, through: :works
has_many :works, foreign_key: :user_id
Responding to your edits & comment
Hacky:
Team.join(:users).where('users.id in (?)', [1,5]).
select { |team| team.users.map(&:id).sort == [1,5] }
Better?
SQL Select only rows where exact multiple relationships exist

Related

Rails select by number of associated records

I have following models in my rails app:
class Student < ApplicationRecord
has_many :tickets, dependent: :destroy
has_and_belongs_to_many :articles, dependent: :destroy
class Article < ApplicationRecord
has_and_belongs_to_many :students, dependent: :destroy
class Ticket < ApplicationRecord
belongs_to :student, touch: true
I need to extract all Students who has less than articles and I need to extract all Students who's last ticket title is 'Something'.
Everything I tried so far takes a lot of time. I tried mapping and looping through all Students. But I guess what I need is a joined request. I am looking for the most efficient way to do it, as database I am working with is quite large.
go with #MCI's answer for your first question. But a filter/select/find_all or whatever (although I havn't heared about filter method in ruby) through students record takes n queries where n is the number of student records (called an n+1 query).
studs = Student.find_by_sql(%{select tmp.id from (
select student_id as id from tickets where name='Something' order by tickets.created_at desc
) tmp group by tmp.id})
You asked
"I need to extract all Students who has less than articles". I'll presume you meant "I need to extract all Students who have less than X articles". In that case, you want group and having https://guides.rubyonrails.org/active_record_querying.html#group.
For example, Article.group(:student_id).having('count(articles.id) > X').pluck(:student_id).
To address your second question, you can use eager loading https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations to speed up your code.
result = students.filter do |student|
students.tickets.last.name == 'Something'
end
Here association is HABTM so below query should work
x = 10
Student.joins(:articles).group("articles_students.student_id").having("count(articles.id) < ?",x)

Order list of records by other created_at record column

In my table:
Users can vote to battles records
Battle records has many votes records (votes by users with user_id in them)
I want to order the battles that the user voted to, by the created_at column of the vote record for each battle.
In english - I want to get list of the battles the user voted to by the time he voted to them.
My current code - the problem is this code return an array, I want to it with order:
scope :picked, -> (user_id) { where("battles.id IN (SELECT DISTINCT(battle_id) FROM votes WHERE user_id = ?)", user_id)
.sort_by { |b| b.votes.find_by(user_id: user_id ).created_at }.reverse }
You'll have to pass a block option when specifying the has_many association.
Assuming you have the following models:
Vote < ActiveRecord::Base
belongs_to :user
belongs_to :battle
end
And:
Battle < ActiveRecord::Base
end
The user model should be something like this:
User < ActiveRecord::Base
has_many :votes, -> { order(created_at: :desc) } # Ordering goes here
has_many :battles, through: :votes
end
Then you'd query the battles directly:
User.first.battles # Scoped by votes.created_at

How to join three tables in Rails?

I have my records set up as follows
Subscription: id, plan_id <- belongs to plan
Plan: id, role_id <- belongs to role
Role: id, name
Given this data
Subscription: id: 1, plan_id: 5
Plan: id: 5, role_id: 10
Role: id: 10, name: 'Gold'
I'm trying to write a join so that I can find subscriptions based on their associated roles, i.e.:
Subscription.joins(:plan).joins(:role).where("roles.name = ?", 'Gold')
But this approach doesn't work. Any help would be appreciated.
Thanks.
If you have proper associations then use this:
Subscription.includes(:plan => [:role]).where("roles.name = 'Gold'").first
You can also write the query manually:
Subscription.joins("INNER JOIN plans ON plans.id = subscriptions.plan_id
INNER JOIN roles ON roles.id = plans.role_id").where("roles.name = 'Gold'").first
If you're trying to find all the subscriptions for a given role, why not start with the role? Rails can take care of these "one step removed" associations if we configure them correctly.
gold_role = Role.where(name: 'Gold').first
gold_role.subscriptions
This is possible via Rails' has_many :through relations. I think this is the correct setup for your models:
class Subscription < ActiveRecord::Base
belongs_to :plan
has_one :role, through: :plan
end
class Plan < ActiveRecord::Base
has_many :subscriptions
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :plans
has_many :subscriptions, through: :plans
end

Need help finding the first created record of each group, in Rails ActiveRecord

I have a table called FeedItems, and basically the user needs to see the first created feed item for each post id.
At the moment I'm using 'group', and SQLite is giving me the last created, for some reason. I've tried to sort the list of feed items before grouping, but it makes no difference.
user_id | post_id | action
----------------------------------------
1 1 'posted'
3 2 'loved' <--- this, being created afterwards
should not appear in the query.
I know this has to do with an INNER JOIN, and I have seen some similar examples of this being done, however the difference is that I'm not sure how to do this AND use the existing query I already have to find out if the users are friends with the current user.
Here's the code for the model:
class FeedItem < ActiveRecord::Base
belongs_to :post
belongs_to :user
default_scope :order => 'created_at desc'
scope :feed_for, lambda { |user| feed_items_for(user).group(:post_id) }
private
def self.feed_items_for(user)
friend_ids = %(SELECT friend_id FROM friendships WHERE (user_id = :user_id OR friend_id = :user_id))
ghosted_ids = %(SELECT pending_friend_id FROM friend_requests WHERE user_id = :user_id)
where("user_id IN (#{friend_ids}) OR user_id IN (#{ghosted_ids}) OR user_id = :user_id", {:user_id => user.id})
end
end
Any help would be greatly appreciated, thanks.
ActiveRecord provides you with dynamic attribute-based finders, which means that you have
Feeds.find_last_by_post_id(117)
If you would prefer to have the fist, you have:
Feeds.where(:post_id => 117).first
And you can always do, which I'm not sure is a "best practice":
Feeds.where(:post_id => 117).order('created_on DESC').first
You can read more about it at:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html

named_scope or find_by_sql?

I have three models:
User
Award
Trophy
The associations are:
User has many awards
Trophy has many awards
Award belongs to user
Award belongs to trophy
User has many trophies through awards
Therefore, user_id is a fk in awards, and trophy_id is a fk in awards.
In the Trophy model, which is an STI model, there's a trophy_type column. I want to return a list of users who have been awarded a specific trophy -- (trophy_type = 'GoldTrophy'). Users can be awarded the same trophy more than once. (I don't want distinct results.)
Can I use a named_scope? How about chaining them? Or do I need to use find_by_sql? Either way, how would I code it?
If you want to go down the named_scope route, you can do the following:
Add a has_many :users to Trophy, such as:
has_many :users, :through => :awards
And the following named_scope:
named_scope :gold, :conditions => { :trophy_type => 'GoldTrophy' }
You can call the following:
Trophy.gold.first.users
You need to call '.first' because the named_scope will return a collection. Not ideal. That said, in your case it's probably perfectly appropriate to use neither find_by_sql or named_scope. How about using good old:
Trophy.find_by_trophy_type('GoldTrophy').users
This will do exactly what you want without having to dig down into the SQL.
I am always comfortable with the "find_by_sql" You can use it
Using find_by_sql as follows
User.find_by_sql("select u.id, u.name, t.trophy_type
from users u, awards a, trophies t
where a.user_id=u.id and
t.trophy_id=a.id and
t.trophy_type = 'GoldTrophy'"
)
I am not sure using "named_scope" But try this
class User < ActiveRecord::Base
named_scope :gold_trophy_holder,
:select=>" users.id, users.name, trophies.trophy_type",
:joins => :awards, "LEFT JOIN awards ON awards.id = trophies.award_id"
:conditions => ['trophies.trophy_type = ?', 'GoldTrophy']
end