Skip to content

Commit a3eb0b3

Browse files
committed
[hibernate#2006] Add test for UnexpectedAccessToTheDatabase error when merging a detached entity with a ToMany association
1 parent 57f426e commit a3eb0b3

File tree

4 files changed

+645
-28
lines changed

4 files changed

+645
-28
lines changed

hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java

+29-28
Original file line numberDiff line numberDiff line change
@@ -261,35 +261,36 @@ private static CompletionStage<Object> replaceCollectionTypeElements(
261261
(Collection<Object>) original, o ->
262262
getReplace( elemType, o, owner, session, copyCache )
263263
.thenCompose( o1 -> {
264-
result.add( o1 );
265-
return completedFuture( result );
264+
result.add( o1 );
265+
return completedFuture( result );
266+
} )
267+
)
268+
.thenApply( unused -> {
269+
// if the original is a PersistentCollection, and that original
270+
// was not flagged as dirty, then reset the target's dirty flag
271+
// here after the copy operation.
272+
// </p>
273+
// One thing to be careful of here is a "bare" original collection
274+
// in which case we should never ever ever reset the dirty flag
275+
// on the target because we simply do not know...
276+
if ( original instanceof PersistentCollection<?> originalPersistentCollection
277+
&& result instanceof PersistentCollection<?> resultPersistentCollection ) {
278+
return preserveSnapshot(
279+
originalPersistentCollection, resultPersistentCollection,
280+
elemType, owner, copyCache, session
281+
)
282+
.thenCompose( v -> {
283+
if ( !originalPersistentCollection.isDirty() ) {
284+
resultPersistentCollection.clearDirty();
285+
}
286+
return voidFuture();
266287
}
267-
)
268-
).thenApply( unused -> {
269-
// if the original is a PersistentCollection, and that original
270-
// was not flagged as dirty, then reset the target's dirty flag
271-
// here after the copy operation.
272-
// </p>
273-
// One thing to be careful of here is a "bare" original collection
274-
// in which case we should never ever ever reset the dirty flag
275-
// on the target because we simply do not know...
276-
if ( original instanceof PersistentCollection<?> originalPersistentCollection
277-
&& result instanceof PersistentCollection<?> resultPersistentCollection ) {
278-
return preserveSnapshot(
279-
originalPersistentCollection, resultPersistentCollection,
280-
elemType, owner, copyCache, session
281-
).thenCompose( v -> {
282-
if ( !originalPersistentCollection.isDirty() ) {
283-
resultPersistentCollection.clearDirty();
284-
}
285-
return voidFuture();
286-
}
287-
).thenApply( v -> result );
288-
}
289-
else {
290-
return result;
291-
}
292-
} );
288+
).thenApply( v -> result );
289+
}
290+
else {
291+
return result;
292+
}
293+
} );
293294
}
294295

295296
private static CompletionStage<Object> replaceMapTypeElements(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/* Hibernate, Relational Persistence for Idiomatic Java
2+
*
3+
* SPDX-License-Identifier: Apache-2.0
4+
* Copyright: Red Hat Inc. and Hibernate Authors
5+
*/
6+
package org.hibernate.reactive;
7+
8+
import java.util.Collection;
9+
import java.util.List;
10+
import java.util.Objects;
11+
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
15+
import io.vertx.junit5.Timeout;
16+
import io.vertx.junit5.VertxTestContext;
17+
import jakarta.persistence.Entity;
18+
import jakarta.persistence.FetchType;
19+
import jakarta.persistence.Id;
20+
import jakarta.persistence.OneToMany;
21+
import jakarta.persistence.Table;
22+
23+
import static java.util.concurrent.TimeUnit.MINUTES;
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
@Timeout(value = 2, timeUnit = MINUTES)
27+
public class OneToManyArrayMergeTest extends BaseReactiveTest {
28+
29+
private final static Long USER_ID = 1L;
30+
private final static Long ADMIN_ROLE_ID = 2L;
31+
private final static Long USER_ROLE_ID = 3L;
32+
private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME";
33+
private final static String UPDATED_LASTNAME = "UPDATED LASTNAME";
34+
35+
@Override
36+
protected Collection<Class<?>> annotatedEntities() {
37+
return List.of( User.class, Role.class );
38+
}
39+
40+
@BeforeEach
41+
public void populateDb(VertxTestContext context) {
42+
Role adminRole = new Role( ADMIN_ROLE_ID, "admin" );
43+
Role userRole = new Role( USER_ROLE_ID, "user" );
44+
User user = new User( USER_ID, "first", "last", adminRole );
45+
test(
46+
context, getMutinySessionFactory()
47+
.withTransaction( s -> s.persistAll( user, adminRole, userRole ) )
48+
);
49+
}
50+
51+
@Test
52+
public void testMerge(VertxTestContext context) {
53+
test(
54+
context, getMutinySessionFactory()
55+
.withTransaction( s -> s.find( User.class, USER_ID ) )
56+
.chain( user -> getMutinySessionFactory()
57+
.withTransaction( s -> s
58+
.createQuery( "FROM Role", Role.class )
59+
.getResultList() )
60+
.map( roles -> {
61+
user.addAll( roles );
62+
user.setFirstname( UPDATED_FIRSTNAME );
63+
user.setLastname( UPDATED_LASTNAME );
64+
return user;
65+
} )
66+
)
67+
.chain( user -> {
68+
assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME );
69+
assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME );
70+
assertThat( user.getRoles() ).hasSize( 2 );
71+
return getMutinySessionFactory()
72+
.withTransaction( s -> s.merge( user ) );
73+
}
74+
)
75+
.chain( v -> getMutinySessionFactory()
76+
.withTransaction( s -> s.find( User.class, USER_ID ) )
77+
)
78+
.invoke( user -> {
79+
Role adminRole = new Role( ADMIN_ROLE_ID, "admin" );
80+
Role userRole = new Role( USER_ROLE_ID, "user" );
81+
assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME );
82+
assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME );
83+
assertThat( user.getRoles() ).containsExactlyInAnyOrder(
84+
adminRole,
85+
userRole
86+
);
87+
}
88+
)
89+
);
90+
}
91+
92+
@Entity(name = "User")
93+
@Table(name = "USER_TABLE")
94+
public static class User {
95+
96+
@Id
97+
private Long id;
98+
99+
private String firstname;
100+
101+
private String lastname;
102+
103+
@OneToMany(fetch = FetchType.EAGER)
104+
private Role[] roles;
105+
106+
public User() {
107+
}
108+
109+
public User(Long id, String firstname, String lastname, Role... roles) {
110+
this.id = id;
111+
this.firstname = firstname;
112+
this.lastname = lastname;
113+
this.roles = new Role[roles.length];
114+
for ( int i = 0; i < roles.length; i++ ) {
115+
this.roles[i] = roles[i];
116+
}
117+
}
118+
119+
public Long getId() {
120+
return id;
121+
}
122+
123+
public String getFirstname() {
124+
return firstname;
125+
}
126+
127+
public void setFirstname(String firstname) {
128+
this.firstname = firstname;
129+
}
130+
131+
public String getLastname() {
132+
return lastname;
133+
}
134+
135+
public void setLastname(String lastname) {
136+
this.lastname = lastname;
137+
}
138+
139+
public Role[] getRoles() {
140+
return roles;
141+
}
142+
143+
public void addAll(List<Role> roles) {
144+
this.roles = new Role[roles.size()];
145+
for ( int i = 0; i < roles.size(); i++ ) {
146+
this.roles[i] = roles.get( i );
147+
}
148+
}
149+
}
150+
151+
@Entity(name = "Role")
152+
@Table(name = "ROLE_TABLE")
153+
public static class Role {
154+
155+
@Id
156+
private Long id;
157+
private String code;
158+
159+
public Role() {
160+
}
161+
162+
public Role(Long id, String code) {
163+
this.id = id;
164+
this.code = code;
165+
}
166+
167+
public Object getId() {
168+
return id;
169+
}
170+
171+
@Override
172+
public boolean equals(Object o) {
173+
if ( o == null || getClass() != o.getClass() ) {
174+
return false;
175+
}
176+
Role role = (Role) o;
177+
return Objects.equals( id, role.id ) && Objects.equals( code, role.code );
178+
}
179+
180+
@Override
181+
public int hashCode() {
182+
return Objects.hash( id, code );
183+
}
184+
185+
@Override
186+
public String toString() {
187+
return "Role{" + code + '}';
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)