Check if ActiveRecord::Relation alread includes JOIN - ruby-on-rails

I'm inside method that adds filter (user.type) to my query/relation.
Sometimes if grouping by the user (which needs INNER join to users table in another module) is selected before filtering I receive an error:
PostgreSQL: PG::DuplicateAlias: ERROR: table name "users" specified more than once
Before error happen JOIN is already in query -
$ pry> relation.to_sql
SELECT \"posts\".* FROM \"posts\"
INNER JOIN users ON users.id = posts.user_id
WHERE \"posts\".\"created_at\" BETWEEN '2019-05-01 00:00:00'
AND '2020-05-01 23:59:59' AND \"users\".\"type\" = 'Guest'"
I wanna fix it, by checking if the table is already joined inside my ActiveRecord::Relation object. I added:
def join_users
return relation if /JOIN users/.match? relation.to_sql
relation.joins('LEFT JOIN users ON users.id = posts.user_id')
end
This solution works, but I wonder - is there any better way to check if JOIN is inside relation?

Perhaps you can use joins_values, which isn't documented, but is an ActiveRecord_Relation public method that returns an array containing the name of the table the current query (object) is constructed with:
Post.joins(:user).joins_values # [:user]
Post.all.joins_values # []

if simple join
Post.joins(:user)
you can find via joins_values
so it will look like Post.joins(:user).joins_values # [:user]
if post has left joins
Post.left_joins(:user)
you can find via left_outer_joins_values
So in this case if you write Post.joins(:user).joins_values # []
so you can fix it by writing Post.joins(:user).left_outer_joins_values # [:user]

Related

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.

Rails joins doesn't return values from joined table

It must be something pretty simple i'm doing wrong. i'm trying to join 3 tables together, where group contains a location_id which references a row in location and a info_id which does the same for info.
Here's my code:
#groups = Group.joins(
'INNER JOIN location on location.id = "group".id',
'INNER JOIN info on info.id = "group".id'
)
This seems to work without errors, but all i'm getting back is the column from my group table. What am I doing wrong here?
P.S. my associations are location and info belong_to group. and group has_one of location and info
Just add a select tag to select the desired columns.
Group.joins(:location, :info).select("location.*, info.*")
You may add some where class if need some special conditions too.
Group.joins(:location, :info).select("location.*, info.*").where("locations.id = conditions.id")
When you say that you have location_id and info_id in groups table, this means that you intend to have belongs_to association in Group.
Pre-requisite:
Group model should have:
belongs_to :location
belongs_to :info
Location and Info should have :
has_one :group
For your question, this is expected. In ActiveRecord, Relation will return the object of the invoker, in this case Group.
For your use case, I think you will need this approach to get the right join working:
Query:
#groups = Group.joins(:location, :info)
# "SELECT `groups`.* FROM `groups` INNER JOIN `locations` ON `locations`.`id` = `groups`.`location_id` INNER JOIN `infos` ON `infos`.`id` = `groups`.`info_id`"
After this, you can iterate on each group to get info and location as something like: #groups.map { |group| [group.location, group.info] }
This will give correct location and info.
Optimization: [group.location, group.info] will make queries again to get location and info. You can optimize this by changing the original query to include location and info data:
#groups = Group.joins(:location, :info).includes(:location, :info)
In Rails / AR we have includes and joins.
You are using joins. As the SQL shows, it only selects Group. You use joins when you need Group results but also want to query through Location and Info.
If you would use all information, which I suspect is your case, like to display details in a table, you should also use includes.
# #groups = Groups.includes(:locations, :info) results an left outer join
#groups = Groups.joins(:locations, :info).includes(:locations, :info) # results inner join
Now when you do some thing like this it will not make additional db calls. But if you use joins it use multiple queries (N+1).
- #groups.each do |group|
tr
td = group.id
td = group.location.lat
td = group.info.description
Use the bullet gem to find if you have such N+1 queries to optimize your project.
If you Google Rails includes vs joins you will find more information on the topic.
If you used this:
Group.joins(:location, :info).select("location.*, info.*").where("locations.id =
conditions.id")
and nothing changed
It is because the column names of the two tables are the same.

How to write query in active record to select from two or more tables in rails 3

I don't want to use join
I want to manually compare any field with other table field
for example
SELECT u.user_id, t.task_id
FROM tasks t, users u
WHERE u.user_id = t.user_id
how can i write this query in Rails ??
Assuming you have associations in your models, you can simply do as follow
User.joins(:tasks).select('users.user_id, tasks.task_id')
you can also do as follow
User.includes(:tasks).where("user.id =tasks.user_id")
includes will do eager loading check the example below or read eager loading at here
users = User.limit(10)
users.each do |user|
puts user.address.postcode
end
This will run 11 queries, it is called N+1 query problem(first you query to get all the rows then you query on each row again to do something). with includes Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
Now when you do;
users = User.includes(:address).limit(10)
user.each do |user|
puts user.address.postcode
end
It will generate just 2 queries as follow
SELECT * FROM users LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.user_id IN (1,2,3,4,5,6,7,8,9,10))
Plus if you don't have associations then read below;
you should be have to look at http://guides.rubyonrails.org/association_basics.html
Assuming your are trying to do inner join, by default in rails when we associate two models and then query on them then we are doing inner join on those tables.
You have to create associations between the models example is given below
class User
has_many :reservations
...# your code
end
And in reservations
class Reservations
belongs_to :user
... #your code
end
Now when you do
User.joins(:reservations)
the generated query would look like as follow
"SELECT `users`.* FROM `users` INNER JOIN `reservations` ON `reservations`.`user_id` = `users`.`id`"
you can check the query by doing User.joins(:reservations).to_sql in terminal
Hopefully it would answer your question
User.find_by_sql("YOUR SQL QUERY HERE")
You can use as follows..
User.includes(:tasks).where("user.id =tasks.user_id").order(:user.id)

Rails: How to sort many-to-many relation

I have a many-to-many relationship between a model User and Picture. These are linked by a join table called Picturization.
If I obtain a list of users of a single picture, i.e. picture.users -> how can I ensure that the result obtained is sorted by either creation of the Picturization row (i.e. the order at which a picture was associated to a user). How would this change if I wanted to obtain this in order of modification?
Thanks!
Edit
Maybe something like
picture.users.where(:order => "created_at")
but this created_at refers to the created_at in picturization
Have an additional column something like sequence in picturization table and define sort order as default scope in your Picturization
default_scope :order => 'sequence ASC'
If you want default sort order based on modified_at then use following default scope
default_scope :order => 'modified_at DESC'
You can specify the table name in the order method/clause:
picture.users.order("picturizations.created_at DESC")
Well, in my case, I need to sort many-to-many relation by a column named weight in the middle-table. After hours of trying, I figured out two solutions to sort many-to-many relation.
Solution1: In Rails Way
picture.users.where(:order => "created_at")
cannot return a ActiveRecord::Relation sorted by Picturization's created_at column.
I have tried to rewrite a default_scope method in Picturization, but it does not work:
def self.default_scope
return Picturization.all.order(weight: :desc)
end
Instead, first, you need to get the ids of sorted Picturization:
ids = Picturization.where(user_id:user.id).order(created_at: :desc).ids
Then, you can get the sorted objects by using MySQL field functin
picture.users.order("field(picturizations.id, #{ids.join(",")})")
which generates SQL looks like this:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1#for instance
ORDER BY field(picturizations.id, 9,18,6,8,7)#for instance
Solution2: In raw SQL Way
you can get the answer directly by using an order by function:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1
order by picturizations.created_at desc

How do I get Rails ActiveRecord to generate optimized SQL?

Let's say that I have 4 models which are related in the following ways:
Schedule has foreign key to Project
Schedule has foreign key to User
Project has foreign key to Client
In my Schedule#index view I want the most optimized SQL so that I can display links to the Schedule's associated Project, Client, and User. So, I should not pull all of the columns for the Project, Client, and User; only their IDs and Name.
If I were to manually write the SQL it might look like this:
select
s.id,
s.schedule_name,
s.schedule_type,
s.project_id,
p.name project_name,
p.client_id client_id,
c.name client_name,
s.user_id,
u.login user_login,
s.created_at,
s.updated_at,
s.data_count
from
Users u inner join
Clients c inner join
Schedules s inner join
Projects p
on p.id = s.project_id
on c.id = p.client_id
on u.id = s.user_id
order by
s.created_at desc
My question is: What would the ActiveRecord code look like to get Rails 3 to generate that SQL? For example, somthing like:
#schedules = Schedule. # ?
I already have the associations setup in the models (i.e. has_many / belongs_to).
I think this will build (or at least help) you get what you're looking for:
Schedule.select("schedules.id, schedules.schedule_name, projects.name as project_name").joins(:user, :project=>:client).order("schedules.created_at DESC")
should yield:
SELECT schedules.id, schedules.schedule_name, projects.name as project_name FROM `schedules` INNER JOIN `users` ON `users`.`id` = `schedules`.`user_id` INNER JOIN `projects` ON `projects`.`id` = `schedules`.`project_id` INNER JOIN `clients` ON `clients`.`id` = `projects`.`client_id`
The main problem I see in your approach is that you're looking for schedule objects but basing your initial "FROM" clause on "User" and your associations given are also on Schedule, so I built this solution based on the plain assumption that you want schedules!
I also didn't include all of your selects to save some typing, but you get the idea. You will simply have to add each one qualified with its full table name.

Resources