JPA: Map<Enum,Enum> & Map<Enum,Boolean> - java

I'm trying to add a Map entity in two ways:
Map<Enum,Enum>:
#Entity
#Table(name = "reservations")
public class Reservation {
...
private Map<Request, RequestImportance> guestsRequests;
...
}
Map<Enum,Boolean>:
public class Room {
...
private Map<Request, Boolean> requestsMap;
}
These are the enums Request and RequestImportance:
public enum Request {
ELEVATORPROXIMITY,
SEAVIEW,
BATHTUB,
BALCONY,
HANDICAPPED,
HIGHFLOOR
}
public enum RequestImportance {
NOT_IMPORTANT,
NICE_TO_HAVE,
MUST;
}
I'm not sure which attributes I'm supposed to use to map to the DB.
I'm using mySQL for this project.
EDIT:
Probably not possible to do that. I figured that I would change the Map to a List and create a new Class which contains an Enum field. That way I can represent the data in the DB.

You could use #ElementCollection for that
#ElementCollection
private Map<Request, RequestImportance> guestsRequests = new HashMap<>();
By default it is mapped to the table reservation_guests_requests that has three columns
reservation_id (refers to Reservation obviously)
guests_requests_key (contains Request ordinal number)
guests_requests (contains RequestImportance ordinal number)
If you are not fine with these names or need to fit the existing table they could be adjusted
#ElementCollection
#CollectionTable(
name = "guests_requests",
joinColumns = #JoinColumn(name = "rsrv_id")
)
#MapKeyColumn(name = "request_n")
#Column(name = "request_importance_n")
private Map<Request, RequestImportance> guestsRequests = new HashMap<>();
This time it will be guests_requests with
rsrv_id (I guess reservation_id is a better name, just showed the possibility to adjust it)
request_n
request_importance_n
On the one hand saving enum ordinal is fine, on the other hand it is sensitive to its order and could make issues if the values are reordered.
Luckily it could be switched to String or any custom index or an abbreviation with AttributeConverter. If you put #Enumerated(EnumType.STRING) above the map it will affect only the value i.e. RequestImportance

Related

Is there a way to specify N fields as the unique key in MongoDB?

I am trying to figure out how to use two or more fields in a document as a unique key for the document.
I have a document similar to this, specified in Java, using Spring Boot annotations:
#Document(collection = "Records")
class MyRecord {
#Id
String name;
String jobType;
String group;
Long numTasks;
...
}
Perhaps I'm missing something, as I'm fairly new to MongoDB. The closes thing I've found is Indexes, but it doesn't seem clear that indexes behave the same as unique keys. I want to be able to query on two fields. Each of those fields doesn't need to be unique, but each pairing of those two fields should be unique through the collection. What isn't clear is if indexes will force that uniqueness.
Thanks in advance!
MongoDB does have unique compound indexes as seen https://docs.mongodb.com/manual/core/index-unique/#unique-compound-index
Sprint Boot docs: https://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/index/CompoundIndex.html
This would seem to make your code something like
#Document(collection = "Records")
#CompoundIndex(def = "{'name': 1, 'jobType': 1}", unique=true)
class MyRecord {
#Id
String name;
String jobType;
String group;
Long numTasks;
...
}

Binding selected (and transformed) column to entity field

I have hibernate query, where I need to transform one column with date_trunc, however I don't know how to bind this transformed value to my joined entity object
Is there any possibility to pass this transformed column to mine hibernate entity?
Of course it's conceptual code in case it wouldn't make sens for you:
#Entity
class SomeClass {
private ZonedDateTime orderTime;
#ManyToOne(targetEntity = SomeStats.class)
#JoinColumn(name = "SOME_STATS_ID", referencedColumnName="ID")
private SomeStats someStats; // Class that I need aggregate
private long id;
// ... other fields that aren't grouped
}
class SomeStats {
private long id;
private double price; // I need to take average of this
private ZonedDateTime paymentTime; // And truncate this
}
I need some idea to make something similar to:
SELECT someclass.*, AVG(somestats.price), date_trunc('hour', somestats.payment_time) as timebucket
FROM SOME_CLASS someclass
JOIN SOME_STATS somestats ON somestats.id = someclass.some_stats_id
GROUP BY someclass.id, timebucket
but with average as SomeStats.price and truncated date as SomeStats.paymentTime, is it even possible?

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.

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.

JPA map collection of Enums

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!).

Categories

Resources