How to model hierarchy using JPA - java

I am looking to build a tree based hierarchy.
#Entity
class Category {
#Id
private String id;
#Column
private String name;
#ManyToOne
private Category parent;
}
Should the above declaration be ManyToOne or OneToOne? I am planning to use ManyToOne. Is this correct?

ManyToOne, if you plan to have a tree, as a parent can have more than one children. Look at this example from Hibernate's test suite: https://github.com/hibernate/hibernate-core/blob/master/hibernate-core/src/test/java/org/hibernate/test/annotations/manytoone/Node.java

Related

Hibernate annotations work better the "wrong" way around?

I've been looking around the net for a decent answer to this but all I've gotten is confused. I'm struggling with how #ManyToOne annotations in hibernate are supposed to work - because #OneToMany with #JoinColumn seems far superior because hibernate inserts the foreign keys properly instead of me having to assign the child object in java before saving.
Basically as the most simple example I have something like this:
Parent Table: id int,
Child Table : id int, parentFk int
public class Parent{
#Id
#GeneratedValue
private Integer id;
#OneToMany
#JoinColumn(name = "parentFk")
private List<Child> children;
//setters and getters
}
public class Child{
#Id
#GeneratedValue
private Integer id;
#Column
private Integer parentFk;
//setters and getters
}
Now if I send some json that maps to a Parent class and ask hibernate to save it, it will save everything in the table including the new id of the parent in the parentFk field. However I've been lead to believe that this is actually the wrong way of doing things and that I should be doing #OneToMany(mappedBy = "id") in the Parent class instead - and then also having #ManyToOne with #JoinColumn and a Parent object in the Child class.
The problem is doing it this way I have to manually set the parent object in the child via java code before hibernate will save the id of the parent in the parentFk field correctly...it just seems like a very long winded of doing something that is already working perfectly for me (albeit I cannot access the parent object from the child).
Moreover I've tried to remove the parentFk field in the child object and use #ManyToOne with a parent object reference, but hibernate doesn't seem to like it. Am I doing this all wrong?
Your mapping should be something like this.
public class Parent{
#Id
#GeneratedValue
private Integer id;
#OneToMany(mappedBy="parent", cascade={CascadeType.ALL})
private List<Child> children;
//setters and getters
}
public class Child{
#Id
#GeneratedValue
private Integer id;
#ManyToOne
#JoinColumn(name="parentFk")
private Parent parent;
//setters and getters
}
You should also have an add(Child child) method on your parent (and remove) to manage the relation ship. And assuming Parent and Child are in the same package I would make the setParent on the Child package protected, i.e no access modifier (setParent(Parent parent) { this.parent=parent}).
public void add(Child child) {
child.setParent(this);
this.children.add(child);
}
When doing it like this hibernate will be able to execute the right queries.
Fixed by using #JsonManagedReference on the #OneToMany mappings and #JsonBackReference on the #ManyToOne

Hibernate Mapping Problems: Multiple Embeddables + ElementCollections

I have a LocalizedString Embeddable that looks like this:
#Embeddable
public class LocalizedString {
#ElementCollection(fetch = FetchType.EAGER)
private Map<String, String> stringMap;
// getter, setter
}
and an Article class that is supposed to make use of the LocalizedString:
#Entity
public class Article {
#Id
#GeneratedValue
private long id;
#Embedded
private LocalizedString title;
#Embedded
private LocalizedString text;
// getter, setter
}
Generating the tables works just fine, but when I try to insert an Article I get the following exception:
Duplicate entry '1-test2' for key 'PRIMARY'
After looking at the database structure it's obvious why. Hibernate only generated one article_string_map table with the a primary key constraint over the article id and the key of the map.
Googling this problem led me to this question on SO and the answer to include the #AttributeOverride annotations:
#Entity
public class Article {
#Id
#GeneratedValue
private long id;
#AttributeOverride(name="stringMap",column=#Column(name="title_stringMap"))
#Embedded
private LocalizedString title;
#AttributeOverride(name="stringMap",column=#Column(name="text_stringMap"))
#Embedded
private LocalizedString text;
}
This does not work either though, since Hibernate now complains about this:
Repeated column in mapping for collection:
test.model.Article.title.stringMap column: title_string_map
I do not understand what exactly is causing this error and I couldn't really translate the things I did find out about it to my specific problem.
My question is, what else do I need to fix to make LocalizedString work as an Embeddable? I'd also like to know why Hibernate is saying that I mapped title_string_map twice, even though I don't mention it twice in my entire project. Is there some kind of default mapping going on that I need to override?
How can I tell Hibernate to map this correctly?
(Also, I don't have a persistence.xml since I'm purely using annotations for configuration)
I figured it out on my own.
In order to map a ElementCollection I had to use #AssociationOverride combined with the joinTable attribute. The working Article class looks like this now:
#Entity
public class Article {
#Id
#GeneratedValue
private long id;
#AssociationOverride(name = "stringMap", joinTable = #JoinTable(name = "title_stringMap"))
#Embedded
private LocalizedString title;
#AssociationOverride(name = "stringMap", joinTable = #JoinTable(name = "text_stringMap"))
#Embedded
private LocalizedString text;
// getters, setters
}

Hibernate and #JoinFormula: org.hibernate.mapping.Formula cannot be cast to org.hibernate.mapping.Column

I'm trying to write a hibernate adapter for an old database schema. This schema does not have a dedicated id column, but uses about three other columns to join data.
On some tables, I need to use coalesce. This is what I came up with so far:
About the definition:
A car can have elements, assigned by the car's user or by the car's group of users.
If FORIGN_ELEMENT holds a user's name, definition will be 'u'
If FORIGN_ELEMENT holds a group's name, definition will be 'g'
This also means, one table (CAR_TO_ELEMENT) is misused to map cars to elements and cargroups to elements. I defined a superclass CarElement and subclasses CarUserElement and CarGroupElement.
state is either "active" or an uninteresting string
I set definitition and state elsewhere, we do not need to worry about this.
Use DEP_NR on the join table. If it's zero, use USR_DEP_NR. I did this with COALESCE(NULLIF()) successfully in native SQL and want to achieve the same in Hibernate with Pojos.
Okay, here we go with the code:
#Entity
#Table(name="CAR")
public class Car extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="USER_NAME")
#Type(type="TrimmedString")
private String username;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=CarGroup.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private CarGroup group;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarUserElement.class, mappedBy="car")
private Set<CarUserElement> elements;
}
#Entity
#Table(name="CAR_GROUP")
public class CarGroup extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="GROUP_NAME")
#Type(type="TrimmedString")
private String group;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Car.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private Set<Car> cars;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarGroupElement.class, mappedBy="car")
private Set<CarGroupElement> elements;
}
#MappedSuperclass
public class CarElement extends TableEntry {
#Id
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Element.class)
#JoinColumns(value={
#JoinColumn(name="ELEMENT_NAME"),
#JoinColumn(name="state"),
})
private Element element;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarGroupElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="GROUP_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
I tried all available versions of hibernate (from 3.5.1 [first version with #JoinColumnsOrFormulas] up to 4.x.x), but I always get this error:
Exception in thread "main" java.lang.ClassCastException: org.hibernate.mapping.Formula cannot be cast to org.hibernate.mapping.Column
at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:351)
at org.hibernate.cfg.annotations.CollectionBinder.bindCollectionSecondPass(CollectionBinder.java:1338)
at org.hibernate.cfg.annotations.CollectionBinder.bindOneToManySecondPass(CollectionBinder.java:791)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:719)
at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:668)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:66)
at org.hibernate.cfg.Configuration.originalSecondPassCompile(Configuration.java:1597)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1355)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1737)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1788)
Other hibernate users seem to have the same problem: They can't get it working with any version, see this thread and other stackoverflow questions:
https://forum.hibernate.org/viewtopic.php?f=1&t=1010559
To be more complete, here's my TrimmedString Class:
https://forum.hibernate.org/viewtopic.php?p=2191674&sid=049b85950db50a8bd145f9dac49a5f6e#p2191674
Thanks in advance!
PS: It works with joining just these three colulmns with just one DEP-NR-Column (i.e. either DEP_NR OR USR_DEP_NR using just #JoinColumns). But I need this coalesce(nullif()).
I ran into a similar problem, and it seems that the issue is that you are using a #Formula inside an #Id. Hibernate wants Ids to be insertable, and Formulas are read-only.
In my case I was able to work around the problem by making the individual columns Id properties on their own, and making the joined object a separate property. I don't know if this would work in your case since you're using two different columns in your formula, but if so your code might look something like:
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#Column(name="DEP_NR")
private Integer depNr;
#Id
#Column(name="USR_DEP_NR")
private Integer usrDepNr;
#Id
#Column(name="FORIGN_ELEMENT")
private String userName;
#Id
#Column(name="STATE")
private String state;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME", insertable = false, updatable = false)),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE", insertable = false, updatable = false))
})
private Car car;
}
Join formulas are very fragile in Hibernate for the time being; I always had a difficult time to get them work properly.
The workaround that helped me often was to create database views which exposed the proper columns (including foreign keys that don't exist in the original tables). Then I mapped the entities to the views using classing Hibernate/JPA mappings.
Sometimes there are redundant joins in the generated SQL when using such entities, but the database optimizes such queries in most cases so that the execution plan is optimal anyway.
Another approach could be using #Subselects, which are some kind of Hibernate views, but I expect them to be less performant than the classic database views.
I ran into the cast exception as well and I'm on Hibernate 5.x.
Until Hibernate dedicates time to fix the issue, I found that while this guy's approach may not be cleanest (he even eludes to that fact!), it works.
You just need to add the #Column mappings (and get/set methods) to your association table objects that are returning null and manually set the values when you populate the relation data. Simple but effective!

jpa: inheritance with self references and non abstract superclass

In my current project I have a inheritance structure that looks like this:
#Entity
#Table(name="groups")
#Inheritance(strategy=InheritanceType.JOINED)
#DiscriminatorValue("G")
#DiscriminatorColumn(name="group_type")
public class Group{ //some annotations removed
private Long id;
private String name;
private Set<Subject> subjects;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="parent_group_id")
private Group parent; ##### tree parent ####
#OneToMany(cascade=CascadeType.ALL, mappedBy="parent")
private Set<Group> subGroups; ##### tree children #####
...
}
My Group objects can have kind of a tree like structure by containing a list of other Group objects.
As some groups are a bit special, there is a second class that extends this class:
#Entity
#DiscriminatorValue("C")
#Table(name="fix_groups")
public class FixGroup extends Group{
private Layout lay;
private Set<Person> instr;
...
}
I tried to use a joined multi table approach (as described here: http://en.wikibooks.org/wiki/Java_Persistence/Inheritance#Joined.2C_Multiple_Table_Inheritance) but it seems not to work with a non abstract superclass like Group!
I get the following exception:
Caused by: java.lang.ClassCastException: org.hibernate.mapping.JoinedSubclass
cannot be cast to org.hibernate.mapping.RootClass
Is there a solution apart from declaring Group as abstract and making a new class Group2 that only extends it?
And if I did so, would this self-reference Set<Group> subGroups still cause problems?
I was able to cause this error by setting the ID in the subclass when it is already mapped in the parent class (in this case Group). For example in the parent class:
#Entity
#Table(name="groups")
#Inheritance(strategy=InheritanceType.JOINED)
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
...
and then setting the id in the subclass like so:
#Entity
#Table(name="sub_groups")
public class SubGroup extends Group {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
...
In this case, Group does not need to be abstract, but you can't define the id on the subclass.
Also, as a side note, if you are using Hibernate with an inheritance of type "join", then the discriminator column and value are not needed. Hibernate only utilizes those if using a single table. Please reference this post for further information:
Hibernate 4: persisting InheritanceType.JOINED discriminator column values

JPA OneToMany and ManyToOne with a composite key is generating a third table

I'm mapping a Filter ---< FilterColumn where Filter presents cardinality one and FilterColumn N. So the mapped classes are:
#Entity
public class Filter implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private String name;
private String caption;
#OneToMany(cascade = CascadeType.MERGE, targetEntity = FilterColumn.class)
private Set<FilterColumn> columns;
// setters and getters
}
#Entity
public class FilterColumn implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private FilterColumnId id;
private String caption;
// getters and setters
#Embeddable
public static class FilterColumnId implements Serializable {
private static final long serialVersionUID = 1L;
#ManyToOne
private Filter filter;
#Column
private String name;
// getters and setters
}
}
But when I start the application with drop-create instruction the following 3 tables are created:
Filter PK(name)
FilterColumn PK(filter_name, name)
Filter_FilterColumn PK(filter_filter_name, filterColumn_filter_name, filterColumn_name)
What I really want is just two tables like:
Filter PK(name)
Filter_Column PK(name, filter_name)
Why do I receive this result? Is there something wrong with my mapping? What should I change?
Thanks in advance.
I think you need a mappedBy on the #OneToMany. Without that, the mapper doesn't know that it can look at the filtercolumn table to find the entities associated with a Filter, so it generates the filter_filtercolumn table.
Not sure off the top of my head how you to a mappedBy with a composite key. Given that you're using an #EmbeddedId, i think it's simply mappedBy = "id".
Can you use a #ManyToOne in a key class like that? Is that a Hibernate extension over and above the JPA spec? Wouldn't you normally need a #MapsId in there somewhere?
Try adding a #JoinColumn annotation on the Filter member of your composite id. The actual column would be whatever the id of the of the Filter table is (or just leave it without a name if you let hibernate generate it all).
Let me know if this works as I had a similar problem and solved it using the above so I do know it's possible. The only other thing mine has is a #ForeignKey annotation but I think hibernate will take care of that for you -- I just did mine because I wanted to stick to a naming convention.

Categories