I'm trying to define this SQL schema in JPA:
TABLE event (id INT)
TABLE chain (predecessor INT, successor INT)
In other words, every event has a number of successors, which are events themselves. I'm trying to do it this way in JPA:
#Entity
public class Event {
#Id Integer id;
#ManyToMany(cascade = CascadeType.PERSIST)
#JoinTable(
name = "chain",
joinColumns = #JoinColumn(name = "successor"),
inverseJoinColumns = #JoinColumn(name = "predecessor")
)
private Collection<Event> predecessors;
}
#Entity
public class Chain {
#Id Integer id;
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "predecessor")
private Event predecessor;
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "successor")
private Event successor;
}
Is it correct?
Normally one would not both define a ManyToMany with a JoinTable and then also separately define the join table as its own Entity. Join tables aren't Entities normally, they're just join tables and the provider manages them under the hood. You're creating a lot of headaches for yourself as far as properly maintaining in memory state of the application when you change one or the other. (Which is necessary if, for example, you want to use L2 caching.)
So, either one works fine, combined, they are sort of oddsauce. Usually if you defined Chain as an entity, you would just have a list of Chain on the Event. Not also redefine it as a JoinTable on Event. Does that make sense?
(and as it is currently strictly defined, it will break if you try to make changes through the collection on Event unless that ID is a database generated sequence.)
Edit: something like this -
#Entity
public class Event {
#Id Integer id;
#OneToMany(cascade = CascadeType.PERSIST, mappedBy="successor")
private Collection<Chain> predecessorChains;
}
What you wrote originally can be made to work as long as you realize that the Collection<Event> predecessors is inherently read only and will get fubared if you try to L2 cache it. The fact that you put a CascadeType on it makes one thing that you wanted to be able to add and remove Events to/from that, which will explode when hibernate tries to execute illegal SQL.
If you use #ManyToMany, you don't need Chain entity (otherwise, if you need Chain entity, for example, to store additional data associated with the relathionship, you need to declare two one-to-many relationships between Event and Chain).
Related
I am still fairly new to Hibernate and I am still on a steep learning curve.
I have an application that will track which people were on which event and visa-versa. I have an Event Class and a Person Class linked via a jointable.
I have forms and helper classes that allow me to enter the data on the separate Person and Event classes, persist it, search it, delete it, change it and list it. This is all tested and working.
When I add people to the event I can list all the events and see the list of people attached to the events but when I output the list of People they all have an Event list of size 0.
It is my understanding that if I attach a person to an event that the person should show up in Event.myPeople and that the event should show up in Person.eventList.
Clearly I am doing something wrong and I suspect that it is in my annotations and Declarations. I have listed both set for Event and Person classes below. Failing that I have a fundamental misunderstanding of Hibernate ,both are likely. On the bright side, the more mistakes I make the faster I learn.
Any idea where I am going wrong?
#Entity
#Table(name = "PERSON")
public class Person implements Serializable {
#Id
#GeneratedValue
#Column(name = "person_id")
private int ID;
private String foreName;
private String surName;
#Temporal(javax.persistence.TemporalType.DATE)
private Date dob; //used to differentiate people with same name
#Temporal(javax.persistence.TemporalType.DATE)
private Date joinDate; //used to filter events outside active dates
#Temporal(javax.persistence.TemporalType.DATE)
private Date endDate; //used to filter events outside active dates
private Boolean active;
#ManyToMany()//cascade=CascadeType.ALL)
#JoinTable(name = "PERSON_EVENT", joinColumns = #JoinColumn(name = "person_id"),
inverseJoinColumns = #JoinColumn(name = "event_id"))
private Set<Event> eventList;
#OneToOne
private Sections mySection;
#Entity
#Table(name = "EVENT")
public class Event implements Serializable {
#Id
#GenericGenerator(name = "generator", strategy = "increment")
#GeneratedValue(generator = "generator")
#Column(name="event_id")
private long id;
private String eventTitle;
private String eventDescription;
private String eventLocation;
#Temporal(javax.persistence.TemporalType.DATE)
private Date startDate;
#Temporal(javax.persistence.TemporalType.DATE)
private Date endDate;
#ManyToMany(cascade = CascadeType.ALL)
private Set<Person> myPeople;
#OneToMany(mappedBy = "myEvent")
private Set<EventType> type;
There is a common misconception about bidirectional relations in Hibernate. Hibernate does not care about consistency in your object tree. If there is a bidirectional relation between events and persons, you have to add the person to the event and the event to the person yourself. Hibernate only persists what you created in memory. Do not expect Hibernate to add any object to any collections, this is the responsibility of the business logic, which shouldn't rely on Hibernate to work properly.
Now, bidirectional relations are special in that inconsistent states in memory cannot even be persisted. With consistent data, Hibernate only has to persist one site of the bidirectional relation, because the other is (or should be) redundant. This is done by marking one part as the "inverse" part. (I'm sorry that I don't know annotation mapping syntax well enough to point to a possible error in you mapping.) "inverse" means to Hibernate nothing more then "ignore when syncing to database", because it is expected to be redundant.
You still have to make sure that the information in both collections are redundant. It actually "works" when you only add the items to the non-inverse collections. But, however, this is not recommended to do because the objects will not be consistent until saved and loaded into a new session.
Also make sure that the bidirectional relation is mapped to the same table using the same foreign keys. I don't know if annotation mapping does detect this automatically.
Hope that helps.
The problem must be due to a missing mappedBy field in the many to many associations.
The field that owns the relationship is required unless the relationship is unidirectional.
I think adding (mappedBy = eventList) will suffice.
You can try to use: #ManyToMany(fetch = FetchType.LAZY)
Let's say I have following model structure:
#Entity
#Table(....)
public class AnnotationGroup{
...
private List<AnnotationOption> options;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#JoinColumn(name = "annotation_group_id", nullable = false)
public List<AnnotationOption> getOptions() {
return options;
}
}
#Entity
#Table(...)
public class AnnotationOption {
private Long id;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Override
public Long getId() {
return id;
}
}
At the moment I have group1 with AnnotationOptions opt1 opt2 and opt3
Then I want to replace all option with only one option opt1
Additionally I have constraint in database:
CONSTRAINT "UQ_ANNOTATION_OPTION_name_annotation_group_id" UNIQUE (annotation_option_name, annotation_group_id)
And this one fires:
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "UQ_ANNOTATION_OPTION_name_annotation_group_id"
Detail: Key (name, annotation_group_id)=(opt1, 3) already exists.
Actually isuue that hibernate removes orphans after update.
Can you suggest something t resolve issue?
There are so many things that are wrong in this example:
EAGER fetching on the #OneToManycollection is almost always a bad idea.
Unidirectional collections are also bad, use the bidirectional one.
If you get this exception, most likely you cleared all the elements and re-added back the ones that you want to be retained.
The best way to fix it is to explicitly merge the existing set of children with the incoming ones so that:
New child entities are being added to the collection.
The child entities that are no longer needed are removed.
The child entities matching the business key (annotation_group_name, study_id) are updated with the incoming data.
According to Hibernate documentation hibernate perform in the following order to preserve foreign-key constraint:
Inserts, in the order they were performed
Updates
Deletion of collection elements
Insertion of collection elements
Deletes, in the order they were performed
For your special need you should manually flush the transaction to force the deletion in database before.
I have a convenient relation set up in which an entity has a one-to-many relationship with another, and that has a many-to-one with another. So, a LISTING has many LISTING_LINE_ITEMS, and those LISTING_LINE_ITEMS have one SERVICE_PERIOD, but a SERVICE_PERIOD has many LISTING_LINE_ITEMS. I have attempted to describe this relationship using JPA's #JoinTable as follows:
LISTING
#OneToMany
#JoinTable (name = "LISTING_LINE_ITEM", joinColumns = #JoinColumn (name = "listing_id"), inverseJoinColumns = #JoinColumn (name = "service_period_id"))
Set<ServicePeriod> servicePeriods;
LISTING_LINE_ITEM
#ManyToOne (fetch = FetchType.EAGER)
#JoinColumn (name = "listing_id", nullable = false)
Listing listing;
#ManyToOne (fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
#JoinColumn (name = "service_period_id")
ServicePeriod servicePeriod;
SERVICE_PERIOD
#ManyToOne
#JoinTable (name = "LISTING_LINE_ITEM", joinColumns = #JoinColumn (name = "service_period_id"), inverseJoinColumns = #JoinColumn (name = "listing_id"))
Listing listing;
The obvious goal is to be able to easily obtain a list of ServicePeriods for a Listing or a single Listing for a ServicePeriod. Currently the way this is set up I'm getting an exception:
org.hibernate.HibernateException: More than one row with the given identifier was found: 361951, for class: com.gonfind.entity.ServicePeriod
I believe this is because a listing has ListingLineItems that refer to the same ServicePeriod. I'm sure that there is a way to accomplish what I'm after but I don't know what it is.
You do appear to have some problems there. On the technical / JPA side:
you cannot use LISTING_LINE_ITEM both as a join table and as an entity table. There are several reasons for this, but the main reason is that you will confuse JPA: it will try to use that table in different, incompatible ways for those two purposes.
in JPA, a bidirectional relationship is owned by exactly one side; the other side uses the mappedBy attribute of its relationship annotation to reference the owning side.
But you also have data design problems. Your constraint that line items' service periods be restricted to one of those separately associated with the same listing constitutes either
a functional dependency between non-key fields, if the listing id is not part of the line item key, or otherwise
a functional dependency on a subset of a key.
In the first case, your data fail to be in third normal form; in the second case they fail to be even in second normal form. Your trouble modeling this with JPA arises in part from the low level of normalization.
Normalizing your data properly would make things a lot easier on multiple levels. To do that, you need to remove the direct association between listings and line items, and instead associate them through service periods. You then would have:
Listing <-- one to many --> ServicePeriod <-- one to many --> LineItem
Of course, that would have implications on the structure of your application, but it's likely to be a long-term development and maintenance win, and maybe even a usability win, for the application to be aligned with the natural structure of your data like that. If you wish, you could put methods on your Listing entity to allow ListingLineItems to be managed to some extent as if they belonged directly to Listings, and vise versa.
That data organization would look something like this:
LISTING
#OneToMany(mappedBy = "listing",
fetch = FetchType.EAGER,
cascade = CascadeType.PERSIST)
Set<ServicePeriod> servicePeriods;
SERVICE_PERIOD
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "listing_id")
Listing listing;
#OneToMany(mappedBy = "servicePeriod",
fetch = FetchType.EAGER,
cascade = CascadeType.PERSIST)
Set<ListingLineItem> lineItems;
LISTING_LINE_ITEM
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "service_period_id")
ServicePeriod servicePeriod;
If you cannot restructure your data more or less that way, then you're stuck jerry-rigging something that cannot fully be described to JPA. I'm imagining a separate join table for Listing <-> ServicePeriod, a non-JPA FK constraint to that table from the entity table for line items, and, of course, proper form for the various bidirectional relationships.
I have some entities with#ManyToMany relation:
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "buses_drivers",
joinColumns = #JoinColumn (name = "driver_id_inner", referencedColumnName = "driver_id"),
inverseJoinColumns = #JoinColumn (name = "bus_id_inner", referencedColumnName = "bus_id"))
private List<Bus> buses;
and
#ManyToMany(mappedBy = "buses", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<Driver> drivers;
When execute saving Driver model with some Bus models, all ok. Tables buses_drivers store all keys those entities. But when saving Bus model with drivers, table doesn't change. I think problem with inverseJoinColmns mapping.
That is the expected behaviour. In a bidirectional many-to-many association one side has to be the inverse side. In your case it is the Bus side because it contains mappedBy:
The field that owns the relationship. Required unless the relationship
is unidirectional.
That means that Driver is the owner of the association and Hibernate will only check that side when maintaining the association.
You should definitely redesign your relations.
Without even getting into the problems with your current save scenario, with bidirectional #ManyToMany + CascadeType.ALL, you're destined to get even more troubles.
For example, deleting one bus will due to cascade, delete all its drivers, which due to cascade again, will delete all its buses. You'll basically end up deleting much more than you probably want. Also, check the SQL generated by these mappings, you'll most likely notice that its far from ideal.
For people doesn't understand from the accepted answer. This is more appropriate : Java: saving entities with ManyToMany association
I came across with this problem in test cases when filling test data.
When there is an owning side you just can save child just with owner.
I have two classes:
class TrainingCourse {
Integer id;
#OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name = "TrainingCourseClass", joinColumns = { #JoinColumn(name = "CourseID") }, inverseJoinColumns = { #JoinColumn(name = "ClassID") })
private Set<TrainingClass> trainingClasses;
}
class TrainingClass {
Integer id;
}
In the database they are mapped using a join table. So this is a unidirectional relationship.
From the UI, when a TrainingCourse is created, a list of previously created TrainingClasses are selected from the UI.
Now if I create the TrainingCourse, then it automatically updates the associated TrainingClasses also. But trainingClass is independent of TrainingCourse and can exist independently. So TrainingClasses are created and updated separately from the TrainingCourse. So saving the TrainingCourse should save data in the TrainingCourse table and it will also save the association in the join Table TrainingCourseClass. Nothing should happen in the table TrainingClass.
However if I add these to the columns:
nullable=false, updatable=false and CascadeType.REMOVE
#OneToMany(cascade = CascadeType.REMOVE, fetch=FetchType.EAGER)
#JoinTable(name = "TrainingCourseClass", joinColumns = { #JoinColumn(name = "CourseID", nullable=false, updatable=false) }, inverseJoinColumns = { #JoinColumn(name = "ClassID", nullable=false, updatable=false) })
private Set<TrainingClass> trainingClasses;
Then the problem is fixed ie creating trainingCourse doesn't update the trainingClass table. Now I am not 100% sure whether it is the right solution or how it is working to solve the problem. There is also another thing called MappedBy. I am not sure whether this is relevant here.
I just used it as a guess and it is working. Moreover, this seems to be really a many-to-many relationship ie The same class can belong to many courses and one course can include many classes. But one-to-many relationship is also working. This is not very convincing. The trainingclass is really unaware of what training courses include it. It looks like the difference between one-to-many and many-to-many is like whether or not to have bidirectional pointers to each other.
Hence please suggest whether the above approach is correct to prevent updating the trainingclass while creating the trainingcourse.
Thanks
Your first mapping uses cascade = ALL. That means that every operation you make on a TrainingCourse (persist, merge, remove, etc.) will also be applied on the associated TrainingClass. That's precisely what you don't want, if I understand correctly. So just don't set any cascade to this association.
Regarding OneToMany vs. ManyToMany: if what you really want is a OneToMany (i.e. a TraningClass should not be associated with more than one TrainingCourse), then you should have a unique contraint on the TrainingCourseClass.ClassID column. That's what guarantees that the association is a OneToMany and not a ManyToMany.