Entity retrieved from database with same case as in query - java

My database contains the following table:
table:
country {
code varchar(255) not null
primary key
};
class:
#Entity
public class Country {
#Id
#Column(name = "code")
private String mCode;
public String getCode() {
return mCode;
}
public void setCode(String code) {
mCode = code;
}
}
sample table rows:
| code |
|------|
| USA |
| UK |
When I retrieve a country using the following CrudRepository:
public interface CountryRepository extends CrudRepository<Country, String> {
}
First scenario:
mRepository.findOne("USA")
It will give me the following result in my rest api:
{
"code": "USA"
}
Second scenario:
mRepository.findOne("UsA")
It will give me the following result in my rest api:
{
"code": "UsA"
}
Third scenario:
mRepository.findOne("Usa")
It will give me the following result in my rest api:
{
"code": "Usa"
}
I have also inspected the same behavior using the debugger and found my object in memory actually have the same behavior.
What I Want: I want the returned data to be the same case as in database.

As already hinted by #Bedla in the comment, you may be using a case insensitive varchar data type in your database. However, this is not recommended for primary keys in Hibernate (and in general), because Hibernate relies on id property value uniqueness when referring to entities in the persistence context (and second-level cache if enabled).
For example, after loading an entity by "USA" and then by "usa" (or after merging a detached "usa" while "USA" has already been loaded, etc) in the same persistence context you may end up with two different instances in the persistence context, meaning further that they would be flushed separately thus overwriting each other changes, etc.
Rather use a case sensitive data type in the database, but allow searching by ignoring case:
public interface CountryRepository extends CrudRepository<Country, String> {
#Query("select c from Country c where upper(c.mCode) = upper(?1)")
Country getCountry(String code);
}
PS Country codes are not good candidates for primary keys in general, because they can change.

Try overriding the equals() and hashcode() methods in your entity such that they take the case of mCase into consideration (and don't use any other fields in these two methods).

you could wrap the CrudRepository with another layer such as service or controller.
public Country findOne(String code){
Country country = mRepository.findOne(keyWord)
if (country !=null){
country.setCode(code)
return country;
}
return null;
}
The entity returned from mRepository is what stored in database. I think you should have another method to do this special handling.

By calling repository.findOne("Usa") (default implementation is SimpleJpaRepository.findOne) Hibernate will use EntityManager.find which instantiates the entity(if it's found in the Database and not present in first and second level cache) and set the passed argument value as primary key, using SessionImpl.instantiate method, instead of using the Select query result.
I've filled a ticket in Hibernate Jira for this issue.
Solution 1: Do not use natural id as a primary key:
As said, it's not recommended to use a business/natural key as primary key, as it may change in the future as business rules change (Business rules can change without permission!!) + If you're using clustered index, a string may be slower for primary key, and perhaps you will find two rows in the database: Country('USA'), Country('USA '), use a Surrogate Key instead, you can check this two SO questions for more details:
What's the best practice for primary keys in tables?
Strings as Primary Keys in SQL Database
If you choose this option don't forget to map a unique constraint for the business key using #UniqueConstraint:
#Entity
#Table(uniqueConstraints = #UniqueConstraint(columnNames = "code"))
public class Country {
// Your code ...
#Column(name = "code", nullable=false)
private String mCode;
//...
}
Solution 2: Change Country.mCode case:
If you have a possibility to store all Country.code in UpperCase :
#Entity
public class Country {
private String mCode;
protected Country(){
}
public Country(String code){
this.mCode = code.toUpperCase()
}
#Id
#Column(name = "code")
public String getCode() {
return this.mCode;
}
// Hibernate will always use this setter to assign mCode value
private void setCode(String code) {
this.mCode = code.toUpperCase();
}
}
Solution 3: Customize the CrudRepository:
While adding a custom function to your repository, you should always get rid of the default findOne(String), so you force others to use the "safest" method.
Solution 3.1:
Use custom implementations for mRepository find method (I named it findOneWithRightStringCase):
public interface CountryRepositoryCustom {
Country findOneWithRightStringCase(String id);
}
public class CountryRepositoryImpl implements CountryRepositoryCustom{
#PersistenceContext private EntityManager entityManager;
#Override
public Country findOneRespectCase(String id) {
try {
return entityManager.createQuery("SELECT c FROM Country c WHERE c.mCode=:code", Country.class).setParameter("code", id).getSingleResult();
} catch (NoResultException ex) {
return null;
}
}
}
public interface CountryRepository extends CrudRepository<Country, String>, CountryRepositoryCustom {
#Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use findOneRespectCase(String) instead!");
}
}
Solution 3.2:
You may add a Query methods for your repository :
public interface CountryRepository extends CrudRepository<Country, String> {
Country findByMCode(String mCode);
#Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use findByMCode(String) instead!");
}
}
Or Use #Query(JPQL):
public interface CountryRepository extends CrudRepository<Country, String> {
#Query("select c from Country c where c.mCode= ?1")
Country selectCountryByCode(String mCode);
#Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use selectCountryByCode(String) instead!");
}
}
Hope it helps!
Note: I'm using Spring Data 1.11.8.RELEASE and Hibernate 5.2.10.Final.

Related

Hibernate #Check to ensure only single occurrence of a particular value in a column

I am implementing a table for storing user roles having columns user_id and role. The business requirement is that there should be a constraint that only one record must exist with the value "ROLE_ROOT" for column role. There is no limit to the number of records for any other value in the role column.
For example:
Valid:
role |user_id|
-------------|-------|
ROLE_ROOT | 3|
ROLE_CUSTOMER| 5|
ROLE_CUSTOMER| 9|
Invalid:
role |user_id|
-------------|-------|
ROLE_ROOT | 3|
ROLE_ROOT | 4|
ROLE_CUSTOMER| 5|
ROLE_CUSTOMER| 9|
The below scenario must not occur at all when persisting data.
I had at first thought about using a trigger on the table to check this constraint before any insert, but I have been asked not to implement triggers or any database specific feature and use Hibernate only.
This leaves only (to the best of my knowledge) the #Check annotation in Hibernate. But I am unable to determine the constraint to set because checks cannot have aggregate functions. Is there any way to use a Hibernate #Check annotation to achieve this? The only other way is to implement this manually but I wanted to make sure that this could be achieved on as low a level as possible given the constraints I have to work with.
#Check is only a way to set a SQL check constraint on an entity :
Arbitrary SQL CHECK constraints which can be defined at the class,
property or collection level.
That is :
a type of integrity constraint in SQL which specifies a requirement
that must be met by each row in a database table
The constraint must be a predicate. It can refer to a single column,
or multiple columns of the table.
And the relevant part :
Common restrictions
Most database management systems restrict check constraints to a
single row, with access to constants and deterministic functions, but
not to data in other tables, or to data invisible to the current
transaction because of transaction isolation.
So it cannot very probably apply to your case because your check for a row relies on the content of other rows.
I think that you fall into business rule cases where you should ensure yourself that within your workflows that constraint is not violated.
You have so 2 ways :
to handle specific cases where the role column may be updated
to have a more cross-cut approach by implementing JPA preUpdate and PreInsert listeners.
At last, nothing can prevent your application code to perform a native query, even with JPA. But well, code review, unit tests and integration tests are also there to fill the gap as much as possible.
#Check wont help you in this case, since it's not meant to query the database as part of validation before persisting the entity. Like other hibernate validator annotations, it is meant for basic constraint checking.
Thus only way is to perform the DB check either before persisting or as part of a Custom annotation. However, since this will be expensive to check DB everytime, you can avoid it by having an in-memory cache (if there is only 1 instance of your application deployed) or a distributed cache (if multiple instances).
Example using custom annotation and in-memory cache:
(Spring Boot Version: 2.3.1.RELEASE, Hibernate validator: 5.2.4.Final)
(If you seek to use in-memory cache, remember to invalidate it, when record with role ROLE_ROOT is deleted)
Entity class - (auto increment id)
#Entity
#Table(name = "my_table")
#CheckRole
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "role")
private String role;
#Column(name = "user_id")
private int userId;
public int getId() {
return id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
}
Repository -
#Repository
public interface MyTableRepository extends JpaRepository<MyEntity, Integer> {
#Transactional(propagation = Propagation.NOT_SUPPORTED)
#Query(value = "SELECT CASE WHEN COUNT(e) > 0 THEN true ELSE false END FROM MyEntity e WHERE e.role = :roleName")
Boolean checkIfRoleExists(#Param("roleName") String roleName);
}
Annotation-
#Target(TYPE)
#Retention(RUNTIME)
#Constraint(validatedBy = RoleValidator.class)
public #interface CheckRole {
String message() default "Cannot have duplicate entry for Role: ROLE_ROOT";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Constraint Validator Impl (Used In-memory Caffeine cache) -
public class RoleValidator implements ConstraintValidator<CheckRole, MyEntity > {
private static final String ROLE_TO_VALIDATE = "ROLE_ROOT";
private LoadingCache<String, Boolean> myCache;
public RoleValidator(MyTableRepository repository) {
myCache = Caffeine.newBuilder()
.maximumSize(1)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(repository::checkIfRoleExists);
}
#Override
public void initialize(CheckRole constraintAnnotation) {
}
#Override
public boolean isValid(MyEntity entity, ConstraintValidatorContext context) {
String roleValue = entity.getRole();
if (roleValue.equals(ROLE_TO_VALIDATE)) {
boolean isValid = !myCache.get(ROLE_TO_VALIDATE);
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Cannot have duplicate entry for Role: " + ROLE_TO_VALIDATE)
.addConstraintViolation();
}
return isValid;
} else {
return true;
}
}
}
Hibernate properties customizer -
#Component
public class ValidatorAddingCustomizer implements HibernatePropertiesCustomizer {
private final ObjectProvider<Validator> provider;
#Autowired
public ValidatorAddingCustomizer(ObjectProvider<Validator> provider) {
this.provider = provider;
}
#Override
public void customize(Map<String, Object> hibernateProperties) {
Validator validator = provider.getIfUnique();
if (validator != null) {
hibernateProperties.put("javax.persistence.validation.factory", validator);
}
}
}
Validation in action -
#RestController
public class MyController {
#Autowired
private MyTableRepository repository;
#GetMapping("/hello")
public void hello() {
MyEntity myEntity = new MyEntity();
myEntity.setRole("ROLE_ROOT");
myEntity.setUserId(3);
repository.save(myEntity); //saves successfully
MyEntity myEntity2 = new MyEntity();
myEntity2.setRole("ROLE_ROOT");
myEntity2.setUserId(4);
repository.save(myEntity2); //Throws Constraint Violation Exception
}
}

JPA Repository.findById() returns null but the value is exist on db

I'm developing Spring boot project, using JPA.
What I wanna know is repository.findById(id) method returns null, whereas data is available in the DB.
Functions save() and findAll() are working fine. When I ran the same code on junit test environment, but it completely worked. If the data is hard coded, like memberRepository.findById("M001");, it working fine.
Entity
#Entity
#Table(name="df_member")
public class DfMember {
#Column(name="member_type")
private String memberType;
#Id
#Column(name="id")
private String id;
...columns...
...Getters/Setters.....
Controller
#ResponseBody
#RequestMapping(value="/checkIdDuplicate", method=RequestMethod.POST)
public boolean checkIdDuplicate(#RequestBody String id) {
return memberService.isExistByUserId(id);
}
MemberService
public boolean isExistByUserId(String id) {
Optional<DfMember> member = memberRepository.findById(id);
return member.isPresent();
}
Repository
public interface MemberRepository extends CrudRepository<DfMember, String> {
}
Should return Member Object but it's null.
While the OP has solved his issue, I have to add my answer to the same problem, but with a different cause, because I'm sure will be helpful after hours of debugging I found in the source code of Hibernate (5.4.18) a try/catch that when a EntityNotFoundException is thrown in the hydration process the findBy returns null, even if the entity exists, and it's hydrated successfully. This is because a related referenced entity doesn't exists and Hibernate expect it to exists
For example I have two entities Unit and Improvement where I store an unit with id 5 to have an improvement with id 0 (which doesn't exists), then unitRepository.findById() returns null, instead of the Unit entity with id 5.
#Entity
#Table(name = "units")
public class Unit {
#ManyToOne(fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "improvement_id")
#Cascade({ CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DELETE })
private Improvement improvement;
}
The reason this happened was because an import script used 0 as value for the improvement_id instead of the original NULL.
Hint: Becareful with disabling Foreign Key checks in import scritps
Best regards
I would like to add something with #Alexpandiyan answer. Instead of first finding record by Id and checking if it is present or not. You can directly check in database if id exists or not by using predefined function existsById like below.
public interface MemberRepository extends CrudRepository<DfMember, String> {
boolean existsById(String id);
}
Updated member service function memberService.isExistByUser().
public boolean isExistByUserId(String id) {
return memberRepository.existsById(id);
}
See documentation https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#new-features.1-11-0
You have to change #RequestBody to #RequestParam. Please update your controller code as below.
#ResponseBody
#RequestMapping(value="/checkIdDuplicate", method=RequestMethod.POST)
public boolean checkIdDuplicate(#RequestParam String id) {
return memberService.isExistByUserId(id);
}
This occurred to me because of a script which was inserting data (while disabling foreign key checks), and one of the referenced ID was not existing in the parent table, i.e. :
SET FOREIGN_KEY_CHECKS=0;
INSERT INTO company(id, name) VALUES (1, 'Stackoverflow');
INSERT INTO employee(id, first_name, last_name, company_id) VALUES (1, 'John', 'Doe', 1);
INSERT INTO employee(id, first_name, last_name, company_id) VALUES (2, 'Jane', 'Doe', 2);
SET FOREIGN_KEY_CHECKS=1;
The Jane Doe record references a target primary key value (company_id=2) that does not exist in the company table.
In that case JPA's findById does not return any record even if it exists.
Fixing data intergrity solves the problem.
public boolean isExistByUserId(String id)
{
Optional<DfMember>member = Optional.ofNullable(memberRepository.findById(id).orElse(new DfMember()));
return member.isPresent();
}
Please update your MemberService code.

Select only some columns from a table

Is there a way to select only some columns from a table using jpa?
My tables are huge and I am not allowed to map all the columns in my entities. I tried to create an entity (as a side note, I don't have PKs in my tables):
#Entity
#Table(name = "SuperCat")
#Getter
#Setter
public class Cat{
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name="nameCat")
private String name;
}
and then in my repository to
public interface CatRepository extends
CrudRepository<Cat, Long> {
#Query(
"SELECT name FROM Cat")
Page<Cat> getAlCats(Pageable pageable);
This is only a simple example, but the idea is the same. I have searched a lot and I found projections, but there you need to map the whole table, then I found native queries, but still doesn't apply. I know I can return an Object and the other solution is to use query with NEW and create my own object (no #entity, like a pojo). But is there a way that I can do this using jpa, to be able to use repository and services, if I am creating my own pojo then i will create a #transactional class put the queries (with NEW) there and this is it. I don't like this approach and I don't think that the jpa does't allow you to select only some columns, but I didn't find a proper way.
Maybe you will ask what is the result if I am doing like this:
I get this error: "Cannot create TypedQuery for query with more than one return using requested result type [java.lang.Long]"
(For new queries, I am talking about : http://www.java2s.com/Tutorials/Java/JPA/4800__JPA_Query_new_Object.htm maybe I was not clear)
You can do the same by using below approach.
Just create a constructor in entity class with all the required parameters and then in jpa query use new operator in query like below.
String query = "SELECT NEW com.dt.es.CustomObject(p.uniquePID) FROM PatientRegistration AS p";
TypedQuery<CustomObject> typedQuery = entityManager().createQuery(query , CustomObject.class);
List<CustomObject> results = typedQuery.getResultList();
return results;
And CustomObject class should look like below with the constructor.
public class CustomObject {
private String uniquePID;
public CustomObject(String uniquePID) {
super();
this.uniquePID = uniquePID;
}
public String getUniquePID() {
return uniquePID;
}
public void setUniquePID(String uniquePID) {
this.uniquePID = uniquePID;
}
}
spring-data-jpa projection not need to map the whole table, just select the necessary fileds :
// define the dto interface
public interface CatDto {
String getName();
// other necessary fields
...
}
#Query(value = "select c.name as name, ... from Cat as c ...)
Page<CatDto> getAllCats(Pageable pageable);
By this way, CatDto is an interface and it only includes some fileds part of the whole table. Its fields name need to match the select field's alias name.

Get ID before saving to database

I use hibernate sequences to generate id of an entity. I use PostgreSQL 9.1.
Is it possible to get entity id before it is saved to database? How?
You explicitely create a separate sequence, get its value, then insert an object with id based on that value. You will have more code, but the ID will be available before the insertion and the guarantees for sequences are exactly the same as for serially given IDs, because they are essentially the same.
In other words:
create your own sequence
make a primary key a simple int not serial
get a number from sequence
use it as an ID for your object
This question has an answer saying how to get next sequence value.
save() method returns the id of the entity that is saved. You can use it!
reference:-> http://docs.jboss.org/hibernate/annotations/3.5/api/org/hibernate/Session.html
You can implement the interface org.hibernate.id.IdentifierGenerator and create a Id generator.
Example:
import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.TimeBasedGenerator;
public class TimeBasedIDGenerator implements IdentifierGenerator {
private static TimeBasedGenerator generator = Generators.timeBasedGenerator();
private static TimeBasedIDGenerator SINGLETON = new TimeBasedIDGenerator();
public static UUID generate() {
return SINGLETON.generateUUID();
}
#Override
public Serializable generate(SessionImplementor session, Object parent) throws HibernateException {
return generator.generate();;
}
}
This can be used in your Entities like this. So the id is generated by the constructor:
#Entity
public EntityClassName {
private UUID uuid;
private Integer mandatoryField;
public EntityClassName() {
}
public EntityClassName(Integer mandatoryField) {
this.uuid = TimeBasedIDGenerator.generate();
this.mandatoryField = mandatoryField;
}
#Id
#Column(name = COLUMN_XXX_UUID)
#Type(type = "java.util.UUID")
public UUID getUuid() {
return uuid;
}
// setter + other properties
}

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