LINUX.ORG.RU

Хранение локализованных данных в базе данных

 , , ,


3

2

Рассмотрим игрушечный пример. Допустим, я разрабатываю онлайн-магазин автомобилей (нет). У каждой марки машины есть название, мощность двигателя и описание. При этом название и мощность двигателя интернациональны, а описание должно быть разное для разных языков. И мне надо всё это сохранить в SQL базу данных (скажем, MySQL).

Я придумал такую конструкцию:

Таблица автомобилей:
    id (число)
    name (строка)
    power (число)
    description_id (число)

Таблица переводов:
    id (число)
    lang_code (строка)
    text (строка)

Соответственно, чтобы получить локализованное описание автомобиля, мы можем сделать запрос к таблице переводов, где id будет равен description_id, а lang_code будет равен нужному языку.

Особенности моей схемы: 1 таблица на все локализованные данные вообще. Можно добавить в таблицу автомобилей колонку localized_name_id, которая будет ссылаться на ту же таблицу переводов (но другую запись). Удалять автомобили тоже легко - нужно просто удалить все записи из таблицы переводов с соответствующим id. Также таблица переводов будет использоваться не только для таблицы автомобилей, но и, скажем, для таблицы кофеварок (у которых могут быть совсем другие наборы локализованных полей).

Таблица переводов, разумеется, имеет составной первичный ключ - id и lang_code (причём сначала идёт id).

А теперь два вопроса:

1) Насколько адекватно такое решение?

2) Как это уложить на ORM SQLAlchemy и Python? Если нет, то, возможно, ORM для других языков умеют такое (например, JPA в Java)? Или я хочу совсем странного?

UPD: Небольшая иллюстрация моего решения:

class Translation(db.Model):
	id = db.Column(db.Integer, primary_key=True)
	code = db.Column(db.String(2), primary_key=True)
	text = db.Column(db.Text, nullable=False)


class Car(db.Model):
	id = db.Column(db.String(64), primary_key=True)
	name_id = db.Column(db.Integer)
	name = db.relationship('Translation', primaryjoin=name_id == Translation.id, cascade='all, delete-orphan')
	description_id = db.Column(db.Integer)
	description = db.relationship('Translation', primaryjoin=description_id == Translation.id, cascade='all, delete-orphan')
	power = db.Column(db.Integer)

С загрузкой, обновлением и удалением переводов проблем быть не должно, но вот с добавлением новых непонятно что будет.

★★★★★

Ответ на: комментарий от slaykovsky

Пользователь системы может добавлять новые машины (если рассматривать игрушечный пример) через веб-интерфейс, в том числе добавляя, изменяя и удаляя локализованные описания. Возможность делать это конечному пользователю и есть ключевая фича системы.

gettext он для локализации самого интерфейса приложения программистом, а не данных, которые оно обрабатывает, пользователем.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 3)

http://sqlalchemy-utils.readthedocs.io/en/latest/internationalization.html

edit1: На самом деле твое решение правильнее и эффективнее. Но эта поделка из sqlalchemy-utils быстро подключается и удобно используется.

edit2: О, там ссылку дают на реализацию как у тебя - https://github.com/kvesteri/sqlalchemy-i18n

ei-grad ★★★★★ ()
Последнее исправление: ei-grad (всего исправлений: 2)
Ответ на: комментарий от ei-grad

http://sqlalchemy-utils.readthedocs.io/en/latest/internationalization.html

Как я понимаю, это решение специфично для PostgreSQL (используется фича хранения JSON в БД), а у меня MySQL.

там ссылку дают на реализацию как у тебя

Там реализация не как у меня. Там на каждую сущность заводят отдельную таблицу с локализованными версиями полей и отношением один ко многим с основной сущностью (которая хранит поля, не требующие локализации). А моё решение подразумевает одну таблицу для локализации любого количества сущностей. Более того, схема таблицы локализаций не зависит от схемы локализуемых сущностей.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от alienclaster

Там я вижу описание двух решений:

1) Каждое локализованное поле имеет отдельную колонку для каждого языка. Если языков много и тем более если они могут добавляться в рантайме (а заранее создать таблицу с сотнями колонок на все случаи жизни выглядит мега-костылём), то это плохо.

2) Добавить к сущности колонку с кодом языка. Каждый язык - отдельная копия сущности. Если есть интернациональные поля, то придётся следить за тем, чтобы они синхронно обновлялись во всех языковых версиях. Тоже плохо.

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple
Таблица автомобилей:
    id (число)
    name (строка)
    power (число)
    description_id (число)

Таблица переводов:
    id (число)
    lang_code (строка)
    text (строка)

А если у тебя:

Таблица автомобилей:
    id (число)
    name (строка)
    power (число)
    seo_optimized_title_id (число)
    short_description_id (число)
    full_description_id (число)
    jeff_asked_me_to_add_yet_another_foo_bar_baz_fucking_description_id (число)

То как?

Deleted ()
Ответ на: комментарий от Deleted

Ничего не изменится. Просто в таблице переводов будет своя строчка на каждый перевод каждого поля основной сущности.

Типа:

Переводы:
    id|lang|text
    1 | ru | Название
    1 | en | Title
    2 | ru | Описание
    2 | en | Description

Машины:
    id|title_id|description_id
    1 | 1      | 2

Что будет, если добавить ещё одно локализованное поле, думаю, сам догадаешься.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 2)
Ответ на: комментарий от KivApple

Значит, у тебя IDы переводов берутся из ниоткуда. Нет таблицы, в которой этот ID выступает первичным ключем.

Deleted ()
Последнее исправление: Deleted (всего исправлений: 1)
Ответ на: комментарий от Deleted

В таблице переводов составной первичный ключ - id + lang. Если у сущности уже есть хоть один вариант локализации для данного поля, то id берётся от него (а lang зависит от добавляемой локали). Если нет, то через auto_increment.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от KivApple

Ок, выглядит работоспособно. Возможно, потребуется обвешать костылями в частных случаях, а в целом норм.

Deleted ()
Ответ на: комментарий от KivApple

Если языков много и тем более если они могут добавляться в рантайме

Реляционные бд вообще не очень подходят для изменения своей структуры в рантайме. С этого как бы стоило начать :)

Но если у тебя интернет-магазин, то зачем добавлять языки в рантайме? Разработчику это не нужно, а если такая возможность предполагается у пользователя, то это уже не интернет-магазин, а маркет-плейс аля амазон. Т.е. первое решение вполне себе норм, если количество языков заранее известно (существуют языковые версии сайта) и нет специфических требований к производительности.

Если же у тебя предполагается маркетплейс, то, на мой взгляд, необходимо либо заранее перечислить все возможные языки, на которых пользователь может заполнить информацию о товаре (все равно язык заполнения будет тесно связан с языковой версией и регионом продаж / seo итд) иначе заполнение на нем лишено всякого смысла или попадает в категорию неизвестных (и ненужных маркетологам твоего проекта) языков. В этом случае очевидно, что маркировать надо каждую сущность (вариант 2) и соответствнно в рамках чистой реляционной парадигмы будет дублирование данных (которые от языка не зависят) и потенциальный их рассинхрон, либо опять же начать применять nosql-решения.

Можно еще предоставить пользователю возможность заполнять отдельно непереводимые поля для товара и к ним аттачить переводимые наборы полей для этого же товара для каждого из языков на которых он хочет это все заполнить. Но это все будет еще хуже и медленнее, чем первый вариант + определенная сложность реализации самого интерфейса для такого заполнения.

Короче sql здесь не годится никак (если предполагается предоставить фукнционал по изменению структуры бд пользователю) и похоже, тебе надо использовать документо-ориентированную бд или, если на определенный динамизм можно наплевать, то рассмотреть промежуточный вариант типа postgresql + jsonfield. Но надо смотреть уже на конкретные требования к системе и какие там запчасти можно прописать жестко, а какие предоставить пользователю.

Если это реально интернет-магазин, а не амазон, то я бы, вероятно, использовал первый вариант решения + postgres + jsonfield.

alienclaster ★★★ ()

Решил прочесть еще и стартовый пост :)

1) Насколько адекватно такое решение?

Представь, как будет тормозить твоя мега-таблица, когда в нее надо будет добавлять новые данные? А кроме инсерта / апдейта - пользователю надо будет как-то отображать все эти переводы у себя в админке. На каждое такое отображение - опять тяжелый запрос к огромной таблице, чтобы выбрать уже переведенные поля для его карточки товара. И так несколько раз пропорционально количеству языковых версий.

alienclaster ★★★ ()
Ответ на: комментарий от alienclaster

В моей задаче апдейты происходят редко, а набор данных не очень большой, так что скорость обновления меня не сильно беспокоит. Что касается отображения, то ведь поиск по индексу очень быстрый. В том числе по части индекса (при условии, что мы ищем по первой части индекса, именно поэтому id в primary_key включен перед lang_code) - что будет происходить при запросе «отобразить в админке все переводы».

KivApple ★★★★★ ()
Ответ на: комментарий от alienclaster

Что-то мне кажется, в админке народ потерпит. Ну а на морде можно денормализовать и закэшировать.

Deleted ()
Ответ на: комментарий от Deleted

Что-то мне кажется, в админке народ потерпит.

Ха-ха. Если это реально будет неудобный способ - просто уйдут к конкуренту, ну или если это для внутренних задач, то будут тебя тихо проклинать.

Ну а на морде можно денормализовать и закэшировать.

Ну мы пока без закешировать рассматриваем. А то так вопросов к базе данных вообще не останется, если все кешировать. Денормализовать как? И зачем, если не с самого начала?

alienclaster ★★★ ()
Ответ на: комментарий от KivApple

В моей задаче апдейты происходят редко, а набор данных не очень большой

А ты точно тогда уверен, что тебе вообще нужна возможность динамически там языки добавлять? Наверняка ведь и динамические поля тебе понадобятся. Или у тебя продается ровно одна модель пылесоса с жестко заданными полями, но с заранее неизвестным поличеством языковых карточек. Че-то странный какой-то интернет-магазин.

Что касается отображения, то ведь поиск по индексу очень быстрый.

Ну сравни, тебе то ли один товар выгрузить по id со всеми его полями, или этот же товар по id + все связанные поля выбрать по составному индексу. Что быстрее? Для маленькой таблички подходит любой способ вообще, как ты понимаешь. Но что это за интернет-магазин такой, где всех наборов полей со всеми языковыми версиями для всех типов товаров и их заранее неопределенных наборов будет мало в одной табличке? :)

Как ты будешь в приведенном тобой примере реализовывать разный набор полей для каждого товара? Для всего этого как раз nosql и используется.

alienclaster ★★★ ()

Использовал как единую таблицу на все переводы, так и по таблице переводов на сущность, перешёл на JSONB, очень доволен.

theNamelessOne ★★★★★ ()
Последнее исправление: theNamelessOne (всего исправлений: 1)
Ответ на: комментарий от KivApple

1. Посмотри, как сделано в odoo.
2. Почему бы тебе в каждой лоеализуемой модели не добавить метод локализации?! Ну, проманкипатчить или типа того орм. Тогда ты не будешь стеснен.
Например, car.brand.i17n('ru') вернет русское название, а car.brand - по умолчанию...

Shadow ★★★★★ ()

«description_id» в таблице автомобилей - избыточное поле.
Достаточно в таблице переводов определить составной первичный ключ - (id, lang_code), где «id», соответственно, будет foreign-ключом на таблицу автомобилей.

vinvlad ()

я бы наверное хранил json или xml в локализуемых полях парсить и сериализовать можно на пыхe если база не поддерживет...

dimzon ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.