Rails query through multiple models - sql

It's been 2 days by now that I'm struggling to write query. So, I'm trying to query within these 3 related models:
class Company...
has_many :programs
end
class Program...
belongs_to :company
has_many :transactions
end
class Transaction...
belongs_to :program
end
As output, I need a list of the amount of all Transactions each Company made and on what date.

Your query should be something like this.
Company.includes(:programs => :transactions).map do |company|
{:company_name => company.name,
:transactions => company.programs.map{|program| program.transactions.map{|t| {:amount => t.amount, :date => t.created_at.strftime("%d-%m-%Y")}}.flatten
}
end
You could change the format of the date by referring to strftime method of Time class from here - http://ruby-doc.org/core-2.2.0/Time.html#method-i-strftime.

Related

How do I chain multiple models to reduce the number of SQL queries

I'm trying to check whether a student has attempted an assigned test or not. I want to chain the relevant models to bring down the number of queries to just 1. The following are my models:
class Test < ActiveRecord::Base
has_many :assigns
has_many :attempts
belongs_to :topic
end
class Topic < ActiveRecord::Base
has_many :tests
has_many :attempts
has_many :assigns, through: :test
end
class Assign < ActiveRecord::Base
belongs_to :test
belongs_to :student
has_many :attempts
end
class Attempt < ActiveRecord::Base
belongs_to :test
belongs_to :topic
belongs_to :assign
belongs_to :student
end
I want to check if a particular student (id: 100) has attempted an assigned test or not, and also retrieve other details such as the topic name of the test. So far I have something like this:
ta = Assign.includes(:test => {:topic => :attempts})
This allows me to retrieve details such as the test_name, topic_name, when it was assigned etc. in a single query. How do I also include the Attempt records of student_id: 100 in the same query? With what I have now, when I retrieve the student's attempt details a brand new query is being generated.
What I want is something like the follwoing without having to touch the database again:
ta.test.attempts.where(student_id: 100)
How do I do all this with just one query?
Okay, since you want all kinds of information from all the joined tables, so you will have to join them up from the beginning.
Attempt.joins(:topic, :test, :assign)
Then you can filter it with the student_id
.where("attempts.student_id" => 100)
Finally, the fields you want
.select("attempts.id as attempt_id, tests.name as test_name, topics.name as topic_name, assigns.created_at as assigned_at")
In summary
Attempt
.joins(:topic, :test, :assign)
.where("attempts.student_id" => 100)
.select("attempts.id as attempt_id, tests.name as test_name, topics.name as topic_name, assigns.created_at as assigned_at")

rails select distinct nested associations and fetch those associations

i have the models User, Company, Product, View
class Company < ActiveRecord::Base
has_many :users
end
class Product < ActiveRecord::Base
has_many :views_by_user, -> { where viewable_type: User },
as: :viewable, class_name: "View"
end
class User < ActiveRecord::Base
has_many :viewed, as: :viewer, class_name: "View"
belongs_to :company
end
class View < ActiveRecord::Base
belongs_to :viewable, polymorphic: true
belongs_to :viewer, polymorphic: true
end
What i did with the above is, when a user views product, i save the data in the views
Now i want the list of distinct companies that have looked at my product(via user) and total count for my serializer. what i have done is,
distinct_users = #product.views_by_user
.includes(viewer: [:company])
.joins("left outer join users on views.viewer_id = users.id")
.select("distinct users.company_id, views.*")
but with this, i would have to do something like
distinct_users.will_paginate(...).map(&:viewer).map(&:company)
is there a better way to do it? also if i use distinct_users.count it throws me an error
PG::UndefinedFunction: ERROR: function count(integer, views) does not exist
LINE 1: SELECT COUNT(distinct users.company_id,...
Start from Company if this is the type of record you actually want. You can use merge to combine the conditions on a relation with those from another. Try this:
Company.joins(:users => :viewed).merge(View.where(viewable: #product))
HTH

Rails: How to set up an IF condition with a JOIN in a has_many :through relationship

I have an application where users can customize a calendar and fill it with a given pool of events. A user can also overwrite a title for his own calendar by an alias. So I have the following has_many :through relation:
class Calendar < ActiveRecord::Base
has_many :event_aliases
has_many :events, :through => :event_aliases
end
class Event < ActiveRecord::Base
attr_accessible :title
has_many :event_aliases
has_many :calendars, :through => :event_aliases
end
class EventAliases < ActiveRecord::Base
attr_accessible :course_id, :calendar_id, :custom_name
belongs_to :event
belongs_to :calendar
end
No I want to deliver the calendar with the aliases. If an event has an alias (custom_name), it should be displayed. Otherwise the default event name (title) should be displayed.
Is there a way to easily set up a query that returns all events for the current calendar whether with a custom_name (if exists) or with the default title?
My current solution is to hardcode an if condition into the query which I would like to avoid.
title_column = "case when custom_name IS NOT NULL then custom_name else title end as title"
# assume we are given a calendar_id
Calendar.find(calendar_id).event_aliases.joins(:event).select(title_column, :event_id).each do |event_alias|
# do further stuff here
end
I also could fetch all event_aliases and run through each of them to get the default title if necessary.
# assume we are given a calendar_id
Calendar.find(calendar_id).event_aliases.each do |event_alias|
title = event_alias.custom_name
if title.nil?
title = Event.find(event_alias.event_id).title
# do further stuff here
end
But this one results in too many queries to me.
So is there any smarter way of accomplishing what I want? Maybe using named scopes or another fancy rails technique?
UPDATE
I ended up with making a "custom" select via the has_many :through relationship. So the only thing changes is the Calendar model:
class Calendar < ActiveRecord::Base
has_many :event_aliases
has_many :events, :through => :event_aliases,
:select => "event_aliases.custom_name as custom_name, events.*"
end
So accessing the custom_name / the title now happens a little like #Doon suggested:
Calendar.find(1).courses.each do |course|
title = course.custom_name || course.title
end
This creates only 2 queries instead of 3:
Calendar Load (0.6ms) SELECT `calendars`.* FROM `calendars` WHERE `calendars`.`id` = 1 LIMIT 1
Event Load (0.7ms) SELECT event_aliases.custom_name as custom_name, events.* FROM `events` INNER JOIN `event_aliases` ON `events`.`id` = `event_aliases`.`event_id` WHERE `event_aliases`.`calendar_id` = 1
what about using includes to grab the events at the same time as you pull the aliases.
Calendar.find(1).event_aliases.includes(:event).each do |e|
puts e.custom_name.blank? ? e.event.title : e.custom_name
end
the SQL Rails generates will look something like this:
Calendar Load (0.2ms) SELECT "calendars".* FROM "calendars" WHERE "calendars"."id" = ? LIMIT 1
EventAlias Load (0.2ms) SELECT "event_aliases".* FROM "event_aliases" WHERE "event_aliases"."calendar_id" = 1
Event Load (0.2ms) SELECT "events".* FROM "events" WHERE "events"."id" IN (1, 2)
also if you want to clean it up a bit you can add a virtual field to the EventAlias
class EventAlias < ActiveRecord::Base
def name
custom_name || self.event.title
end
end
As long as you use the includes, the queries will be be the same.

Named_scope with joins

Can someone help me understand how to convert this sql query to a named_scope or maybe a method?
Background: A trip can have many trip_runs. I'm trying to be able to say TripRun.upcoming and return only valid runs from valid trips based on the following query
SELECT r.*
FROM trip_run r
LEFT JOIN trips t
ON r.trip_id = t.id
WHERE r.starts_on > NOW()
AND t.is_booked = 1
AND t.is_cancelled IS NULL
thank you
this assumes you're using Rails 3 and AREL. Edit your model files for Trip and TripRun like so:
class Trip < ActiveRecord::Base
has_many :trip_runs
scope :booked, where("is_booked = 1 and isnull(is_cancelled)")
end
class TripRun < ActiveRecord::Base
belongs_to :trip
scope :upcoming, where("starts_on > NOW()")
end
Then access like this:
Trip.booked.trip_runs.upcoming
There are lots of ways to mix and match these patterns to get similar effects and create methods to access the data.
Based on Joshua's answer, but for rails 2.3:
class Trip < ActiveRecord::Base
has_many :trip_runs
named_scope :booked, :conditions => 'is_booked = 1 AND is_cancelled IS NULL'
end
class TripRun < ActiveRecord::Base
belongs_to :trip
named_scope :upcoming, :conditions => 'starts_on > NOW()'
end
Trip.booked.trip_runs.upcoming
or alternatively:
class Trip < ActiveRecord::Base
has_many :trip_runs
end
class TripRun < ActiveRecord::Base
belongs_to :trip
named_scope :upcoming,
:conditions => 'trip.is_booked = 1 AND trip.is_cancelled IS NULL
AND trip_runs.starts_on > NOW()',
:joins => :trip
end
TripRun.upcoming
That will use an INNER JOIN not a LEFT JOIN, but since you're looking for rows with trip.is_booked set to a non-null value, the results will be the same and the query will be no slower.

How to filter by association count?

Let's say I have models that look like this:
class Foo < ActiveRecord::Base
has_many :bars, :through => :cakes
has_many :cakes
end
class Bar < ActiveRecord::Base
has_many :foos, :through => :cakes
has_many :cakes
end
class Cake < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
How would I get all foos which had 10 or more bars (and therefore 10 or more cakes)?
Foo.all(:joins => :cakes,
:group => "cakes.foo_id",
:having => "count(cakes.bar_id) >= 10")
okay i tried the answer above, but had a problem.
for our purposes Father has_many :sons, ok?
i wanted to find Fathers that had zero sons.
the above did not work, because it produced an inner join... thereby filtering out all fathers without sons.
the following did work for me:
Father.includes(:sons).group('fathers.id').having( 'count(sons.id)=0' )
and it also happens to work for any other filter you'd require
Father.includes(:sons).group('fathers.id').having( 'count(sons.id)=3' )