organisation/optimisation de relation many to many

a marqué ce sujet comme résolu.

Bonjour,

Je poursuis mon premier projet avec Symfony. Aujourd'hui j'ai un problème en rapport avec la base de donnée et les entités.

J'ai un ensemble de contenus matérialisés par une entité Content. Chaque contenu a une catégorie et peut être disponible en une ou plusieurs langues. Pas de problème avec les catégories, j'ai une entité Category, mais pour les langues je ne sais pas trop comment faire et c'est pour cela que je viens vous demander conseil.

Il faut que je sois en mesure d'afficher facilement une liste de contenus dans une catégorie donnée et seulement les contenus dans les langues sélectionnées par l'utilisateur. Par exemple l'utilisateur pourrait demander tous les contenus de la catégorie sciences qui sont disponibles soit en françAis, soit en anglais (et alors tous les contenus qui sont disponibles en l'un ou l'autre, pas nécessairement les deux, seraient retournés).

Si on met Symfony, Doctrine et le DQL de côté et qu'on revient au PHP standard et au b.a.ba des bases de donnée brutes sans surcouche, je vois deux principales solutions possibles :

Solution 1: on ajoute à la table Content une colonne numérique languages, et les langues disponibles pour un contenu sont matérialisés par un champ de bits (bit field). Par exemple si on suppose que 1=françAis, 2=allemand, 4=italien et 8=anglais, on pourrait facilement récupérer tous les contenus qui sont soit en françAis, soit en anglais, dans une catégorie donnée, avec une requête de ce genre :

1
2
3
4
5
select ...
from Content c
where 
c.category = ...
and (languages&9) # 9 = 1|8

Avantages:

  • C'est simple et rapide.

Inconvénients:

  • Pas pratique de mettre à jour la liste des langues, et je risque d'être embêté quand l'application sera traduite en 64 langues, mais on s'en fout: ça ne changera pas tous les 4 matins et dépasser 64 est quand même très peu probable.
  • Ca peut paraître un peu hacky comme idée; en tout cas c'est pas mainstream comme façon de penser.

Solution 2: plus classique, on crée une table de liaison ContentLanguage:

1
2
3
4
5
6
create table ContentLanguage (
content int,
language char(2),
primary key(content,language),
foreign key (content) references Content(id)
);

ON peut alors récupérer des contenus avec une requête de ce type :

1
2
3
4
5
6
select ...
from Content c, ContentLanguage cl
where
c.id = cl.content # jointure
and c.category = ...
and cl.languages in ('fr', 'en')

Si je ne crée pas de table Language, c'est volontaire. Je m'en fous, la liste de toutes les langues disponibles ne risque pas de changer tous les 4 matins. Donc à mon sens ça ferait une jointure inutile pour pas grand chose. Ca serait plus propre du point de vue de la normalisation et tuti quanti, mais franchement, osef.

Toute cette digression pour en revenir maintenant à Symfony, Doctrine et DQL: comment je fais pour implémenter soit la solution 1, soit la solution 2 ?

Pour la solution 1, à priori ce n'est pas compliqué, il suffit d'ajouter un champ dans ma classe entité :

1
2
3
4
5
6
7
8
class Content {
//...
/**
*@ORM\Column(type="integer")
*/
protected $languages;
//...
}

Mais juste après, je m'aperçois que je suis piégé (pour ne pas dire b…). Les opérateurs bit à bit n'existent visiblement pas en DQL d'après la doc. C'est con, ça aurait presque été trop facile.

En ce qui concerne la solution 2, les relations many to many, ça se fait très bien avec Doctrine. Mais je me retrouve à être obligé de créer une entité ContentLanguage bidon qui ne me servira strictement à rien du tout. Ca me paraît lourd, extrêmement lourd, inutilement lourd.

Il n'y a pas moyen de ne pas devoir créer cette classe ? Genre pouvoir stocker les langues bêtement dans un array dans Content ? Parce que franchement je n'en vois pas l'utilité.

Au final, ça me donne l'impression que Doctrine est un énorme machin qui apporte plus de contrariétés que ce qu'il n'en résoud, et qu'il a été conçu pour des noob qui ne maîtrisent pas bien SQL.

Que faire ? IL y a une astuce que j'ai raté qui me permettrait d'implémenter ma solution n°1, ou bien je dois aller absolument au bout de la solution n°2 avec deux classes et deux tables ContentLanguage et Language, et en fait la pratique montre que ce n'est pas si lourd que ça ?

Merci pour vos réponses et bon week-end !

+0 -0

Salut,

Symfony est capable de le gérer tout seul. Tu ajoutes un attribut country à ton entité, et tu utilises un CountryType dans le formulaire. Tu as également la possibilité d'utiliser une validation existante de l'entité afin d'être sûr que l'info rentrée est bien celle attendue. Ça se base sur une norme ISO, au final tu auras 'fr', 'us', etc. dans tes entités, sans avoir à t'embêter à créer toi-même tes langues.

Cependant, avec cette méthode, un même "content" disponible en français et en anglais n'est pas lié. Mais je pense que ça peut aisément se faire avec une table intermédiaire. Ou alors regarder du côté des extensions Doctrine, je crois qu'il existe des solutions pour ce genre de choses (translatable ?).

+0 -0

Symfony est capable de le gérer tout seul. Tu ajoutes un attribut country à ton entité, et tu utilises un CountryType dans le formulaire.

Je connais pas Symfony mais je suis curieux de savoir comment il gère ce mapping Country <=> Language d'une façon sensée. La page dont tu donnes le lien ne mentionne nulle part la norme ISO dont tu parles, on ne sait pas laquelle est utilisée.

Si l'utilisateur choisi USA comme pays et un autre GB, ce sera 'en' pour les deux ou 'en_US' et 'en_GB' ? Si l'utilisateur choisi Suisse comme pays, Symfony part du principe que tu lis 4 langues ?

Bref, je vois pas en quoi un CountryType peut aider et je serais ravi qu'on m'explique. C'est peut-être la bonne solution, j'en sais rien vu que je connais pas Symfony, je suis juste curieux de savoir comment ça pourrait résoudre le problème.

+0 -0

Autant pour moi (oui moi je l'écris comme ça). Dans ce cas, il s'agit de Language et non Country. Le principe reste cependant le même.

Un attribut $language à l'entité et un LanguageType Field dans le formulaire de création/modification. Pour ce qui est de la valeur enregistrée, je n'ai pas d'exemple sur ma machine, j'avoue que je ne sais pas (même si je pense que ça fait du 'en_US' effectivement).

+0 -0

J'utilise déjà un LanguageType dans le formulaire, mais c'était pas ça la question. Pardon, je ne me suis peut-être pas très très bien exprimé.

Un contenu peut être disponible en plusieurs langues, techniquement c'est donc une relation many to many. Mais j'aimerais bien, soit éviter de créer une entité ContentLanguage, une entité Language et une table Language qui ne servent à rien, soit savoir comment faire pour partir sur la solution bit field. A moins que vous ayiez une troisième alternative encore meilleure ?

Ce que j'ai peut-être omis de dire clairement c'est qu'un même contenu, i.e. une ligne unique dans la base de donnée, peut avoir plusieurs langues. L'entité Content ne contient pas elle-même les ressources en base, elles sont stockées ailleurs.

Sinon pour répondre à cette question:

Je connais pas Symfony mais je suis curieux de savoir comment il gère ce mapping Country <=> Language d'une façon sensée.

Il se base sur l'extension intl de PHP qui elle-même prend les données dans la bibliothèque ICU en C/C++. Donc à mon avis c'est fiable, ICU c'est quand même la référence pour tout ce qui touche à unicode.

+0 -0

Dans tous les cas, je ne comprends pas pourquoi tu veux passer par une entité ContentLanguage, Language et une table Language, puisque Symfony le gère automatiquement. Si une entité possède un attribut $language, qui est donné automatiquement par SF, normalement c'est bon.

En ce qui concerne ton problème, regarde du côté de l'extension Doctrine translatable, il me semble qu'elle permet de gérer ce genre de cas de figure.

+0 -0

Dans tous les cas, je ne comprends pas pourquoi tu veux passer par une entité ContentLanguage, Language et une table Language, puisque Symfony le gère automatiquement. Si une entité possède un attribut $language, qui est donné automatiquement par SF, normalement c'est bon.

Ce n'est pas un attribut $language mais bien $language*s* que j'ai.

Je ne comprends pas trop en quoi cette extension Translatable que tu mentionnes m'aide à résoudre mon problème. Ce n'est pas des champs individuels spécifiques comme $title, $name, , etc. que je cherche à traduire en plusieurs langues. C'est simplement le stockage du fait que tel contenu est disponible en plusieurs langues.

Les données traduites elles-mêmes sont stockées ailleurs, en-dehors de la base. Pour le moment je ne m'en occupe pas.

EDIT: Abruti de markdown qui prend mes $ pour du LaTeX

+0 -0

Salut !

Une collection de langues, enregistrée dans un tableau JSON ou même sérialisé (supportés nativement par Doctrine), ne ferait-elle pas l'affaire ?

1
2
3
4
5
<?php
/**
 * @ORM\Column(type="json_array")
 */
private $languages;

Une liste de langues dans une entité (stockée ici sous forme de tableau JSON)

1
2
3
4
<?php
$builder->add('languages', 'collection', array(
    'type' => 'language',
)

Une collection de langues dans un ***Type

+0 -0

De deux choses l'une :

  • soit Doctrine permet dé-sérialise les données pour permettre de faire des recherches dedans (mais avec quelque chose d'aussi simple que WHERE languages.value = quekchose plutôt que WHERE quekchose IN content.languages), auquel cas, je pense en effet que les performances ne seront pas terribles ;
  • soit il n'y a pas de dé-sérialisation quand tu recherches quelque chose, et du coup tu peux utiliser WHERE content.languages LIKE '%quekchose%', qui sera quand-même moins gourmand, surtout au niveau PHP.
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte