Rails - can't access custom join data - ruby-on-rails

I've got a really complicated query (which finds bus connections between two towns) and I haven't got any idea how to access data from joins (I'd like to know at which stop does the connection start and at which does it end). Is it possible to access this data using ActiveRecord?
Course.joins("INNER JOIN stop_times as start_stop ON start_stop.course_id=courses.id")
.joins("INNER JOIN stop_times as end_stop ON end_stop.course_id = courses.id")
.joins('INNER JOIN stops as start_stopi ON start_stop.stop_id = start_stopi.id')
.joins('INNER JOIN stops as end_stopi ON end_stop.stop_id = end_stopi.id')
.where('start_stop.hour>= ? OR (start_stop.hour>= ? AND start_stop.minute>= ?)',hour,(hour+1)%24,minute)
.where('start_stopi.town_id = ? and end_stopi.town_id = ?',start_town,end_town)
.where('start_stop."order"<end_stop."order"').order('start_stop.minute ASC').order('start_stop.hour ASC')
EDIT:
I've managed to rewrite it to use active record joins, although it broken my names, it works.
Course.joins(end_stop_times: :stop).joins(start_stop_times: :stop)
.where('start_stop_times_courses.hour>= ? OR (start_stop_times_courses.hour>= ? AND start_stop_times_courses.minute>= ?)',hour,(hour+1)%24,minute)
.where('stops_stop_times.town_id = ? and stops.town_id = ?',start_town,end_town)
.where('start_stop_times_courses."order"<stop_times."order"')
.order('start_stop_times_courses.minute ASC').order('start_stop_times_courses.hour ASC')
Using this new query models are:
class Course < ActiveRecord::Base
belongs_to :carrier
has_many :end_stop_times, class_name: 'StopTime'
has_many :start_stop_times, class_name: 'StopTime'
class Stop < ActiveRecord::Base
belongs_to :town
class StopTime < ActiveRecord::Base
belongs_to :stop
belongs_to :course

You need to add sth like:
your_query.select('courses.*, start_stopi.id as start_stop_id, end_stopi.id as end_stop_id)
and then you can access it by calling start_stop_id and end_stop_id on course object.
However you should probably use association for this kind of operations. Could you show us you models?

Check your log for the output of this query, you should find that it starts with select courses.* - therefore it will not bring through data from the included tables.
You can add some select other_table.some_column statements to your query, but this isn't the rails way.
I would suggest you separate your scope into the relevant models - put scopes in the stop_times model (and others) so that you can call the scopes on the object you actually want to get data from.

When you're constructing custom SQL of that complexity I think you've taken the Rails-way of doing things too far. You're using practically no activerecord association information to construct it, and you've built a programming construct that is horribly ugly and difficult to read.
I'd advise that you rewrite it as well formatted SQL
results = ActiveRecord::Base.connection.execute(
"select c.*
from courses c
join stop_times ss on ss.course_id = c.id
join stop_times es on es.course_id = c.id
... etc ...
where (start_stop.hour >= #{ActiveRecord::Base.sanitize(hour)} or
... etc ...")
Now it could be that you can improve your models and associations to the point where this level of complexity is not required (eg. the associations between courses, stop_times (start) and stop_times (end) could probably be encapsulated in activerecord pretty well, but at the moment you seem to be falling between the pure SQL and the pure activerecord approaches in a very uncomfortable way.

Related

having in ActiveRecord

I have been trying to find a solution to my problem for a few days, so I am turning towards the community, hopefully I am not missing something obvious here.
I have 2 models in rails:
class Room
has_many :accesses
end
class Access
belongs_to :accessor, polymorphic: true
end
Accessor can be of 2 types: Person or Team
I am trying to find the most efficient way to find the rooms that a user has access to, but which are not accessible from any teams.
I tried:
Room.joins(:accesses).where(accesses: {accessor: Person.find(1234)}).where.not(accesses: {accessor_type: Team'})
But that returns the rooms that people have accesses to, it does not filter out the ones that Team AND People have access to.
I am thinking the having clause is the way to go, in which it would count the number of Teams accesses to rooms, and keep the rooms that have 0 team accesses. Though all my attempts are failing.
I would love to hear any advice.
Left join
Instead of using HAVING, which requires us to add a GROUP BY, I'd start with a LEFT JOIN and a WHERE.
You can do this by left-joining to the room_accesses table specifically on "Team" accessor_type. We're left-joining because we're going to scope this join to only team accesses, and select only the rows where no such accesses exist. An inner join would not return these rows at all. We'll need to use a table alias as we're already using the room_accesses table to join to the person you are looking up.
We may as well admit Rails isn't great at this level of query abstraction, so let's just construct the raw SQL fragments for our first solution:
person = Person.find(1234)
person.rooms.joins(
"LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'"
).where("team_accesses.id IS NULL")
This generates, for SQLite,
SELECT "rooms".* FROM "rooms"
INNER JOIN "room_accesses"
ON "rooms"."id" = "room_accesses"."room_id"
LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'
WHERE "room_accesses"."accessor_id" = 1
AND "room_accesses"."accessor_type" = 'Person'
AND (team_accesses.id IS NULL)
Having
You can do this with aHAVING by similarly joining to room_accesses again with the team_accesses alias, grouping by rooms.id (since we want at most one record per room), and selecting the groups HAVING a zero count of team accesses:
person.rooms.joins(
"LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'"
).group("rooms.id").having("COUNT(team_accesses.id) = 0")
generates:
SELECT "rooms".* FROM "rooms"
INNER JOIN "room_accesses"
ON "rooms"."id" = "room_accesses"."room_id"
LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'
WHERE "room_accesses"."accessor_id" = 1
AND "room_accesses"."accessor_type" = 'Person'
GROUP BY rooms.id
HAVING (COUNT(team_accesses.id) = 0)
Using associations instead of raw SQL
You can get halfway there in Rails by defining a scoped association:
class Room < ApplicationRecord
has_many :room_accesses
has_many :team_accesses, ->{ where accessor_type: "Team" }, class_name: "RoomAccess"
end
Assuming you're using a recent version of ActiveRecord, this allows you to do
person.rooms.left_joins(:team_accesses)
However, the table name used for this left joins is "team_accesses_rooms", which is predictable in this simple case but not part of the public API to my knowledge and subject to being changed if other joins are used in this same query. Still, if you're feeling daring:
person.rooms.left_joins(:team_accesses).where(team_accesses_rooms: {id: nil})
Frankly I would not recommend this method as you're relying on a table alias that you're not in control of and is not obvious where it comes from. With the raw SQL, you are in control of it and it's obvious where it came from.

ActiveRecord sort model on attribute of last has_many relation

I've been digging around for this for awhile... I can't find a graceful solution. I have loans and loans has_many :decisions. decisions has an attribute that I care about, called risk_rating.
I'd like to sort loans based on the most recent decision (based on created_at, per usual), but by the risk_rating.
Loan.includes(:decisions).references(:decisions).order('decisions.risk_rating DESC') doesn't work...
I want loans... sorted by their most recent decision's risk_rating. This seems like it should be easier than it is.
I'm currently doing this outside of the database like this, but it's chewing up time and memory:
Loan.all.sort do |x,y|
x.decisions.last.try(:risk_rating).to_f <=> y.decisions.last.try(:risk_rating).to_f
end
I'd like to show the performance I'm getting with the proposed answer, along with an inaccuracy...
Benchmark.bm do |x|
x.report{ Loan.joins('LEFT JOIN decisions ON decisions.loan_id = loans.id').group('loans.id').order('MAX(decisions.risk_rating) DESC').limit(10).map{|l| l.decisions.last.try(:risk_rating)} }
end
user system total real
0.020000 0.000000 0.020000 ( 20.573096)
=> [0.936775, 0.934465, 0.932088, 0.922352, 0.921882, 0.794724, 0.919432, 0.918385, 0.916952, 0.914938]
The order isn't right. That 0.794724 is out of place.
To that extent... I'm only seeing one attribute in the proposed answer. I don't see the connection =/
Alright, it looks like I'm working late tonight because I couldn't help but jump in:
class Loan < ApplicationRecord
has_many :decisions
has_one :latest_decision, -> { merge(Decision.latest) }, class_name: 'Decision'
end
class Decision < ApplicationRecord
belongs_to :loan
def latest
t1 = arel_table
t2 = arel_table.alias('t2')
# Self join based on `loan_id` prefer latest `created_at`
join_on = t1[:loan_id].eq(t2[:loan_id]).and(
t1[:created_at].lt(t2[:created_at]))
where(t2[:loan_id].eq(nil)).joins(
t1.create_join(t2, t1.create_on(join_condition), Arel::Nodes::OuterJoin)
)
end
end
Loan.includes(:latest_decision)
This doesn't sort, just provides the latest decision for each loan. Throwing an order that references access_codes messes things up because of the table aliasing. I don't have the time to work that kink out now, but I bet you can figure it out if you check out some of the great resources on Arel and how to use it with ActiveRecord. I really enjoy this one.
At first let's write sql-query which will select necessary data. SO contains a question which may helps here: Select most recent row with GROUP BY in MySQL. My best version:
SELECT loans.*
FROM loans
LEFT JOIN (
SELECT loan_id, MAX(id) as id
FROM decisions
GROUP BY loan_id) d ON d.loan_id = loans.id
LEFT JOIN decisions ON decisions.id = d.id
ORDER BY decisions.risk_rating DESC
This code suppose MAX(id) gives id of the recent row in group.
You may do the same query by this Rails code:
sub_query =
Decision.select('loan_id, MAX(id) as id').
group(:loan_id).to_sql
Loan.
joins("LEFT JOIN (#{sub_query}) d ON d.loan_id = loans.id").
joins("LEFT JOIN decisions ON decisions.id = d.id").
order("decisions.risk_rating DESC")
Unfortunately, I don't have MySQL at hand and I can't try this code. Hope it will work.

activerecord exists subquery

I have the following two models:
class Client < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :client
end
I want to query clients with orders specified in a list (order_1, order_2), at the same time, the client's all orders are needed. I can do this by SQL below:
SELECT *
FROM CLIENTS C
JOIN ORDERS O
ON C.ID = O.CLIENT_ID
WHERE EXISTS
(SELECT *
FROM CLIENTS C1
JOIN ORDERS O1
ON C1.ID = O1.CLIENT_ID
WHERE O1.ID IN ('order_1', 'order_2')
AND C1.ID = C.ID
);
Is there any way to do this in rails way? The following code would give the satisfied clients, but client.orders returns only the specified orders.
clients.includes(:orders).where(orders: { id: ['order_1', 'order_2'] })
I don't know how to get all information in one query.
I know this is years later, but here's how to do it (based on this blog post).
Order.where(client_id:
Client.joins(:orders)
.where(orders: {id: ['order_1', 'order_2']})
.select(:id)
)
A gem that exists to do that: activerecord_where_assoc (I'm the author)
With it, you can do what you want this way:
clients.includes(:orders).where_assoc_exists(:orders, id: ['order_1', 'order_2'])
Doing it without a gem makes it easy to do mistakes or have annoying side effects, such as the one you mentionned of not having every records. Here is a whole document about the problems.
Read more in the documentation. Here is an introduction and examples.

How to write complex query in Ruby

Need advice, how to write complex query in Ruby.
Query in PHP project:
$get_trustee = db_query("SELECT t.trustee_name,t.secret_key,t.trustee_status,t.created,t.user_id,ui.image from trustees t
left join users u on u.id = t.trustees_id
left join user_info ui on ui.user_id = t.trustees_id
WHERE t.user_id='$user_id' AND trustee_status ='pending'
group by secret_key
ORDER BY t.created DESC")
My guess in Ruby:
get_trustee = Trustee.find_by_sql('SELECT t.trustee_name, t.secret_key, t.trustee_status, t.created, t.user_id, ui.image FROM trustees t
LEFT JOIN users u ON u.id = t.trustees_id
LEFT JOIN user_info ui ON ui.user_id = t.trustees_id
WHERE t.user_id = ? AND
t.trustee_status = ?
GROUP BY secret_key
ORDER BY t.created DESC',
[user_id, 'pending'])
Option 1 (Okay)
Do you mean Ruby with ActiveRecord? Are you using ActiveRecord and/or Rails? #find_by_sql is a method that exists within ActiveRecord. Also it seems like the user table isn't really needed in this query, but maybe you left something out? Either way, I'll included it in my examples. This query would work if you haven't set up your relationships right:
users_trustees = Trustee.
select('trustees.*, ui.image').
joins('LEFT OUTER JOIN users u ON u.id = trustees.trustees_id').
joins('LEFT OUTER JOIN user_info ui ON ui.user_id = t.trustees_id').
where(user_id: user_id, trustee_status: 'pending').
order('t.created DESC')
Also, be aware of a few things with this solution:
I have not found a super elegant way to get the columns from the join tables out of the ActiveRecord objects that get returned. You can access them by users_trustees.each { |u| u['image'] }
This query isn't really THAT complex and ActiveRecord relationships make it much easier to understand and maintain.
I'm assuming you're using a legacy database and that's why your columns are named this way. If I'm wrong and you created these tables for this app, then your life would be much easier (and conventional) with your primary keys being called id and your timestamps being called created_at and updated_at.
Option 2 (Better)
If you set up your ActiveRecord relationships and classes properly, then this query is much easier:
class Trustee < ActiveRecord::Base
self.primary_key = 'trustees_id' # wouldn't be needed if the column was id
has_one :user
has_one :user_info
end
class User < ActiveRecord::Base
belongs_to :trustee, foreign_key: 'trustees_id' # relationship can also go the other way
end
class UserInfo < ActiveRecord::Base
self.table_name = 'user_info'
belongs_to :trustee
end
Your "query" can now be ActiveRecord goodness if performance isn't paramount. The Ruby convention is readability first, reorganizing code later if stuff starts to scale.
Let's say you want to get a trustee's image:
trustee = Trustee.where(trustees_id: 5).first
if trustee
image = trustee.user_info.image
..
end
Or if you want to get all trustee's images:
Trustee.all.collect { |t| t.user_info.try(:image) } # using a #try in case user_info is nil
Option 3 (Best)
It seems like trustee is just a special-case user of some sort. You can use STI if you don't mind restructuring you tables to simplify even further.
This is probably outside of the scope of this question so I'll just link you to the docs on this: http://api.rubyonrails.org/classes/ActiveRecord/Base.html see "Single Table Inheritance". Also see the article that they link to from Martin Fowler (http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html)
Resources
http://guides.rubyonrails.org/association_basics.html
http://guides.rubyonrails.org/active_record_querying.html
Yes, find_by_sql will work, you can try this also:
Trustee.connection.execute('...')
or for generic queries:
ActiveRecord::Base.connection.execute('...')

ARel: Add additional conditions to an outer join

I have the following models in my Rails application:
class Shift < ActiveRecord::Base
has_many :schedules
scope :active, where(:active => true)
end
class Schedule < ActiveRecord::Base
belongs_to :shift
end
I wish to generate a collection of all active shifts and eager load any associated schedules that have occurs_on between two given dates. If a shift has no schedules between those dates, it should still be returned in the results.
Essentially, I want to generate SQL equivalent to:
SELECT shifts.*, schedules.*
FROM shifts
LEFT JOIN schedules ON schedules.shift_id = shifts.id
AND schedules.occurs_on BETWEEN '01/01/2012' AND '01/31/2012'
WHERE shifts.active = 't';
My first attempt was:
Shift.active.includes(:schedules).where("schedules.occurs_on BETWEEN '01/01/2012' AND '01/31/2012')
The problem is that the occurs_on filtering is done in the where clause, and not in the join. If a shift has no schedules in that period, it is not returned at all.
My second attempt was to use the joins method, but this does an inner join. Again, this will drop all shifts that have no schedules for that period.
I'm frustrated because I know the SQL I want AREL to generate, but I can't figure out how to express it with the API. Anyone?
you could try some pretty raw AREL. Disclaimer: I didn't have actual Schedule and Shift classes so i couldn't test this properly, but i used some existing tables to troubleshoot it on my own machine.
on = Arel::Nodes::On.new(
Arel::Nodes::Equality.new(Schedule.arel_table[:shift_id], Shift.arel_table[:id]).\
and(Arel::Nodes::Between.new(
Schedule.arel_table[:occurs_on],
Arel::Nodes::And.new(2.days.ago, Time.now)
))
)
join = Arel::Nodes::OuterJoin.new(Schedule.arel_table, on)
Shift.joins(join).where(active: true).to_sql
You can use a SQL fragment as the argument of your joins method call :
Shift.active.joins('LEFT OUTER JOIN schedules ON schedules.occurs_on...')
You can construct a raw sql query using Arel as follows:
#start_date
#end_date
#shift = Shift.arel_table
#schedule = Schedule.arel_table
#shift.join(#schedule)
.on(#schedule[:shift_id].eq(#shift[:id])
.and(#schedule[:occurs_on].between(#start_date..#end_date)))
.to_sql

Resources