I have two fields of an entity class which I don't want to be unique but to instead be used as composite fields for a key which must itself be unique. For example I have two fields (name and version) which can be the same for other records but together they must be unique. What is the best way to do that using Hibernate (with annotations)? I am using Hibernate Validator for other fields but I am not sure of a way to use that to validate that two fields together compose a unique key. I am using a generic entity class which has an id generic type which can be swapped out for a composite key class but I have yet to get that to work very well.
This will create a unique key on the database:
#Table( name = "MYTABLE",
uniqueConstraints = { #UniqueConstraint( columnNames = { "NAME", "VERSION" } ) } )
This will be enforced by the database on a update or persist.
You'd need to write your own custom validator if you wanted to enforce this using Hibernate Validator.
We usually will wrap the two fields in an inner key class which is marked as #Embeddable. For example:
#Entity
public class Foo {
#EmbeddedId()
private Key key;
...
#Embeddable
public static class Key {
#Column(nullable=false)
private String name;
#Column(nullable=false)
private int version;
protected Key () {
// for hibernate
}
public Key (String name, int version) {
this.name = name;
this.version = version;
}
...
// You probably want .equals and .hashcode methods
}
}
Related
I have an entity 'Product' and I want the primary key in ES to be used as a combination of 'id' and 'name' attributes. How can we do that using spring data elastic search.
public class Product {
#Id
private String id;
#Id
private String name;
#Field(type = FieldType.Keyword)
private Category category;
#Field(type = FieldType.Long)
private double price;
#Field(type = FieldType.Object)
private List<ValidAge> age;
public enum Category {
CLOTHES,
ELECTRONICS,
GAMES;
}
}
One way to achieve this would be the following:
first rename your id property, I changed it to documentId here. This is necessary, because in Spring Data
Elasticsearch an id-property can be either annotated with #Id or it can be namend id. As there can only be one
id-property we need to get this out of the way. It can have the name id in Elasticsearch, set by the #Field
annotation, but the Java property must be changed.
second, add a method annotated with #Id and #AccessType(AccessType.Type.PROPERTY) which returns the value you
want to use in Elasticsearch.
third, you need to provide noop-setter for this property. This is necessary because Spring Data Elasticsearchsoe
not check the id property to be read only when populating an entity after save or when reading from the index.
This is a bug in Spring Data Elasticsearch, I'll create an issue for that
So that comes up with an entity like this:
#Document(indexName = "composite-entity")
public class CompositeEntity {
#Field(name="id", type = FieldType.Keyword)
private String documentId;
#Field(type = FieldType.Keyword)
private String name;
#Field(type = FieldType.Text)
private String text;
#Id
#AccessType(AccessType.Type.PROPERTY)
public String getElasticsearchId() {
return documentId + '-' + name;
}
public void setElasticsearchId(String ignored) {
}
// other getter and setter
}
The repository definition would be straight forward:
public interface CompositeRepository extends ElasticsearchRepository<CompositeEntity,
String> {
}
Remember that for every method that needs an Elasticsearch Id, you'll need to create like it's done in the entity
class.
I am not sure about spring data elasticsearch but spring jpa provides the facility of defining composite primary key by using #IdClass where we can define a separate class(let us say class A) in which we can define all the fields which we want to be a part of composite key Then we can use #IdClass(A.class) in entity class and use #Id annotation on all the fields which should be the part of the composite key
you can refer to this article, although I am not sure whether the same concept will be applicable for spring data es - https://www.baeldung.com/jpa-composite-primary-keys
I am using JPA and have a view I would like to access. I am using a mapped entity with an embedded Id in many of my other classes to access tables with similar requirements. However here whenever there are nulls in the view that comprise the id, the whole object is returned as null. There are the right number of entities returned when i query, but they are null.
Here are the classes:
{
#Entity
#Table(name = "VW_PRODUCT")
public class VwProduct implements Serializable {
#EmbeddedId
private VwProductId id;
public VwProduct() {
}
}
{
#Embeddable
public class VwProductId implements java.io.Serializable {
#Column(name = "PROD_NAME", nullable=true)
private String prodName;
#Column(name = "PROD_CTGRY", nullable=true)
private String prodCtgry;
#Column(name = "PROD_SBCTGRY", nullable=true)
private String prodSbctgry;
}
I omitted things like getters and setters and hashcode but i think my question is clear; how do I access this view, when some of its values are null?
Thank you!
If you have a primary key column with null, and search on that column will always return no objects.
There are three possible solutions that I am aware of, in order of complexity/wonkiness. (For people not working on a read-only view, do not do any of the following. You will blow your foot off with an incredibly large shotgun.)
The easiest answer is to change your definition of the view and add something like a rowid or generated serial and then make that the primary key.
The second answer, and this is both implementation specific and hibernate specific, is to have a primary key of #ID #ROWID String id;
The last answer, is more complex, is by mapping all three of your fields to a "NullableString" UserType, and have a nullSafeGet that maps null to something non-null, like '' or something. You'll get duplicates, but since this is a read-only view, you don't really care.
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.
Lets say we have User entity class. User can be friends with other users. How can i map this self-reference collection field without creating a new entity called Connection or creating multiple entries in the database?
#Entity
public class User {
...
#ManyToMany
private Collection<User> friends;
...
}
USER_ID-FRIEND_ID
1 - 2
2 - 1 (duplicate... I don't need it)
Following is snapshot from my code for ElementEntity:
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<ElementEntity> children;
#JoinColumn(name = "ParentId", referencedColumnName = "ElementId")
#ManyToOne(fetch = FetchType.LAZY)
private ElementEntity parent;
Where on database there are fields:
ElementId - primary key;
ParentId relation with parent
You can't - you need both records in the database.
Actually, for friendship relations, I'd say that a graph database like neo4j is the proper thing to use. There you have the two users and simply add an edge "friends".
At least you will need a relational table.
So you have a USER table and a FRIENDS:
user_id friend_id
1 2
But #Bozho answer is way better than mine (neo4j).
Well, in fact you can.
You can use annotations like #PreUpdate, #PrePersists, #PostUpdate and so to convert manually the elements of a collection. This way your entity can render then them way you want while in database you only store a raw text.
A more pausible alternative will be to use #Convert annotation, available since jpa 2.1 (#UserType in hibernate). It tells jpa to convert the field into another type everytime it read/save in database.
For it you should use #Convert anotation, specifying and AttributeConverter object.
For example
public class Parent {
#Id
private Integer id;
#Convert(converter = FriendConverter.class)
private Set<Parent>friends;
}
And converter class like the following:
#Component
public class FriendConverter implements AttributeConverter<List, String>{
#Autowired
private SomeRepository someRepository;
#Override
public String convertToDatabaseColumn(List attribute) {
StringBuilder sb = new StringBuilder();
for (Object object : attribute) {
Parent parent = (parent) object;
sb.append(parent.getId()).append(".");
}
return sb.toString();
}
#Override
public List convertToEntityAttribute(String dbData) {
String[] split = dbData.split(".");
List<Parent>friends = new ArrayList<>();
for (String string : split) {
Parent parent = someRepository.findById(Integer.valueOf(string));
friends.add(accion);
}
return friends;
}
}
It is a dummy implementation but it gives you the idea.
As a personal comment, I do recommend to map the relationship as it should. In the future it will avoid you problems. AttributeConverter comes in handy when working with enums
I'm using java persistence to save a list of entities that are associated to another entity. Here's a quick rundown of where I'm having some problems.
#Entity public class Offer implements Serializable {
#Id private Long offerId;
#OneToMany
#Column List<OfferCategory> offerCategories;
}
#Entity public class OfferCategory implements Serializable {
#Embeddable public static class Id implements Serializable
{
#Column(name="offer_id")
private Long offerId;
#Column(name="category_id")
private Long categoryId;
public Id() {}
public Id(Long offerId, Long categoryId) {
this.offerId = offerId;
this.categoryId = categoryId;
}
public boolean equals(Object o) {
if(o != null && o instanceof Id) {
Id other = (Id) o;
return this.offerId.equals(other.offerId) &&
this.categoryId.equals(other.categoryId);
}
else
return false;
}
public int hashCode() {
return offerId.hashCode() + categoryId.hashCode();
}
}
#EmbeddedId private Id id = new Id();
}
Essentially, due to an architecture I cannot change, I need to save a list of Categories as being assigned to an Offer.
Right now, I'm getting a list of Categories from the user and then putting them into the offerCategories field of Offer. However, this doesn't work for new Offers, because there's no way for me to set the ID of a new item.
I'm new to JPA and Seam, so if someone could give me a nudge in the right direction it would be greatly appreciated.
I have not tried using a composite ID before, but one thing to note is that #Column is only used to change the properties of the database column the field is using. It doesn't stipulate a relation, so you still need something like this:
#OneToMany
List<OfferCategory> offerCategories;
As I looked into tutorial I found this:
You cannot use an IdentifierGenerator to generate composite keys. Instead the application must assign its own identifiers.
So you have to assign the id by yourself. Maybe you can make a DB sequence and fetch its values with native query?
And one remark - if you want to use List mapping (order of Categories in Offer is defined by database), you need an index column to contain the index in list. If the order of categories is not important, Set would be more convenient.
Well, my solution for now is that I persist each one (creating keys for new entries), then stuff them into a list, then stuff them into their container object, then persist that object.