Combine a join query with a normal condition - ruby-on-rails

Users have a main category and multiple sub-categories
I would like to get all users who belong to a category, regardless if it is their main or sub.
#users = User.joins(:sub_categories).where('sub_category_id = ? OR type = ?', #sub_category.id, "User::#{category}User").page(params[:page])
A user's main category is also their STI type.
The query works, however I am getting duplicate results when trying to include the user's main type.
The query that is generated is:
User Load (0.0ms) SELECT "users".* FROM "users" INNER JOIN "user_sub_categories_users" ON "user_sub_categories_users"."user_id" = "users"."id" INNER JOIN "user_sub_categories" ON "user_sub_categories"."id" = "user_sub_categories_users"."sub_category_id" WHERE "users"."deleted_at" IS NULL AND (sub_category_id = 1 OR type = 'User::ModelUser') ORDER BY "users"."created_at" DESC LIMIT 20 OFFSET 0
EDIT: A user can not belong to a sub-category if its their main, so its safe to simply join the two conditions together

Because there is more than one group for each user, your join is creating multiple rows for each user, e.g.:
User Group
-------|------
UserA |GroupA
UserA |GroupB
UserB |GroupA
UserC |GroupA
UserC |GroupB
Three users, but five rows!
You can safely add a .uniq on the end of your query if you're just interested in the distinct users. In the context of an ActiveRecord query, .uniq will be converted to SQL's DISTINCT().

Related

StatementInvalid Rails Query

I've got the following query that works:
jobs = current_location.jobs.includes(:customer).all.where(complete: complete)
However, when I add a where clause to query the first name of the customer table, I get an error.
jobs = current_location.jobs.includes(:customer).all.where(complete: complete).where("customers.fist_name = ?", "Bob")
Here is the error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "customers"
LINE 1: ...bs"."complete" = $2 AND "jobs"."status" = $3 AND (customers....
^
: SELECT "jobs".* FROM "jobs" INNER JOIN "jobs_users" ON "jobs"."id" = "jobs_users"."job_id" WHERE "jobs_users"."user_id" = $1 AND "jobs"."complete" = $2 AND "jobs"."status" = $3 AND (customers.last_name = 'Bob') ORDER BY "jobs"."start" DESC LIMIT $4 OFFSET $5
The current_location method:
def current_location
return current_user.locations.find_by(id: cookies[:current_location])
end
Location Model
has_many :jobs
has_and_belongs_to_many :customers
Job Model
belongs_to :location
belongs_to :customer
Customer Model
has_many :jobs
has_and_belongs_to_many :locations
How can I fix this issue?
includes will only join the table if you set a reference to the association.
When using includes you ensure a reference to the association in 2 fashions:
You can use the references method this will join the table whether or not there are any query conditions (If you MUST use raw SQL as shown in your question then this is the method you would need to use) e.g.
current_location.jobs
.includes(:customer)
.references(:customer)
Or you can use the hash finder version of where: (Please note that when using an associative reference in the where clause you must reference the table name, in this case customers and not the association name customer)
current_location.jobs
.includes(:customer)
.where(customers: {first_name: "Bob" })
Both of these will eager load the customer for the jobs referenced.
The first option (references) will OUTER JOIN the customers table so that all the jobs are loaded even if they have no customers as long as no query conditions reference the customers table.
The second option (using where) will OUTER JOIN the customers table but given the query parameter against the customers table it will act very much like an INNER JOIN.
If you only need to search the jobs based on customer information then joins is a better choice as this will create an INNER JOIN with the customers table but will not try to load any of the customer data in the query e.g.
current_location.jobs.joins(:customer).where(customers: {first_name: "Bob" })
joins will always include the associated table regardless of a reference in the query.
Sidenote: the all in both your queries is completely unnecessary
includes(:customer) does not necessarily join the customers table into the SQL query. You need to use joins(:customer) to force Rails to join the customers table into the SQL query and make it available to query conditions.
jobs = current_location.jobs
.joins(:customer)
.includes(:customer)
.where(complete: complete)
.where(customers: { first_name: 'Bob' })

Count records after where clause

I have three models: Catalog, Upload and Product. A product belongs to a catalog, and an upload belongs to a product.
I need to count the number of uploads for all the products of a given catalog.
This is the way I've been doing it so far, which is incredibly slow for a large amount of uploads or products:
#products = Product.where(catalog_id: 123)
#uploads_count = Upload.where(product_id: #products.pluck(:id)).count
I'd like to avoid loading all the products just for a count.
Should I use raw SQL or is there a better way to do this with ActiveRecord ?
This should do it for you:
Upload.joins(:product).where(products: { catalog_id: 123 }).count
Using joins creates an INNER JOIN between the two tables, allowing you to query the products table as above.
Note the singular and plural uses of product - the joins should reflect the association (the upload belongs to one product), while the where clause always uses the table name, typically pluralised.
The SQL will look similar to:
SELECT "uploads".* FROM "uploads"
INNER JOIN "products"
ON "products"."id" = "uploads"."product_id"
WHERE "products"."catalog_id" = 123
If you need to have more information on the catalog you can also include this, something like the following:
Upload.joins(product: :catalog).where(products: { catalogs: { whatever: 'you want to query' } }).count
Bear in mind, using joins is just for a query such as this. If you need to access attributes of the product or catalog, you should use another approach, such as includes, to preload the data and avoid N + 1 queries. There's a good read here if you're interested.
Another way to avoid selecting records is to use sub-query. This can be done the following way:
query = User.where(id: 1..100)
User.where(id: query.select(:id)).count
# [DEBUG] (10.5ms) SELECT COUNT(*) FROM "users" WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE ("users"."id" BETWEEN $1 AND $2)) [["id", 1], ["id", 100]]
# => 33
So, User.where(id: 1..100) prepares a query, that can be used as a sub-select. .select(:field) tells what field you are interested in.
Though for a basic count, SRack provides a good answer.

Rails Postgres query to exclude any results that contain one of three records on join

This is a hard problem to describe but I have Rails query where I join another table and I want to exclude any results where the join table contain one of three conditions.
I have a Device model that relates to a CarUserRole model/record. In that CarUserRole record it will contain one of three :role - "owner", "monitor", "driver". I want to return any results where there is no related CarUserRole record where role: "owner". How would I do that?
This was my first attempt -
Device.joins(:car_user_roles).where('car_user_roles.role = ? OR car_user_roles.role = ? AND car_user_roles.role != ?', 'monitor', 'driver', 'owner')
Here is the sql -
"SELECT \"cars\".* FROM \"cars\" INNER JOIN \"car_user_roles\" ON \"car_user_roles\".\"car_id\" = \"cars\".\"id\" WHERE (car_user_roles.role = 'monitor' OR car_user_roles.role = 'driver' AND car_user_roles.role != 'owner')"
Update
I should mention that a device sometimes has multiple CarUserRole records. A device can have an "owner" and a "driver" CarUserRole. I should also note that they can only have one owner.
Anwser
I ended up going with #Reub's solution via our chat -
where(CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not)
Since the car_user_roles table can have multiple records with the same car_id, an inner join can result in the join table having multiple rows for each row in the cars table. So, for a car that has 3 records in the car_user_roles table (monitor, owner and driver), there will be 3 records in the join table (each record having a different role). Your query will filter out the row where the role is owner, but it will match the other two, resulting in that car being returned as a result of your query even though it has a record with role as 'owner'.
Lets first try to form an sql query for the result that you want. We can then convert this into a Rails query.
SELECT * FROM cars WHERE NOT EXISTS (SELECT id FROM car_user_roles WHERE role='owner' AND car_id = cars.id);
The above is sufficient if you want devices which do not have any car_user_role with role as 'owner'. But this can also give you devices which have no corresponding record in car_user_roles. If you want to ensure that the device has at least one record in car_user_roles, you can add the following to the above query.
AND EXISTS (SELECT id FROM car_user_roles WHERE role IN ('monitor', 'driver') AND car_id = cars.id);
Now, we need to convert this into a Rails query.
Device.where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not
).where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: ['monitor', 'driver']).exists
).all
You could also try the following if your Rails version supports exists?:
Device.joins(:car_user_roles).exists?(role: ['monitor', 'driver']).exists?(role: 'owner').not.select('cars.*').distinct
Select the distinct cars
SELECT DISTINCT (cars.*) FROM cars
Use a LEFT JOIN to pull in the car_user_roles
LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id
Select only the cars that DO NOT contain an 'owner' car_user_role
WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner')
Select only the cars that DO contain either a 'driver' or 'monitor' car_user_role
AND (car_user_roles.role IN ('driver','monitor'))
Put it all together:
SELECT DISTINCT (cars.*) FROM cars LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner') AND (car_user_roles.role IN ('driver','monitor'));
Edit:
Execute the query directly from Rails and return only the found object IDs
ActiveRecord::Base.connection.execute(sql).collect { |x| x['id'] }

Rails group_by and sorting upon field in related model

I have a User that belongs to a User_type. In the user_type model, there's a field called position to handle the default sorting when displaying user_types and their users.
Unfortunataly this does not work when searching with Ransack, so I need to search from the User model and use group_by to group the records based on their user_type_id.
This works perfectly, but I need a way to respect the sorting that is defined in the user_type model. This is also dynamic, so there's no way of telling what the sorting is from the user model.
Therefor I think I need to loop through the group_by array and do the sorting manually. But I have no clue where to start. This is the controller method:
def index
#q = User.ransack(params[:q])
#users = #q.result(distinct: true).group_by &:user_type
end
How do I manipulate that array to sort on a field that in the related model?
Try to add this line to Usertype model
default_scope order('position')
First of all there is n+1 query problem. You are not joining user_types table to users and application calls SELECT on user_types n times where n is a number of Users + another one SELECT call to grab users:
...
UserType Load (0.2ms) SELECT "user_types".* FROM "user_types" WHERE "user_types"."id" = $1 LIMIT 1 [["id", 29]]
UserType Load (0.2ms) SELECT "user_types".* FROM "user_types" WHERE "user_types"."id" = $1 LIMIT 1 [["id", 7]]
...
So you need to include user_types and order by user_types.position:
#q.result(distinct: true).includes(:user_type).order('user_types.position')
There are a lot of examples for ordering here:
http://apidock.com/rails/ActiveRecord/QueryMethods/order
Your case (Ordering on associations) is also available
Information about n+1 query:
What is SELECT N+1?

Sorting 2 arrays that have been added together

In my app, users can create galleries that their work may or may not be in. Users have and belong to many Galleries, and each gallery has a 'creator' that is designated by the gallery's user_id field.
So to get the 5 latest galleries a user is in, I can do something like:
included_in = #user.galleries.order('created_at DESC').uniq.first(5)
# SELECT DISTINCT "galleries".* FROM "galleries" INNER JOIN "galleries_users" ON "galleries"."id" = "galleries_users"."gallery_id" WHERE "galleries_users"."user_id" = 10 ORDER BY created_at DESC LIMIT 5
and to get the 5 latest galleries they've created, I can do:
created = Gallery.where(user_id: id).order('created_at DESC').uniq.first(5)
# SELECT DISTINCT "galleries".* FROM "galleries" WHERE "galleries"."user_id" = 10 ORDER BY created_at DESC LIMIT 5
I want to display these two together, so that it's the 5 latest galleries that they've created OR they're in. Something like the equivalent of:
(included_in + created).order('created_at DESC').uniq.first(5)
Does anyone know how to construct an efficient query or post-query loop that does this?
order isn't available, but you can use sort - or sort_by as suggested by jvnill in this answer
As you've stated, you can use the following code:
(included_in + created).sort_by(&:created_at).uniq.first(5)
How about:
Gallery.joins("LEFT JOIN galleries_users ON galleries.id = galleries_users.gallery_id")
.where("galleries_users.user_id = :user_id OR galleries.user_id = :user_id", user_id: id)
.order("galleries.created_at DESC")
.limit(5)
I'm assuming the name of your HABTM join table is galleries_users (which is the default).
Union the two queries and select the order from the union.

Resources