How to add conditional where clauses in rails - sql

I am a rails newbie and am trying to perform a search on a table with rails, and i'm just using my sql knowledge to do this. But this just doesn't seems like rails or ruby even...
Is there any better way to do what i'm doing below? (basically, only pass date arguments to sql if they are filled)
def search(begin_date=nil, end_date=nil)
subject = " and created_at "
if !(begin_date.nil? || end_date.nil?)
where_part = subject + "BETWEEN :begin_date AND :end_date"
else if (begin_date.nil? && end_date.nil?)
where_part = ""
else if(begin_date.nil?)
where_part = subject + " <= :end_date"
else if (end_date.nil?)
where_part = subject + " >= :begin_date"
end
end
end
end
User.joins(places: {containers: {label: :user}}).where("users.id= :user_id "+where_part, user_id: self.id, begin_date:begin_date, end_date:end_date).group(...).select(...)
end
EDIT
user.rb
has_many :containers
has_many :user_places
has_many :places, through: :user_places
has_many :labels
place.rb
has_many :containers
has_many :user_places
has_many :users, through: :user_places
container.rb
belongs_to :label
belongs_to :place
belongs_to :user
label.rb
belongs_to :user
has_many :containers
Basically, i want to get a count of the number of containers within a given user's labels or with a direct relationship, per location, and want to be able to filter it by begin and end dates.
Either of this dates may be nil, and so i would need to address this in my "query".
My question is : How can i do this the rails way? I took a look at http://guides.rubyonrails.org/active_record_querying.html and perhaps i could use the except command here somewhere...but this relationship model just seems a bit complex to do this with ActiveRecord...how may I?, i really think i should use ActiveRecord, but how?
Thank you

You can apply multiple where calls to a query so you can build your base query:
query = User.joins(...)
.group(...)
.select(...)
.where('users.id = :user_id', :user_id => self.id)
and then add another where call depending on your date interval:
if(begin_date && end_date)
query = query.where(:created_at => begin_date .. end_date)
# or where('created_at between :begin_date and :end_date', :begin_date => begin_date, :end_date => end_date)
elsif(begin_date)
query = query.where('created_at >= :begin_date', :begin_date => begin_date)
elsif(end_date)
query = query.where('created_at <= :end_date', :end_date => end_date)
end
Each where call adds another piece to your overall WHERE clause using AND so something like:
q = M.where(a).where(b).where(c)
is the same as saying WHERE a AND b AND c.

I cant think of a great reason why you would actually want to generate SQL in your code. Active record seems like a much more efficient solution for your needs, unless there is a reason why you cant use that.
Link explaining how to join tables with active record

Related

Rails Many-to-many relationship with extension generating incorrect SQL

I'm having an issue where a many-to-many relationship with an "extension" is generating incorrect SQL.
class OrderItem < ApplicationRecord
belongs_to :buyer, class_name: :User
belongs_to :order
belongs_to :item, polymorphic: true
end
class User < ApplicationRecord
has_many :order_items_bought,
-> { joins(:order).where.not(orders: { state: :expired }).order(created_at: :desc) },
foreign_key: :buyer_id,
class_name: :OrderItem
has_many :videos_bought,
-> { joins(:orders).select('DISTINCT ON (videos.id) videos.*').reorder('videos.id DESC') },
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
end
user.videos_bought.confirmed generates this SQL:
Video Load (47.0ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" "order_items_videos_join" ON
"order_items_videos_join"."item_id" = "videos"."id" AND
"order_items_videos_join"."item_type" = $1 INNER JOIN
"orders" ON "orders"."id" = "order_items_videos_join"."order_id" INNER JOIN
"order_items" ON "videos"."id" = "order_items"."item_id" WHERE
"order_items"."buyer_id" = $2 AND ("orders"."state" != $3) AND "order_items"."item_type" = $4 AND
"orders"."state" = $5 ORDER BY videos.id DESC, "order_items"."created_at" DESC LIMIT $6
Which returns some Video records which are joined with orders that do NOT have state confirmed. I would expect all orders to have state confirmed.
If I use raw SQL everything works fine:
has_many :videos_bought,
-> {
joins('INNER JOIN orders ON orders.id = order_items.order_id')
.select('DISTINCT ON (videos.id) videos.*')
.reorder('videos.id DESC')
},
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
Now user.videos_bought.confirmed generates this SQL:
Video Load (5.4ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" ON
"videos"."id" = "order_items"."item_id" INNER JOIN orders ON
orders.id = order_items.order_id WHERE
"order_items"."buyer_id" = $1 AND ("orders"."state" != $2) AND
"order_items"."item_type" = $3 AND "orders"."state" = $4 ORDER BY
videos.id DESC, "order_items"."created_at" DESC LIMIT $5
Which seems more succinct because it avoids the auto generated order_items_videos_join name. It also only returns orders that have state confirmed.
Any idea what is going on? Does ActiveRecord just generate faulty SQL sometimes?
Using rails 5.1.5. Upgrading to latest made no difference.
I'm hoping to get an explanation on why Rails generates the order_items_videos_join string in the first case but not in the second case. Also, why the second SQL query produces incorrect results. I can edit the question with more code and data samples if needed.
ActiveRecord does not just generate faulty SQL sometimes, but there's a little nuance to it such that starting simple is best when it comes to defining relationships. For example, let's rework your queries to get that DISTINCT ON out of there. I've never seen a need to use that SQL clause.
Before chaining highly customized association logic, let's just see if there's simpler way to query first, and then check to see whether there's a strong case for turning your queries into associations.
Looks like you've got a schema like this:
User
id
Order
id
state
OrderItem
id
order_id
buyer_id (any reason this is on OrderItem and not on Order?)
item_type (Video)
item_id (videos.id)
Video
id
A couple of tidbits
No need to create association extensions for query conditions that would make perfectly good scopes on the model. See below.
A perfectly good query might look like
Video.joins(order_item: :order).
where(order_items: {
buyer_id: 123,
order: {
state: 'confirmed'
}).
# The following was part of the initial logic but
# doesn't alter the query results.
# where.not(order_items: {
# order: {state: 'expired'}
# }).
order('id desc')
Here's another way:
class User < ActiveRecord::Base
has_many :order_items, foreign_key: 'buyer_id'
def videos_purchased
Video.where(id: order_items.videos.confirmed.pluck(:id))
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, class_name: 'User', foreign_key: 'buyer_id'
belongs_to :item, polymorphic: true
scope :bought, -> {where.not(orders: {state: 'cancelled'})}
scope :videos, -> {where(item_type: 'Video')}
end
class Video < ActiveRecord::Base
has_many :order_items, ...
scope :confirmed, -> {where(orders: {state: 'confirmed'})}
end
...
user = User.first()
user.videos_purchased
I might have the syntax a little screwy when it comes to table and attribute names, but this should be a start.
Notice that I changed it from one to two queries. I suggest running with that until you really notice that you have a performance problem, and even then you might have an easier time just caching queries, than trying to optimize complex join logic.

Compare the last element in Rails active record

I have two models Student and StudentRecord. Student Record has start_date, end_date and class_course_id among its attributes and it belongs to Student
scope = Student.eager_load(:student_record)
Now, I want to get the students whose latest(according to start_date) student_record's class_course_id is the same as a given class_course_id. something like:
scope = scope.where("student_records.order(start_date asc).last.class_course_id = ?", params[:class_course_id])
Obviously, the above statement is not correct but I hope it describes what I would like to get.
The below should do
Student.eager_load(:student_record).where("student_records.class_course_id = ?", params[:class_course_id]).order('student_records.start_date asc').last
Use Order by clause and descend to get the latest dates .order("student_records.start_date DESC") and in the where clause, the records will be filtered out .where("student_records.class_course_id = ?", params[:class_course_id]). The where will come first then, the order by desc will sort it correctly.
scope.where("student_records.class_course_id = ?", params[:class_course_id]).order("student_records.start_date DESC")
And you can do .limit(5) to get the first 5 records that are the latest start_dates.
If you want all students, then this is a bit non-trivial in active record.
Identifying the last Student Record sounds like an important thing that would benefit from a scope:
def self.latest_for_student
where("not exists (select null from student_records sr2 where sr2.student_id = student_records.student_id and sr2.start_date > student_records.start_date)")
end
... which means "return rows for which there does not exist another row in student_records for the same student_id, and a greater start_date"
Or...
def self.latest_for_student
where("student_records.start_date = (select max(start_date) from student_records sr2 where sr2.student_id = student_records.student_id)")
end
... which means "return rows for which the start date is equal to the maximum start date for that student id"
Then you can:
class Student
has_one :last_student_record, -> {merge(StudentRecord.latest_for_student)}, :class_name => "StudentRecord"
has_one :last_class_course, :through => :last_student_record, :source => :class_course
end
class ClassCourse
has_many :last_records_for_student, -> {merge(StudentRecord.latest_for_student)}, :class_name => "StudentRecord"
has_many :students_having_as_last_course, :through => : last_records_for_student, :source => :student
end
Then you should be able to:
#course.students_having_as_last_course
Bit complex ... could be syntax errors ... let me know if there are.

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.

Can I push this rails calculation into the database?

I'm trying to increase my app's efficiency by doing work in the database rather than in the app layer, and I'm wondering if I can move this calculation into the database.
Models:
class Offer < ActiveRecord::Base
has_many :lines
has_many :items, :through => :lines
end
class Line < ActiveRecord::Base
belongs_to :offer
belongs_to :item
# also has a 'quantity' attribute (integer)
end
class Item < ActiveRecord::Base
has_many :lines
has_many :offers, :through => :lines
# also has a 'price' attribute (decimal)
end
What I want to do is calculate the price of an offer. Currently I have a price method in the Offer class:
def price
self.lines.inject(0) do |total, line|
total + line.quantity * line.item.price
end
end
I suspect it may be possible to do a Offer.sum calculation instead that would get the answer directly from the DB rather than looping through the records, but the Calculations section of the ActiveRecord query guide doesn't have enough detail to help me out. Anybody?
Thanks!
You're correct that you can do this with sum. Something like this:
class Offer < ActiveRecord::Base
# ...
def price
self.lines.sum 'lines.quantity * items.price', :joins => :item
end
end
When you call e.g. Offer.find( some_id ).price the above will construct a query something like this:
SELECT SUM( lines.quantity * items.price ) AS total
FROM lines
INNER JOIN items ON items.id = lines.item_id
WHERE lines.offer_id = <some_id>
;
Sometimes you're better off with SQL.
SELECT SUM( lines.quantity * items.price ) AS total
FROM offers
INNER JOIN lines ON offers.id = lines.offer_id
INNER JOIN items ON items.id = lines.item_id
WHERE offers.id = 1
;

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.