JPA map collection of Enums - java

Is there a way in JPA to map a collection of Enums within the Entity class? Or the only solution is to wrap Enum with another domain class and use it to map the collection?
#Entity
public class Person {
public enum InterestsEnum {Books, Sport, etc... }
//#???
Collection<InterestsEnum> interests;
}
I am using Hibernate JPA implementation, but of course would prefer implementation agnostic solution.

using Hibernate you can do
#ElementCollection(targetElement = InterestsEnum.class)
#JoinTable(name = "tblInterests", joinColumns = #JoinColumn(name = "personID"))
#Column(name = "interest", nullable = false)
#Enumerated(EnumType.STRING)
Collection<InterestsEnum> interests;

The link in Andy's answer is a great starting point for mapping collections of "non-Entity" objects in JPA 2, but isn't quite complete when it comes to mapping enums. Here is what I came up with instead.
#Entity
public class Person {
#ElementCollection(targetClass=InterestsEnum.class)
#Enumerated(EnumType.STRING) // Possibly optional (I'm not sure) but defaults to ORDINAL.
#CollectionTable(name="person_interest")
#Column(name="interest") // Column name in person_interest
Collection<InterestsEnum> interests;
}

tl;dr A short solution would be the following:
#ElementCollection(targetClass = InterestsEnum.class)
#CollectionTable
#Enumerated(EnumType.STRING)
Collection<InterestsEnum> interests;
The long answer is that with this annotations JPA will create one table that will hold the list of InterestsEnum pointing to the main class identifier (Person.class in this case).
#ElementCollections specify where JPA can find information about the Enum
#CollectionTable create the table that hold relationship from Person to InterestsEnum
#Enumerated(EnumType.STRING) tell JPA to persist the Enum as String, could be EnumType.ORDINAL

I was able to accomplish this in this simple way:
#ElementCollection(fetch = FetchType.EAGER)
Collection<InterestsEnum> interests;
Eager loading is required in order to avoid lazy loading inizializing error as explained here.

I'm using a slight modification of java.util.RegularEnumSet to have a persistent EnumSet:
#MappedSuperclass
#Access(AccessType.FIELD)
public class PersistentEnumSet<E extends Enum<E>>
extends AbstractSet<E> {
private long elements;
#Transient
private final Class<E> elementType;
#Transient
private final E[] universe;
public PersistentEnumSet(final Class<E> elementType) {
this.elementType = elementType;
try {
this.universe = (E[]) elementType.getMethod("values").invoke(null);
} catch (final ReflectiveOperationException e) {
throw new IllegalArgumentException("Not an enum type: " + elementType, e);
}
if (this.universe.length > 64) {
throw new IllegalArgumentException("More than 64 enum elements are not allowed");
}
}
// Copy everything else from java.util.RegularEnumSet
// ...
}
This class is now the base for all of my enum sets:
#Embeddable
public class InterestsSet extends PersistentEnumSet<InterestsEnum> {
public InterestsSet() {
super(InterestsEnum.class);
}
}
And that set I can use in my entity:
#Entity
public class MyEntity {
// ...
#Embedded
#AttributeOverride(name="elements", column=#Column(name="interests"))
private InterestsSet interests = new InterestsSet();
}
Advantages:
Working with a type safe and performant enum set in your code (see java.util.EnumSet for a description)
The set is just one numeric column in the database
everything is plain JPA (no provider specific custom types)
easy (and short) declaration of new fields of the same type, compared with the other solutions
Drawbacks:
Code duplication (RegularEnumSet and PersistentEnumSet are nearly the same)
You could wrap the result of EnumSet.noneOf(enumType) in your PersistenEnumSet, declare AccessType.PROPERTY and provide two access methods which use reflection to read and write the elements field
An additional set class is needed for every enum class that should be stored in a persistent set
If your persistence provider supports embeddables without a public constructor, you could add #Embeddable to PersistentEnumSet and drop the
extra class (... interests = new PersistentEnumSet<>(InterestsEnum.class);)
You must use an #AttributeOverride, as given in my example, if you've got more than one PersistentEnumSet in your entity (otherwise both would be stored to the same column "elements")
The access of values() with reflection in the constructor is not optimal (especially when looking at the performance), but the two other options have their drawbacks as well:
An implementation like EnumSet.getUniverse() makes use of a sun.misc class
Providing the values array as parameter has the risk that the given values are not the correct ones
Only enums with up to 64 values are supported (is that really a drawback?)
You could use BigInteger instead
It's not easy to use the elements field in a criteria query or JPQL
You could use binary operators or a bitmask column with the appropriate functions, if your database supports that

Collections in JPA refer to one-to-many or many-to-many relationships and they can only contain other entities. Sorry, but you'd need to wrap those enums in an entity. If you think about it, you'd need some sort of ID field and foreign key to store this information anyway. That is unless you do something crazy like store a comma-separated list in a String (don't do this!).

Related

How to persist an enum with JPA?

I'm using MySQL and JPA.
I have an enum that has it's own table.
I have a field in an entity (entity1) that uses this enum. This field is annotated with: #Enumeration(EnumType.STRING).
1 - is it correct to persist this field in entity1 as a column in the db when it has it's own table?
2 - if I am using #JsonProperty on my other fields and answer to 1 is "yes", must I use #JsonProperty on the enum field too?
3 - what's the point in having the enum in a separate table?
Currently, with just the #Enumeration annotation and a column for the enum for entity1 I get error: was annotated as enumerated, but its java type is not an enum
1- How I addressed similar problem was as follows :
I defined the enum in a separate entity :
#Entity
#Table(name="CALC_METHOD")
public class CalculationMethod {
public CalculationMethod() {
super();
// TODO Auto-generated constructor stub
}
#Id
#Enumerated(EnumType.STRING)
#Column(name="METHOD_NAME")
private CalculationMethodId calcMethodID;
#Column(name="DISPLAY_TEXT")
private String displayName;
.
.
.
.
then I refered to it in another entity as follows :
#OneToOne
#JoinColumn(name="CALCULATION_METHOD",referencedColumnName="METHOD_NAME")
private CalculationMethod calculationMethod;
that way it's stored in a seprate table, yet referenced from another entity with no duplication ... the point here is that you can't map enum scoped variables so when I needed to store a display name for the enum value, I needed to make it a separate attribute as you see
3- why to store it in table? because from the java POV it was really an enum , and I want to apply some calculation methods polymorpically (like calculate some value in the refering entity using the calculation method, so I defined calculate() method for each calculation method , each with a different implementation then call it while calculating a whole) the I wanted it to be always read with the same value and display name from many places in the code , and If I want to modify the display name, it's done only # one place -thus consistency and maintainability-
2- it depends on the requirement and your json model
For your situation I normaly use an entity on BBDD for the ENUM like:
AuthenticationType
id, name, value : (0, CERT, Certificate)
Where name is the real ENUM and value is the text I want to represent on the views.
For that you need the following:
public enum AuthenticationTypeEnum{
CERT, PASS;
}
#Entity
#Table(name = "AuthenticationType")
public class AuthenticationType{
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
#Enumerated(EnumType.STRING)
private AuthenticationTypeEnum name; // REAL ENUM TYPE
#Column(name = "VALUE")
private String value;
....
}
#Entity...
class Authentication{
private String login;
...
#ManyToOne
private AuthenticationType type; // ENUM USE
...
}
In that way you can edit the value of your ENUM on BBDD without changing your code, for me this is one of the best options.
Hope this helps.
When you persist the entity use the cascade all on JPA to persist also the enum entity.
NOTE: On normal situations, the enums not change, so you set them only ones. They are a prerequisite to the application so they change on rare circumstances.

How to get key generated by Hibernate when I map a collection with #collectionId?

I am working on Quiz management. All mapping is done by Hibernate annotation for question to options. Question is an entity whereas all options are embedded object so I mapped option as follows :
QuestionMasterDTO's mapping for TabkidsMCQOptionMasterDTO :
#ElementCollection(fetch=FetchType.EAGER,targetClass=TabkidsMCQOptionMasterDTO.class)
#Fetch(FetchMode.SUBSELECT)
#Cascade(org.hibernate.annotations.CascadeType.ALL)
#CollectionTable(name="TABKIDS_MCQ_OPTION_MASTER",joinColumns={#JoinColumn(name="TMOM_QUESTION_ID")})
#GenericGenerator(name="hilo-gen",strategy="hilo")
#CollectionId(columns={#Column(name="TMOM_ID")},generator="hilo-gen", type=#Type(type="long"))
public Collection<IOptionMaster> getOptions() {
return options;
}
Where TabkidsMCQOptionMasterDTO is :
#Embeddable
public class TabkidsMCQOptionMasterDTO implements IOptionMaster {
private String optionText;
private boolean correct;
#Column(name = "TMOM_OPTION_TEXT")
public String getOptionText() {
return optionText;
}
#Column(name = "TMOM_IS_CORRECT")
public boolean isCorrect() {
return correct;
}
//setters omitted
}
Now in above mapping you can see I am using a generator i.e. hilo-gen and assigning a unique id to every option available in collection and that column name is 'TMOM_ID'.
This line :
#GenericGenerator(name="hilo-gen",strategy="hilo")
#CollectionId(columns={#Column(name="TMOM_ID")},generator="hilo-gen", type=#Type(type="long"))
Now when I fetch a question from database by using Hibernate criteria I am getting all options associated with the question but not getting unique option id i.e. TMOM_ID. How to get this id ??
Hibernate mainly uses two type of mapping Entity Type and Value Type.
Entity type means It will have its own existence in the world i.e. It must have a primary key.
Whereas Value type don't have its own existence this means value type always dependent on Entity type.
As your problem I can see Option does not have its won existence because it must always dependent of Question which is an entity .
So from my point of view if you want to access Option Id, Option must also be an entity type this means You have to use #Entity on top of TabkidsMCQOptionMasterDTO rather than making it as #Embeddable.
So here you have to use #OneToMany in your question master and from Other side in TabkidsMCQOptionMasterDTO you have to use #ManyToOne mapping.
I hope this will help to achieve what you want to get.

Best practice for storing global data in J2EE App using Hibernate

I'm looking for the best solution to store Java EE application's global data using Hibernate. It will consist of key value pairs. Example:
projectStarted = "10-11-11"
developerNumber = 3
teamLeader = "John"
As you see, all of this entries have different types.
For now I see two options:
1) Create GlobalData entity. Each field of it will be represented as unique column in the table and will contain unique setting. This way I have no problems with type casting, but I would like to avoid it in case where there will be big amount of settings.
2) Create Setting entity. Each of it will contain two fields: key(Primary key) and value and will be represented as unique record in the table. This is preferable solution, but It's seems to me that I will get a lot of type casting, because settings can be any type.
So basically, I'm looking for the way to implement second solution without getting a lot of troubles from different types. Can anybody help me?
Thanks.
Edit 1.
Yeah, thanks Christian. Just got similar idea.
What if I will have Settings entity, which will be like:
#Entity
#Table(name = "settings")
public class Setting {
#Column
private String key;
#Column
private String value;
#Column
private String converterClassFullName; //example by.lugovsky.MyConverter
//Getters, setters
}
And GlobalData class.
public class GlobalData {
private Date projectStarted;
private int developerNumber;
private String teamLeader;
Set<Setting> settings;
//Getters and setters for all, except settings.
}
So basically my idea is to convert Setting entity before persisting/updating/ etc. I can do this in my DAO, but I was wondering, if I could annotate GlobalData class with #Entity annotation as well without creating new table. This way I can set OneToMany annotation to Setting's set and Perform conversions in the internal #PrePersist etc. methods.
Will Hibernate allow me to do this?
Thanks again
You could store a Converter-Class into the db and the let it run through the given converter for a property before using the value. JSF offers Converter API:
public interface Converter{
public Object getAsObject(FacesContext fc, UIComponent component, String value) throws ConverterException;
public String getAsString(FacesContext fc, UIComponent component, Object obj) throws ConverterException;
}
If you have a schema with
name: String
value: String
converter: Class
then you could do something like this:
PropertyEntry pe = // Get from OR-Mapper
Converter c = (Converter) pe.getConverter().newInstance();
Object o = c.getAsObject(null, null, pe.getValue());
// use the object o instead of value
For even more coolness you could also define a field in the class which will not be persisted which you could use to hold the converted value within the object.

Hibernate - #ElementCollection - Strange delete/insert behavior

#Entity
public class Person {
#ElementCollection
#CollectionTable(name = "PERSON_LOCATIONS", joinColumns = #JoinColumn(name = "PERSON_ID"))
private List<Location> locations;
[...]
}
#Embeddable
public class Location {
[...]
}
Given the following class structure, when I try to add a new location to the list of Person's Locations, it always results in the following SQL queries:
DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson
And
A lotsa' inserts into the PERSON_LOCATIONS table
Hibernate (3.5.x / JPA 2) deletes all associated records for the given Person and re-inserts all previous records, plus the new one.
I had the idea that the equals/hashcode method on Location would solve the problem, but it didn't change anything.
Any hints are appreciated!
The problem is somehow explained in the page about ElementCollection of the JPA wikibook:
Primary keys in CollectionTable
The JPA 2.0 specification does not
provide a way to define the Id in the
Embeddable. However, to delete or
update a element of the
ElementCollection mapping, some unique
key is normally required. Otherwise,
on every update the JPA provider would
need to delete everything from the
CollectionTable for the Entity, and
then insert the values back. So, the
JPA provider will most likely assume
that the combination of all of the
fields in the Embeddable are unique,
in combination with the foreign key
(JoinColunm(s)). This however could be
inefficient, or just not feasible if
the Embeddable is big, or complex.
And this is exactly (the part in bold) what happens here (Hibernate doesn't generate a primary key for the collection table and has no way to detect what element of the collection changed and will delete the old content from the table to insert the new content).
However, if you define an #OrderColumn (to specify a column used to maintain the persistent order of a list - which would make sense since you're using a List), Hibernate will create a primary key (made of the order column and the join column) and will be able to update the collection table without deleting the whole content.
Something like this (if you want to use the default column name):
#Entity
public class Person {
...
#ElementCollection
#CollectionTable(name = "PERSON_LOCATIONS", joinColumns = #JoinColumn(name = "PERSON_ID"))
#OrderColumn
private List<Location> locations;
...
}
References
JPA 2.0 Specification
Section 11.1.12 "ElementCollection Annotation"
Section 11.1.39 "OrderColumn Annotation"
JPA Wikibook
Java Persistence/ElementCollection
In addition to Pascal's answer, you have to also set at least one column as NOT NULL:
#Embeddable
public class Location {
#Column(name = "path", nullable = false)
private String path;
#Column(name = "parent", nullable = false)
private String parent;
public Location() {
}
public Location(String path, String parent) {
this.path = path;
this.parent= parent;
}
public String getPath() {
return path;
}
public String getParent() {
return parent;
}
}
This requirement is documented in AbstractPersistentCollection:
Workaround for situations like HHH-7072. If the collection element is a component that consists entirely
of nullable properties, we currently have to forcefully recreate the entire collection. See the use
of hasNotNullableColumns in the AbstractCollectionPersister constructor for more info. In order to delete
row-by-row, that would require SQL like "WHERE ( COL = ? OR ( COL is null AND ? is null ) )", rather than
the current "WHERE COL = ?" (fails for null for most DBs). Note that
the param would have to be bound twice. Until we eventually add "parameter bind points" concepts to the
AST in ORM 5+, handling this type of condition is either extremely difficult or impossible. Forcing
recreation isn't ideal, but not really any other option in ORM 4.
We discovered that entities we were defining as our ElementCollection types did not have an equals or hashcode method defined and had nullable fields. We provided those (via #lombok for what it's worth) on the entity type and it allowed hibernate (v 5.2.14) to identify that the collection was or was not dirty.
Additionally, this error manifested for us because we were within a service method that was marked with the annotation #Transaction(readonly = true). Since hibernate would attempt to clear the related element collection and insert it all over again, the transaction would fail when being flushed and things were breaking with this very difficult to trace message:
HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]
Here is an example of our entity model that had the error
#Entity
public class Entity1 {
#ElementCollection #Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}
public class Entity2 {
private UUID someUUID;
}
Changing it to this
#Entity
public class Entity1 {
#ElementCollection #Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}
#EqualsAndHashCode
public class Entity2 {
#Column(nullable = false)
private UUID someUUID;
}
Fixed our issue. Good luck.
I had the same issue but wanted to map a list of enums: List<EnumType>.
I got it working like this:
#ElementCollection
#CollectionTable(
name = "enum_table",
joinColumns = #JoinColumn(name = "some_id")
)
#OrderColumn
#Enumerated(EnumType.STRING)
private List<EnumType> enumTypeList = new ArrayList<>();
public void setEnumList(List<EnumType> newEnumList) {
this.enumTypeList.clear();
this.enumTypeList.addAll(newEnumList);
}
The issue with me was that the List object was always replaced using the default setter and therefore hibernate treated it as a completely "new" object although the enums did not change.

How to map Hashtable component of a class, using JPA?

I have this class:
public class DBRow {
public String url;
public String title;
public static Hashtable<String, Integer> words;
public Vector<String> keywords;
}
What I must do is store many instances of this class in a database. I'm using Hibernate and JPA to get the job done. Using JPA I've come so far (really not far):
#Entity
#Table(name="MAIN_TABLE")
public class DBRow {
#Column(name="url")
public String url;
#Column(name="title")
public String title;
public static Hashtable<String, Integer> words;
public Vector<String> keywords;
}
I want my database to have 3 tables - MAIN_TABLE - auto-icremented id as primary key, url and title; HASH - containing a key-value pair from the Hashtable<String, Integer> and id to refer to which instance of DBRow class it belongs (and also to relate to the MAIN_TABLE); VECTOR - pretty much the same story like HASH but with a Vector<String>. So what I'm asking is hot to map the hashtable and the vector, using JPA to get it done?? I've been trying to find a way to do this but haven't found one so far... or maybe I'm not on the right way! Any suggestions are welcome. Thank you in advance.
This is not possible with JPA 1.0 (at least, not with standard annotations) and since you did not mention your JPA provider, I will only cover JPA 2.0. In JPA 2.0, the #ElementCollection annotation can be used to map a collection of basic types or embeddable objects.
Below some examples taken from EclipseLink/Development/JPA 2.0/new collection mappings:
Example 1 - An element collection representing a basic collection mapping.
#ElementCollection
#Column(name="DESIGNATION")
// CollectionTable will default in this case, Entity name + "_" + attribute name
// JoinColumns will default in this case which are different from BasicCollection collection table default
private Collection<String> designations;
Example 2 - An element collection representing an basic map mapping.
#ElementCollection
#MapKeyColumn(name="Q_DATE")
#Column(name="QUOTE")
#CollectionTable(name="EXPERT_QUOTES", joinColumns=#JoinColumn(name="EBC_ID"))
public Map<Date, String> getQuotes() {
return quotes;
}
Now, I second one of the comment to your question and I would consider using "moderner" data structure like ArrayList and HashMap.
Also note that static (and final) entity fields are always considered to be transient i.e. can't be persisted.

Categories

Resources