Unable to order by translated column in Rails using Mobility - ruby-on-rails

This question is based on an issue posted to the Mobility GitHub project.
Context
Rails: 5.0.6
Mobility: 0.4.2 (with table backend)
I'm working with an articles table that supports multiple article types (e.g., blog post, case study, knowledge base article). This table includes a column to track the number of times an article is viewed—an integer column that increments every time a show action is called for an article.
In implementing translations for these articles, I want to track the number of views for each translation individually, not for the main article object. In an effort to achieve this, I included the views property as one of the translated properties on my object:
class Article < ApplicationRecord
include Taggable
include PgSearch
translates :title, type: :string
translates :subtitle, type: :text
translates :body, type: :text
translates :views, type: :integer
multisearchable :against => [:title, :subtitle, :body]
has_and_belongs_to_many :products
attachment :hero_image, content_type: %w(image/jpeg image/png image/gif)
validates :title, :body, :posted_on, presence: true
scope :current, -> { where 'posted_on < ?', Date.tomorrow }
scope :news_articles, -> { where type: ['BlogPost', 'CaseStudy'] }
def log_view(by = 1)
self.views ||= 0
self.views += by
self.save(touch: false)
end
def to_param
"#{id} #{title}".parameterize
end
def published?
posted_on < Date.tomorrow
end
end
Expected Behavior
In my controller, I want to list the top ten most viewed articles, which I get with this query:
#top_articles = Article.current.news_articles.order(views: :desc, posted_on: :desc).limit(10)
I expect to receive an array of articles, as I did before implementing Mobility.
Actual Behavior
What I get instead is #<Article::ActiveRecord_Relation:0x233686c>. If I then try to convert that to an array with #top_articles.to_a, I get this error:
Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1 [["LIMIT", 10]]
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column articles.views does not exist
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
Changing the query to include i18n:
#top_articles = Article.i18n.current.news_articles.order(views: :desc, posted_on: :desc).limit(10)
… returns #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods:0x00000000050a86d8>:0x286c3e0>, and when I try to convert that to an array, I get the same thing:
Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1 [["LIMIT", 10]]
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column articles.views does not exist
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
It turns out that while Mobility supports the use of translated fields in the where clause, it does not currently support them in the order clause of an Active Record query.
Workaround attempts
1. Reference the translation table in the order clause
Based on feedback from the gem author, I tried the query:
Article.i18n.current.news_articles.order('article_translations.views desc', 'articles.posted_on desc')
… which returns a #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods>> object, and to_a returns this error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "article_translations"
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY article_tr...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-12') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY article_translations.views desc, articles.posted_on desc LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
2. Add a joins or includes clause for the translation table
Article.i18n.joins(:article_translations).order('article_translations.views desc', 'articles.posted_on desc').limit(10)
This query, again, returns a #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods>> object, and to_a results in:
ActiveRecord::ConfigurationError: Can't join 'Article' to association named 'article_translations'; perhaps you misspelled it?
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/associations/join_dependency.rb:231:in `find_reflection'
3. Add has_many :article_translations to model
Adding a relation to the model throws back this error:
uninitialized constant Article::ArticleTranslation
So…
What should I try next?

UPDATE
Ordering by translated attributes is now supported as of version 0.8.0/
Just do this:
Article.i18n.current.news_articles.
order(:views => :desc, :'articles.posted_on' => :desc)
and Mobility will handle everything (you don't need to join the translation table, etc.)
ORIGINAL ANSWER
You were right that you need to join the translations table, but the association is named translations, not article_translations.
In any case, there is a method join_translations that joins the translation table, so this should work:
Article.i18n.
current.
news_articles.
join_translations.
order('article_translations.views desc', 'articles.posted_on desc')

Related

ActiveRecord query (includes or joins)

I have two classes:
class Customer
has_many :packages
end
class Package
belongs_to :customer
end
How can I do a query like this?
Customer.includes(:packages).where(packages: 'expires_at < Date.current')
With a sample test from console, I got it: Customer Load (26.0ms) SELECT "customers".* FROM "customers" INNER JOIN "packages" ON "packages"."customer_id" = "customers"."id" WHERE (packages.expires_at < '2019-03-13') LIMIT $1 [["LIMIT", 11]]
Traceback (most recent call last):
ActiveRecord::StatementInvalid (PG::UndefinedFunction: ERROR: operator does not exist: integer = uuid)
LINE 1: ...INNER JOIN "packages" ON "packages"."customer_id" = "custome...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "customers".* FROM "customers" INNER JOIN "packages" ON "packages"."customer_id" = "customers"."id" WHERE (packages.expires_at < '2019-03-13') LIMIT $1
irb(main):003:0>
You can pass the where expression within quotes and bind the value for expires_at.
Customer.includes(:packages).where('packages.expires_at < ?', Date.current)
You can do this:
Customer.includes(:packages).where('packages.expires_at < ?', Date.current).references(:packages)
Customer.joins(:packages).where("packages.expires_at < CURRENT_DATE")
or
Customer.includes(:packages).where("packages.expires_at < CURRENT_DATE").references(:packages)
You can do this:
Customer.includes([:packages]).where(["packages.expires_at < ?', Date.today]).references(:packages).order("Customers.created_at desc")

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column for `where` query when using includes

I have the following associations:
mobile_application.rb
has_many :events
event.rb
belongs_to :mobile_application
The following runs properly:
MobileApplication.includes(:events)
#=> MobileApplication Load (1.6ms) SELECT `mobile_applications`.* FROM `mobile_applications` ORDER BY created_at desc
# Event Load (1.3ms) SELECT `events`.* FROM `events` WHERE `events`.`mobile_application_id` IN (746, 745, 744, ....
but when I tried with the following,
MobileApplication.includes(:events).where("events.expiry_date >= ?", Time.zone.now)
it throws an error:
MobileApplication Load (1.2ms) SELECT `mobile_applications`.* FROM `mobile_applications` WHERE (events.expiry_date >= '2019-02-18 07:34:40.738517') ORDER BY created_at desc
Mysql2::Error: Unknown column 'events.expiry_date' in 'where clause': SELECT `mobile_applications`.* FROM `mobile_applications` WHERE (events.expiry_date >= '2019-02-18 07:34:40.738517') ORDER BY created_at desc
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'events.expiry_date' in 'where clause': SELECT `mobile_applications`.* FROM `mobile_applications` WHERE (events.expiry_date >= '2019-02-18 07:34:40.738517') ORDER BY created_at desc
Update
Please suggest how I can filter it. Using references also throws the following error & also got same with answer provided by Marek Lipka,
ActiveRecord::StatementInvalid: Mysql2::Error: Column 'created_at' in
order clause is ambiguous: SELECT `mobile_applications`.`id` AS t0_r0,
`mobile_applications`.`name` AS t0_r1, ...
I guess it is due to column ambiguity created by default_scope present in model to order it by created_at which is present in both associated tables.
By default, includes doesn't perform left join, it performs two separate DB queries instead. You can force left join using eager_load, like this:
MobileApplication.eager_load(:events).where('events.expiry_date >= ?', Time.zone.now)
or, if you in fact don't need eager loading (I don't know that), you can simply use joins:
MobileApplication.joins(:events).where('events.expiry_date >= ?', Time.zone.now)
About the error with using references or eager_load: You clearly try to do ordering by created_at somewhere (though you didn't include this in the question) like:
order('created_at DESC')
So, obviously, DB doesn't know what table you have in mind with joins, because there is created_at column both in mobile_applications or events. So you need to specify the 'target' table, like:
order('mobile_applications.created_at DESC')
From rails 4.x, you must add keyword references:
MobileApplication.includes(:events).references(:events).where("events.expiry_date >= ?", Time.zone.now)

Order an association in Active Record to override default sort order defined in association

So I have a class called agency and in that class I have the following:
class Agency
has_many :users, order: 'last_name ASC, first_name ASC'
end
And, when I do the following:
irb(main):004:0> agency.users.order('active desc').pluck(:active)
and that generates the following
SQL (22.0ms) SELECT "users"."active" FROM "users" WHERE "users"."agency_id" = 4040 ORDER BY last_name ASC, first_name ASC, active desc
So, what I want is to override the order in the agency class and not have it sort by last_name or first_name. How can I do that?
Use reorder
agency.users.reorder('active desc').pluck(:active)
The SQL generated would be
SELECT "users"."active" FROM "users" WHERE "users"."agency_id" = 4040 ORDER BY active desc
From the Guides,
The reorder method overrides the default scope order, for example:
class Article < ActiveRecord::Base
has_many :comments, -> { order('posted_at DESC') }
end
Article.find(10).comments.reorder('name')
The SQL that would be executed:
SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
In case the reorder clause is not used, the SQL executed would be:
SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC

Searching model based on relationships

I have a Topic model that belongs_to a Trip. The Trip has a start_date and end_date.
I want to find Topics based on the Trips date. How would I set up this query in rails?
Topic.joins(:trip).where('trip.start_date > ?', Time.now)
this throws the following error.
Topic Load (0.3ms) SELECT "topics".* FROM "topics" INNER JOIN "trips" ON "trips"."id" =
"topics"."trip_id" WHERE (trip.start_date < '2014-10-22 13:17:37.764743') ORDER BY created_at DESC
SQLite3::SQLException: no such column: trip.start_date: SELECT "topics".* FROM "topics" INNER JOIN
"trips" ON "trips"."id" = "topics"."trip_id" WHERE (trip.start_date < '2014-10-22 13:17:37.764743')
ORDER BY created_at DESC
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: trip.start_date: SELECT
"topics".* FROM "topics" INNER JOIN "trips" ON "trips"."id" = "topics"."trip_id" WHERE
(trip.start_date < '2014-10-22 13:17:37.764743') ORDER BY created_at DESC
How am I structuring this query wrong?
Table names in Rails are by convention plural (contrary to model names) and it's the case here, according to the log. So it should be:
Topic.joins(:trip).where('trips.start_date > ?', Time.now)

How to pass a Relation as a subquery in Rails 4?

The example below works in Rails 3 but fails in Rails 4.
The reason is that the Relation returned by accessing the association now uses actual database parameter rather than interpolating the "owning" id.
class Blog < ActiveRecord::Base
has_many :posts, :dependent => :destroy
end
class Post < ActiveRecord::Base
belongs_to :blog
end
b = Blog.first # provided something exists of course
query = Post.where(id: b.posts.where("'complicated query' = ''")); 1 # just to avoid printing in console
puts query.to_sql
# SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (SELECT "posts"."id" FROM "posts" WHERE "posts"."blog_id" = $1 AND ('complicated query' = ''))
query.to_a
# raises the error:
# PG::UndefinedParameter: ERROR: there is no parameter $1
# LINE 1: ...M "posts" WHERE "posts"."blog_id" = $1 AND
# ^
Note the $1 parameter in the subquery which is obviously not provided from the main query (thus the error).
So the question is how can we do the same thing in Rails 4 now (preferably with minimal changes)?
Answering my own question.
This actually isn't a breaking change in Rails 4. It should still works as expected.
It is squeel gem that breaks it https://github.com/ernie/squeel/issues/272

Resources