Order on a associated column - ruby-on-rails

I has Model that has_many ModelItems. ModelItems belongs_to OtherModel which has a 'code' column.
I am looking to do something like this:
Model.find(1).model_items.includes(:other_model).order("other_model.code" :desc)
I am trying to sort based on the text of that related code column.
I have even tried:
ModelItems.where(model_id: 1).includes(:other_model).order("other_model.code" :desc)
I know I need an include or join here but no matter what I do I get a variation of this error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "other_model"
UPDATE
This is an example of not using the real model names bites you. I had it right all along.
The include was singular and the order model name needed to be plural - for clarity lets change other_model to widgit:
Model.find(1).model_items.includes(:widgit).order("widgits.code ASC")

includes in this context will execute 2 queries to create a pseudo outer join (also preload) and is conceptually as follows
SELECT * FROM model_items WHERE model_items.model_id = 1
SELECT * FROM other_models WHERE other_models.model_item_id IN ( [IDS FROM PREVIOUS QUERY])
You can enforce single query execution in a few ways:
eager_load - (ModelItems.eager_load(:other_model)) This is how includes works when you have the join table referenced in a hash finder query condition or when you add references.
references - (ModelItems.includes(:other_model).references(:other_model)) This enforces the eager_load path for include
where Hash finder method (ModelItems.includes(:other_model).where(other_models: {name: 'ABC'})) Here includes intelligently realizes that you have placed a condition on the relationship with other_model and will automatically create the join so that the query is not malformed. However this sort of query represents as an outer join but performs like an inner join which is less efficient *
However if you do not need the information in other_model and just want to use this as a sort mechanism then you can use joins (INNER JOIN) or left_joins (OUTER JOIN) which will allow you to sort this data but will not retrieve the attributes or instantiate any related objects under the other_model relationship
ModelItems.joins(:other_model)
# OR
ModelItems.left_joins(:other_model)
*These options can be combined as well as in the case of the includes where hash finder method I always recommend the following ModelItems.joins(:other_model).includes(:other_model).where(other_models: { name: 'ABC'}) (INNER JOIN). This will return the same data set as ModelItems.includes(:other_model).where(other_models: {name: 'ABC'}) (LEFT OUTER JOIN) however by utilizing an INNER JOIN it becomes more efficient than its LEFT OUTER JOIN version
Sidenote order("other_models.code" :desc) this is not valid. Instead you need to include the order direction in the String or make that String and Symbol e.g. (("other_models.code DESC") or ("other_models.code": :desc))

Add references
ModelItems
.where(model_id: 1)
.includes(:other_model)
.references(:other_model)
.order("other_model.code DESC")

Related

Rails: How to force ActiveRecord generate alias for an association every time (just like Hibernate in Java does it), not only when it's ambiguous?

I work on a project where there is STI Item with 5 subclasses (Item1, Item2 ... Item5). This STI (items table) is mapped over a join table item_parents to Parent record (parents table) record. The mapping is done via has_many trough:.
Each of the items has two fields: name, code both are strings. Parent has many fields, but for the sake of example let's say it has name, created_at.
On the frontend, they are displayed in one table, like this:
Parent.name | Parent.created_at | Item1.name | Item1.code | Item2.name | Item2.code | ...
Users can configure filtering for each of the columns. It can be any combination or no filter at all. For example, they can choose the following combination:
Parent.created_at before 2020.02.22
Item1.name containing 'abc'
Item2.name containing 'xyz'
Item3.code equals 'Z12'
The filtering code implemented like this:
def search(filters)
filters.reduce(Parent.all) { |query, (key, value)| apply_filter(query, key, value) }
end
def apply_filter(query, key, value)
case filter_key
when :parent_name_contains
query.where(Parent.arel_table[:name].matches("%#{value}%"))
when :parent_created_at_before
query.where(Parent.arel_table[:created_at].lt(value))
when :item1_name_contains
query.joins(:item1s).where(Item1.arel_table[:name].matches("%#{value}%"))
when :item2_name_contains
query.joins(:item2s).where(Item2.arel_table[:name].matches("%#{value}%"))
when :item1_code_equals
query.joins(:item1s).where(Item1.arel_table[:name].eq(value))
when :item2_code_equals
query.joins(:item2s).where(Item2.arel_table[:name].eq(value))
# ... and so on for all the filters
else
query
end
end
The problem
When I query by fields of two or more different subclasses of Item, ActiveRecord fails to generate correct WHERE clause. It does not use the alias that it has assigned for the association in JOIN clause.
Let's say I want to filter by Item1.name = 'i1' and Item2.name = 'i2', then what rails generates is this:
SELECT "parents".*
FROM "parents"
INNER JOIN "item_parents"
ON "item_parents"."parent_id" = "parents"."id"
INNER JOIN "items"
ON "items"."id" = "item_parents"."item_id"
AND "items"."item_type" = 'Item::Item1'
INNER JOIN "item_parents" "item_parents_parents_join"
ON "item_parents_parents_join"."parent_id" = "parents"."id"
INNER JOIN "items" "item2s_parents" -- OK. join has an alias
ON "item2s_parents"."id" = "item_parents_parents_join"."item_id"
AND "item2s_parents"."item_type" = 'Item::Item2'
WHERE "items"."name" = 'i1'
AND "items"."name" = 'i2' -- Wrong! Must be "item2s_parents"."name" = 'i2'
As a result, I have zero rows returned, because it's impossible to have an item with name equal to 'i1' AND 'i2' at the same time.
What I tried
It seemed to be a good idea to write a custom joins_item method, that would dig the query and check whether it has other joins called on it before (AR stores such information in query.values[:joins] and query.values[:left_outer_joins]) and if there is, then it would return another Arel::Table instance having the correct alias. If there is nothing joined before, then I don't need alias and return the default Arel::Table.
But then I found out that AR resolves aliases at the moment of building SQL. So even though I could guess the correct alias (or no alias) at the moment of joining it can change in the end. And this is actually what happens when you do left_outer_joins first and then joins. AR always places INNER JOINs before LEFT OUTER JOINs in the resulting SQL.
So the question is...
Is there a way to force AR to alias everything when I do joins or left_outer_joins with Arel, or any other more or less maintainable workaround/fix/monkey patch for this issue?

Ambiguous reference on column when grouping by association

I'm grouping a list of Bug reports on a known collection of users that are related to the report (that is, the user that is responsible for the report and the user that is currently assigned to it).
The Model Bug (AR, Rails 4.2.x) thus has, among others, two associations assigned_to and responsible, which are resolved to the foreign keys assigned_to_id, responsible_id.
Bugs can also be related to a project, which may also have a responsible user set, thus they also possess a responsible_id foreign key.
As we're grouping on both attributes from the report itself and the associated project, we want to include the associated project in the returned query.
I can then get a hash count of <User> => count through the following statement, grouping on the association name of the bug report:
Bug.group(:assigned_to)
.includes(:project)
.references(:projects)
.count
which correctly produces the desired result: A collection of Users (assignees) and the Bugs they are being assigned to.
For responsibles, the same query:
Bug.group(:responsible)
.includes(:project)
.references(:projects)
.count
yields an error, since the attribute responsible_id is both contained in the query by bugs and the associated projects.
SELECT COUNT(DISTINCT "bugs"."id") AS count_id,
responsible_id AS responsible_id
FROM "bugs"
LEFT OUTER JOIN "projects" ON "projects"."id" = "bugs"."project_id"
GROUP BY "bugs"."responsible_id"
If I instead group on the explicit attribute itself using Bugs.group('bugs.responsible_id'), I get a valid response, however in the form of responsible_id => count.
SELECT COUNT(DISTINCT "bugs"."id") AS count_id,
bugs.responsible_id AS bugs_responsible_id
FROM "bugs"
LEFT OUTER JOIN "projects" ON "projects"."id" = "bugs"."project_id"
WHERE <condition>
GROUP BY bugs.responsible_id
Is there a way to force using the association, but namespace the query as in the second query?
Of course I could process the result and expand it to the responsible users, however since the grouping is part of a larger querying functionality, I only get to manipulate the grouping identifier without extensive changes to the query builder.
I don't think there is a fix for this now (in rails 4.2.4). This will however become easy in rails 5.
If you absolutely must solve the problem now, you could patch ActiveRecord::Calculations#execute_grouped_calculation with the fix available in rails 5 for your app. Simply add an initializer at config/initializers e.g. active_record_calculations_patch.rb with the following (abbreviated) content. You can copy the original code from your rails version and then add the fix:
module ActiveRecord
module Calculations
def execute_grouped_calculation(operation, column_name, distinct)
...
else
group_fields = group_attrs
end
# LINE OF CODE COPIED OVER FROM THE FIX
group_fields = arel_columns(group_fields)
# END OF COPIED OVER CODE
group_aliases = group_fields.map { |field|
column_alias_for(field)
...
end
end
end

Count, empty? fails for ActiveRecord with outer joins

I have two models, Monkey and Session, where Monkey has_many Session. I have a scope for Monkey:
scope :with_session_counts, -> {
joins("LEFT OUTER JOIN `sessions` ON `sessions`.`monkey_id` = `monkeys`.`id`")
.group(:id)
.select("`monkeys`.*, COUNT(DISTINCT `sessions`.`id`) as session_count")
}
in order to grab the number of associated Sessions (even when 0).
Querying #monkeys = Monkey.with_session_counts works as expected. However, when I test in my view:
<% unless #monkeys.empty?%>
I get this error:
Mysql2::Error: Column 'id' in field list is ambiguous:
SELECT COUNT(*) AS count_all, id AS id FROM `monkeys`
LEFT OUTER JOIN `sessions` ON `sessions`.`monkey_id` = `monkeys`.`id`
GROUP BY `monkeys`.`id`
How would I convince Rails to prefix id with the table name in presence of the JOIN?
Or is there a better alternative for the OUTER JOIN?
This applies equally to calling #monkeys.count(:all). I'm using RoR 4.2.1.
Update:
I have a partial fix for my issue (specify group("monkeys.id") explicitly) I wonder whether this is a bug in the code that generates the SELECT clause for count(:all). Note that in both cases (group("monkeys.id") and group(:id)) the GROUP BY part is generated correctly (i.e. with monkeys.id), but in the latter case the SELECT only contains id AS id. The reason I say 'partial' is because it works in that it does not break a call to empty?, but a call to count(:all) returns a Hash {monkey_id => number_of_sessions} instead of the number of records.
Update 2:
I guess my real question is: How can I get the number of associated sessions for each monkey, so that for all intents and purposes I can work with the query result as with Monkey.all? I know about counter cache but would prefer not to use it.
I believe it is not a bug. Like you added on your update, you have to specify the table that the id column belongs to. In this case group('monkeys.id') would do it.
How would the code responsible for generating the statement know the table to use? Without the count worked fine because it adds points.* to the projection and that is the one used by group by. However, if you actually wanted to group by Sessions id, you would have to specify it anyway.

Using Named Scopes as Subqueries in Rails

In my Rails app, I want to join one table with a named scope of another table. Is there a way to do this without having to rewrite the named scope in pure SQL for my join statement?
Basically, is there a way to do something like this?
class Foo < ActiveRecord::Base
scope :updated_today, where('updated_at > ?', DateTime.now.prev_day)
end
Bar.joins(Foo.updated_today)
Where Bar.joins generates the following SQL:
SELECT * FROM bars
INNER JOIN
(SELECT * FROM foos WHERE updated_at > 2012-8-9) AS t0
ON bar_id = bars.id
I don't believe there's any method specifically designed for doing this. You can, however, use the to_sql method of ActiveRecord::Relation to get the full SQL Query for the scope as a string, which you can then use as a subquery in a join statement, like so:
Bar.joins("INNER JOIN (#{Foo.updated_today.to_sql}) as t0 ON bar_id = bars.id")
You can use the merge method to join scopes to another model's query:
Bar.joins(:foos).merge(Foo.updated_today)
I haven't seen a ton of documentation on this (the Rails API doesn't even have any documentation on the method itself), but here is a pretty decent blog post giving a reasonably detailed example.
Also, just noticed that this is mentioned in a RailsCast on Advanced Queries in Rails 3.

Rails 3 Comparing foreign key to list of ids using activerecord

I have a relationship between two models, Registers and Competitions. I have a very complicated dynamic query that is being built and if the conditions are right I need to limit Registration records to only those where it's Competition parent meets a certain criteria. In order to do this without select from the Competition table I was thinking of something along the lines of...
Register.where("competition_id in ?", Competition.where("...").collect {|i| i.id})
Which produces this SQL:
SELECT "registers".* FROM "registers" WHERE (competition_id in 1,2,3,4...)
I don't think PostgreSQL liked the fact that the in parameters aren't surrounded by parenthesis. How can I compare the Register foreign key to a list of competition ids?
you can make it a bit shorter and skip the collect (this worked for me in 3.2.3).
Register.where(competition_id: Competition.where("..."))
this will result in the following sql:
SELECT "registers".* FROM "registers" WHERE "registers"."competition_id" IN (SELECT "competitions"."id" FROM "competitions" WHERE "...")
Try this instead:
competitions = Competition.where("...").collect {|i| i.id}
Register.where(:competition_id => competitions)

Resources