named_scope or find_by_sql? - 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

Related

query include methods rails

I have a Student model and that model has_many subscriptions. I need to return all students who have subscriptions. this object must contain only the student's name, email, phone and status, in addition to the subscription id and recurrence_code. I thought something more or less like this but I can't succeed in the query:
students = Student.all.includes(:subscriptions)
students.each do |student|
student.select(:id, :name, :email, :phone, :status, subscription: {
methods: [:id, :recurrency_code]}
end
This is a classic inner join scenario
class Student < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :student
end
I find it helpful to break these problems into steps:
"Only Students where a Subscription record is present" is a standard inner join:
Student.joins(:subscriptions).uniq
"object must contain only the student's name, email, phone and status"
Student.joins(:subscriptions).select(:name, :email, :phone, :status).uniq
"in addition to the subscription id and recurrence_code"
students = Student.joins(:subscriptions)
.select(
'students.name, students.email,'\
'students.phone, students.status, '\
'subscriptions.id as subscription_id,'\
'subscriptions.recurrence_code as subscription_recurrence_code'
)
A few notes:
1. Using select with joins
#vee's SO Answer here points out:
If the column in select is not one of the attributes of the model on which the select is called on then those columns are not displayed. All of these attributes are still contained in the objects within AR::Relation and are accessible as any other public instance attributes.
This means if you load an individual record (e.g. students.first), you will only see the Student attributes by default. But you can still access the Subscription attributes by the as name you set in the query. E.g.:
students.first.subscription_recurrence_code
2. Use .uniq to eliminate duplicates.
Because of the has_many relationship, the query Student.joins(:subscriptions) will return a record for each subscription, which means each student will appear in the result the same number of times as they have subscriptions. Calling .uniq (short for unique) will remove duplicates from the result.
I'm agree with the Chiperific response, but I disagree to use the uniq method because it doesn't call the 'DISTINCT' in the SQL query.
Rails: uniq vs. distinct
For me it's better to use distinct. So the query could be as this:
Student.joins(:subscriptions).distinct.select(
:name, :email, :phone, :status,
'subscriptions.id AS subscription_id',
'subscriptions.recurrence_code'
)

Reverse query on a has_many :through

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

How to convert this sql joins/count statements in clean ruby ActiveRecord

I'm trying to convert with no success my find_by_sql statement into a pure ActiveRecord query.
It is:
Corner.find_by_sql('SELECT corners.id, corners.name, count(members.*) FROM corners LEFT JOIN places ON corners.id = places.ubicacion_id LEFT JOIN members ON places.id = members.place_id GROUP BY corners.id,corners.name ORDER BY corners.name;')
nicely formatted, the sql expression would be:
SELECT corners.id,
corners.name,
count(members.*)
FROM corners
LEFT JOIN places ON corners.id = places.ubicacion_id
LEFT JOIN members ON places.id = members.place_id
GROUP BY corners.id,
corners.name
ORDER BY corners.name;
In very old versions of ActiveRecord, my approach would be using the find :all and then passing a hash of options, but this way is deprecated:
Corner.find( :all,
:joins => "LEFT JOIN places ON corners.id = places.ubicacion_id",
:joins => "LEFT JOIN members ON places.id = members.place_id",
:group => "corners.id,corners.name",
:order => "corners.name",
:select => "corners.id, corners.name, count(members.*)"
)
Which one would be the best approach to rewrite in the ActiveRecord way the query? This last snippet works well, but it makes no difference on using it rather than the plain sql one:
Corner.joins("LEFT JOIN places ON corners.id = places.ubicacion_id").joins("LEFT JOIN members ON places.id = members.place_id").group("corners.id,corners.name").order("corners.name").select("corners.id, corners.name, count(members.*)")
Many thanks!
It looks like you probably want to set up some model relationships to make this more ActiveRecord-like. You can find descriptions of how to do this in the Active Record Associations documentation.
Consider these relationships:
class Member < ActiveRecord::Base
belongs_to :place
end
class Place < ActiveRecord::Base
belongs_to :corner
has_many :members
end
class Corner < ActiveRecord::Base
has_many :places, foreign_key: "ubicacion_id"
end
Given those, you should be able to do something like this:
Corner.
select("corners.id, corners.name, count(members.*)").
joins(:places, :members).
group("corners.id, corners.name").
order("corners.name")
Each of the methods chained in the query will refine the query incrementally, much like building a native SQL statement. You can find the official documentation for these methods in the Active Record Query Interface

Rails - Pass current_user into SQL query

I'm looking for a way to pass the Rails/Devise variable "current_user" into a SQL query.
I have an app with two models, Users and Tips. Users can friend other users and send tips to each other. I'm trying to display a user's friend list ordered by the number of tips that the user has sent to each friend, so that the friend to whom the user has sent the most tips shows up at the top of the friend list, and so on.
I've read that RoR isn't equipped to handle this kind of query easily, so I've been able to put together the following SQL query, which works fine:
def friend_list
#friends = User.find_by_sql("SELECT users.*,
COUNT(tips.id) AS c FROM users, tips
WHERE tips.recipient_id = users.id
AND tips.user_id = 3
GROUP BY users.id ORDER BY c DESC")
end
The only problem is, I have manually entered a user.id there ("3") when ideally that "3" would be replaced by "current_user" so that each time a user loads this list they get their friend list ranked by who they themselves have sent tips to, rather than everyone just seeing user 3's ranking.
In an ideal world this would look something like:
AND tips.user_id = current_user
but that doesn't work. How can I pass a variable into this query so that it is different for each person viewing?
UPDATE:
User model (excerpt):
has_many :tips
has_many :received_tips, :class_name => "Tip", :foreign_key => "recipient_id"
Tip model (excerpt):
belongs_to :user
belongs_to :recipient, :class_name => "User"
You cannot access current_user in models if using devise...though there are few good ways to do it
##method one---in your controller
#users=User.find(current_user.id)
##method two---in your controller
#users=User.get_user_details(current_user.id)
##in model
def self.get_user_details(current_user_id)
User.find(current_user_id)
end
###you can also use in this way in controller
#all_videos = Video.where("videos.user_id !=?",current_user.id)
.........so this can be your solution............
##in controller or pass current_user and user it in your model
def friend_list
#friends = User.joins(:tips).select("users.* and count(tips.id) as c").where ("tips.users_id= ?",current_user.id).group("users.id").order("c DESC")
##or
#friends = User.all(:joins => :tips, :select => "users.*, count(tips.id) as tips_count", :group => "users.id",:order=>"tips_count DESC")
end
You can try this
def friend_list
#friends = User.find_by_sql("SELECT users.*,
COUNT(tips.id) AS c FROM users, tips
WHERE tips.recipient_id = users.id
AND tips.user_id = ?
GROUP BY users.id ORDER BY c DESC", current_user.id)
end
Anyway it is not that hard to do this query using active_record.
I know this is old, but I want to add that you could also use string injection.
def friend_list
#Declare the query string you want to pass
#query = "SELECT users.*,
COUNT(tips.id) AS c FROM users, tips
WHERE tips.recipient_id = users.id
AND tips.user_id = #{current_user.id}
GROUP BY users.id ORDER BY DESC"
#friends = User.find_by_sql(#query)
end

Can I :select multiple fields (*, foo) without the extra ones being added to my instances (Instance.foo=>bar)

I'm trying to write a named scope that will order my 'Products' class based on the average 'Review' value. The basic model looks like this
Product < ActiveRecord::Base
has_many :reviews
Review < ActiveRecord::Base
belongs_to :product
# integer value
I've defined the following named scope on Product:
named_scope :best_reviews,
:select => "*, AVG(reviews.value) score",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "score desc"
This seems to be working properly, except that it's adding the 'score' value in the select to my Product instances, which causes problems if I try to save them, and makes comparisons return false (#BestProduct != Product.best_reviews.first, becuase Product.best_reviews.first has score=whatever).
Is there a better way to structure the named_scope? Or a way to make Rails ignore the extra field in the select?
I'm not a Rails developer, but I know SQL allows you to sort by a field that is not in the select-list.
Can you do this:
:select => "*",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "AVG(reviews.value) desc"
Wow, so I should really wait before asking questions. Here's one solution (I'd love to hear if there are better approaches):
I moved the score field into the inner join. That makes it available for ordering but doesn't seem to add it to the instance:
named_scope :best_reviews,
:joins => "INNER JOIN (
SELECT *, AVG(value) score FROM reviews GROUP BY reviews.product_id
) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "reviews.score desc"