Squeel query comparing 2 variables - ruby-on-rails

Using squeel, I'm able to generate the following request
Payment.joins(:account => :preference).where({:account => {:preference => {:currency => :currency } } }).to_sql
=> SELECT "payments".* FROM "payments" INNER JOIN "accounts" ON "accounts"."id" = "payments"."account_id" INNER JOIN "preferences" ON "preferences"."account_id" = "accounts"."id" WHERE "preferences"."currency" = "preferences"."currency"
However, how to get? (CHANGE IN CAPITAL)
=> SELECT "payments".* FROM "payments" INNER JOIN "accounts" ON "accounts"."id" = "payments"."account_id" INNER JOIN "preferences" ON "preferences"."account_id" = "accounts"."id" WHERE "preferences"."currency" = "PAYMENTS"."currency"
If the solution work fine with meta_where as well that's even better ;-)

Thanks to IRC guys hron84, injekt and SIGe
Here's the solution :
Payment.joins(:account => :preference).where{account.preference.currency == ~currency }.to_sql

Related

Eager loading model instances using single SQL statement with Mobility table backend

Using Rails 7 and Mobility 1.2.9:
# config/initializers/mobility.rb
Mobility.configure do |config|
config.plugins do
backend :table
active_record
reader
writer
backend_reader
query
cache
dirty
presence
end
end
# app/models/content.rb
class Content < ApplicationRecord
extend Mobility
translates :slug, :title
end
How to find contents by slug, instantiate them into Content objects and query their translated attributes using one single SQL statement (for an unchanging locale)? The examples below with and without eager_loading run 2 SQL statements.
> content = Content.i18n.find_by(slug: "foo")
Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'foo' LIMIT $1 [["LIMIT", 1]]
> content.title
Content::Translation Load (0.3ms) SELECT "content_translations".* FROM "content_translations" WHERE "content_translations"."content_id" = $1 [["content_id", "..."]]
> content = Content.i18n.eager_load(:translations).find_by(slug: "foo")
SQL (0.6ms) SELECT DISTINCT "contents"."id" FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' LIMIT $1 [["LIMIT", 1]]
SQL (0.8ms) SELECT "contents"."id" AS t0_r0, "contents"."created_at" AS t0_r1, "contents"."updated_at" AS t0_r2, ..., "content_translations"."id" AS t1_r0, "content_translations"."locale" AS t1_r1, "content_translations"."created_at" AS t1_r2, "content_translations"."updated_at" AS t1_r3, "content_translations"."slug" AS t1_r4, "content_translations"."title" AS t1_r5, "content_translations"."content_id" AS t1_r6 FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' AND "contents"."id" = $1 [["id", "..."]]
> content.title
# no DB statement, but there were 2 statements at instantiation
NB: I do not want to pluck attributes but instead to create model instances.

Rails is breaking SQL query when modifying order

Context
We have a Rails app that is retrieving conversations with the following raw SQL query:
SELECT sub.*,
profiles.status AS interlocutor_status
FROM (
SELECT DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
FROM "conversations"
LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id"
WHERE (post_applicant = 1234567 OR post_owner = 1234567)
AND "conversation_preferences"."user_id" = 1234567
ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
AND (last_activity_on >= '2021-01-19 04:40:22.881985')
AND (state = 'active')
ORDER BY profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25
We generate this query using the following ActiveRecord code:
def fetch
distinct = Conversation.left_outer_joins(:preferences)
.left_outer_joins(post: :category)
.left_outer_joins(:messages)
.where('post_applicant = :id OR post_owner = :id', id: current_user.id)
.where(conversation_preferences: { user_id: current_user.id })
.select(
<<-SQL.squish
DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, #{current_user.id}), NULLIF(post_applicant, #{current_user.id})) AS interlocutor_id
SQL
)
.order(:id, 'messages.created_at DESC')
Conversation.includes(post: :category)
.from(distinct, :sub)
.select('sub.*, profiles.status AS interlocutor_status')
.joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
.joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
.where.not('profiles.status' => :pending)
.order('profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC')
end
Problem
We want to stop ordering by profiles.status. To do this, we naturally removed it from the last order statement:
order('sub.unread_counter DESC, sub.last_activity_on DESC')
That's the problem. Doing that is entirely breaking the generated query, that generate an error which is irrelevant here because we don't want the modified query (note how it is different from the 1st one):
SELECT sub.*,
profiles.status AS interlocutor_status,
"conversations"."id" AS t0_r0,
"conversations"."post_id" AS t0_r1,
"conversations"."post_owner" AS t0_r2,
"conversations"."post_applicant" AS t0_r3,
"conversations"."created_at" AS t0_r4,
"conversations"."updated_at" AS t0_r5,
"posts"."id" AS t1_r0,
"posts"."title" AS t1_r1,
"posts"."description" AS t1_r2,
"categories"."id" AS t2_r0,
"categories"."name" AS t2_r1
FROM (
SELECT DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
FROM "conversations"
LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id"
WHERE (post_applicant = 1234567 OR post_owner = 1234567)
AND "conversation_preferences"."user_id" = 1234567
ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
AND (last_activity_on >= '2021-01-19 05:04:06.084499')
AND (state = 'active')
ORDER BY sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25
I know without a bit of context it'll be hard to help us but if someone knows why ActiveRecord is changing the query after trying to just remove profiles.status from the order statement, that would be awesome. Thanks in advance
NOTE: modifying the 1st raw SQL directly (from our postgres client) does works. The issue is not the first query, but maybe how ActiveRecord is handling it
Finally found a way to make it work using preload instead of includes. We wanted to avoid having seperate queries to load posts and categories but since performance is not affected by it, we don't mind it.
Here is how it look like:
Conversation.preload(post: :category)
.from(distinct, :sub)
.select('sub.*, profiles.status AS interlocutor_status')
.joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
.joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
.where.not('profiles.status' => :pending)
.order('sub.unread_counter DESC, sub.last_activity_on DESC')
Which generates 3 queries:
-- Query 1
SELECT sub.*, profiles.status AS interlocutor_status
FROM (
SELECT DISTINCT
....
-- Query 2
SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (............)
-- Query 3
SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (..........)
Thanks to everyone for the help in comments (max and Sebastian Palma)

Rails 4 - Eager loading with multiple ON conditions

I have an Article and each Article has exactly one User and either zero or one ArticleVote.
My code is:
#articles = Article
.eager_load(:user)
.eager_load(:article_vote)
.where(article_votes: { user_id: session[:user_id]})
.order(created_at: :desc)
Which produces the following SQL:
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id`
WHERE `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
The problem with this query is if no vote exists (which sometimes it won't), then no records are returned. What I really need is this (note the where is gone and the condition is moved to the left join):
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id` and `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
I saw in the documentation I can do something like:
has_one :article_vote, -> { where(user_id: 1) }
Which will put it in the left join vs the where, but this doesn't let me specify the user_id at query time, which makes it not viable.
Can this be done?

group by in view or controller

i am trying to loop through a list of products which a supplier sells via its different variants. i can get the list of products to display, but i wish to group these by the product id as to only display it once.
in my controller i have
#supplier = Supplier.joins(products: :variants).find(params[:id])
in my view i have
- #supplier.variants.group_by(&:product_id).each do |product_id, item|
= render :partial => 'product', :locals => {:item => item }
and my partial
= link_to shopping_supplier_path(item) do
%li.mdl-list__item.mdl-list__item--three-line
%span.mdl-list__item-primary-content
%span= item.product.name
%span.mdl-list__item-text-body
= item.product.description.downcase
%span.mdl-list__item-secondary-content
%i.material-icons
chevron_right
%hr
which when the sql executes returns the following query
Started GET "/shopping/suppliers/latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1" for 127.0.0.1 at 2016-04-19 23:22:08 +1000
Processing by Shopping::SuppliersController#show as HTML
Parameters: {"id"=>"latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"}
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 1]]
Supplier Load (32.7ms) SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."permalink" = $1 ORDER BY "suppliers"."id" ASC LIMIT 1 [["permalink", "latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"]]
Supplier Load (41.9ms) SELECT "suppliers".* FROM "suppliers" INNER JOIN "variant_suppliers" ON "variant_suppliers"."supplier_id" = "suppliers"."id" INNER JOIN "variants" ON "variants"."id" = "variant_suppliers"."variant_id" INNER JOIN "products" ON "products"."id" = "variants"."product_id" INNER JOIN "variants" "variants_products" ON "variants_products"."product_id" = "products"."id" WHERE "suppliers"."permalink" = $1 ORDER BY "suppliers"."id" ASC LIMIT 1 [["permalink", "latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"]]
Variant Load (0.9ms) SELECT "variants".* FROM "variants" INNER JOIN "variant_suppliers" ON "variants"."id" = "variant_suppliers"."variant_id" WHERE "variant_suppliers"."supplier_id" = $1 [["supplier_id", 1]]
Rendered shopping/suppliers/_product.html.haml (53.5ms)
error
NoMethodError at /shopping/suppliers/latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1
undefined method `name' for #<Array:0x007faa5302d5a0>
Use SQL joins. When you create a query joining the tables that you are going to use, they will be previously loaded in memory, so the famous n+1 queries will not occur.
#supplier = Supplier.joins(variants: :products).find(params[:id])
# This will translate to something like this
SELECT * FROM suppliers
INNER JOIN variants ON variants.supplier_id = suppliers.id
INNER JOIN products ON products.variant_id = variants.id
WHERE suppliers.id = ?
Remember to always avoid the lazy loading of your associations.

Rails: How would I retrieve records where child element includes any of another array?

I have a User and Document model, which both have tag_lists (from acts_as_taggable)
I want to find the Documents that have any of the tags a certain user has. So for example,
user.tag_list = ["ceo" , "sales"]
documentA.tag_list = ["ceo"]
documentB.tag_list = ["all-users']
documentC.tag_list = ["sales", "marketing"]
documentD.tag_list = ["marketing"]
I want to do something like
Document.where(tag_list.includes_any?("all-users" || user.tag_list))
-> which would retrieve document A,B,C.
Obviously the syntax is wrong, but you get the idea. How would I do this in an efficient way?
-------update
Just fyi, the tag_list is not a direct attribute of either User or Document model, but a active record relation from the acts_as_taggable gem: how would I call this in a .where operator?
2.1.1 :036 > Document.last.tag_list
Document Load (3.7ms) SELECT "assignable_objects".* FROM "assignable_objects" WHERE "assignable_objects"."type" IN ('Document') ORDER BY "assignable_objects"."id" DESC LIMIT 1
ActsAsTaggableOn::Tag Load (3.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL) [["taggable_id", 52], ["taggable_type", "AssignableObject"]]
=> []
This might help you:
Document.where((tag_list-["all-users", user.tag_list].flatten).size != tag_list.size)

Resources