1

I have a simple problem with the mobility gem. I have a simple relation in my models. Let's say Company has many Employees, and Employees have a translated attribute :job_function that uses backend: :table.

class Company < ApplicationRecord
    has_many :employees
end

class Employee < ApplicationRecord
    extend Mobility

    translates :job_function, type: :string, locale_accessors: true, backend: :table
end

If I try to do:

Company.first.employees.map(&:job_function)

I get the n+1 problem. Each of the :job_function translations is loaded individually.

How do I tell Mobility to eager load them all in one go before I start mapping over the collection?

I could not find any example of this in the documentation...

Mladen Danic
  • 3,600
  • 2
  • 25
  • 24

2 Answers2

1

You can just use pluck, which is supported by Mobility:

Company.first.employees.i18n.pluck(:job_function)
Chris Salzberg
  • 27,099
  • 4
  • 75
  • 82
  • i tried but the `pluck` return nil – Lam Phan Sep 25 '21 at 15:52
  • but `Company.first.employees.i18n.pluck(:job_function)` work. you missed `i18n` – Lam Phan Sep 25 '21 at 16:22
  • Thanks! That's correct, updated the answer. – Chris Salzberg Sep 26 '21 at 00:58
  • 1
    Salzberge, did you have any simple solution in case we want to pre-load employees with their translations (and avoid N+1)?, i mean maybe the OP not only query the translation job_functions but also need to do something with employees, the pluck will not allow to load employees. – Lam Phan Sep 26 '21 at 10:21
-1

You could includes employees + theirs job_function translation, it's n+2 query

here's my demo, with Product model contains many Versions model which has been set up mobility for the name attribute, those corresponding to your Company, Employee, job_function respectively.

class Version < ApplicationRecord
  belongs_to :product

  extend Mobility
  translates :name, locale_accessors: [:en, :ja], backend: :table
end

class Product < ApplicationRecord
 has_many :versions

 has_many :translation_versions, -> { 
   i18n{name.not_eq(nil)}
   .select("versions.*, 
            version_translations_#{Mobility.locale}.name AS translate_name")
  }, class_name: "Version"
end

With default locale :en

Product.includes(:translation_versions).first.translation_versions
   .map(&:translate_name)

# SELECT "products".* FROM "products" ...
# SELECT versions.*, version_translations_en.name AS translate_name 
#  FROM "versions" LEFT OUTER JOIN "version_translations" "version_translations_en" 
#  ON "version_translations_en"."version_id" = "versions"."id" 
#  AND "version_translations_en"."locale" = 'en' 
#  WHERE "version_translations_en"."name" IS NOT NULL AND "versions"."product_id" = ? ...

# => ["v1", "v2"]

With locale :ja

Mobility.locale = :ja
Product.includes(:translation_versions).first.translation_versions
   .map(&:translate_name)
# ...
# => ["ja v1", "ja v2"]

So just one query.

In case your backend setting is KeyValue, the translation tables separate not only locale but also the type (String, Text, ...), but you already decide which type for the attribute, right ? for example: name:string. So that you still only need to setup dynamic locale.

class Product < ApplicationRecord
 has_many :translation_versions, -> { 
  i18n{name.not_eq(nil)}
   .select("versions.*, 
     Version_name_#{Mobility.locale}_string_translations.value AS translate_name")
  }, class_name: "Version"
end

the query is same as above.

Of course, you could make translation_versions more generally by separate it into a module, replace Version by class name, name by the target attribute and make a dynamic function something like translation_#{table_name}.

Lam Phan
  • 3,405
  • 2
  • 9
  • 20