Sorry for the long explanation in advance, I have been searching for similar issues to no avail.
I have two Tables, let’s call them Table A and Table B
A has a foreign key Association with B i.e. Table B’s tB_id value is a foreign key in Table A
Code Reference
TableA
@Entity
@Table(name = "tableA")
data class TableA(
@Id @Column(name = "id") val id: String,
@Column(name = "name") val name: String,
@ManyToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "tB_id", referencedColumnName = "tB_id", nullable = false)
val table2: Table2,
)
TableB
@Entity
@Table(name = "tableB")
data class TableB(
@Id @Column(name = "tB_id") val tB_id: String,
@Column(name = "name") val name: String,
)
Issue
I tried creating a list of objects of TableA and saving them
val x = listOf(TableA("id1","name1",TableB("idB1","nameB1"),
TableA("id2","name2",TableB("idB1","nameB1"))
tableARepo.saveAll(x)
It throws an error saying A different object with the same identifier value was already associated with the session since it does not understand that both the objects are the same (atleast data wise)
What works
If I extract the TableB object out and pass the reference instead, the code (test) works I can’t use it since this is a list of data sent by users that is converted to a list of TableA objects
Question
Is there a way for me to save the object using saveAll() in the current structure ?
Is there a another alternative where I don’t have to destructure the input List ?
Can this also be a feature request if it is not supported ?
First of all, this identity thing is a requirement by JPA and Spring Data JPA uses the JPA APIs of Hibernate behind the scenes. If you don’t like that and would rather want to work with fully detached objects, I would suggest you either use the StatelessSession API of Hibernate, or you save and flush objects one by one, and in between, clear the persistence context.
But ask yourself, what should Hibernate do in this case? You have two distinct objects that refer to the same row in TableB. What if one object has different state than the other? Which state should be written to the database? Which object should be returned by Hibernate when you ask it to find the TableB entity for that identifier?
See how this leads to all kinds of questions that have really no good answer?
But I think there is something else going on here. Do you really need to flush changes to TableB rows when saving TableA rows? Or are you just referring to those objects by the foreign key?
If you are just referring to those objects, you should obtain a reference to a managed object by primary key through tableBRepo.getOne("idB1") and set that object (Hibernate will take care of deduplication) for the association in a TableA entity.
When you really need to save TableB rows alongside, you should think about the first few questions and then you will naturally find a solution where you will have to deduplicate data yourself and in code explicitly decide which TableB data you want to save.
I don’t think stateless is the way to go since it actually increases the amount of work at the same time takes away the flexibility of hibernate.
Just to add a little more context, the reason you see the objects as a listof(TableAs) is because it gets translated from a User request to java objects. i.e.
so that is why it shows two different objects (of Table A and B)
I understand if the error pops up if there is inconsistent behavior, but I don’t completely understand what prevents it from confirming that the old object is same as the new one
Currently as far as I understand it just checks if there is an old object to be written and fails if it is not set to be deleted before.
Object old = persistenceContext.getEntity( key );
if ( old != null ) {
if ( persistenceContext.getEntry( old ).getStatus() == Status.DELETED ) {
source.forceFlush( persistenceContext.getEntry( old ) );
}
else {
throw new NonUniqueObjectException( id, persister.getEntityName() );
}
}
Would a check if the objects match before throwing an exception be an issue ? I guess it can decide to not write it even since it is already present.
I think I’ll be going with the approach of splitting the data into multiple writes using the tableBRepo.getOne("idB1") but I think it’s weird that my problem was that rare of an occurrence.
Hibernate is stateful by default, and manages a persistence context composed of managed entities. This means that if you do a change to one object that is part of the persistence context, Hibernate will be able to flush the change without you having to tell Hibernate that it needs to flush the object.
When you call EntityManager.find, Hibernate has to serve objects from the persistence context, and only if that doesn’t contain the object, put the object read from the database into the persistence context. So there may only be one managed entity object in a persistence context for a row with a certain primary key.
This in turn implies that you also can’t association two distinct objects referring to the same row. What if you alter the object which wasn’t put into the persistence context?
Having multiple objects for the same row simply doesn’t work. There are lots of cases which might not apply to your particular case, but are problematic in general, hence Hibernate doesn’t support it.