Hibernate #ElementCollection - Better solution needed - java

I'm using Hibernate 3.5.a-Final as ORM-Layer in a web application. I've got several Beans with the same code-sniplet wich makes me think that this design isn't the best one around. But I can't figure out how to implement a better one in hibernate.
Requirements
Several classes need to contain localized descriptions in multiple locales
These need to be persisted into the db
They have to be searchable by substring for all locales (show up if the seachstring is a substring of any description)
Localized descriptions should be queryable without loading the master-object (by master-object-id, -type and locale)
Current solution (doesn't solve the last requirement)
Each class contains a HashMap annotated as
#ElementCollection(fetch=FetchType.EAGER)
#CollectionTable(name = "localized[X]Descriptions", joinColumns = #JoinColumn(name = "id"))
#MapKeyJoinColumn(name = "locale")
public Map<Locale, String> getLocalizedDescriptions() {
return localizedDescriptions;
}
[X] beeing the name of the class
For Each class the is an additional table (generated by hibernate)
create table localized[X]Descriptions (
id integer not null,
localizedDescriptions varchar(255),
localizedDescriptions_KEY varchar(255),
primary key (id, localizedDescriptions_KEY)
)
For some reason the #MapKeyJoinColumn gets ignored...
What I'd prefer would be a single table like this:
create table localizedDescriptions (
class varchar(255) not null,
id integer not null,
locale varchar(50) not null,
description varchar(255) not null,
primary key (class, id, locale)
)
It would be a big plus if the implementation would be queryable using the criteria-api (which isn't compatible to #ElementCollections as far as I know).
But I can't figure out how to implement this. Any pointers would be very welcome

I found my own solution...
I just use
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="masterClass", discriminatorType=DiscriminatorType.INTEGER)
#Table(name="localizedDescriptions")
public class LocalizedDescriptions{
private Integer id;
private Locale locale;
private String description;
[Getters, Setters]
}
as my parent-class for all localized descriptions and extend it like
#Entity
public class LocalizedSomeDescription extends LocalizedDescription {
private Some master;
/**
* #return the master
*/
#ManyToOne
public Some getMaster() {
return master;
}
Which gets used like this:
#Table
#Entity
public class Some {
private Map<Locale, LocalizedSomeDescription> names = new HashMap<Locale, LocalizedSomeDescription>();
#OneToMany
#JoinColumn(name="master_id")
#MapKeyColumn(name="locale")
public Map<Locale, LocalizedSomeDescription> getDescriptions() {
return descriptions;
}
}
This results in something very similar to my intended table design
create table localizedDescriptionss (
masterClass integer not null,
id integer not null auto_increment,
locale varchar(255),
description varchar(255),
master_id integer,
primary key (id)
)
using mappedBy="master" in all subclasses might seem like an abuse of hibernate inheritance but all other solutions would include one row per subclass which would be null in every other, which seems to me like a very bad table design. I still have to find out what's the 'sensible default' for a discriminatorType=DiscriminatorType.INTEGER and if I need to override that default.

Related

How to join Hibernate?

public class Primary{
private long id;
#Id
#GeneratedValue(
strategy = GenerationType.IDENTITY
)
#Column(
name = "id"
)
public void getId(){return id;}
//other vars
}
public class Secondary{
//other vars
private Primary primary;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "primary_id", unique = true)
public Primary getPrimary(){return primary;}
}
From this, it is pretty easy to get a Primary Object from a Secondary Object. However, how do I get Secondary Object from a Primary Object without selecting twice in Hibernate?
The Primary class should look like this:
#OneToMany(mappedBy="primary", cascade={CascadeType.ALL})
private List<Secondary> secondaries;
// getter and setter
And you should make a small modification in Secondary:
#JoinColumn(name = "id", unique = true)
public Primary getPrimary(){return primary;}
Cause join column should refer to the joined field(id) in Primary.
The answer could be quite different based on what you are looking for.
Based on your current mapping, assuming you have a Primary instance on hand, you can get its corresponding Secondary by querying. E.g. by HQL:
from Secondary s where s.primary = :primary
and pass in your primary as the parameter.
If you are looking for way to navigate from a Primary object instance, you could have created a bi-directional mapping:
class Primary {
#OneToMany(mappedBy="primary", ...)
private Set<Secondary> secondaries;
}
class Secondary {
#ManyToOne
private Primary primary;
}
You could refer to my (very old) answer on related question on how to define it. https://stackoverflow.com/a/13812047/395202
However, simply having a bi-directional relationship DOES NOT avoid "selecting twice", if your "selecting twice" means running 2 SQL queries in DB.
To reduce such round-trip, there are several way to solve. First one is to declare the relationship as EAGER fetch. However this is a way that I don't usually recommend so I won't go deeper.
Another (imho, more appropriate) way is to do a join fetch when you are fetching Primary. To fetch the Primary instance with its related Secondary instances, use a HQL like :
from Primary p left join fetch p.secondaries where ....
Add this Setter method in the Secondary Class
public Primary getPrimary() {
return primary;
}
And get the primary object from Secondary Class

Persist non-primitive data in JPA

I am creating a program that needs to interact with a database. It's a bare-bones inventory management system, so the entities are 'Item' and 'Patron'.
Edit: This is a Vaadin application using Spring boot and spring data JPA
First I will start with my 2 classes and omit getters/setters for brevity.
#Table(name="item")
#Entity
public class Item implements Serializable, Cloneable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private long barcode;
#NotNull
private String name, type;
#NotNull
private boolean isAvailable;
#Nullable
private boolean isLate;
#Nullable
private String notes;
#Nullable
private Patron currentPatron;
#Nullable
private Patron[] history;
#Nullable
private Date checkOutDate, dueDate;
public Item() {}
public Item(long barcode, String name, String type, boolean isAvailable) {
this.barcode = barcode;
this.name = name;
this.type = type;
this.isAvailable = isAvailable;
}
public Item(long barcode, String name, String type, String notes, boolean isAvailable) {
this.barcode = barcode;
this.name = name;
this.type = type;
this.notes = notes;
this.isAvailable = isAvailable;
}
public Item(long barcode, String name, String type, String notes, boolean isAvailable, Date checkOutDate, Date dueDate, boolean isLate, Patron currentPatron, Patron[] history) {
this.barcode = barcode;
this.name = name;
this.type = type;
this.notes = notes;
this.isAvailable = isAvailable;
this.checkOutDate = checkOutDate;
this.dueDate = dueDate;
this.isLate = isLate;
this.currentPatron = currentPatron;
this.history = history;
}
}
#Entity
#Table(name="patron")
public class Patron {
#Id
private long id;
#NotNull
private String name, email;
#Nullable
private Item[] checkedOutItems;
#Nullable
private List<Item> itemHistory;
#Nullable
private boolean owesFines;
#Nullable
private int finesOwed;
public Patron() {}
public Patron(long id, String name, String email, boolean owesFines) {
this.id = id;
this.name = name;
this.email = email;
this.owesFines = owesFines;
}
public Patron(long id, String name, String email, Item[] checkedOutItems, List<Item> itemHistory, boolean owesFines, int finesOwed) {
this.id = id;
this.name = name;
this.email = email;
this.checkedOutItems = checkedOutItems;
this.itemHistory = itemHistory;
this.owesFines = owesFines;
this.finesOwed = finesOwed;
}
In practice the Patron Object is instantiated by scanning their campus ID with a MSR. Then that data populates the name, email and ID fields of the patron class.
When checking out an item, the patron would first swipe their card with the MSR (the system would confirm they are in the DB, add them if not).
After their magnetic strip is scanned, the QR code for the item they want is scanned so we can tie that item to them.
When an item is checked out to a patron, we need to get their id, name and email from the Patron table and then populate the rest of its variables: check_out_date, due_date, etc.
A patron can check out many items, but only one item can be checked out to a patron. Does This establish a OneToMany relationship? Patron -> Item(
My thought process was as follows:
For Patron Objects
have an array of Items to store the barcode of the items they currently have.
have an arraylist of items to store info about what patron had it and when List<Item> history, that way the code is as simple as history.addToFront(something)
For Item Objects
have a Patron object to see who has it
have an arraylist of patrons to see all the times it was checked out
Q1: Is it redundant to have an array and a list as instance data for both classes?
Q1.2: Are an array of objects and a list of objects even appropriate data structures for a scenario like this?
Q1.3: Is there a difference in using javax.persistence.*; and org.springframework.data.annotation.*; for something like ID and is there a difference between import javax.validation.constraints.NotNull; and import org.springframework.lang.NonNull;
Q2: Does this produce a OneToMany relationship between Patron and Items?
Q3: In order to achieve this, I believe I need some additional tables in my database. I was thinking something like this: (And I realize I will need to include the appropriate spring annotations when implementing the new schema)
Item table
create table item(barcode int(10) primary key, name varchar(64) not null, type varchar(64) not null, availability boolean, is_late boolean, note varchar(255), check_out_date Datetime, due_date Datetime); #foreign keys for currentPatron and Patron History
Patron table
create table patron(id int(10) primary key, name varchar(64) not null, email varchar(64) not null, owes_fines boolean, fines_owed int); #foreign key to item table?
Patron_Item_History table
: This would pull id, name, email from the patron table, and then id, check_out_date, due_date from the item table?
Item_Patron_History table: Similar structure to the above table?
Thank you in advance.
OK here goes,
I am assuming you're building you application with Spring Boot, Hibernate as your ORM and probably some kind or relational database (MySQL).
Regarding db design:
Yes the Patreon object here is the owning entity with a OneToMany relation to the Item entity (since one Patreon may have N objects).
Your Patreon entity could do with the following redesing:
1) Try to use non-primitive types especially for table keys (long id -> Long id).
2) Lose the array of checkedOutItems as well as the itemHistory list. First of all relations should be modelled using collections and not arrays. Secondly you don't need those two.
You'll never store the checkedOutItems nor the itemHistory this way. Instead create a List<Item> items that will store the Patreon items while describing the relation (here are some examples: http://www.baeldung.com/hibernate-one-to-many)
3) Again with the Item entity you need to lose the array of history. The only thing you need there is a reference to the owning entity (Patreon in this case) thus completing the ManyToOne side of the relation.
4) Note that Date fields should be annotated with #Temporal also providing the correct type (you can read up for more).
5) Item class in general should do with a redesign.
5) After all the above are in place and assuming you're using Spring, you can create a Repository with which you can query a Patreon object thus retrieving an object along with it's related entities (Items).
Regarding your questions:
Q1: Yes it see. See above for more.
Q1.2: No arrays are not. Lists or better yet Sets are more suited.
Q1.3: Yes there is. The first one a JPA annotation used in relational
databases while the second one is Spring Data specific annotation
used by databases and frameworks that are not of this type
(relational) or do not have a standard persistence API defined (like
JPA). For the NonNull and NotNull are roughly the same with the first
one actually supersetting the latter one (something that is done
often). The only difference I see is the target. You can read for
more here:
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/NonNull.html
https://docs.oracle.com/javaee/7/api/javax/validation/constraints/NotNull.html
Q2: Yes there is. See above.
Q3: With a bit of clever desing I do not see the need for more, but
hey If you feel it'll help you, why not. Just don't overkill the
desingn and it's complexity
I spent a long time yesterday just thinking about a solution in my head.
I made the changes you mentioned to both classes. Long is an object and long is a primitive, you can serialize Long, is that why you recommended I use it instead?
I went on fiddle to test out my thoughts and here's what I came up with. It works as I want it to, but I would then need to implement it in my repository.. Something like repo.checkout(item, patron) should suffice? As for everything else, like populating a list for the client to view, is mainly java logic from here on out?
Anyway, Here's my solution!
create table item (
barcode bigint not null auto_increment primary key,
name varchar(20) not null,
type varchar(20) not null,
is_available boolean not null,
is_late boolean null,
notes varchar(255) null,
check_out_date datetime null,
due_date datetime null
#create index idx_barcode (barcode));
create table patron (
trinity_id bigint not null primary key,
name varchar(30) not null,
email varchar(20) not null,
owes_fines boolean not null,
fines_owed int null
#create index idx_trinity_id (trinity_id));
create table checked_out_items (
ref_id bigint primary key auto_increment not null,
patron_id bigint not null,
item_id bigint not null,
item_available boolean not null,
item_check_out_date datetime null,
item_due_date datetime null);
alter table checked_out_items
add constraint fk_patron_id
foreign key (patron_id) references patron(trinity_id),
add constraint fk_item_id
foreign key (item_id) references item(barcode)
#add constraint fk_item_available
#add constraint fk_check_out_date
#add constraint fk_due_date
#foreign key (item_available references item(is_available)
#foreign key (item_check_out_date) references item(check_out_date)
#foreign key (item_due_date) references item(due_date)
on update cascade
on delete cascade;
insert into patron values(0000000,'Test Erino','test#erino.edu',0,null);
insert into item values(1,'Chromebook','Laptop',0,null,null,null,null);
insert into checked_out_items(patron_id,item_id,item_available,item_check_out_date,item_due_date)
select patron.trinity_id,item.barcode,item.is_available,item.check_out_date,item.due_date
from patron
inner join item;
and lastly:
select * from item;
select * from patron;
select * from checked_out_items;

Hibernate mapping of similar columns

if I create a table like:
CREATE TABLE IF NOT EXISTS `user`(
`id` INT NOT NULL AUTO_INCREMENT,
`email_1_type` INT NULL,
`email_1` VARCHAR(255) NULL,
`email_2_type` INT NULL,
`email_2` VARCHAR(255) NULL,
`email_3_type` INT NULL,
`email_3` VARCHAR(255) NULL,
PRIMARY KEY (`id`)
)
can I map email columns as list? Like:
public class Email{
private int type;
private String email;
// getter & setter
}
#Entity
#Table(name = "info_req_chat_time_detail")
public class User{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private List<Email> emails;
// getter & setter
}
Don't do that. Java collections types are used to map relationships to other tables that need foreign keys.
https://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/collections.html
As a requirement persistent collection-valued fields must be declared
as an interface type (see Example 7.2, “Collection mapping using
#OneToMany and #JoinColumn”). The actual interface might be
java.util.Set, java.util.Collection, java.util.List, java.util.Map,
java.util.SortedSet, java.util.SortedMap or anything you like
("anything you like" means you will have to write an implementation of
org.hibernate.usertype.UserCollectionType).
It actually would be good design to have another table of just Phones containing of id(FK), emailType, emailValue. Then you could do what you're listing.
But if you're trying to keep it simple and know for sure about 3 phones, don't do that. You never know when you'll want to modify or augment just one of the SQL columns. If you need to use a List in the Java class, build a private List<Email> getAllEmailsAsList() method collecting all the individual email fields and packaging them into a list for you.
There's nothing wrong with having the 6 extra member fields in your #Entity class to handle this table's creation.

One-to-one mapping hibernate and not null

I'm new to hibernate and quite new to MySQL too.
I have the following two tables:
CREATE TABLE storeman.user (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(80) NOT NULL,
display_name VARCHAR(50),
password CHAR(41),
active BOOLEAN NOT NULL DEFAULT FALSE,
provisional BOOLEAN NOT NULL DEFAULT FALSE,
last_login TIMESTAMP,
PRIMARY KEY (id),
UNIQUE INDEX (email)
);
CREATE TABLE storeman.user_preferences (
id INT NOT NULL AUTO_INCREMENT,
notify_login BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
CONSTRAINT id_foreign FOREIGN KEY (id) REFERENCES user (id) ON DELETE CASCADE
);
In Eclipse, with hibernate tools I have generated the domain code classes. User.java looks like this (siplified):
#Entity
#Table(name = "user", catalog = "storeman", uniqueConstraints = #UniqueConstraint(columnNames = "email"))
public class User implements java.io.Serializable {
private Integer id;
[...]
private UserPreferences userPreferences;
public User() {
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}
[...]
#OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
public UserPreferences getUserPreferences() {
return this.userPreferences;
}
}
My issue is with getUserPreferences: of course, that would return null if creating a new user or reading from the db where the corresponding row in the user_detail table does not exist. This is correct, however it forces me to check if userPreferences is not null before accessing its members. And from a coding point of view it is not so handy. So I changed User.getUserPreferences method like this, to get a default value:
#OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
public UserPreferences getUserPreferences() {
if (this.userPreferences==null)
this.userPreferences = new UserPreferences();
return this.userPreferences;
}
This is working fine, however if I ever would need to re-generate domain code (User.java) with hibernate tools, that change will be lost. So my question is: is there a way (even by modifying mySQL table/relationships) to automatically have userPreferences always set?
There is no way to do this outside of your code (at least not that I can think of), with some configuration or something like that.
One thing you can do is to initialize the relation when you declare it
private UserPreferences userPreferences = new UserPreferences()
but that also won't survive code regeneration. The only other way I can think of is to put this initialization code into some util method so you can maintain it there regardless of regeneration of entity code.
UserUtils.getUserPreferences(User user)
However, this would only work for the code you write, if some framework needs it you will again get null values because it will not use your util method (the first approach is better in this case).
Do bear in mind that, when you initialize this object on a managed entity, the new object will be persisted into the database.
User user = userDAO.getUser(id);
user.getUserPreferences(); // this code initializes the relation (new UserPreference())
After these lines, you will get a row in user_preferences table if cascade is configured in that manner, or you will get an exception complaining about transient entity found in entity you are trying to persist.
All that being said, is it really that hard to check if something is null, especially if by business rules it is allowed to be null?

JPA Database structure for internationalisation

I am trying to get a JPA implementation of a simple approach to internationalisation. I want to have a table of translated strings that I can reference in multiple fields in multiple tables. So all text occurrences in all tables will be replaced by a reference to the translated strings table. In combination with a language id, this would give a unique row in the translated strings table for that particular field. For example, consider a schema that has entities Course and Module as follows :-
Course
int course_id,
int name,
int description
Module
int module_id,
int name
The course.name, course.description and module.name are all referencing the id field of the translated strings table :-
TranslatedString
int id,
String lang,
String content
That all seems simple enough. I get one table for all strings that could be internationalised and that table is used across all the other tables.
How might I do this in JPA, using eclipselink 2.4?
I've looked at embedded ElementCollection, ala this... JPA 2.0: Mapping a Map - it isn't exactly what i'm after cos it looks like it is relating the translated strings table to the pk of the owning table. This means I can only have one translatable string field per entity (unless I add new join columns into the translatable strings table, which defeats the point, its the opposite of what I am trying to do). I'm also not clear on how this would work across entites, presumably the id of each entity would have to use a database wide sequence to ensure uniqueness of the translatable strings table.
BTW, I tried the example as laid out in that link and it didn't work for me - as soon as the entity had a localizedString map added, persisting it caused the client side to bomb but no obvious error on the server side and nothing persisted in the DB :S
I been around the houses on this about 9 hours so far, I've looked at this Internationalization with Hibernate which appears to be trying to do the same thing as the link above (without the table definitions it hard to see what he achieved). Any help would be gratefully achieved at this point...
Edit 1 - re AMS anwser below, I'm not sure that really addresses the issue. In his example it leaves the storing of the description text to some other process. The idea of this type of approach is that the entity object takes the text and locale and this (somehow!) ends up in the translatable strings table. In the first link I gave, the guy is attempting to do this by using an embedded map, which I feel is the right approach. His way though has two issues - one it doesn't seem to work! and two if it did work, it is storing the FK in the embedded table instead of the other way round (I think, I can't get it to run so I can't see exactly how it persists). I suspect the correct approach ends up with a map reference in place of each text that needs translating (the map being locale->content), but I can't see how to do this in a way that allows for multiple maps in one entity (without having corresponding multiple columns in the translatable strings table)...
(I'm Henno who replied to hwellman's blog.) My initial approach was very similar to your approach and it does the job. It meets the requirement that any field from any entity can reference a localized String Map with a general database table that does not have to reference other more concrete tables. Indeed I also use it for multiple fields in our Product entity (name, description, details). I also had the "problem" that JPA generated a table with only a primary key column and a table for the values that referenced this id. With OpenJPA I had no need for a dummy column:
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 simply stores Locale as a String. Because we don't really need an extra entity StringI18NSingleValue so I think your mapping using #ElementCollection is a bit more elegant.
There is an issue you have to be aware of though: do you allow sharing a Localised with multiple entities, and how do you prevent orphaned Localised entities when the owning entity is removed? Simply using cascade all is not sufficient. I decided to see a Localised as much as possible as a "value object" and not allow it to be shared with other entities so that we don't have to think about multiple references to the same Localised and we can safely use orphan removal. So my Localised fields are mapped like:
#OneToOne(cascade = ALL, orphanRemoval = true)
Depending on my use case I also use fetch = EAGER/LAZY and optional = false or true. When using optional = false I use #JoinColumn(nullable=false) so OpenJPA generates a not null constraint on the join column.
Whenever I do need to copy a Localized to another entity, I do not use the same reference but I create a new Localized instance with the same contents and no id yet. Otherwise you may get hard to debug problems where changinIf you don't do this you are still sharing an instance with multiple entities and you may get surprising bugs where changing a Localised String can change another String at another entity.
So far so good, however in practice I found that OpenJPA has N+1 select problems when selecting entities that contain one or more Localized Strings. It does not efficiently fetch an element collection (I reported this as https://issues.apache.org/jira/browse/OPENJPA-1920). That problem is probably solved by using a Map<Locale, StringI18NSingleValue>. However OpenJPA can also not efficiently fetch structures of the form A 1..1 B 1..* C which is also what happens here (I reported this as https://issues.apache.org/jira/browse/OPENJPA-2296). This can seriously affect the performance of your application.
Other JPA providers may have similar N+1 select problems. If the performance of fetching Category is of concern to you, I would check whether or not the number of queries used for fetching Category depends on the number of entities. I know that with Hibernate you can force batch fetching or subselect to solve these kind of problems. I also know EclipseLink has similar features that may or may not work.
Out of desperation to solve this performance issue I actually had to accept living with a design I don't really like: I simply added a String field for each language I had to support to the Localised. For us this is possible because we currently only need to support a few languages. This resulted in only one (denormalized) Localised table. JPA can then efficiently join the Localised table in queries, but this will not scale well for many languages and does not support an arbitrary number of languages. For maintainability I kept the external interface of Localised the same and only changed the implementation from a Map to a field-per-language so that we may easily switch back in the future.
OK, I think I have it. It looks like a simplified version of the first link in my question will work, just using a ManyToOne relationship to a Localised entity (with a different joinColumn for each text element in your main entity) and a simple ElementCollection for the Map within that Localised entity. I coded a slightly different example than my question, with just one entity (Category), having two text elements that need multiple entries for each locale (name and description).
Note this was done against Eclipselink 2.4 going to MySQL.
Two notes about this approach - as you can see in the first link, using ElementCollection forces a separate table to be created, which results in two tables for the translatable strings - one just holds the ID (Locaised) that is the FK in the main one (Localised_strings) that holds all the Map info. The name Localised_strings is the automatic/default name - you can use another one with the #CollectionTable annotation. Overall, this isn't ideal from a DB point of view but not the end of the world.
Second is that, at least for my combination of Eclipselink and MySQL, persisting to a single (auto generated) column table gives an error :( So i've added in a dummy column w a default value in the entity, this is purely to overcome that issue.
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);
}
}
So these generate tables as follows :-
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 Main to test it...
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();
}
}
This produces output :-
This is the business category
La Business
and the following table entries :-
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"
Uncommenting the em.remove correctly deletes both the Category and it's associated Locaised/Localised_strings entries.
Hope that all helps someone in the future.
I know it's a bit late, but I implemented the following approach:
#Entity
public class LocalizedString extends Item implements Localizable<String>
{
#Column(name = "en")
protected String en;
#Column(name = "en_GB")
protected String en_GB;
#Column(name = "de")
protected String de;
#Column(name = "de_DE")
protected String de_DE;
#Column(name = "fr")
protected String fr;
#Column(name = "fr_FR")
protected String fr_FR;
#Column(name = "es")
protected String es;
#Column(name = "es_ES")
protected String es_ES;
#Column(name = "it")
protected String it;
#Column(name = "it_IT")
protected String it_IT;
#Column(name = "ja")
protected String ja;
#Column(name = "ja_JP")
protected String ja_JP;
}
The entity has no setters and getters! Instead the Localizable interface defines common get/set methods:
public class Localizable<T> {
private final KeyValueMapping<Locale, T> values = new KeyValueMapping<>();
private T defaultValue = null;
/**
* Generates a {#link Localizable} that only holds one value - for all locales.
* This value overrides all localalized values when using
* {#link Localizable#toString()} or {#link Localizable#get()}.
*/
public static <T> Localizable<T> of(T value) {
return new Localizable<>(value);
}
public static <T> Localizable<T> of(Locale locale, T value) {
return new Localizable<>(locale, value);
}
private Localizable(T value) {
this.defaultValue = value;
}
private Localizable(Locale locale, T value) {
this.values.put(locale, value);
}
public Localizable() {
}
public void set(Locale locale, T value) {
values.put(locale, value);
}
/**
* Returns the value associated with the default locale
* ({#link Locale#getDefault()}) or the default value, if it is set.
*/
public T get() {
return defaultValue != null ? defaultValue : values.get(Locale.getDefault());
}
public T get(Locale locale) {
return values.get(locale);
}
/**
* Returns the toString of the value for the default locale
* ({#link Locale#getDefault()}).
*/
#Override
public String toString() {
if (defaultValue != null) {
return defaultValue.toString();
}
return toString(Locale.getDefault());
}
/**
* Returns toString of the localized value.
*
* #return null if there is no localized.
*/
public String toString(Locale locale) {
return values.transformValue(locale, v -> v.toString());
}
public Map<Locale, T> getValues() {
return Collections.unmodifiableMap(values);
}
public T getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(T defaultValue) {
this.defaultValue = defaultValue;
}
}
The huge advantage of this approach is that you only have one localizable entity and the localized values are stored in columns (instead of having one entity for each localization).
Here is one way to do it.
Load all translated strings from the database into a cache lets call it MessagesCache it would have a method called public String getMesssage(int id, int languageCode). You can use google guava immutable collections to store this in memory cache. You can also use a Guava LoadingCache to store the cache valued if you wanted to load them on Demand. If you have such a cache you can the write code like this.
#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;
}
}
The main problem I see with this approach is to that you need to know the locale that you are referencing in the entity, i would suggest that the task of picking the correct language for descriptions should be done not in the entity but in a higher level abstraction, such as Dao or a Service.

Categories