Skip to content

Postgres Backends (Column Attribute)

Chris Salzberg edited this page Jan 29, 2018 · 23 revisions

There are two PostgreSQL-specific backends which use a single column to store translations for a translated attribute: Jsonb and Hstore. Although the storage formats have many differences, the Mobility implementations are very similar, since in general we will be using them to store depth-1 strings (the Jsonb backend also supports storing other data formats such as integers, hashes, etc, with the same interface, see caveats below). Nonetheless, Jsonb would be the preferred choice over Hstore unless you have legacy constraints that require Hstore.

Setup

To use either of these backends, you will need to first add a column on your model for the translated attribute.

Rails/ActiveRecord

There is no Mobility generator for this since the normal migration generator works fine. So to add translated attributes title and content to a model Post, you would use:

rails generate migration AddTitleAndContentToPosts title:jsonb content:jsonb

For hstore, just replace "jsonb" with "hstore". Once the migration is created, make sure to change the actual migration to set the default value of the column(s) to an empty string (''), so it looks like this:

def change
  add_column :posts, :title, :jsonb, default: ''
  add_column :posts, :content, :jsonb, default: ''
end

Then run the migration with rake db:migrate to add the column(s) to your model table.

Using the backend is then as simple as setting your default backend to :jsonb (or :hstore), and defining the attributes on the model:

class Post < ApplicationRecord
  extend Mobility
  translates :title, :content
end

Translated attributes title and content are now available. Behind the scenes, when you store values in different locales, Mobility saves these to a single hash for the attribute. You can see the hash using read_attribute:

post = Post.create(title: "foo")
post.title
#=> "foo"
Mobility.with_locale(:ja) { post.title = "あああ" }
post.save
post = Post.first
post.read_attribute(:title)
#=> {"en"=>"foo", "ja"=>"あああ"}

Mobility also provides querying shortcuts (as with other backends), so you can easily query by translated attribute using the i18n scope (or i18n dataset for Sequel users):

Post.i18n.where(title: "foo").to_sql
#=> SELECT "posts".* FROM "posts" WHERE ("posts"."title" @> '{"en":"foo"}')

As with other backends, if you would prefer to directly extend the query methods on your model, you can set the default scope to i18n:

class Post < ApplicationRecord
  translates :title, :content
  default_scope { i18n }
end

Sequel

Mostly the same as above, except that there are no generators. Also, note that for Sequel you need to explicitly enable the pg_json or pg_hstore extensions before you can use jsonb/hstore columns.

If DB is your database instance, you can do this with:

DB.extension :pg_json

for jsonb, or

DB.extension :pg_hstore

for hstore. If you do not do this, you will get errors of the form: The AND operator requires at least 1 argument.

Caveats

Querying and storing non-string translated attributes on a jsonb column

Mobility allows you to translate any data type on a jsonb column. Thus it is possible to, say, store and translate arrays of data:

post.title = { a: "b" }
Mobility.with_locale(:ja) { post.title = { c: "d" } }
post.save
post["title"]
#=> {"en"=>{"a"=>"b"}, "ja"=>{"c"=>"d"}}
post.title_en
#=> {"a"=>"b"}
post.title_ja
#=> {"c"=>"d"}

You can also querying on hash-valued attributes:

Mobility.locale = :ja
Post.i18n.find_by(title: { c: "d" }) }
#=> #<Post id: ..., content: {"en"=>{"a"=>"b"},"ja"=>{"c"=>"d"}} ... >

Beware of querying on array values, however, since Mobility will interpret them not as an array but as a set of values to match, e.g.:

Post.i18n.find_by(title: ["foo", "bar"])

will look for posts with a title that is either "foo" or "bar", rather than a post with the array-valued title ["foo", "bar"]. See this issue for background on this.

Clone this wiki locally