I have a model that has a composite key id and a map of strings that is mapped as an element collection.
#Entity
#Audited
#NoArgsConstructor
#AllArgsConstructor
#Data
public class DemoModel {
#EmbeddedId
private DemoCompositeKey id;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name="demo_model_collection", joinColumns = {
#JoinColumn(name="demo_one", referencedColumnName = "one"),
#JoinColumn(name="demo_two", referencedColumnName = "two")
})
#MapKeyColumn(name = "key_thing")
#Column(name = "value_thing")
#EqualsAndHashCode.Exclude()
private Map<String, String> someCollection = new HashMap<>();
}
The component key is simple in itself as you can already see from the collection table mapping
#Embeddable
#AllArgsConstructor
#NoArgsConstructor
public class DemoCompositeKey implements Serializable {
Integer one;
Integer two;
}
And a simple repo.
#Repository
public interface DemoRepository extends JpaRepository<DemoModel, DemoCompositeKey> {}
This setup has been working without any issues for quite a while, but recently I decided I want to audit this entity and added the #Audited tag and added Hibernate Envers to the project.
Inserting the entity initially does not cause any issues and works as expected. But once I try to update one of the elements of the collection the application errors out with the following message (line breaks added by me for readability:
org.springframework.orm.jpa.JpaSystemException: Error accessing field
[java.lang.Integer com.example.demo.DemoCompositeKey.one] by reflection
for persistent property [com.example.demo.DemoCompositeKey#one] : A;
nested exception is org.hibernate.property.access.spi.PropertyAccessException:
Error accessing field [java.lang.Integer com.example.demo.DemoCompositeKey.one]
by reflection for persistent property [com.example.demo.DemoCompositeKey#one] : A
So as you can see from the error, it appears that it tries to compare the Map key string against the composite key and of course, fails.
The original production code for DemoModel has some additional attributes and it made sense from a modeling perspective for it to have a composite key.
Repo with issue: https://github.com/lances101/spring-boot-envers-issue
It seems like Envers is really having trouble with this. Am I defining the Map improperly? Is it a problem because I'm using a composite key?
Update 2021/03/17: swapping for a non-composite primary key does work, but then that is a design change and this is most likely a bug / lacking support of within Envers itself.
Ticket created: https://hibernate.atlassian.net/browse/HHH-14480
I know the basic usage of #MapKeyColumn usage, eg:
#MapKeyColumn(name = "phone_type", table = "phones")
#OneToMany(mappedBy="customer")
private Map<PhoneType, Phone> customerPhones;
that gets all the customer phones categorized by PhoneType enum.
That works fine if one Customer can have only one Phone of each type.
What I need is to create a mapping that would give me map of collections of phones, eg:
#MapKeyColumn(name = "phone_type", table = "phones")
#OneToMany(mappedBy="customer")
private Map<PhoneType, Collection<Phone>> customerPhones;
When I tried above example I've got a:
org.hibernate.AnnotationException: Use of #OneToMany or
#ManyToMany targeting an unmapped class: com.myorg.myproject.Customer.customerPhones[java.util.Collection]
Is there any way of doing that?
using Hibernate Core 4.1.7, JPA annotations, java 1.7
Easy to fine are examples about Map<String, Entity> reading Hibernate doc on collections, or Map<String, String> here (stackoverflow).
Hard to find are examples on Map<String, Set<String>> (or even Map<String, Map<String, String>> just for curiosity about daisy chaning) why I ask this question here.
All I want is to save entities (accounts) containing named, multi-valued properties (=account attributes).
I have all working with 3 entity types: Account -> #OneToMany -> AccountAttribute -> #OneToMany -> AccountAttributeValue
But wrapping native Java types with my own Classes seems a bit silly to me. Cloning the Map<String, String> example I would like to have something like
#Entity
public class Account {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="key")
private Long key;
#ElementCollection(fetch=FetchType.EAGER)
#JoinTable(name = "Attribute",
joinColumns = #JoinColumn(name = "fkAccount"))
#MapKeyColumn(name = "name")
// next line is working for Map<String, String>, but not here!
#Column(name = "value")
private Map<String, Set<String>> attributes = new HashMap<String, Set<String>>();
// ... omitting: constructor, getters, setters, toString()
}
Which gives me
Initial SessionFactory creation failed: org.hibernate.MappingException: Could not determine type for: java.util.Set, at table: Attribute, for columns:[org.hibernate.mapping.Column(attributes)]
As DB layout I have created 2 Tables.
- Table Account just having a key to point foreign key to
- Table Attribute containing named value in each line.
E.g. for multivalued attributes I thought of it containing 2 lines with same fkAccount and name but different value - yes, I could have normalized even more, but I want to read my data in acceptable time :-)
CREATE TABLE IF NOT EXISTS `foo`.`Account` (
`key` INT NOT NULL AUTO_INCREMENT ,
...
CREATE TABLE IF NOT EXISTS `foo`.`Attribute` (
`fkAccount` INT NOT NULL ,
`name` VARCHAR(45) NOT NULL ,
`value` VARCHAR(45) NOT NULL
...
Any hints or alternate DB layout proposals appreciated.
EDIT - SOLVED
Solution from Tom (as far as I understood) working for me
Thanky you guys, what an experience, solution in 1 day!
The table layout just mentioned above in my question works now with this classes.
#Entity
public class Account {
/* ... omitting "key", see as above */
/* NEW: now #CollectionTable
replaces #JoinTable / #MapKeyColumn / #Column from above
*/
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name="AccountAttribute",
joinColumns=#JoinColumn(name="fkAccount"))
private Set<AccountAttribute> attributes = null;
// ... omitting: constructor, getters, setters, toString()
}
and NEW
#Embeddable
public class AccountAttribute {
#Column(name="attributeName")
private String attributeName = null;
#Column(name="attributeValue")
private String attributeValue = null;
// ... omitting: constructor, getters, setters, toString()
}
JPA doesn't give you any way to map collections of collections of anything. You can map primitives, references to entities, embeddables, and collections of any of the preceding things.
Collection there means a set, list, or map; there isn't a multimap type here which would help you.
Therefore, there is sadly no way to map exactly the structure you want to map.
I think the closest you could come would be to define an embeddable class Attribute, containing a name and a value, then map a Set<Attribute>. You could then convert this to a Map<String, Set<String>> in code.
It's a shame there's no way to do this. I assume the JPA spec authors either didn't think of it, or thought it was an obscure enough corner case that it wasn't worth dealing with.
There's two ways to model this in the database, and it comes down to how many tables you want. If you want three, then the way you have working is basically right, although you could trim it to two entities (Account and AccountAttribute) where AccountAttribute contains a Set of values.
You can't model it with three tables and just an Account entity, because you don't have enough identifiers. The VALUE table would have to have a compound key made up of the account id and some kind of attribute key, your tables would have to look like:
ACCOUNT (id, ...)
ACCOUNT_ATTRIBUTE(account_id, account_attribute_id, ...)
ACCOUNT_ATTRIBUTE_VALUE(account_id, account_attribute_id, value, ...)
if AccountAttribute is an entity, then it has an ID. If not, it doesn't, and so how would you key the ACCOUNT_ATTRIBUTE_VALUE table?
This is borne out by the JPA spec, as mentioned in this other answer.
Now, you COULD do this in two tables with just an Account entity, by collapsing that Set in to some serialized form and persisting it as binary, or XML, or whatever. Not sure if that's worth the effort to you, but the column you sketched out (value varchar(45)) is almost certainly not long enough.
in JPA2 when we are using Embed-able (Basic Type like String.. etc ) object in Entity using with #ElementCollection and #CollectionTable annotation , the new table is created , but in new table how to declare primary-key contraint in column ? following is my code
public class Employee {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String name;
private String salary;
#Transient
private String phnNum;
#Enumerated(EnumType.STRING)
private EmployeeType type;
#ElementCollection
#CollectionTable(name="vacations" , joinColumns=#JoinColumn(name="Emp_Id"))
private Collection<Vacation> vacationBooking;
#ElementCollection
private Set<String> nickNames;
...................
with this code the "vacation" and "employee_nickname" two tables are created in schema. but i want to declare the one primary-key column in both table . what i do for this?
It looks like a primary key per se is not supported by JPA 2.0:
From Wikibooks:
The JPA 2.0 specification does not provide a way to define the Id in the Embeddable. However, to delete or update an 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 (JoinColumn(s)). This however could be inefficient, or just not feasible if the Embeddable is big, or complex.
Some JPA providers may allow the Id to be specified in the Embeddable, to resolve this issue. Note in this case the Id only needs to be unique for the collection, not the table, as the foreign key is included. Some may also allow the unique option on the CollectionTable to be used for this. Otherwise, if your Embeddable is complex, you may consider making it an Entity and use a OneToMany instead.
Do you mean that you want to assign 'id' from Employee table as foreign key to the Vacation table?
In that case, you should use #OneToMany instead of #ElementCollection
I've got a Map containing MyObject instances. The MyObject class uses JPA to persist its fields:
#OneToMany(cascade = CascadeType.ALL)
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, MyObject> results = new HashMap<String, MyObject>();
We changed the value stored by the Map to a List:
private Map<String, List<MyObject>> results = new HashMap<String, List<MyObject>>();
But upon launching we receive a stack trace:
Caused by: org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class: com.me.myapp.MyObject.results[java.util.List]
at org.hibernate.cfg.annotations.CollectionBinder.bindManyToManySecondPass(CollectionBinder.java:1150)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:680)
at org.hibernate.cfg.annotations.MapBinder$1.secondPass(MapBinder.java:107)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:66)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1221)
at org.hibernate.cfg.AnnotationConfiguration.secondPassCompile(AnnotationConfiguration.java:383)
at org.hibernate.cfg.Configuration.buildMappings(Configuration.java:1206)
at org.springframework.orm.hibernate3.LocalSessionFactoryBean.buildSessionFactory(LocalSessionFactoryBean.java:673)
at org.springframework.orm.hibernate3.AbstractSessionFactoryBean.afterPropertiesSet(AbstractSessionFactoryBean.java:211)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1368)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1334)
... 30 more
Does Hibernate not support persisting a Map containing (as values) List types? Or are my annotations incorrect? I haven't found this particular configuration in any of the documentation or examples.
The objects stored in the map must be the target object of the oneToMany association (always mapped). You can't store arbitrary objects or collections there.
Personally, I don't think this is a good use of Hibernate. There's no abstraction here. It would make sense if you had a model object with a one-to-many relation expressed as a child List as a data member in the parent.
My advice? Don't use Hibernate. Use straight JDBC, Spring's JDBC template, or something like iBatis.
ORM stands for "Object Relational Mapping". You have tables, so you've got the relational part. You've got data that you can assign to columns in tables.
But it sounds to me like you've got no Objects. So why use Hibernate?