How to convert complex Postgres SQL query / subqueries to active record in Ruby on Rails 5.2? - sql

My application requires a text field and its translations to be displayed all at once in the edit view.
Available translations and even the list of configured languages evolve over time, so that the edit view must display the available translations, and create empty fields for missing ones.
Thus, the _form.html.erb file contains nested fields to handle these translations.
<%= f.fields_for translation_fields, translations.sort_by { |e| [ e.language == current_language.to_s ? 0 : 1, e.language ] } do |locution| %>
But this only allows handling of existing translations.
To allow additional languages support, I need to create a right-join with the list of languages, and initialise empty records accordingly. The corresponding SQL query for providing translations fields for the name of Playground object with id=555 is:
select 555 as document_id, 'Playground' as document_type, 'name' as field_name, property as language,
case when property = language then existing_translations.id else null end as id,
case when property = language then existing_translations.translation else null end as translation
from
(select *
from dqm_app.translations
where document_id = 555 and field_name = 'name' and document_type = 'Playground'
) as existing_translations
right outer join
(select property
from dqm_app.parameters inner join dqm_app.parameters_lists on parameters.parameters_list_id = parameters_lists.id
where parameters_lists.CODE = 'LIST_OF_LANGUAGES') as langues on property = language
As a result, we can see herunder that only a french translation exists, and other translations are to be created:
To reproduce this in Active Record, I created subqueries, which I need to join:
<% existing_translations = Translation.where({document_id: document_id, document_type: document_type, field_name: field_name}) %>
<% languages = Parameter.where("parameters_list_id = (select id from parameters_lists where code = 'LIST_OF_LANGUAGES')") %>
How can I create a right outer join between these subqueries?
Is there a better way to achieve this?
Thanks a lot!
Note: I finally solved any complex query by using AREL, starting with
this cheat sheet.

Related

Rails SQL in controller

I'm using Xeditable and RABL in a Rails app.
I have a workorder that belongs to a workgroup.
I want to assign the workorder to an employee in that workgroup.
I'm using this as the source in the Xeditable:
data-source="/employees.json?workgroup=<%= workorder.workgroup.id%>"
And this is the code I'm trying in the employee controller:
def index
#employees = Employee.order(:first_name)
#employees = Employee.joins(:empgroups).where(:workgroup_id => params[:workgroup]) if params[:workgroup].present?
end
This is the SQL that gets generated:
SELECT "employees".* FROM "employees" INNER JOIN "empgroups" ON "empgroups"."employee_id" = "employees"."id" WHERE "employees"."workgroup_id" = 2
The issue is the WHERE should be `WHERE "empgroups"."workgroup_id" = 2
How do I change this line of code?
#employees = Employee.joins(:empgroups).where(:workgroup_id => params[:workgroup]) if params[:workgroup].present?
Thanks for the help!
You can use nested hash syntax:
#employees = Employee.joins(:empgroups).where(
empgroups: { workgroup_id: params[:workgroup] }
) if params[:workgroup].present?
This should work:
Employee.joins(:empgroups).where(:empgroups => {:workgroup_id => params[:workgroup]})
The :workgroup_id is actually an attribute/column on a joined table, and not on the base table that you are querying from, so you need to specify where that column is located during the where clause.
Remember that in a pinch you can also type in SQL for the where clause, but will also have to remember to properly reference the right table.
Employee.joins(:empgroups).where("empgroups.workgroup_id = ?", params[:workgroup])

sql select with conditional field replacement

I'm using postgres. I have a single table inheritance 'listings' table. Listings can have many variations and belong to a parent through parent_id. Both parent and child have a 'title' field. Occasionally, and unfortunately, the variation title is null, and we need to keep it that way because it reflects how the original data was imported, so doing a data migration to populate variation.title with it's parent's title is not an option.
But when we display the data in our web view, we always want to show a title.
I would like to query against all the listings (parents and variation) for a merchant, and if the title is null for the child, then replace the null with the parent's title. Right now I'm doing this in ruby on rails, which can be slow as it has to loop through each ruby object. Is there a way to do this in SQL?
We are doing something like this now:
listings = Listing
.includes(:parent)
.where(:merchant_id => current_merchant.id)
.where(product_id: params[:ids])
.select(:type, :id, :sku, :product_id, :title, :quantity, :price, :channel_id, :status, :state, :parent_id)
#fix blank titles
listings.each do |l|
next if l.title
l.title = l.parent.title if l.parent_id
end
I think it is possible. Based on this docs
listings = Listing
.includes(:parent)
.where(:merchant_id => current_merchant.id)
.where(product_id: params[:ids])
.select('type, COALESCE(listings.title, parents.title), ...)
I think it will work if listings.title is NULL. If it can be blank string you can use CASE statement.
PS maybe you should use LEFT JOIN for table parents instead of including it.

Scope with association and ActiveRecord

I have an app that records calls. Each call can have multiple units associated with it. Part of my app has a reports section which basically just does a query on the Call model for different criteria. I've figured out how to write some scopes that do what I want and chain them to the results of my reporting search functionality. But I can't figure out how to search by "unit". Below are relevant excerpts from my code:
Call.rb
has_many :call_units
has_many :units, through: :call_units
#Report search logic
def self.report(search)
search ||= { type: "all" }
# Determine which scope to search by
results = case search[:type]
when "open"
open_status
when "canceled"
cancel
when "closed"
closed
when "waitreturn"
waitreturn
when "wheelchair"
wheelchair
else
scoped
end
#Search results by unit name, this is what I need help with. Scope or express otherwise?
results = results. ??????
results = results.by_service_level(search[:service_level]) if search[:service_level].present?
results = results.from_facility(search[:transferred_from]) if search[:transferred_from].present?
results = results.to_facility(search[:transferred_to]) if search[:transferred_to].present?
# If searching with BOTH a start and end date
if search[:start_date].present? && search[:end_date].present?
results = results.search_between(Date.parse(search[:start_date]), Date.parse(search[:end_date]))
# If search with any other date parameters (including none)
else
results = results.search_by_start_date(Date.parse(search[:start_date])) if search[:start_date].present?
results = results.search_by_end_date(Date.parse(search[:end_date])) if search[:end_date].present?
end
results
end
Since I have an association for units already, I'm not sure if I need to make a scope for units somehow or express the results somehow in the results variable in my search logic.
Basically, you want a scope that uses a join so you can use a where criteria in against the associated model? Is that correct?
So in SQL you're looking for something like
select * from results r
inner join call_units c on c.result_id = r.id
inner join units u on u.call_unit_id = c.id
where u.name = ?
and the scope would be (from memory, I haven't debugged this) something like:
scope :by_unit_name, lambda {|unit_name|
joins(:units).where('units.unit_name = ?', unit_name)
}
units.name isn't a column in the db. Changing it to units.unit_name didn't raise an exception and seems to be what I want. Here's what I have in my results variable:
results = results.by_unit_name(search[:unit_name]) if search[:unit_name].present?
When I try to search by a different unit name no results show up. Here's the code I'm using to search:
<%= select_tag "search[unit_name]", options_from_collection_for_select(Unit.order(:unit_name), :unit_name, :unit_name, selected: params[:search].try(:[], :unit_name)), prompt: "Any Unit" %>

Rails ActiveRecord Join Query With conditions

I have following SQL Query:
SELECT campaigns.* , campaign_countries.points, offers.image
FROM campaigns
JOIN campaign_countries ON campaigns.id = campaign_countries.campaign_id
JOIN countries ON campaign_countries.country_id = countries.id
JOIN offers ON campaigns.offer_id = offers.id
WHERE countries.code = 'US'
This works perfectly well. I want its rails active record version some thing like:
Campaign.includes(campaign_countries: :country).where(countries: {code: "US"})
Above code runs more or less correct query (did not try to include offers table), issue is returned result is collection of Campaign objects so obviously it does not include Points
My tables are:
campaigns --HAS_MANY--< campaign_countries --BELONGS_TO--< countries
campaigns --BELONGS_TO--> offers
Any suggestions to write AR version of this SQL? I don't want to use SQL statement in my code.
I some how got this working without SQL but surely its poor man's solution:
in my controller I have:
campaigns = Campaign.includes(campaign_countries: :country).where(countries: {code: country.to_s})
render :json => campaigns.to_json(:country => country)
in campaign model:
def points_for_country country
CampaignCountry.joins(:campaign, :country).where(countries: {code: country}, campaigns: {id: self.id}).first
end
def as_json options={}
json = {
id: id,
cid: cid,
name: name,
offer: offer,
points_details: options[:country] ? points_for_country(options[:country]) : ""
}
end
and in campaign_countries model:
def as_json options={}
json = {
face_value: face_value,
actual_value: actual_value,
points: points
}
end
Why this is not good solution? because it invokes too many queries:
1. It invokes query when first join is performed to get list of campaigns specific to country
2. For each campaign found in first query it will invoke one more query on campaign_countries table to get Points for that campaign and country.
This is bad, Bad and BAD solution. Any suggestions to improve this?
If You have campaign, You can use campaign.campaign_countries to get associated campaign_countries and just get points from them.
> campaign.campaign_countries.map(&:points)
=> [1,2,3,4,5]
Similarly You will be able to get image from offers relation.
EDIT:
Ok, I guess now I know what's going on. You can use joins with select to get object with attached fields from join tables.
cs = Campaign.joins(campaign_countries: :country).joins(:offers).select('campaigns.*, campaign_countries.points, offers.image').where(countries: {code: "US"})
You can than reference additional fields by their name on Campaign object
cs.first.points
cs.first.image
But be sure, that additional column names do not overlap with some primary table fields or object methods.
EDIT 2:
After some more research I came to conclusion that my first version was actually correct for this case. I will use my own console as example.
> u = User.includes(:orders => :cart).where(:carts => { :id => [5168, 5167] }).first
> u.orders.length # no query is performed
=> 2
> u.orders.count # count query is performed
=> 5
So when You use includes with condition on country, in campaign_countries are stored only campaign_countries that fulfill Your condition.
Try this:
Campaign.joins( [{ :campaign_countries => :countries}, :offers]).where('`countries`.`code` = ?', "US")

ActiveRecord::Relation joins with more conditions than just the foreign key

Is there any way to specify more than one conditions for a left outer join using ActiveRecord::Relation?
Take the following SQL statement for example. How can anyone rewrite this using ActiveRecord::Relation objects?
SELECT `texts`.*, `text_translations`.translation FROM `texts` LEFT OUTER JOIN `text_translations` ON `text_translations`.`id` = `texts`.`id` AND `text_translations`.`locale` = 'en'
Is there any way to do this under ActiveRecord 3.0.3+?
Thanks in advance.
first you should consider to use rails/activerecord conform relations. This means the foreign key in the text_translations table should be called text_id
Create your models and associations like this:
class Text < ActiveRecord::Base
# all possible translations!
has_many :text_translations
scope :with_translation_for, lambda { |lang| {
:select => "texts.*, tt.translation",
:joins => "LEFT OUTER JOIN text_translations AS tt ON tt.text_id = texts.id AND tt.locale = #{ActiveRecord::Base.sanitize(lang)}"
}}
# return nil if translation hasn't been loaded, otherwise you get a nasty NoMethod exception
def translation
read_attribute(:translation)
end
end
and
class TextTranslation < ActiveRecord::Base
# every translation belongs to a text
belongs_to :text
# define a scope for the language
scope :language, lambda { |lang| where(['locale = ?', lang]) }
end
How to use:
texts = Text.with_translation_for('en')
texts.each do |c_text|
unless c_text.translation.nil?
puts c_text.translation
else
puts "No translation available!"
end
end
Now to the pro and cons, the way using LEFT OUTER join will load you all texts even if there isn't a translation for a text in the desired language. The con is that you won't get the "TextTranslation" model object.
Anotherway is to load only the text which have the desired translation. You can do it like:
texts = Text.includes(:text_translations).where(:text_translations => {:locale => 'en'})
now texts[i].text_translations will return an array with all TextTranslations model object for this text matching the locale 'en'. But texts without a translation in the locale "en" won't show up.
Edit
Connected to your comment:
The problem about using .join(:tablename) on a relation is that, it will result in an INNER JOIN so this is not an option. You have to explicitly declare the LEFT join. Another thing is that if you use something like Text.includes(:text_translations).where(['text_translations.locale = ?', 'en']) the condition will be applied to the SQL query as whole and not on the possible LEFT join itself. What you actually can do is to declare associations like
has_many :english_translations, :class_name => 'TextTranslation', :conditions => ['locale = ?', 'en']
This way you can manage to load only english translations by eager loading (without any joins at all):
Text.includes(:english_translations).all
Checkt this out:
Ruby On Rails Guide about Joining Tables
ActiveRecord Association Docs, Search for LEFT OUTER JOIN