Postgres — substitute referenced columns with corresponding data

时间:2017-08-05 11:31:20

标签: sql postgresql internationalization

I'm trying to approach translations (aka i18n) in Postgres, so far I've come up with a following pattern for storing strings in master language and its translations:

-- available translation languages
CREATE TYPE lang AS ENUM ('fr', 'de', 'cn');

-- strings in master language, e.g. en
CREATE TABLE strings (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  string text NOT NULL
);

-- strings translations in other languages, e.g. fr, de
CREATE TABLE translations (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  string_id uuid NOT NULL REFERENCES strings (id) ON UPDATE CASCADE ON DELETE CASCADE,
  lang lang NOT NULL,
  string text NOT NULL
);

-- a collection of things with a name and a description
CREATE TABLE things (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name uuid REFERENCES strings (id) ON UPDATE CASCADE,
  description uuid REFERENCES strings (id) ON UPDATE CASCADE
);

So basically a Thing has a name and a description and they both reference Strings by id. A String has a master language text (strings.string), and also there are Translations which reference Strings by id.

A small example:

db=# select id, name, description from things;
                  id                  |                 name                 |             description
--------------------------------------+--------------------------------------+--------------------------------------
df2ac652-cae7-4c90-ad85-05793e67ba47 | ce5a6cb6-6f14-4775-bed8-62ed871fdefc | 635e144d-f64f-4e2b-90f8-1280b1b7d24e
(1 row)

db=# select strings.id, strings.string from strings inner join things on (strings.id = things.name or strings.id = things.description);                                                                                id                  |              string
--------------------------------------+-----------------------------------
ce5a6cb6-6f14-4775-bed8-62ed871fdefc | Cool Thing
635e144d-f64f-4e2b-90f8-1280b1b7d24e | Some Cool Thing description here
(2 rows)

The only problem is that I can't figure out a proper efficient way to retrieve Things with substituted values for a particular language. Say, I want to retrieve it in master language, then I'd probably do a join:

SELECT
  things.id AS id,
  strings.string AS name
FROM things
INNER JOIN strings
ON (things.name = strings.id);

This would return:

                  id                 |   name
-------------------------------------+------------
df2ac652-cae7-4c90-ad85-05793e67ba47 | Cool Thing
(1 row)

But I cannot add description, since I've already used strings.string AS name in the above query.

Maybe my approach to i18n is fundamentally wrong and I'm not seeing a simpler solution here. Any help is very much appreciated.

2 个答案:

答案 0 :(得分:1)

You can just chain the joins together:

SELECT t.id AS id, s.string AS name, trfr.string as name_fr
FROM things t INNER JOIN
     strings s
     ON t.name = s.id INNER JOIN
     translations trfr
     ON tr.id = t.name AND lang = 'fr';

I do find your data model a bit confusing.

First, use serial for the primary key instead of uuids, unless you have a real business reason for using uuids. Numbers are much easier to work with.

Second, having a table with string and string_id is just confusing. Your names should be clearer. Maybe something like: string_id and string_in_language.

Third, I would not make lang an enumerated type. I would make it a reference table. You might definitely want to store additional information about the language -- say, the default first day of the week to use, or the full name, or the default currency symbol.

答案 1 :(得分:0)

Gordon Linoff gave me a hint, so I'll post an answer in case anyone else has the same issue

SELECT
  t.id AS id,
  tn.string AS name, -- name translation
  td.string AS description -- description translation
FROM things t
INNER JOIN translations tn
  ON t.name = tn.string_id
INNER JOIN translations td
  ON t.description = td.string_id
WHERE tn.lang = 'fr' AND td.lang = 'fr';

To use a fallback master language:

SELECT
  t.id AS id,
  COALESCE(tn.string, sn.string) AS name, -- name translation
  COALESCE(td.string, sd.string) AS description -- description translation
FROM things t
LEFT OUTER JOIN strings sn
  ON t.name = sn.id
LEFT OUTER JOIN strings sd
  ON t.description = sd.id
LEFT OUTER JOIN translations tn
  ON t.name = tn.string_id
  AND tn.lang = 'fr'
LEFT OUTER JOIN translations td
  ON t.description = td.string_id
  AND td.lang = 'fr';