From 1dd00683886118edd9605855f6bb7c7bfb562487 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Fri, 17 Jan 2025 10:31:17 +0100 Subject: [PATCH] [#2006] Add test for UnexpectedAccessToTheDatabase error when merging a detached entity with a ToMany association --- .../reactive/engine/impl/CollectionTypes.java | 57 ++--- .../reactive/OneToManyArrayMergeTest.java | 190 +++++++++++++++++ .../reactive/OneToManyMapMergeTest.java | 199 ++++++++++++++++++ .../reactive/OneToManyMergeTest.java | 184 ++++++++++++++++ 4 files changed, 602 insertions(+), 28 deletions(-) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java index 09252d86b..4cc2193f4 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java @@ -236,35 +236,36 @@ private static CompletionStage replaceCollectinTypeElements( (Collection) original, o -> getReplace( elemType, o, owner, session, copyCache ) .thenCompose( o1 -> { - result.add( o1 ); - return completedFuture( result ); + result.add( o1 ); + return completedFuture( result ); + } ) + ) + .thenApply( unused -> { + // if the original is a PersistentCollection, and that original + // was not flagged as dirty, then reset the target's dirty flag + // here after the copy operation. + //

+ // One thing to be careful of here is a "bare" original collection + // in which case we should never ever ever reset the dirty flag + // on the target because we simply do not know... + if ( original instanceof PersistentCollection originalPersistentCollection + && result instanceof PersistentCollection resultPersistentCollection ) { + return preserveSnapshot( + originalPersistentCollection, resultPersistentCollection, + elemType, owner, copyCache, session + ) + .thenCompose( v -> { + if ( !originalPersistentCollection.isDirty() ) { + resultPersistentCollection.clearDirty(); + } + return voidFuture(); } - ) - ).thenApply( unused -> { - // if the original is a PersistentCollection, and that original - // was not flagged as dirty, then reset the target's dirty flag - // here after the copy operation. - //

- // One thing to be careful of here is a "bare" original collection - // in which case we should never ever ever reset the dirty flag - // on the target because we simply do not know... - if ( original instanceof PersistentCollection originalPersistentCollection - && result instanceof PersistentCollection resultPersistentCollection ) { - return preserveSnapshot( - originalPersistentCollection, resultPersistentCollection, - elemType, owner, copyCache, session - ).thenCompose( v -> { - if ( !originalPersistentCollection.isDirty() ) { - resultPersistentCollection.clearDirty(); - } - return voidFuture(); - } - ).thenApply( v -> result ); - } - else { - return result; - } - } ); + ).thenApply( v -> result ); + } + else { + return result; + } + } ); } private static CompletionStage replaceMaptypeElements( diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java new file mode 100644 index 000000000..e2eb826d9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java @@ -0,0 +1,190 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyArrayMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsExactlyInAnyOrder( + adminRole, + userRole + ); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private Role[] roles; + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.roles = new Role[roles.length]; + for ( int i = 0; i < roles.length; i++ ) { + this.roles[i] = roles[i]; + } + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Role[] getRoles() { + return roles; + } + + public void addAll(List roles) { + this.roles = new Role[roles.size()]; + for ( int i = 0; i < roles.size(); i++ ) { + this.roles[i] = roles.get( i ); + } + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java new file mode 100644 index 000000000..7a1096d3d --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java @@ -0,0 +1,199 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyMapMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsEntry( + adminRole.getCode(), + adminRole + ); + assertThat( user.getRoles() ).containsEntry( + userRole.getCode(), + userRole + ); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private Map roles = new HashMap(); + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + for ( Role role : roles ) { + this.roles.put( role.getCode(), role ); + } + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Map getRoles() { + return roles; + } + + public void addAll(List roles) { + this.roles.clear(); + for ( Role role : roles ) { + this.roles.put( role.getCode(), role ); + } + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + public String getCode() { + return code; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java new file mode 100644 index 000000000..e298493ea --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java @@ -0,0 +1,184 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.getRoles().clear(); + user.getRoles().addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsExactlyInAnyOrder( + adminRole, + userRole + ); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private List roles = new ArrayList<>(); + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + for ( Role role : roles ) { + this.roles.add( role ); + } + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public List getRoles() { + return roles; + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +}