Self-referencing ManyToMany-relationship not updating - java

I modeled a unidirectional #ManyToMany self-referencing relationship. A test may require other tests in order to be executed:
#Entity
public class Test{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToMany
#JoinTable(
name = "required_",
joinColumns = #JoinColumn(name = "test_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "required_test_id", referencedColumnName = "id")
)
private Set<Test> requiredTests;
}
Each test is described by an XML-file.
In the XML the required tests are referenced by name.
Now i'm trying to import all the tests, but so far the dependency-relationships between the tests are not correctly saved in the DB. I guess I'm doing something wrong.
This is what I did (pseudo-code):
//Each test is imported with this method:
private void import(TestCaseXml testCaseXml) {
Test test= testRepository.findByName(testCaseXml.getName()).orElse(new Test());
test.setRequiredTests(fetchAlreadyExistentOrCreateRequiredTestsDeclaredIn(testCaseXml));
testRepository.save(test);
}
private Set<Test> fetchAlreadyExistentOrCreateRequiredTestsDeclaredIn(
final TestCaseXml testCaseXml) {
final List<String> requiredTestcasesByName = testCaseXml.getNameOfRequiredTests();
return requiredTestcasesByName.stream()
.map(name -> testRepository.findByName(name)
.orElse(testRepository.save(new Test().setName(name))))
.collect(Collectors.toSet());
}
// The import of all the tests is packed in one single transaction.
So far, as result: only one dependency is persisted (let's say Test A requires B, C and D), then the joint table would look like ( A - D ).
Does anyone would have a clue?
Edit after Chris' feedback
I made some more experiments, I'd like to share here since the outcome is really confusing.
First scenario: Let's say I have 3 tests I want to import (A, B and C) and they will be processed in this order.
Test A requires Test B and Test C.
Test B and C have no requirement.
When Test A is being imported, at some point fetchAlreadyExistentOrCreateRequiredTestsDeclaredIn() will be called. I debugged it and can confirm that the method returns a Set containing Test B and Test C, both of them with name and id (The presence of the id is a bit surprising - may be Hibernate flushed before the end of the global transaction?). Anyway, this result do not confirm Chris' hypothesis, since it does return a Set with the 2 expected tests.
Nevertheless: I repeated this first scenario, but this time using List instead of Set, as Chris suggested, and indeed it did work. To be honest, I don't understand why.
Now it gets still a bit more weird:
Second scenario: I have 3 tests I want to import (A, B and C) and they will be processed in this order.
Test A has no requirement
Test B requires Test A and C
Test C has no requirement
This will throw an Exception
java.sql.SQLIntegrityConstraintViolationException: (conn=819) Duplicate entry 'Test A' for key 'XYZ'
Somehow it seems I fixed this by getting rid of the functional syntax in fetchAlreadyExistentOrCreateRequiredTestsDeclaredIn()
I replaced
return requiredTestcasesByName.stream()
.map(name -> testRepository.findByName(name)
.orElse(testRepository.save(new Test().setName(name))))
.collect(Collectors.toList());
with this:
final var requiredTests = new ArrayList<Test>();
for (final String name: requiredTestcasesByName) {
final Test test = testRepository.findByName(testcaseName).isPresent()
? testRepository.findByName(name).get()
: testRepository.save(new Test().setName(name));
requiredTests.add(test);
}
return requiredTests;
After performing these 2 changes (List instead of Set, and get rid of the functional syntax) it seems to work as expected. I'd like to understand what is happening behind the scene.
Edit 27.06.22
I setup a demo project to reproduce this strange behaviour:
https://github.com/JulienDeBerlin/manyToMany/tree/master

I'm pretty confident you have implemented equals and hashcode methods in your entity classes, and that they rely on the ID. Your code can then be broken into the following equivalent sequence:
Set set = new HashSet();
Test b = new Test().setName(b);
set.add(b);
Test c = new Test().setName(c);
set.add(c);
Test d = new Test().setName(d);
set.add(d);
assertEquals(set.size(),1);
Why? If you check what is returned from each testRepository.save call, they do not have their IDs generated. JPA does NOT guarantee sequences are set on persist calls (which are underneath your Spring repository.save call), but does guarantee they will be set on the instance when the transaction is synchronized (flushed or committed) to the database. As they are all in the same transaction, that only happens AFTER they are added to the set. Your hashcode/equality methods have already dealt with all 3 and determined they are the same instance (null id), so replaces the existing one with the latest one added.
Simplest solution is to return a list instead:
private List<Test> fetchAlreadyExistentOrCreateRequiredTestsDeclaredIn(
final TestCaseXml testCaseXml) {
final List<String> requiredTestcasesByName = testCaseXml.getNameOfRequiredTests();
return requiredTestcasesByName.stream()
.map(name -> testRepository.findByName(name)
.orElse(testRepository.save(new Test().setName(name))))
.collect(Collectors.toList());
}
I'd also suggest fixing or just outright removing your Equals/hashcode methods from your entities, betting you don't really need them.

Related

Hibernate update before insert in one to many

I am getting the constraint violation exception because of the order of operations performed by Hibernate. I have the following entities defined.
#Entity
public class A {
#Id
private Integer id;
#OneToMany(mappedBy = "a", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<B> bList;
public void setBList(List<B> bList) {
if(CollectionUtils.isNotEmpty(this.bList)) {
this.bList.clear();
}
if(CollectionUtils.isNotEmpty(bList)) {
this.bList.addAll(bList);
}
}
}
#Entity
#Table(uniqueConstraints={#UniqueConstraint(columnNames = {"name", "a_id", "isDeleted"})})
public class B {
#Id
private Integer id;
private String name;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name="a_id")
private A a;
private boolean isDeleted;
}
When I set the new list of Bs (containing one item updated as deleted and a new item having the same values in the columns corresponding to constraint) in entity A and save entity A, I get constraint violation.
Hibernate is performing insert of the new item before updating the old item as deleted leading to constraint violation when in fact the data is correct in the application.
Am I doing something wrong here or Is there any configuration or fix for this?
Answer changed on 2021/05/07 due to comment from the OP pointing out it was missing the point
There are 2 things you should change for things to work
You should not rely on Hibernate to guess the right order of operations for you. It relies on heuristics that might not fit your intent. In your case, you should call EntityManager.flush after your soft-delete of the old B and before persisting the new one.
Your unique constrain will cause problems anyway, when you'll soft-delete your second B, that is identical regarding unique columns. More hereafter
In general, ensuring this kind of constrains in DB is a bad idea. If you try and update/insert an entity that violates them, then you'll get an obscure PersistenceException and it will be hard to warn your users about the exact cause. So you will have to programmatically check those constrains before insertion/update anyways. Hence, you'd better remove them and ensure unicity through your program, unless they're vital to data integrity. Same goes for not-nullable columns and other constrains that are pure business logic.
Now last advice from experience: for soft-delete column, use a TimeStamp rather than a boolean. Same effort updating and reading your records, but it gives you some valuable information about when a record was deleted.

Hibernate - At least one child

This question sort of asks what I'm trying to achieve, but there isn't really an answer : Hibernate validateManyToOnehas at least one
I have two objects (A and B). A is the parent. B is the child. It's a one to many relationship, however, I need there to always be at least one B for each A. There are default values for all fields in B, so if an A is created without a B then a default B can be added to make sure there is always one B. If one or more B objects are added A then there's no need to create a default B.
This is A:
[Fields]
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "key", nullable = false)
#Fetch(value = FetchMode.SUBSELECT)
private List<B> b = new ArrayList<>();
...
#PrePersist
protected void onCreate() {
// Default values configured here, for example
if (fieldA1 == null) {
fieldA1 = "A DEFAULT";
}
...
}
This is B:
[Fields]
#PrePersist
protected void onCreate() {
// Default values configured here, for example
if (fieldB1 == null) {
fieldB1 = "B DEFAULT";
}
...
}
I thought I could use the same #PrePersist annotation in A, check if there are any B objects, and if not create a default B:
#PrePersist
protected void onCreate() {
// Deafult values configured here
...
if (b.size() == 0) {
b.add(new B());
}
}
That doesn't work. If I create an A with no B objects then in the log I just get:
Handling transient entity in delete processing
and A is created without the B. If I try and create an A with at least one B then I get the following error:
Caused by: org.hibernate.TransientObjectException: object references
an unsaved transient instance - save the transient instance before
flushing
Any ideas how I can make this work?
Without knowing more of your code (and it might be a little too much for SO anyways) I'll have to make some assumptions but I'd say that when onCreate() is being called the Hibernate session is already in a "flushing" state and thus won't accept any new entities.
Currently I could think of 2 options which we also use in some cases:
Throw a CDI event and have an event handler (asynchronously) trigger the creation of the default element in a new transaction once the current transaction is completed successfully.
Create a sub-session in Hibernate that forks from the current one and uses the same transaction. Then use this sub-session to create your default element.
Here's how we do it in a Hibernate PostInsertEventListener:
if( postInsertEvent.getEntity() instanceof ClassThatNeedsToBeHandled ) {
ClassThatNeedsToBeHandled insertedEntity = (ClassThatNeedsToBeHandled )postInsertEvent.getEntity();
Session subSession = postInsertEvent.getSession().sessionWithOptions()
.connection() //use the same connection as the parent session
.noInterceptor() //we don't need additional interceptors
.flushMode( FlushMode.AUTO ) //flush on close
.autoClose( true) //close after the transaction is completed
.autoJoinTransactions( true ) //use the same transaction as the parent session
.openSession();
subSession.saveOrUpdate( new SomeRelatedEntity( insertedEntity ) );
}
One thing to keep in mind is that our SomeRelatedEntity is the owner of the relation. Your code indicates that A would be the owner but that might cause problems because you'd have to change the A instance during flush to get the relation persisted. If B was the owning side (it has a backreference to A and in A you have a mappedBy in your #OneToMany) it should work.
Edit:
Actually, there might be a 3rd option: when you create a new A add a default element and when real elements are added you remove it. That way you'd not have to mess with the Hibernate sessions or transaction scopes.

Hibernate annotation for single select object (with where) instead of on-to-many collection

Currently we have a class that looks something like that (depersonalised and non-relevant parts removed):
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractTable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#OrderBy("CREATED_ON DESC")
private Set<MainTableState> states;
...
public MainTableState getActiveState(){
if(this.states == null || this.states.isEmpty()){
return null;
}
MainTableState latest = states.iterator().next();
// The reason we use this for-loop, even though we have the #OrderBy annotation,
// Is because we can later add states to this list, which aren't automatically ordered
for(MainTableState state : states){
if(state.getCreatedOn() != null && latest.getCreatedOn() != null &&
state.getCreatedOn().after(latest.getCreatedOn()){
latest = state;
}
}
return latest;
}
...
}
So currently it will retrieve all MainTableStates from the DB by default, and if we need the activeState we use the for-loop method. Obviously this is pretty bad for performance. Currently we don't use this list at all (the purpose was to have a history of states, but this has been postponed to the future), but we do use the getActiveState() method quite a bit, mostly to show a String inside of the MainTableState-class in the UI.
In addition, even if we would always use a TreeSet and keep it sorted so we won't need the loop but only need states.iterator().next() instead, it will still initialize the list of states. With some heavy performance testing we had more than 1 million MainTableState-instances when it crashed with an java.lang.OutOfMemoryError: GC overhead limit exceeded.
So, we want to change it to the following instead:
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractEntity {
#???
private MainTableState activeState;
...
public MainTableStates getActiveState(){
return activeState;
}
...
}
So, my question, what should I put at the #??? to accomplish this? I'm assuming I need the #Formula or something similar, but how can I say to hibernate it should return a MainTableState object? I've seen #Formula being used with MAX for a date, but that was to get that date-property, not get an entire object based on that max date.
After #user2447161's suggestion I've used a #Where-annotation, which does indeed help to reduce the Collection size to 1 (sometimes), but I have two more related questions:
How to use #OnToMany and #Where but get a single object, instead of a list of objects of size one? Is this even possible? Here in a answer from December 2010 it is stated it isn't. Has this been fixed somewhere in the last six years?
How to deal with the random alias in the where clause? I could do something like this:
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#Where(clause = "CREATED_ON = (SELECT MAX(mts.CREATED_ON) FROM MAIN_TABLE_STATES mts WHERE mts.FK_MAIN_ID = ???.MAIN_ID)")
private Set states; // TODO Get single object instead of collection with size 1
The problem with is that ??? is a random alias generated by hibernate (sometimes it's this_, sometimes it's something along the lines of mainTable_1_, etc.). How to set this alias for the entire query to the DB to use it here? I also tried MAIN_TABLE.MAIN_ID instead which doesn't work, and with no alias it also doesn't work because it uses the MainTableState-alias instead of MainTable-alias (like this below).
from
MAIN_TABLE this_
left outer join
MAIN_TABLE_STATUSES mainstat2_
on this_.main_id=mainstat2_.fk_main_id
and (
mainstat2_.created_on = (
SELECT
MAX(mts.created_on)
FROM
MAIN_TABLE_STATUSES mts
WHERE
-- mainstat2_.main_id should be this_.main_id instead here:
mts.fk_main_id = mainstat2_.main_id
)
)
Well, regarding your question #2, as it looks like you need a quick solution with minimal impact in your existing code, this may be acceptable: you can use an Interceptor to deal with the alias and generate the right sql statement. Do this:
use a unique string as alias placeholder in your #Where clause, for instance:
...WHERE mts.FK_MAIN_ID = ${MAIN_TABLE_ALIAS}.MAIN_ID...
if your application doesn't have one yet, create an Interceptor class extending EmptyInterceptor and configure it as a SessionFactory interceptor
override the onPrepareStatement method to replace the placeholder with the alias found after 'from MAIN_TABLE' with something like this:
public String onPrepareStatement(String sql) {
String modifiedSql = sql;
if (sql.contains("${MAIN_TABLE_ALIAS}")) {
String mainTableAlias = findMainTableAlias(sql);
modifiedSql = sql.replace("${MAIN_TABLE_ALIAS}", mainTableAlias);
}
return modifiedSql;
}
Be aware that this method will be called for every sql statement that hibernate generates in your application.
Additionaly, your #Where clause only works properly when a join is used, so you should set the fetch mode explicitly adding
#Fetch(FetchMode.JOIN)
to the states property to avoid that hibernate may use the select mode.

Limit the size of a collection in JPA

Say I have an entity like this
#Entity
Class A{
//fields
#Onetomany
Set<B> b; //
}
Now, how do I limit the number of 'B's in the collection in such a way that, when there is a new entry in the collection, the oldest one is removed, some thing like removeEldestEntry we have in a LinkedHashMap.
I am using MySQL 5.5 DB with Hibernate. Thanks in advance.
EDIT
My goal is not to have more than N number of entries in that table at any point of time.
One solution I have is to use a Set and schedule a job to remove the older entries. But I find it dirty. I am looking for a cleaner solution.
I would use the code to manually enforce this rule. The main idea is that the collection B should be well encapsulated such that client only can change its content by a public method (i.e addB()) . Simply ensure this rule inside this method (addB()) to ensure that the number of entries inside the collection B cannot larger than a value.
A:
#Entity
public class A {
public static int MAX_NUM_B = 4;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<B> b= new LinkedHashSet<B>();
public void addB(B b) {
if (this.b.size() == MAX_NUM_B) {
Iterator<B> it = this.b.iterator();
it.next();
it.remove();
}
this.b.add(b);
}
public Set<B> getB() {
return Collections.unmodifiableSet(this.b);
}
}
B:
#Entity
public class B{
#ManyToOne
private A a;
}
Main points:
A should be the owner of the relationship.
In A , do not simply return B as client can bypass the checking logic implemented in addB(B b) and change its content freely.Instead , return an unmodifiable view of B .
In #OneToMany , set orphanRemovalto true to tell JPA to remove the B 's DB records after its corresponding instances are removed from the B collection.
There is one API provided by Apache Commons Collection. Here you can use the class CircularFifoBuffer for your reference of the same problem you have, if you want example shown as below that you can achive that
Buffer buf = new CircularFifoBuffer(4);
buf.add("A");
buf.add("B");
buf.add("C");
buf.add("D"); //ABCD
buf.add("E"); //BCDE
I think you will have to do it manually.
One solution that comes to mind is using #PrePersist and #PreUpdate event listeners in entity A.
Within the method annotated with above annotations , you check if size of Set<B> , if it is above the max limit, delete the oldest B entries(which may be tracked by a created_time timestamp property of B)

JPA ManyToMany ConcurrentModificationException issues

We have three entities with bidirectional many-to-many mappings in a A <-> B <-> C "hierarchy" like so (simplified, of course):
#Entity
Class A {
#Id int id;
#JoinTable(
name = "a_has_b",
joinColumns = {#JoinColumn(name = "a_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "b_id", referencedColumnName = "id")})
#ManyToMany
Collection<B> bs;
}
#Entity
Class B {
#Id int id;
#JoinTable(
name = "b_has_c",
joinColumns = {#JoinColumn(name = "b_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "c_id", referencedColumnName = "id")})
#ManyToMany(fetch=FetchType.EAGER,
cascade=CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH})
#org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
private Collection<C> cs;
#ManyToMany(mappedBy = "bs", fetch=FetchType.EAGER,
cascade={CascadeType.MERGE,CascadeType.PERSIST, CascadeType.REFRESH})
#org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
private Collection<A> as;
}
#Entity
Class C {
#Id int id;
#ManyToMany(mappedBy = "cs", fetch=FetchType.EAGER,
cascade={CascadeType.MERGE,CascadeType.PERSIST, CascadeType.REFRESH})
#org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
private Collection<B> bs;
}
There's no conecpt of an orphan - the entities are "standalone" from the application's point of view - and most of the time we're going to have a fistful of A:s, each with a couple of B:s (some may be "shared" among the A:s), and some 1000 C:s, not all of which are always "in use" by any B. We've concluded that we need bidirectional relations, since whenever an entity instance is removed, all links (entries in the join tables) have to be removed too. That is done like this:
void removeA( A a ) {
if ( a.getBs != null ) {
for ( B b : a.getBs() ) { //<--------- ConcurrentModificationException here
b.getAs().remove( a ) ;
entityManager.merge( b );
}
}
entityManager.remove( a );
}
If the collection, a.getBs() here, contains more than one element, then a ConcurrentModificationException is thrown. I've been banging my head for a while now, but can't think of a reasonable way of removing the links without meddling with the collection, which makes underlying the Iterator angry.
Q1: How am I supposed to do this, given the current ORM setup? (If at all...)
Q2: Is there a more reasonable way do design the OR-mappings that will let JPA (provided by Hibernate in this case) take care of everything. It'd be just swell if we didn't have to include those I'll be deleted now, so everybody I know, listen carefully: you don't need to know about this!-loops, which aren't working anyway, as it stands...
This problem has nothing to do with the ORM, as far as I can tell. You cannot use the syntactic-sugar foreach construct in Java to remove an element from a collection.
Note that Iterator.remove is the only safe way to modify a collection during iteration; the behavior is unspecified if the underlying collection is modified in any other way while the iteration is in progress.
Source
Simplified example of the problematic code:
List<B> bs = a.getBs();
for (B b : bs)
{
if (/* some condition */)
{
bs.remove(b); // throws ConcurrentModificationException
}
}
You must use the Iterator version to remove elements while iterating. Correct implementation:
List<B> bs = a.getBs();
for (Iterator<B> iter = bs.iterator(); iter.hasNext();)
{
B b = iter.next();
if (/* some condition */)
{
iter.remove(); // works correctly
}
}
Edit: I think this will work; untested however. If not, you should stop seeing ConcurrentModificationExceptions but instead (I think) you'll see ConstraintViolationExceptions.
void removeA(A a)
{
if (a != null)
{
a.setBs(new ArrayList<B>()); // wipe out all of a's Bs
entityManager.merge(a); // synchronize the state with the database
entityManager.remove(a); // removing should now work without ConstraintViolationExceptions
}
}
If the collection, a.getBs() here, contains more than one element, then a ConcurrentModificationException is thrown
The issue is that the collections inside of A, B, and C are magical Hibernate collections so when you run the following statement:
b.getAs().remove( a );
this removes a from b's collection but it also removes b from a's list which happens to be the collection being iterated over in the for loop. That generates the ConcurrentModificationException.
Matt's solution should work if you are really removing all elements in the collection. If you aren't however another work around is to copy all of the b's into a collection which removes the magical Hibernate collection from the process.
// copy out of the magic hibernate collection to a local collection
List<B> copy = new ArrayList<>(a.getBs());
for (B b : copy) {
b.getAs().remove(a) ;
entityManager.merge(b);
}
That should get you a little further down the road.
Gray's solution worked! Fortunately for us the JPA people seem to have been trying to implement collections as close to official Sun documentation on the proper use of List<> collections has indicated:
Note that Iterator.remove is the only safe way to modify a collection during iteration; the behavior is unspecified if the underlying collection is modified in any other way while the iteration is in progress.
I was all but pulling out my hair over this exception thinking it meant one #Stateless method could not call another #Stateless method from it's own class. This I thought odd as I was sure that I read somewhere that nested transactions are allowed. So when I did a search on this very exception, I found this posting and applied Gray's solution. Only in my case I happened to have two independent collections that had to be handled. As Gray indicated, according the Java spec on the proper way to remove from a member from a Java container, you need to use a copy of the original container to iterate with and then do your remove() on the original container which makes a lot of sense. Otherwise, the original container's link list algorithm gets confused.
for ( Participant p2 : new ArrayList<Participant>( p1.getFollowing() )) {
p1.getFollowing().remove(p2);
getEm().merge(p1);
p2.getFollowers().remove(p1);
getEm().merge(p2);
}
Notice I only make a copy of the first collection (p1.getFollowing()) and not the second collection (p2.getFollowers()). That is because I only need to iterate from one collection even though I need to remove associations from both collections.

Categories

Resources