I'm trying to use SQL to get information from a Postgres database using Rails.
This is what I've tried:
Select starts_at, ends_at, hours, employee.maxname, workorder.wonum from events where starts_at>'2018-03-14'
inner join employees on events.employee_id = employees.id
inner join workorders on events.workorder_id = workorders.id;
I get the following error:
ERROR: syntax error at or near "inner"
LINE 2: inner join employees on events.employee_id = employees.id
Sami's comment is correct, but since this question is tagged with ruby-on-rails you can try to use ActiveRecord's API to do the same:
Make sure that your models relations are defined
class Event < ActiveRecord::Base
belongs_to :employee
belongs_to :workorder
end
And then you can do something like:
Event
.where('starts_at > ?', '2018-03-14')
.joins(:employee, :workorder)
or
Event
.joins(:employee, :workorder)
.where('starts_at > ?', '2018-03-14')
And you don't need to worry which one goes first.
In general, it's suboptimal to create the SQL queries in rails if you don't absolutely need to because they're harder to maintain.
You request should look at this :
select starts_at, ends_at, hours, employee.maxname, workorder.wonum
from events
inner join employees on events.employee_id = employees.id
inner join workorders on events.workorder_id = workorders.id
where starts_at>'2018-03-14';
Related
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.
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('...')
The problem is that when a Restaurant does not have any MenuItems that match the condition, ActiveRecord says it can't find the Restaurant. Here's the relevant code:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
def self.with_meals_of_the_week
includes({menu_items: :meal}).where(:'menu_items.date' => Time.now.beginning_of_week..Time.now.end_of_week)
end
end
And the sql code generated:
Restaurant Load (0.0ms)←[0m ←[1mSELECT DISTINCT "restaurants".id FROM "restaurants"
LEFT OUTER JOIN "menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN "meals" ON "meals"."id" = "menu_items"."meal_id" WHERE
"restaurants"."id" = ? AND ("menu_items"."date" BETWEEN '2012-10-14 23:00:00.000000'
AND '2012-10-21 22:59:59.999999') LIMIT 1←[0m [["id", "1"]]
However, according to this part of the Rails Guides, this shouldn't be happening:
Post.includes(:comments).where("comments.visible", true)
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded.
The SQL generated is a correct translation of your query. But look at it,
just at the SQL level (i shortened it a bit):
SELECT *
FROM
"restaurants"
LEFT OUTER JOIN
"menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN
"meals" ON "meals"."id" = "menu_items"."meal_id"
WHERE
"restaurants"."id" = ?
AND
("menu_items"."date" BETWEEN '2012-10-14' AND '2012-10-21')
the left outer joins do the work you expect them to do: restaurants
are combined with menu_items and meals; if there is no menu_item to
go with a restaurant, the restaurant is still kept in the result, with
all the missing pieces (menu_items.id, menu_items.date, ...) filled in with NULL
now look aht the second part of the where: the BETWEEN operator demands,
that menu_items.date is not null! and this
is where you filter out all the restaurants without meals.
so we need to change the query in a way that makes having null-dates ok.
going back to ruby, you can write:
def self.with_meals_of_the_week
includes({menu_items: :meal})
.where('menu_items.date is NULL or menu_items.date between ? and ?',
Time.now.beginning_of_week,
Time.now.end_of_week
)
end
The resulting SQL is now
.... WHERE (menu_items.date is NULL or menu_items.date between '2012-10-21' and '2012-10-28')
and the restaurants without meals stay in.
As it is said in Rails Guide, all Posts in your query will be returned only if you will not use "where" clause with "includes", cause using "where" clause generates OUTER JOIN request to DB with WHERE by right outer table so DB will return nothing.
Such implementation is very helpful when you need some objects (all, or some of them - using where by base model) and if there are related models just get all of them, but if not - ok just get list of base models.
On other hand if you trying to use conditions on including tables then in most cases you want to select objects only with this conditions it means you want to select Restaurants only which has meals_items.
So in your case, if you still want to use only 2 queries (and not N+1) I would probably do something like this:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
cattr_accessor :meals_of_the_week
def self.with_meals_of_the_week
restaurants = Restaurant.all
meals_of_the_week = {}
MenuItems.includes(:meal).where(date: Time.now.beginning_of_week..Time.now.end_of_week, restaurant_id => restaurants).each do |menu_item|
meals_of_the_week[menu_item.restaurant_id] = menu_item
end
restaurants.each { |r| r.meals_of_the_week = meals_of_the_week[r.id] }
restaurants
end
end
Update: Rails 4 will raise Deprecation warning when you simply try to do conditions on models
Sorry for possible typo.
I think there is some misunderstanding of this
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any
posts, all the posts would still be loaded. By using joins (an INNER
JOIN), the join conditions must match, otherwise no records will be
returned.
[from guides]
I think this statements doesn't refer to the example Post.includes(:comments).where("comments.visible", true)
but refer to one without where statement Post.includes(:comments)
So all work right! This is the way LEFT OUTER JOIN work.
So... you wrote: "If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded." Ok! But this is true ONLY when there is NO where clause! You missed the context of the phrase.
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
I have just optimised some Ruby code that was in a controller method, replacing it with a direct database query. The replacement appears to work and is much faster. Thing is, I've no idea how Rails managed to figure out the correct query to use!
The purpose of the query is to work out tag counts for Place models within a certain distance of a given latitude and longitude. The distance part is handled by the GeoKit plugin (which basically adds convenience methods to add the appropriate trigonometry calculations to the select), and the tagging part is done by the acts_as_taggable_on_steroids plugin, which uses a polymorphic association.
Below is the original code:
places = Place.find(:all, :origin=>latlng, :order=>'distance asc', :within=>distance, :limit=>200)
tag_counts = MyTag.tagcounts(places)
deep_tag_counts=Array.new()
tag_counts.each do |tag|
count=Place.find_tagged_with(tag.name,:origin=>latlng, :order=>'distance asc', :within=>distance, :limit=>200).size
deep_tag_counts<<{:name=>tag.name,:count=>count}
end
where the MyTag class implements this:
def MyTag.tagcounts(places)
alltags = places.collect {|p| p.tags}.flatten.sort_by(&:name)
lasttag=nil;
tagcount=0;
result=Array.new
alltags.each do |tag|
unless (lasttag==nil || lasttag.name==tag.name)
result << MyTag.new(lasttag,tagcount)
tagcount=0
end
tagcount=tagcount+1
lasttag=tag
end
unless lasttag==nil then
result << MyTag.new(lasttag,tagcount)
end
result
end
This was my (very ugly) first attempt as I originally found it difficult to come up with the right rails incantations to get this done in SQL. The new replacement is this single line:
deep_tag_counts=Place.find(:all,:select=>'name,count(*) as count',:origin=>latlng,:within=>distance,:joins=>:tags, :group=>:tag_id)
Which results in an SQL query like this:
SELECT name,count(*) as count, (ACOS(least(1,COS(0.897378837271255)*COS(-0.0153398733287034)*COS(RADIANS(places.lat))*COS(RADIANS(places.lng))+
COS(0.897378837271255)*SIN(-0.0153398733287034)*COS(RADIANS(places.lat))*SIN(RADIANS(places.lng))+
SIN(0.897378837271255)*SIN(RADIANS(places.lat))))*3963.19)
AS distance FROM `places` INNER JOIN `taggings` ON (`places`.`id` = `taggings`.`taggable_id` AND `taggings`.`taggable_type` = 'Place') INNER JOIN `tags` ON (`tags`.`id` = `taggings`.`tag_id`) WHERE (places.lat>50.693170735732 AND places.lat<52.1388692642679 AND places.lng>-2.03785525810908 AND places.lng<0.280035258109084 AND (ACOS(least(1,COS(0.897378837271255)*COS(-0.0153398733287034)*COS(RADIANS(places.lat))*COS(RADIANS(places.lng))+
COS(0.897378837271255)*SIN(-0.0153398733287034)*COS(RADIANS(places.lat))*SIN(RADIANS(places.lng))+
SIN(0.897378837271255)*SIN(RADIANS(places.lat))))*3963.19)
<= 50) GROUP BY tag_id
Ignoring the trig (which is from GeoKit, and results from the :within and :origin parameters), what I can't figure out about this is how on earth Rails was able to figure out from the instruction to join 'tags', that it had to involve 'taggings' in the JOIN (which it does, as there is no direct way to join the places and tags tables), and also that it had to use the polymorphic stuff.
In other words, how the heck did it (correctly) come up with this bit:
INNER JOIN `taggings` ON (`places`.`id` = `taggings`.`taggable_id` AND `taggings`.`taggable_type` = 'Place') INNER JOIN `tags` ON (`tags`.`id` = `taggings`.`tag_id`)
...given that I never mentioned the taggings table in the code! Digging into the taggable plugin, the only clue that Rails has seems to be this:
class Tag < ActiveRecord::Base
has_many :taggings, :dependent=>:destroy
...
end
Anybody able to give some insight into the magic going on under the hood here?
The acts_as_taggable_on_steroids plugin tells your Place model that it has_many Tags through Taggings. With this association specified, ActiveRecord knows that it needs to join taggings in order to get to the tags table. The same thing holds true for HABTM relationships. For example:
class Person < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :people
end
>> Person.first(:joins => :tags)
This produces the following SQL:
SELECT "people".*
FROM "people"
INNER JOIN "people_tags" ON "people_tags".person_id = "people".id
INNER JOIN "tags" ON "tags".id = "people_tags".tag_id
LIMIT 1