Converting Complex SQL Joins into Rails ActiveRecord Query - ruby-on-rails

I have the following SQL which executes correctly in PGAdmin, but try as I might, I cannot get the join working and executing correctly.
SELECT "Reading_ID", "Pixel_ID", "Level", "Readings"."Time"
FROM "PixelReadings"
INNER JOIN "Readings" ON ("Reading_ID" = "Readings"."ID")
INNER JOIN "Sources" ON ("Readings"."Source_ID" = "Sources"."ID")
INNER JOIN location_mappings ON ("Sources"."ID" = "location_mappings"."source_id")
WHERE ("Readings"."Time" BETWEEN '2016-07-25 06:03:07 UTC' AND '2016-07-26 06:03:07 UTC')
AND "location_mappings"."location_id" = 16
AND ("location_mappings"."use_order" = 0)
AND "Pixel_ID" = ("location_mappings"."pixel_y" * 511) + "location_mappings"."pixel_x"
ORDER BY ("Reading_ID") ASC
LIMIT 4;
Can anyone help with converting this into an ActiveRecord Query. I have 2 specific problems, trying to get
.includes(readings: {sources: :location_mappings})
And how to correctly format the 'where' methods. Any help would be greatly appreciated.
"Reading_ID", "Pixel_ID", "Level" are all the columns in the PixelReadings schema - I need all the PixelReading columns plus the Time column from the Readings table.

Are the table names are correctly set on your rails models? I see that the table names are not in correspondence with the rails conventions. If you haven't set them, you can set em like this (do the same for all models that doesn't have table names different from rails conventions):
class PixelReading < ActiveRecord::Base
self.table_name = "PixelReadings"
end
Your query would look something like this
PrixelReading
.joins(readings: {sources: :location_mappings})
.where("Readings.Time BETWEEN '2016-07-25 06:03:07 UTC' AND '2016-07-26 06:03:07 UTC'")
.where(location_mappings: { location_id: 16, use_order: 0 })
.where("Pixel_ID = (location_mappings.pixel_y * 511) + location_mappings.pixel_x")
.order("PixelReadings.Reading_ID ASC")
.select("PixelReadings.*, Readings.Time")
.limit(4)

Related

Rails: Using group() on a joined table column without raw SQL

I have a small problem with grouping an ActiveRecord::Relation. I am trying to group a query by a joined table column without using raw SQL.
The code at the moment looks like that:
Sale::Product.joins(stock_product::supplier).group('core_suppliers.id').first
Result:
Sale::Product Load (42989.5ms) SELECT `sale_products`.* FROM `sale_products` INNER JOIN `stock_products` ON `stock_products`.`deleted_at` IS NULL AND `stock_products`.`id` = `sale_products`.`stock_product_id` INNER JOIN `core_suppliers` ON `core_suppliers`.`id` = `stock_products`.`core_supplier_id` GROUP BY core_suppliers.id ORDER BY `sale_products`.`id` ASC LIMIT 1
I tried to solve this problem by using merge:
Sale::Product.joins(stock_product: :supplier).merge(::Core::Supplier.group(:id)).first
Result:
Sale::Product Load (32428.4ms) SELECT `sale_products`.* FROM `sale_products` INNER JOIN `stock_products` ON `stock_products`.`deleted_at` IS NULL AND `stock_products`.`id` = `sale_products`.`stock_product_id` INNER JOIN `core_suppliers` ON `core_suppliers`.`id` = `stock_products`.`core_supplier_id` GROUP BY `sale_products`.`core_supplier_id` ORDER BY `sale_products`.`id` ASC LIMIT 1
I don't understand why Active::Record doesn't group my association by the column of the merged table. Especially since this way works with ```order()````.
Thanks for your help in advance
You can try Arel library that was introduced in Rails 3 for use in constructing SQL queries.
Just replace ::Core::Supplier.group(core_supplier: :id) to ::Core::Supplier.arel_table[:id] in your code:
Sale::Product.joins(stock_product::supplier).group(::Core::Supplier.arel_table[:id]).first
Update
If you don't want to use Arel directly in your queries you can hide Arel implementation in ApplicationRecord like this:
class ApplicationRecord < ActiveRecord::Base
def self.[](attribute)
arel_table[attribute]
end
end
And than your query can be rewritten like this:
Sale::Product.joins(stock_product::supplier).group(::Core::Supplier[:id]).first

Query on ruby on Rails

How do you query on Ruby on Rails or translate this query on Ruby on Rails?
SELECT
orders.item_total,
orders.total,
payments.created_at,
payments.updated_at
FROM
public.payments,
public.orders,
public.line_items,
public.variants
WHERE
payments.order_id = orders.id AND
orders.id = line_items.order_id AND
This is working on Postgres but I'm new to RoR and it's giving me difficulty on querying this sample.
So far this is what I have.
Order.joins(:payments,:line_items,:variants).where(payments:{order_id: [Order.ids]}, orders:{id:LineItem.orders_id}).distinct.pluck(:email, :id, "payments.created_at", "payments.updated_at")
I have a lot of reference before asking a question here are the links.
How to combine two conditions in a where clause?
Rails PG::UndefinedTable: ERROR: missing FROM-clause entry for table
Rails ActiveRecord: Pluck from multiple tables with same column name
ActiveRecord find and only return selected columns
https://guides.rubyonrails.org/v5.2/active_record_querying.html
from all that link I produced this code that works for testing.
Spree::Order.joins(:payments,:line_items,:variants).where(id: [Spree::Payment.ids]).distinct.pluck(:email, :id)
but when I try to have multiple queries and pluck a specific column name from a different table it gives me an error.
Update
So I'm using Ransack to query I produced this code.
#search = Spree::Order.ransack(
orders_gt: params[:q][:created_at_gt],
orders_lt: params[:q][:created_at_lt],
payments_order_id_in: [Spree::Order.ids],
payments_state_eq: 'completed',
orders_id_in: [Spree::LineItem.all.pluck(:order_id)],
variants_id_in: [Spree::LineItem.ids]
)
#payment_report = #search.result
.includes(:payments, :line_items, :variants)
.joins(:line_items, :payments, :variants).select('payments.response_code, orders.number, payments.number')
I don't have error when I remove the select part and I need to get that specific column. Is there a way?
You just have to make a join between the tables and then select the columns you want
Spree::Order.joins(:payments, :line_items).pluck("spree_orders.total, spree_orders.item_total, spree_payments.created_at, spree_payments.updated_at")
or
Spree::Order.joins(:payments, :line_items).select("spree_orders.total, spree_orders.item_total, spree_payments.created_at, spree_payments.updated_at")
That is equivalent to this query
SELECT spree_orders.total,
spree_orders.item_total,
spree_payments.created_at,
spree_payments.updated_at
FROM "spree_orders"
LEFT OUTER JOIN "spree_payments" ON "spree_payments"."order_id" = "spree_orders"."id"
LEFT OUTER JOIN "spree_line_items" ON "spree_line_items"."order_id" = "spree_orders"."id"
You can use select_all method.This method will return an instance of ActiveRecord::Result class and calling to_hash on this object would return you an array of hashes where each hash indicates a record.
Order.connection.select_all("SELECT
orders.item_total,
orders.total,
payments.created_at,
payments.updated_at
FROM
public.payments,
public.orders,
public.line_items,
public.variants
WHERE
payments.order_id = orders.id AND
orders.id = line_items.order_id").to_hash

how to write a join condition in Ruby on Rails?

what I'm trying to do is to write something like the next query:
SELECT *
FROM Customers c
LEFT JOIN CustomerAccounts ca
ON ca.CustomerID = c.CustomerID
AND c.State = 'NY'
Notice that I'm not using any WHERE clause, but I need to my JOIN have a condition. I cannot make it work in Ruby on Rails.
Can you help me out?
You can join the tables with LEFT JOIN. Just pass the join condition in joins and you will get the expected result
Customer.joins("LEFT JOIN CustomerAccounts
ON CustomerAccounts.CustomerID = Customers.CustomerID
AND Customers.State = 'NY'")
#=> SELECT * FROM Customers LEFT JOIN CustomerAccounts ON CustomerAccounts.CustomerID = Customers.CustomerID AND Customers.State = 'NY'
Note: just .joins() does INNER JOIN so you need to specify the join with condition
Your SQL code, translated to activerecord, would look as follows (using joins):
Customer.where(state: 'NY').joins(:customer_accounts)
The code assumes, you have the association set up:
class Customer
has_many :customer_accounts
end

Add computable column to multi-table select clause with eager_load in Ruby on Rails Activerecord

I have a query with a lot of joins and I'm eager_loading some of associations at the time. And I need to compute some value as attribute of one of models.
So, I'm trying this code:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
Where SQL function call in select operates data from associated addresses and ous tables.
I'm getting next SQL (so my in_zone column getting calculated and returned as first column before other columns for all eager_loaded models):
SELECT SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone, "SERVICE_OBJECTS"."ID" AS t0_r0, "SERVICE_OBJECTS"."TYPE" AS t0_r1, <omitted for brevity> AS t2_r36 FROM "SERVICE_OBJECTS" INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID" INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID" INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID" WHERE "OUS"."ID" IN (SELECT "OUS"."ID" FROM "OUS" WHERE "OUS"."SECTOR_CODE" = :a1) [["sector_code", "5"]]
But it seems like that in_zone isn't accessible from either model used in query.
I need to have calculated in_zone as attribute of ServiceObject model object, how I can accomplish that?
Ruby on Rails 4.2.6, Ruby 2.3.0, oracle_enhanced adapter 1.6.7, Oracle 12.1
I have successfully replicated your issue and it turns out that this is a known issue in Rails. The problem is that when using eager_load, Rails maps the columns of all eager-loaded tables into table and column aliases in the form of t0_r0, t0_r1, etc... (you can see these in the SQL that you pasted in the question). And while doing that, it simply ignores the custom columns in the select, probably because it cannot determine which eager-loaded table it should attribute the custom column to. It is sad that this issue is open for more than 2 years now...
Nevertheless I think I found a workaround. It seems that if you don't eager load the tables but manually join them (with joins), you can as well include them (with includes) and the custom columns will be returned as there will be no column aliasing taking place. The point is that you must not use associations in the joins clauses but you have to specify the joins yourself. Also note that you must specify all columns from the main table in the select manually too (see the service_objects.* in the select).
Try the following approach:
ServiceObject
.joins('INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID"')
.joins('INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID"')
.joins('INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID"')
.includes(:service_days, :address)
.where(ous: {id: OU.where(sector_code: 5)})
.select('service_objects.*, SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
The computation in the select should still work as the related tables are joined together but there should be no column aliasing present.
Of course this approach means that you'll get three queries instead of just one but unless you return a huge amount of records, the following two queries run by the includes clause should be very fast as they simply load the relevant records using foreign keys.
That monkey patch helped #Envek:
module ActiveRecord
Base.send :attr_accessor, :_row_
module Associations
class JoinDependency
JoinBase && class JoinPart
def instantiate_with_row(row, *args)
instantiate_without_row(row, *args).tap { |i| i._row_ = row }
end; alias_method_chain :instantiate, :row
end
end
end
end
then it is possible to do:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
.first
._row_['in_zone']

ActiveRecord with find_by_sql: order by a field from a joined table

I have a model MobileApp and I use find_by_sql to do a complex SQL query with several INNER JOIN. The SQL query is the following :
SELECT DISTINCT mobile_apps.*, satisfaction_scores.id, satisfaction_scores.score FROM mobile_apps
INNER JOIN bucket_applications ON mobile_apps.id = bucket_applications.mobile_app_id
INNER JOIN satisfaction_scores ON mobile_apps.id = satisfaction_scores.mobile_app_id
WHERE NOT EXISTS
(SELECT * FROM feeds
WHERE feeds.mobile_app_id = mobile_apps.id
AND feeds.target_id = 41
AND feeds.left IS TRUE) ORDER BY satisfaction_scores.score DESC;
And I do a MobileApp.find_by_sql(<the_query>).
My problem is that I need to sort my MobileApp by score of SatisfactionScore. Consequently, I need to add in the SELECT the field score to have the ORDER BY working with score. I think the SQL query is right, but ActiveRecord doesn't like to have in the SELECT columns not from table mobile_apps and therefore returns wrong ids of MobileApp.
MobileApp.find_by_sql(<the_query>) =>
[#<MobileApp id: 41>,
#<MobileApp id: 42>,
#<MobileApp id: 43>,
#<MobileApp id: 44>]
But
MobileApp.ids => [153, 156, 159, 162, 165]
Is there a better way to do it ?
Thanks.
By removing the satisfaction_scores.id from the SELECT, the MobileApp ids returned by the find_by_sql are correct. Besides the column satisfaction_scores.id was not essential for the query.

Resources