Inner Join in Rails options_from_collection_for_select - ruby-on-rails

I have created below join query to fetch group name details.I am using below query in postgresql.
select DISTINCT e.group_id,e.groupname from user_group_table u, execution_groups e where (u.user_id=e.user_id or u.group_id=e.group_id) and u.user_id=12;
How to write above join query using "options_from_collection_for_select" in rails?
I have tried below code
user_groups = UserGroupTable.where(:user_id => id)
#execution_group_options = ExecutionGroup.where(user_id: user_groups.select(:user_id)).or(ExecutionGroup.where(group_id: user_groups.select(:group_id)))
Getting below error
SELECT "execution_groups".* FROM "execution_groups" WHERE "execution_groups"."user_id" IN (SELECT "user_group_table"."user_id" FROM "user_group_table" WHERE "user_group_table"."user_id" = ?) [["user_id", 6]]
"Threw an exception inside test.new() undefined method `or' for #<ExecutionGroup::ActiveRecord_Relation:0x00007faf9e10ee38>"
Threw an exception inside test.new() undefined method `or' for #<ExecutionGroup::ActiveRecord_Relation:0x00007faf9e10ee38>

Let's start of by writing that query in the Rails query syntax.
I'll assume that user_group_table is available through the UserGroup model and execution_groups is available through the ExecutionGroup model.
# in controller
user_groups = UserGroup.where(user_id: 12)
#execution_group_options =
ExecutionGroup.where(user_id: user_groups.select(:user_id))
.or(ExecutionGroup.where(group_id: user_groups.select(:group_id)))
This produces the following SQL:
SELECT execution_groups.*
FROM execution_groups
WHERE
execution_groups.user_id IN (
SELECT user_group_table.user_id
FROM user_group_table
WHERE user_group_table.user_id = 12
)
OR
execution_groups.group_id IN (
SELECT user_group_table.group_id
FROM user_group_table
WHERE user_group_table.user_id = 12
)
Which should produce the same collection (assuming there are no ExecutionGroup records that have the same group_id and groupname).
With the above in place you can use the following in your view:
options_from_collection_for_select(#execution_group_options, 'group_id', 'groupname')
If there are ExecutionGroup records with both the same group_id and groupname. You can instead use:
options_for_select(#execution_group_options.distinct.pluck(:groupname, :group_id))
If you don't have access to or (introduced in Ruby on Rails 5), things become slightly more complicated. You should be able to use the internal arel interface.
# in controller
user_groups = UserGroup.where(user_id: 12)
exec_groups_table = ExecutionGroup.arel_table
#execution_group_options = ExecutionGroup.where(
exec_groups_table[:user_id].in(user_groups.select(:user_id).arel)
.or(exec_groups_table[:group_id].in(user_groups.select(:group_id).arel))
)
This should produce the exact same query. Other than the controller code nothing changes.

Related

Trouble with active record join tables that do not have any associations

I am trying to filter out all the objects(photos) that are not in a scheduled post table. I have the sql statement working:
Select * from photos p left join scheduled_posts sp on sp.photo_url = p.url where p.trashed = false and sp.photo_url is null
I am trying to get this to work in rails using active record but the two tables are not associated with each-other and I having trouble using .join because of this and am getting an undefined method 'join' error. Here is my current code:
#objects = "#{#objects.table_name}".joins("left join scheduled_posts sp on sp.photo_url = p.url ").where("photo_url #> ?", nil)
"#{#objects.table_name}" is a string, and #joins is not a method on String. Instead of calling it on the table name, you need to convert the table name to the name of the model class, and then call #constantize on it to get the class constant, which you can call #joins on.
classify(#objects.table_name).constantize.joins # ...

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

say `Post` is a model, a class that inherits from `ApplicationRecord`, in rails. Then, what does Post.arel_table.create_table_alias does?

Lets say I have this code:
new_and_updated = Post.where(:published_at => nil).union(Post.where(:draft => true))
post = Post.arel_table
Post.from(post.create_table_alias(new_and_updated, :posts))
I have this code from a post about arel, but does not really explains what create_table_alias does. Only that at the end the result is an active activeRecord::Relation object, that is the result of the previously defined union. Why is needed to pass :posts, as a second param for create_table_alias, is this the name of the table in the database?
The Arel is essentially as follows
alias = Arel::Table.new(table_name)
table = Arel::Nodes::As.new(table_definition,alias)
This creates a SQL alias for the new table definition so that we can reference this in a query.
TL;DR
Lets explain how this works in terms of the code you posted.
new_and_updated= Post.where(:published_at => nil).union(Post.where(:draft => true))
This statement can be converted into the following SQL
SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1
Well that is a great query but you cannot select from it as a subquery without a Syntax Error. This is where the alias comes in so this line (as explained above in terms of Arel)
post.create_table_alias(new_and_updated, :posts)
becomes
(SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1) AS posts -- This is the alias
Now the wrapping Post.from can select from this sub-query such that the final query is
SELECT
posts.*
FROM
(SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1) AS posts
BTW your query can be simplified a bit if you are using rails 5 and this removes the need for the rest of the code as well e.g.
Post.where(:published_at => nil).or(Post.where(:draft => true))
Will become
SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL OR posts.draft = 1
From the Rails official doc, from query method does this:
Specifies table from which the records will be fetched.
So, in order to fetch posts from the new_and_updated relation, we need to have an alias table which is what post.create_table_alias(new_and_updated, :posts) is doing.
Rubydoc for Arel's create_table_alias method tells us that the instance method is included in Table module.
Here :posts parameter is specifying the name of the alias table to create while new_and_updated provides ActiveRecord::Relation object.
Hope that helps.

Arel: Need help converting SQL statement into Arel code for Rails

Trying to get the following SQL to work in a Rails query.
The Query:
select addr.*
from addresses addr
join users u
on addr.addressable_type = 'User'
and addr.addressable_id = u.id
join customers c
on c.id = u.actable_id
and u.actable_type = 'Customer'
where c.account_id = 1
and c.site_contact = 't'
This is my Rails code:
# Inside my account.rb model
def site_addresses
a = Address.arel_table #Arel::Table.new(:addresses)
u = User.arel_table #Arel::Table.new(:users)
c = Customer.arel_table #Arel::Table.new(:customers)
# trying to debug/test by rendering the sql. Eventually, I want
# to return a relation array of addresses.
sql = Address.
joins(u).
on(a[:addressable_type].eq("User").and(a[:addressable_id].eq(u[:id]))).
joins(c).
on(c[:id].eq(u[:actable_id]).and(u[:actable_type].eq("Customer"))).
where(c[:account_id].eq(self.id).and(c[:site_contact].eq(true))).to_sql
raise sql.to_yaml #trying to debug, I'll remove this later
end
end
I'm getting errors like "unknown class: Arel::Table". Im not using Arel correctly because the SQL code is valid (I can run it on the database just fine)
Try the following:
a.join(u).on(a[:addressable_type].eq("User")... # Using the arel_table and "join" instead
I based my answer from the docs:
users.join(photos).on(users[:id].eq(photos[:user_id]))

Define left join conditions when using .includes in ActiveRecord

I am trying to find the best way to include a referenced model on what is essentially a compound key.
I have ChecklistItem (a list of things to do daily) and then ChecklistChecks (which ties the ChecklistItem together with a User for a particular day. These checklists can either be for all Stores (with a null store_id) or for a particular Store.
This pulls all of the ChecklistItems and their associated checks:
ChecklistItem.includes(:checklist_checks).where(store_id: [nil,#store.id], list_type: 'open')
The problem is that there would be checks from multiple days in there. What I need is to pull all of the ChecklistItems and any checks from a specific day.
I tried adding conditions like this:
ChecklistItem.includes(:checklist_checks).where(store_id: [nil,#store.id], list_type: 'open', checklist_checks: {store_id: #store.id, report_date: #today})
The problem is that will only pull ChecklistItems that have an associated ChecklistCheck.
It is generating SQL that is essentially:
SELECT
checklist_items.*,
checklist_checks.*
FROM
checklist_items
LEFT OUTER JOIN
checklist_checks
ON
checklist_checks.checklist_item_id = checklist_items.id
WHERE
checklist_items.list_type = 'open'
AND
checklist_checks.store_id = 1
AND
checklist_checks.report_date = '2015-05-03'
AND
(checklist_items.store_id = 1 OR checklist_items.store_id IS NULL)
I think the problem is that the conditions on checklist_checks is in the WHERE clause. If I could move them to the ON clause of the join, everything would work.
Is there a Rails way to end up with something like this?
SELECT
checklist_items.*,
checklist_checks.*
FROM
checklist_items
LEFT OUTER JOIN
checklist_checks
ON
checklist_checks.checklist_item_id = checklist_items.id
AND
checklist_checks.store_id = 1
AND
checklist_checks.report_date = '2015-05-03'
WHERE
checklist_items.list_type = 'open'
AND
(checklist_items.store_id = 1 OR checklist_items.store_id IS NULL)
UPDATE:
I found this: enter link description here
It suggests using find_by_sql and then passing the result array and model to be included to ActiveRecord::Associations::Preloader.new.preload
I tried that, and my find_by_sql pulls the right stuff, but the id column is nil in the resulting objects.
#store = Store.find(1)
#today = Date.today - 1.days
#open_items = ChecklistItem.find_by_sql(["SELECT checklist_items.*, checklist_checks.* FROM checklist_items LEFT OUTER JOIN checklist_checks ON checklist_checks.checklist_item_id = checklist_items.id AND checklist_checks.store_id = ? AND checklist_checks.report_date = ? WHERE checklist_items.list_type='open' AND (checklist_items.store_id=? OR checklist_items.store_ID IS NULL)", #store.id, #today, #store_id])
ActiveRecord::Associations::Preloader.new.preload(#open_items, :checklist_checks)
> #open_items.first.name
=> "Turn on the lights"
> #open_items.first.id
=> nil
A solution using Arel to generate a custom join clause:
class ChecklistItem < ActiveRecord::Base
has_many :checklist_checks
# ...
def self.superjoin(date, store_id)
# build the ON clause for the join
on = Arel::Nodes::On.new(
Arel::Nodes::Equality.new(ChecklistChecks.arel_table[:checklist_item_id], ChecklistItem.arel_table[:id]).\
and(ChecklistItem.arel_table[:store_id].eq(1)).\
and(ChecklistChecks.arel_table[:report_date].eq(date))
)
joins(Arel::Nodes::OuterJoin.new(ChecklistChecks.arel_table, on))
.where(store_id: [nil, store_id], list_type: 'open' )
end
end
I bundled it up into a model method to make it easier to test in the rails console.
irb(main):117:0> ChecklistItem.superjoin(1,2)
ChecklistItem Load (0.5ms) SELECT "checklist_items".* FROM "checklist_items" LEFT OUTER JOIN "checklist_checks" ON "checklist_checks"."checklist_item_id" = "checklist_items"."id" AND "checklist_items"."store_id" = 1 AND "checklist_checks"."report_date" = 1 WHERE (("checklist_items"."store_id" = 2 OR "checklist_items"."store_id" IS NULL)) AND "checklist_items"."list_type" = 'open'
=> #<ActiveRecord::Relation []>

Resources