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
+ // 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 + '}';
+ }
+ }
+}