Diagrama entidad relación de base de datos Primera solución (variación número columnas) |
Para evitarnos problemas las buenas prácticas de diseño de las bases de datos dicen que las tablas han de estar normalizadas y siguiengo la solución anterior tendríamos variaciones en el número de columnas con lo que inclumpliríamos la primera forma normal (1FN). ¿Pero porque inclumplir esta forma normal puede ser un problema? Porque en el momento que tengamos que soportar un nuevo idioma deberemos modificar el esquema de la base de datos añadiendo un campo por cada concepto de las entidades que haya que internacionalizar. Y hacer esto en una base de datos que está en producción puede ser una fuente de problemas y un peligro. Además, en tablas con muchos registros (de unos cuantos miles o cientos de miles) añadir una columna puede ser muy lento pudiendo llegar a horas o más tiempo cosa que dependiendo del proyecto no es viable al poder tener que realizar paradas largas por mantenimiento.
La solución que voy a explicar a continuación es crear una tabla Diccionario la cual relacionaremos con las tablas de los datos de dominio (Producto) y una tabla Traduccion que relacionaremos con la tabla Diccionario y que contendrá las traducciones para cada idioma. La tabla Traduccion contendrá tres campos la clave primaria del diccionario, el idioma de la traduccion y el literal de la traducción propiamente dicho con lo que ya no tendremos variaciones en el número columnas. En la tabla Traduccion la clave primaria estará formada por la clave del diccionario y el campo locale. El esquema que tendríamos sería el siguiente:
Diagrama entidad relación de base de datos. Producto-Diccionario 1:1 Diccionario-Traduccion 1:N |
// Producto.java (Código resumido) ... @Entity @Table(name = "Producto") public class Producto implements Serializable { ... @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "nombre_id", nullable = true) private Diccionario nombre; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "descripcion_id", nullable = true) private Diccionario descripcion; ... } // Diccionario.java package com.blogspot.elblogdepicodev.domain; import java.io.Serializable; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import javax.persistence.Table; import org.apache.commons.lang3.StringUtils; import com.blogspot.elblogdepicodev.misc.AppThreadLocal; @Entity @Table(name = "diccionario") public class Diccionario implements Serializable { private static final long serialVersionUID = -1210174827995726573L; private static final int LONGITUD_TEXTO_ABREVIADO = 10; @Id @GeneratedValue private Long id; @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "diccionario_id") @MapKeyColumn(name = "locale") private Map<String, Traduccion> traducciones; public Diccionario() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Map<String, Traduccion> getTraducciones() { if (traducciones == null) { traducciones = new HashMap<String, Traduccion>(); } return traducciones; } public void setTraducciones(Map<String, Traduccion> textos) { this.traducciones = textos; } // public String getTexto() { return getTexto(getLocale()); } public void setTexto(String texto) { setTexto(getLocale(), texto); } public String getTexto(Locale locale) { Traduccion t = getTraducciones().get(locale.toString()); if (t == null) { return null; } return t.getTexto(); } public void setTexto(Locale locale, String texto) { Traduccion t = getTraducciones().get(locale.toString()); if (t == null) { t = new Traduccion(this, locale.toString()); getTraducciones().put(locale.toString(), t); } t.setTexto(texto); } // public boolean equals(Diccionario d) { if (d == null) { return false; } return getId().equals(d.getId()); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public String toString() { Map m = new HashMap(); m.put("id", getId()); m.put("texto", StringUtils.abbreviate(getTexto(), LONGITUD_TEXTO_ABREVIADO)); return m.toString(); } private Locale getLocale() { return AppThreadLocal.getPreferencias().getLocale(); } } // Traduccion.java package com.blogspot.elblogdepicodev.domain; import java.io.Serializable; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import org.apache.commons.lang3.StringUtils; import com.blogspot.elblogdepicodev.misc.Utilidades; @Entity @Table(name = "traduccion") public class Traduccion implements Serializable { private static final long serialVersionUID = -1210174827995726573L; private static final int LONGITUD_TEXTO_ABREVIADO = 10; @Id private TraduccionPK id; @Column(length = 65536) private String texto; public Traduccion() { } public Traduccion(Diccionario diccionario, String locale) { this.id = new TraduccionPK(diccionario, locale); } public TraduccionPK getId() { return id; } public void setId(TraduccionPK id) { this.id = id; } public String getTexto() { return texto; } public void setTexto(String texto) { this.texto = texto; } // public Locale getLocale() { return Utilidades.getLocale(id.getLocale()); } // public boolean equals(Traduccion t) { if (t == null) { return false; } return getId().equals(t.getId()); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public String toString() { Map m = new HashMap(); m.put("id", getId()); m.put("locale", getId().getLocale()); m.put("texto", StringUtils.abbreviate(getTexto(), LONGITUD_TEXTO_ABREVIADO)); return m.toString(); } } // TraduccionPK.java package com.blogspot.elblogdepicodev.domain; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import javax.persistence.Basic; import javax.persistence.Embeddable; import javax.persistence.ManyToOne; @Embeddable public class TraduccionPK implements Serializable { private static final long serialVersionUID = 4445136951104395835L; @ManyToOne private Diccionario diccionario; @Basic private String locale; public TraduccionPK() { } public TraduccionPK(Diccionario diccionario, String locale) { this.diccionario = diccionario; this.locale = locale; } public Diccionario getDiccionario() { return diccionario; } public void setDiccionario(Diccionario diccionario) { this.diccionario = diccionario; } public String getLocale() { return locale; } public void setLocale(String locale) { this.locale = locale; } // public boolean equals(TraduccionPK t) { if (t == null) { return false; } return diccionario.equals(t.getDiccionario()) && getLocale().equals(t.getLocale()); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public String toString() { Map m = new HashMap(); m.put("dicionario", getDiccionario().toString()); m.put("locale", getLocale()); return m.toString(); } } // Utilidades.java public class Utilidades { ... public static Locale getLocale(String locale) { String[] s = locale.split("_"); switch (s.length) { case 1: { return new Locale(s[0]); } case 2: { return new Locale(s[0], s[1]); } case 3: { return new Locale(s[0], s[1], s[2]); } default: { throw new IllegalArgumentException(); } } } ... }
Esta solución carga las traducciones para todos los idiomas de un diccionario cuando posiblemente solo necesitemos la traducción de un idioma en concreto, tal vez si no quisiésemos que se carguen todos los textos de las traducciones podríamos sacar el campo texto a otra tabla y relacionarla con la Traduccion con un id con lo que los campos de tablas nos quedarían: Traduccion (diccionario_id, locale, texto_id), Texto (id, texto). Aunque con esta última solución necesitamos lanzar una sql más por cada texto que queramos acceder.
Otro problema común que se nos suele presentar en las entidades de dominio es como hacer búsquedas de texto completo («full text seach») en algunas propiedades de esas entidades y con esto no me refiero al débil like de SQL, en el enlace anterior doy varias soluciones (Hibernate Search, SQL, elasticsearch).
Referencia:
http://www.hibernate.org/
http://www.postgresql.org/about/
http://dev.mysql.com/doc/refman/5.0/en/column-count-limit.html
http://es.wikipedia.org/wiki/Normalizaci%C3%B3n_de_bases_de_datos
http://support.microsoft.com/kb/283878/es