2012-11-17 16 views
15

Sto cercando di ottenere un'implementazione JPA di un approccio semplice all'internazionalizzazione. Voglio avere una tabella di stringhe tradotte che posso fare riferimento in più campi in più tabelle. Quindi tutte le occorrenze di testo in tutte le tabelle saranno sostituite da un riferimento alla tabella delle stringhe tradotte. In combinazione con un id della lingua, questo darebbe una riga unica nella tabella delle stringhe tradotte per quel particolare campo. Ad esempio, si consideri uno schema che ha entità del corso e il modulo come segue: -JPA Struttura del database per l'internazionalizzazione

Corso int course_id, nome int, descrizione int

modulo int module_id, nome int

Il corso.nome, course.description e module.name fanno tutti riferimento al campo id della tabella delle stringhe tradotte: -

TranslatedString int id, String Lang, contenuti String

Che tutto sembra abbastanza semplice. Ottengo una tabella per tutte le stringhe che potrebbero essere internazionalizzate e quella tabella viene utilizzata su tutte le altre tabelle.

Come posso fare questo in JPA, utilizzando eclipselink 2.4?

Ho esaminato ElementCollection incorporato, ala questo ... JPA 2.0: Mapping a Map - non è esattamente quello che sto cercando perché sembra che rapporta la tabella di stringhe tradotta al pk del tavolo proprietario. Ciò significa che posso avere solo un campo stringa traducibile per entità (a meno che non aggiungo nuove colonne di join nella tabella delle stringhe traducibili, che sconfigge il punto, è l'opposto di quello che sto cercando di fare). Inoltre, non sono chiaro su come questo funzionerebbe attraverso le entrate, presumibilmente l'id di ogni entità dovrebbe utilizzare una sequenza di database estesa per garantire l'unicità della tabella delle stringhe traducibile.

BTW, ho provato l'esempio come illustrato in questo collegamento e non ha funzionato per me, non appena l'entità ha aggiunto una mappa localizzata di stringhe, persistendo ha causato il bombardamento da parte del client ma nessun errore evidente sul lato server e nulla persisteva nel DB: S

Sono stato in giro per le case su questo circa 9 ore finora, ho guardato questo Internationalization with Hibernate che sembra stia cercando di fare la stessa cosa del link sopra (senza le definizioni della tabella è difficile vedere cosa ha ottenuto). Qualsiasi aiuto sarebbe stato realizzato con gratitudine a questo punto ...

Modificare 1 - re AMS anwser qui sotto, non sono sicuro che risolva veramente il problema. Nel suo esempio lascia la memorizzazione del testo descrittivo in qualche altro processo. L'idea di questo tipo di approccio è che l'oggetto entità prende il testo e le impostazioni internazionali e questo (in qualche modo!) Finisce nella tabella delle stringhe traducibili. Nel primo link che ho dato, il ragazzo sta tentando di farlo utilizzando una mappa incorporata, che ritengo sia l'approccio giusto. La sua strada però ha due problemi: uno non sembra funzionare! e due se funziona, memorizza l'FK nel tavolo incorporato invece del contrario (credo, non riesco a farlo funzionare così non riesco a vedere esattamente come persiste). Sospetto che l'approccio corretto finisca con un riferimento alla mappa al posto di ogni testo che deve essere tradotto (la mappa è locale-> contenuto), ma non riesco a vedere come farlo in un modo che consenta più mappe in un'entità (senza avere più colonne corrispondenti nella tabella delle stringhe traducibili) ...

risposta

6

OK, penso di averlo. Sembra che una versione semplificata del primo collegamento nella mia domanda funzionerà, usando solo una relazione ManyToOne a un'entità localizzata (con una joinColumn diversa per ogni elemento di testo nell'entità principale) e un ElementCollection semplice per la Mappa all'interno di quell'entità localizzata . Ho codificato un esempio leggermente diverso dalla mia domanda, con una sola entità (Categoria), con due elementi di testo che richiedono più voci per ogni locale (nome e descrizione).

Nota: ciò è stato fatto con Eclipselink 2.4 diretto a MySQL.

Due note su questo approccio: come si può vedere nel primo collegamento, utilizzando ElementCollection si costruisce una tabella separata da creare, che si traduce in due tabelle per le stringhe traducibili: una tiene semplicemente l'ID (Locaised) che è il FK in quello principale (localised_strings) che contiene tutte le informazioni sulla mappa. Il nome Localised_strings è il nome automatico/predefinito: puoi usarne un altro con l'annotazione @CollectionTable. Nel complesso, questo non è l'ideale da un punto di vista DB ma non dalla fine del mondo.

In secondo luogo, almeno per la mia combinazione di Eclipselink e MySQL, persistendo in una singola tabella (generata automaticamente), la tabella restituisce un errore :(Quindi ho aggiunto un valore predefinito nella colonna fittizia nell'entità, questo . è esclusivamente quello di superare questo problema

import java.io.Serializable; 
import java.lang.Long; 
import java.lang.String; 
import java.util.HashMap; 
import java.util.Map; 

import javax.persistence.*; 


@Entity 

public class Category implements Serializable { 

@GeneratedValue(strategy = GenerationType.IDENTITY) 
@Id 
private Long id; 
@ManyToOne(cascade=CascadeType.ALL) 
@JoinColumn(name="NAME_ID") 
private Localised nameStrings = new Localised(); 

@ManyToOne(cascade=CascadeType.ALL) 
@JoinColumn(name="DESCRIPTION_ID") 
private Localised descriptionStrings = new Localised(); 

private static final long serialVersionUID = 1L; 

public Category() { 

    super(); 
} 

public Category(String locale, String name, String description){ 
    this.nameStrings.addString(locale, name); 
    this.descriptionStrings.addString(locale, description); 
} 
public Long getId() { 
    return this.id; 
} 

public void setId(Long id) { 
    this.id = id; 
} 

public String getName(String locale) { 
    return this.nameStrings.getString(locale); 
} 

public void setName(String locale, String name) { 
    this.nameStrings.addString(locale, name); 
} 
public String getDescription(String locale) { 
    return this.descriptionStrings.getString(locale); 
} 

public void setDescription(String locale, String description) { 
    this.descriptionStrings.addString(locale, description); 
} 

} 




import java.util.HashMap; 
import java.util.Map; 

import javax.persistence.ElementCollection; 
import javax.persistence.Embeddable; 
import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.GenerationType; 
import javax.persistence.Id; 

@Entity 
public class Localised { 

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY) 
    private int id; 
    private int dummy = 0; 
    @ElementCollection 
    private Map<String,String> strings = new HashMap<String, String>(); 

    //private String locale;  
    //private String text; 

    public Localised() {} 

    public Localised(Map<String, String> map) { 
     this.strings = map; 
    } 

    public void addString(String locale, String text) { 
     strings.put(locale, text); 
    } 

    public String getString(String locale) { 
     String returnValue = strings.get(locale); 
     return (returnValue != null ? returnValue : null); 
    } 

} 

Quindi questi generano tabelle come segue: -

CREATE TABLE LOCALISED (ID INTEGER AUTO_INCREMENT NOT NULL, DUMMY INTEGER, PRIMARY KEY (ID)) 
CREATE TABLE CATEGORY (ID BIGINT AUTO_INCREMENT NOT NULL, DESCRIPTION_ID INTEGER, NAME_ID INTEGER, PRIMARY KEY (ID)) 
CREATE TABLE Localised_STRINGS (Localised_ID INTEGER, STRINGS VARCHAR(255), STRINGS_KEY VARCHAR(255)) 
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_DESCRIPTION_ID FOREIGN KEY (DESCRIPTION_ID) REFERENCES LOCALISED (ID) 
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_NAME_ID FOREIGN KEY (NAME_ID) REFERENCES LOCALISED (ID) 
ALTER TABLE Localised_STRINGS ADD CONSTRAINT FK_Localised_STRINGS_Localised_ID FOREIGN KEY (Localised_ID) REFERENCES LOCALISED (ID) 

a principale per provarlo ...

import java.util.List; 

import javax.persistence.EntityManager; 
import javax.persistence.EntityManagerFactory; 
import javax.persistence.Persistence; 
import javax.persistence.Query; 

public class Main { 
    static EntityManagerFactory emf = Persistence.createEntityManagerFactory("javaNetPU"); 
    static EntityManager em = emf.createEntityManager(); 

    public static void main(String[] a) throws Exception { 
    em.getTransaction().begin(); 


    Category category = new Category(); 

    em.persist(category); 

    category.setName("EN", "Business"); 
    category.setDescription("EN", "This is the business category"); 


    category.setName("FR", "La Business"); 
    category.setDescription("FR", "Ici es la Business"); 

    em.flush(); 

    System.out.println(category.getDescription("EN")); 
    System.out.println(category.getName("FR")); 


    Category c2 = new Category(); 
    em.persist(c2); 

    c2.setDescription("EN", "Second Description"); 
    c2.setName("EN", "Second Name"); 

    c2.setDescription("DE", "Zwei Description"); 
    c2.setName("DE", "Zwei Name"); 

    em.flush(); 


    //em.remove(category); 


    em.getTransaction().commit(); 
    em.close(); 
    emf.close(); 

    } 
} 

Questo produce un output: -

This is the business category 
La Business 

e le seguenti voci della tabella: -

Category 
"ID" "DESCRIPTION_ID" "NAME_ID" 
"1"   "1"     "2" 
"2"   "3"     "4" 

Localised 
"ID" "DUMMY" 
"1"   "0" 
"2"   "0" 
"3"   "0" 
"4"   "0" 

Localised_strings 

"Localised_ID" "STRINGS"      "STRINGS_KEY" 
"1"     "Ici es la Business"     "FR" 
"1"     "This is the business category"  "EN" 
"2"     "La Business"      "FR" 
"2"     "Business"      "EN" 
"3"     "Second Description"     "EN" 
"3"     "Zwei Description"    "DE" 
"4"     "Second Name"      "EN" 
"4"     "Zwei Name"       "DE" 

commentando l'em.remove correttamente elimina sia Categoria e è associato Locaised voci/Localised_strings.

Spero che tutto aiuti qualcuno in futuro.

+0

Questo è fantastico! Funziona bene con JPA2.0 + Hibernate 4.2 + MSSQL2008. Hai mai pensato ad eventuali problemi come le prestazioni? Mi piacerebbe ottenere le stringhe localizzate caricate con impazienza. A proposito, non c'è bisogno per la colonna fittizia nella tabella localizzata. – curious1

+0

Come ho detto sopra, penso che il problema del dummy column sia dovuto alla mia particolare combinazione di Eclipselink e MySQL: sembra che la tua combinazione di Hibernate e MSQL permetta di mantenere una singola colonna auto. I requisiti di volume per la particolare applicazione che stavo cercando erano minuscoli, quindi non ho trascorso molto tempo sui problemi di prestazioni. – IrishDubGuy

+0

Qualche idea su quale sia l'approccio migliore per ottenere solo la stringa che corrisponde al valore di intestazione del linguaggio accept in un'applicazione spring rest resto? – aycanadal

-1

Ecco un modo per farlo.

Carica tutte le stringhe tradotte dal database in una cache, consente di chiamarlo MessagesCache, avrebbe un metodo chiamato getMesssage di string pubblico (int id, int languageCode). Puoi utilizzare le raccolte immutabili di google guava per archiviarle nella cache di memoria. È anche possibile utilizzare una cache di caricamento Guava per archiviare la cache valutata se si desidera caricarli su richiesta. Se hai una tale cache puoi scrivere il codice come questo.

@Entity 
public Course { 
    @Column("description_id") 
    private int description; 

    public String getDescription(int languageCode) 
    { 
     return this.messagesCache(description, languageCode); 
    } 


    public String setDscription(int descriptionId) 
    { 
     this.description = descriptionId; 
    } 
} 

Il problema principale che vedo con questo approccio è quello che è necessario sapere il locale che si fa riferimento nell'entità, vorrei suggerire che il compito di raccogliere la lingua corretta per le descrizioni dovrebbe essere non fatto in l'entità ma in un'astrazione di livello superiore, come Dao o un servizio.

+0

Questo non risolve il problema, ignora il modo in cui il testo viene memorizzato, ad esempio. Vedi la mia modifica sopra. – IrishDubGuy

+0

Il ritorno di una stringa in un setter non funzionerà. Setter e getter in JPA dovrebbero pubblicare e restituire gli stessi valori. –

9

(Sono Henno che ha risposto al blog di hwellman.) Il mio approccio iniziale era molto simile al tuo approccio e fa il lavoro. Soddisfa il requisito che qualsiasi campo di qualsiasi entità possa fare riferimento a una Mappa di stringhe localizzata con una tabella di database generale che non deve fare riferimento ad altre tabelle più concrete. Anzi, lo uso anche per più campi nella nostra entità prodotto (nome, descrizione, dettagli). Ho anche avuto il "problema" che JPA ha generato una tabella con solo una colonna chiave primaria e una tabella per i valori che hanno fatto riferimento a questo ID.Con OpenJPA non ho avuto bisogno di una colonna fittizia:

public class StringI18N { 

    @OneToMany(mappedBy = "parent", cascade = ALL, fetch = EAGER, orphanRemoval = true) 
    @MapKey(name = "locale") 
    private Map<Locale, StringI18NSingleValue> strings = new HashMap<Locale, StringI18NSingleValue(); 
... 

OpenJPA semplicemente memorizza Locale come una stringa. Perché non abbiamo davvero bisogno di un'entità aggiuntiva StringI18NSingleValue, quindi penso che il tuo mapping usando @ElementCollection sia un po 'più elegante.

C'è un problema di cui devi essere a conoscenza: autorizzi la condivisione di un Localized con più entità, e come prevedi le entità Localizzate orfane quando l'entità proprietaria viene rimossa? Semplicemente usando cascade all non è sufficiente. Ho deciso di vedere un Localized il più possibile come un "oggetto valore" e di non permetterlo di essere condiviso con altre entità in modo da non dover pensare a più riferimenti allo stesso Localized e possiamo tranquillamente usare la rimozione di un orfano. Così i miei campi localizzati sono mappati come:

@OneToOne(cascade = ALL, orphanRemoval = true) 

A seconda del mio caso d'uso anche io uso prendere = EAGER/LAZY e facoltativo = vero o falso. Quando si utilizza facoltativo = false, utilizzo @JoinColumn (nullable = false) in modo che OpenJPA generi un vincolo non nullo sulla colonna di join.

Ogni volta che ho bisogno di copiare una localizzata ad un altro soggetto, non uso lo stesso riferimento ma creare una nuova istanza localizzato con lo stesso contenuto e nessuna id ancora. Altrimenti potrebbe essere difficile eseguire il debug dei problemi in cui, se non lo fai, stai ancora condividendo un'istanza con più entità e potresti ricevere bug sorprendenti dove la modifica di una stringa localizzata può cambiare un'altra stringa in un'altra entità.

Fin qui tutto bene, ma in pratica ho scoperto che OpenJPA ha N + 1 selezionare i problemi quando si selezionano le entità che contengono uno o più localizzati Strings. Non recupera in modo efficiente una raccolta di elementi (l'ho segnalata come https://issues.apache.org/jira/browse/OPENJPA-1920). Questo problema è probabilmente risolto utilizzando una mappa < Locale, StringI18NSingleValue >. Tuttavia OpenJPA può anche non recuperare in modo efficiente le strutture del modulo A 1..1 B 1 .. * C, che è anche ciò che accade qui (l'ho segnalato come https://issues.apache.org/jira/browse/OPENJPA-2296). Questo può seriamente influire sulle prestazioni della tua applicazione.

Altri provider JPA possono avere problemi di selezione N + 1 simili. Se le prestazioni di recupero della Categoria ti riguardano, verificherei se il numero di query utilizzate per il recupero della Categoria dipende dal numero di entità. So che con Hibernate puoi forzare il recupero in batch o la sottoselezione per risolvere questo tipo di problemi. So anche che EclipseLink ha caratteristiche simili che potrebbero funzionare o meno.

Dalla disperazione per risolvere questo problema di prestazioni, in realtà ho dovuto accettare di vivere con un progetto che non mi piace molto: ho semplicemente aggiunto un campo String per ogni lingua che dovevo supportare per Localized. Per noi questo è possibile perché al momento abbiamo solo bisogno di supportare alcune lingue. Ciò ha provocato solo una tabella localizzata (denormalizzata). JPA può quindi unire in modo efficiente la tabella localizzata nelle query, ma questo non si adatta bene a molte lingue e non supporta un numero arbitrario di lingue. Per la manutenibilità ho mantenuto l'interfaccia esterna di Localized e ho modificato l'implementazione da una mappa a un campo per lingua, in modo da poter facilmente tornare indietro nel tempo.

+1

Grazie per il post :) Un commento interessante riguarda le implicazioni dell'eliminazione dei proprietari. Nel mio caso d'uso non condividerò Localized, ci sarà un Localized univoco per ogni istanza di testo, non li uso in alcun senso come una costante, applicabile a più proprietari, quindi dovrei essere ok. Il mio caso d'uso è sempre relativamente leggero sul caricamento dei dati, quindi spero di non notare alcun problema di efficienza. In caso di doverti normalizzare alla fine, ero così determinato a non farlo! Anche se non ho ancora finito l'app, non ho mai pensato di andare in produzione, quindi dovrei farlo ancora: D – IrishDubGuy

+0

BTW - Sto facendo questo nel contesto di un'app GWT e scopro che (almeno in Eclipse) il mio soluzione non andrebbe oltre una connessione GWT-RPC. Alla fine ho trovato una soluzione a questo e lo posterò a breve – IrishDubGuy

Problemi correlati