From d3f939cd3908255094329377cce17af5ab122425 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 20 Jan 2025 10:48:36 +0100 Subject: [PATCH] [#2006] Add test for UnexpectedAccessToTheDatabase error when merging a detached entity with a ToMany association --- .../reactive/OneToManyArrayMergeTest.java | 190 +++++++++++++++ .../reactive/OneToManyMapMergeTest.java | 199 +++++++++++++++ .../reactive/OneToManyMergeTest.java | 227 ++++++++++++++++++ 3 files changed, 616 insertions(+) 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/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..76d7f7732 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java @@ -0,0 +1,227 @@ +/* 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 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 + ); + } + ) + ); + } + + @Test + public void testMergeRemovingCollectionElements(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.clearRoles(); + 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() ).isNull(); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).isNullOrEmpty(); + } + ) + ); + } + + @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; + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.roles = List.of( roles ); + } + + public User(Long id, String firstname, String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + } + + 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; + } + + public void clearRoles() { + this.roles = null; + } + } + + @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 + '}'; + } + } +}