Unique Constraint Violation on Hibernate #ManyToMany Relationship - java

I have 3 entities in my database, let's call them A, B, and C.
A and B share a many-to-many relationship with one another. A has a SortedSet of Bs, but B does not reference A (no collection or w/e configured). So we have the following.
// Inside class A
#ManyToMany
#JoinTable(name = "a_b",
joinColumns = {#JoinColumn(name = "a_id")},
inverseJoinColumns = {#JoinColumn(name = "b_id")})
#LazyCollection(LazyCollectionOption.FALSE)
#SortNatural
private SortedSet<B> bSet = new TreeSet<B>();
B and C have a one-to-many relationship with one another (1 B to many Cs). C has a B in it's entity, but B does not have a reference to it's many C entities. So we have the following
// Inside class C
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "b_id", nullable = false, updatable = true)
protected B b;
We have a sync process that runs a nightly job to update the A entities and their associations to B entities (which doesn't change very often). We end up having something similar to the following (really, it's a lot more complicated with DAOs, services, etc).
// Get the A value to be updated
A aToUpdate = entityManager.find(A.class, idForA);
// Out of scope of the question, but we need to figure out B via a string field on C
C cValue = myDao.getByProperty("fieldName", fieldValue);
// Determine the B values to set on aToUpdate
B bToSetOnA = cValue.getB();
TreeSet<B> bSet = new TreeSet<>();
bSet.add(bToSetOnA);
// Update aToUpdate
aToUpdate.setBSet(bSet);
aToUpdate = entityManager.merge(aToUpdate);
entityManager.flush();
When this happens, the following error occurs.
ERROR [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (Sync Thread)
Duplicate entry 'myAId-almostMyBId' for key 'uk_a_b'
One interesting thing is that the almostMyBId is 1 character short of the actual B ID. But only the full ID appears in the a_b table.
When I look through the code base, there is a uk_a_b constraint on an index on the a_b table. This is from liquibase.
<createIndex indexName="uk_a_b" tableName="a_b" unique="true">
<column name="a_id"/>
<column name="b_id"/>
</createIndex>
If I remove the aToUpdate.setBSet(bSet); line, the error disappears.
I added logging and confirmed that the IDs of the new bSet are the same as the old ones on aToUpdate.
Somehow, Hibernate seems to be trying to re-add the association, even though we're doing a merge and the associations haven't really changed.
I've tried changing a few CascadeType and FetchType things here and there, but the error doesn't seem to go away. Anyone have an idea of what's going on?

This is very specific to my situation, but figured I'd post the answer anyways in case anyone read my question and has been working on it.
Another developer on my team was tasked with speeding up a report based on the a and a_b tables. To avoid needing to do a JOIN and a WHERE clause, the developer copied the data over from a_b into a new table (using the WHERE clause) and added triggers so that whenever things were updated, it would insert into the new table. This new table had a constraint named the same as the a_b table (namely, uk_a_b). The duplicate case was not properly handled, so an error was being thrown. Due to the name similarities, it seemed to be the a_b table causing the issue when it was really the new table. Fun times.

Related

JPA Inverse Join with OneToMany causing More than one row with the given identifier

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.

Hibernate: Criteria query taking a long time with three-level relationship

I'm having trouble using Criteria to find all the objects that belong to a certain entity. My model is like the following (just showing the relevant code):
#Entity...
Class A {
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ide_b", referencedColumnName = "ide_b", nullable = false)
private B b;
}
#Entity...
class B {
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ide_c", referencedColumnName = "ide_c", nullable = false)
private C c;
}
#Entity...
class C {
...
}
My Criteria query is as simple as this (actually, there would be some filters, but they're not being used):
Criteria criteria = getSession().createCriteria(A.class);
criteria.list(); // MY SYSTEM STAYS HERE FOREVER WHEN RUNNING AGAINST A REAL DATABASE
Would anybody have a clue on this issue? The system just stays forever on the line "criteria.list()" and never returns.
I've already tested the SQL that it generates directly on the database and it works just fine.
I've already tested this query both with code involving only class A having a reference to B and class A having a reference to C (directly). They both work. This third level in the association seems to be causing problems... Note: my Hibernate version is an old one, like 3.0.0
You need to remove fetch type join. Eager is fine for your data layout, but do your really need that much data at once?
With default fetch, hibernate will query table A only. Then for each B's foreign key, queries B only once per key. Same done for C.
i.e. once b_id=1 is fetched from B, it wont be fetched again even it used with million rows of A. Hibernate's 2 nd level cache handles it.
With join type fetch, for each row from A, you'll get 1 single row containing columns of all 3 tables.
If your relation was OneToMany, then you'll get A x B x C rows returned. But since ManyToOne, there is no such issue.
Your problem is even this query returns 1 big row for each items at A, there is too much B and C replication. So DB response is huge so processing is hard as well for both DB and your app.
Well, actually I think the problem is something else. I've changed both relationships (A -> B and B -> C) to lazy (and removed the #Fetch). Then I queried for a specific A object. So far so good. I was able to get the object and I was able to call a.getB() successfully.
Nonetheless, when I call b.getC() Hibernate does not return the C object to me (I mean, Hibernate gets stuck at this line).
The query Hibernate creates to fetch the C object when I call b.getC() is the following:
select
myCtable0_.id as id1_85_0_,
myCtable0_.name as nam2_85_0_
from
MyTableC myCtable0_
where
myCtable0_.id in (?, ?)
The C table has an id field (the primary key) and a name (varchar).
Another option is to fetch all data separately. Then hibernate cache will have each entity and then, I think, criteria query will be faster.

JPA/Hibernate: Map many-to-many relationship when join table has own primary key

I have three tables with simple structure:
pub [id, name]
days [id, name]
pub_days [id, pub_id, days_id]
For some unholy reason, somebody thought that compound identity for pub_days table (that would be pub_id + days_id) is not enough and added own primary key. I can't change it now, other and larger system depends on that. #sigh
I am trying to map this to Hibernate with standard #ManyToMany JPA annotation like so (I omitted getters, setters, #Entitiy annotations and other clutter):
class Pub {
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name = "pub_days",
joinColumns = {#JoinColumn(name = "pub_id")},
inverseJoinColumns = {#JoinColumn(name = "days_id")})
#OrderBy("id")
private List<Day> pubOpeningDays;
}
class Day {
#Id Long id;
String name.
}
when I execute following code:
Day day = repository.find(Day.class, id);
pub.getPubOpeningDays().add(day);
repository.persist(pub);
I get this error:
ERROR: ORA-01400: cannot insert NULL into ("PUB"."pub_days"."id")
Sadly, that makes perfect sense, because I haven't mapped that ID anywhere. The thing is, I don't even want to. I want it to be generated, but not sure how do I overcome this issue with #ManyToMany mapping. Any ideas?
What you can do is like I mentioned in my comments you can create a separate entity CD which will in turn connect with two classes A and B, Now relationship would be many to many between A and B, and hence A (many to many) CD (many to many) B. Now as per your requirement whenever you need to fetch the instance of A or B, what you can do is simply fire a query in the DB with proper parameters i.e id of a or id of b this will help you get your required result.
I only see two choices, either you change your mapping to a list of PubDay as samwise-gamgee told you in the comments or you add a trigger on insert on table pub_days which set a value for the column id if it is null (it could be using a sequence). But this approach depends on the features supported by your DB.

Eclipselink inserting entity with 0 ID

I just discovered a rather strange problem with EclipseLink (both 2.3.0 and 2.4.1) and just wanted to ask if anyone can confirm that this is a bug and not just that I am missing something obvious ...
Basically, I have two entities involved, let us simply call them A and B. A had an eager (if that matters), non-cascading many-to-one unidirectional reference to B modelled in the database with a join column. The DB table A contains the columns ID (PK), B_ID (FK to B.ID), and more, the table B contains the columns ID and a few more.
Now, I had a list of As, which should be updated with references to new B instances. In pseudo-code something like:
for(A a : list) {
// using some criteria, check if perhaps a matching B
// entity is already found in the database
B b = perhapsGetExistingBFromDatabase();
if(b == null) {
// no existing B was found, create a new
b = new B();
// set values in B
b.setFoo(4711),
b.setBar(42);
// save B
em.merge(b);
}
// set reference from A to B ...
a.setB(b);
// ... and update A
em.merge(a);
}
Since the reference was non-cascading, it was necessary to merge both b and a. Everything worked as expected.
Then, someone (I) changed the cascade type of the relationship from none to merge/persist, since that was required somewhere else in the code. I expected the old code to work, merging b is not really required, shouldn't IMHO however hurt? A brief test confirmed that it still worked, the new B entity was inserted and A updated accordingly.
BUT, it only works if there is only one A entity in the list. Running through the loop a second time causes EclipseLink to auto-flush the session since perhapsGetExistingBFromDatabase does a "SELECT .. FROM B", there is a merged B entity cached and it wants the database table to be up to date. Using FINEST logging level and breakpoints in the code, I can verify that EclipseLink determines that it is required to generate an id for the new B entity, it invokes the sequence generator and also sets the id in the correct field of the entity. Still, EclipseLink invokes SQL statements similar to these:
INSERT INTO B (ID, ...) VALUES(0, ...);
UPDATE A SET B_ID = 0 WHERE ID = ...;
The generated id is lost somewhere and EclipseLink tries to create a new entity with id=0. The data is indeed invalid and later, EclipseLink also throws a PersistenceException: Null or zero primary key encountered in unit of work clone.
Bug or my mistake?
Edit: James asked for the mapping of B.ID:
#Id
#GeneratedValue(generator = "sq_idgen", strategy = GenerationType.SEQUENCE)
#SequenceGenerator(name = "sq_idgen", sequenceName = "...", allocationSize = 100)
#Column(name = "id")
protected long id;
Note, that removing the unneccesary em.merge(b); solves my problem. It is just not obvious to me why the invocation of merge causes EclipseLink to fail completely, trying to insert a B instance without a populated id.
That is odd, how is the Id of B mapped?
Seems like the merged might somehow be getting two separate instances of B (as there is nothing to identify them as being the same as they have no Id). Not that merge() is normally not required, it is only required for detached objects, such as when using serialization (try using persist instead).
To avoid the flush you can set the flushMode in the EntityManager or persistence unit to COMMIT instead of AUTO.

JPA: implicit cascades for relationships mapped as #ManyToMany #JoinTable?

I have the following mapping:
#Entity
#Table(name = "Prequalifications")
public class Prequalification implements Serializable
{
...
#ManyToMany
#JoinTable(name = "Partnerships", joinColumns = #JoinColumn(name = "prequalification_id", referencedColumnName = "id"), inverseJoinColumns = #JoinColumn(name = "company_id", referencedColumnName = "id"))
private Set<Company> companies;
...
}
In a #ManyToMany + #JoinTable mapped relationship, isn't it kind of implicit that the association (link) entities (here Partnerships) are automatically persisted, removed, etc. even though
by default, relationships have an empty cascade set
? The above quote was taken from "Pro JPA 2, by Mike Keith".
Executing
em.merge(prequalification);
on the above entity does persist the associated partnerships without any cascade types specified.
Am I correct that this implicit cascade has to be performed? This isn't mentioned anywhere I looked...
The rows in the join table will be inserted/deleted as part of the owning Entity (if bi-directional the side without the mappedBy). So if you persist or remove or update the Prequalification the join table rows will also be inserted or deleted.
The target Company objects will not be cascaded to. So on remove() they will not be deleted, if the list is updated they will not be deleted unless orphanRemovla is set. Persist should also not be cascaded, but what happens when you have references to "detached" objects is somewhat of a grey area. Technically an error should be thrown, because the object is new and the relationship was not cascade persist. It may also try to insert and get a constraint error. It should not cascade the persist, although your object model is technically in an invalid state, so what occurs may depend on the provider.
Wanted to add a comment, but don't have enough rep for it.
I had the same question as #D-Dá´™um: "Where in the docs can we find a reference to this behaviour?"
I found it in the Hibernate docs (many-to-many).
If you scroll just a bit just below the code example there, you will find:
When an entity is removed from the #ManyToMany collection, Hibernate simply deletes the joining record in the link table. Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.
Where the "link table" refers to the "join table".
Hope this helps.

Categories

Resources