ruby-on-railsmobility

Unable to order by translated column in Rails using Mobility


This question is based on an issue posted to the Mobility GitHub project.

Context

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?


Solution

  • 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')