Skip to content

Commit 3a6b464

Browse files
committed
[hibernate#1787] Test support for @SoftDelete
1 parent 856a218 commit 3a6b464

File tree

1 file changed

+397
-0
lines changed

1 file changed

+397
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
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.Arrays;
9+
import java.util.Collection;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.concurrent.CompletionStage;
13+
import java.util.function.Predicate;
14+
import java.util.function.Supplier;
15+
16+
import org.hibernate.annotations.SoftDelete;
17+
import org.hibernate.annotations.SoftDeleteType;
18+
import org.hibernate.reactive.testing.SqlStatementTracker;
19+
import org.hibernate.type.YesNoConverter;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
24+
import io.smallrye.mutiny.Uni;
25+
import io.vertx.junit5.VertxTestContext;
26+
import jakarta.persistence.Entity;
27+
import jakarta.persistence.Id;
28+
import jakarta.persistence.Table;
29+
import jakarta.persistence.criteria.CriteriaBuilder;
30+
import jakarta.persistence.criteria.CriteriaDelete;
31+
import jakarta.persistence.criteria.Root;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2;
35+
import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType;
36+
import static org.hibernate.reactive.util.impl.CompletionStages.loop;
37+
38+
/*
39+
* Tests validity of @SoftDelete annotation value options
40+
* as well as verifying logged 'create table' and 'update' queries for each database
41+
*
42+
* @see org.hibernate.orm.test.softdelete.SimpleSoftDeleteTests
43+
*/
44+
public class SoftDeleteTest extends BaseReactiveTest {
45+
46+
private static SqlStatementTracker sqlTracker;
47+
48+
static final Deletable[] activeEntities = {
49+
new ActiveEntity( 1, "active first" ),
50+
new ActiveEntity( 2, "active second" ),
51+
new ActiveEntity( 3, "active third" ),
52+
};
53+
static final Deletable[] deletedEntities = {
54+
new DeletedEntity( 1, "deleted first" ),
55+
new DeletedEntity( 2, "deleted second" ),
56+
new DeletedEntity( 3, "deleted third" ),
57+
};
58+
static final Deletable[] implicitEntities = {
59+
new ImplicitEntity( 1, "implicit first" ),
60+
new ImplicitEntity( 2, "implicit second" ),
61+
new ImplicitEntity( 3, "implicit third" )
62+
};
63+
64+
@Override
65+
protected Collection<Class<?>> annotatedEntities() {
66+
return List.of( ActiveEntity.class, DeletedEntity.class, ImplicitEntity.class );
67+
}
68+
69+
@BeforeEach
70+
public void populateDB(VertxTestContext context) {
71+
test( context, getMutinySessionFactory()
72+
.withTransaction( session -> session.persistAll( activeEntities ) )
73+
.call( () -> getMutinySessionFactory().withTransaction( session -> session.persistAll( deletedEntities ) ) )
74+
.call( () -> getMutinySessionFactory().withTransaction( session -> session.persistAll( implicitEntities ) ) )
75+
);
76+
}
77+
78+
// We need to actually empty the tables, or the populate db will fail the second time
79+
@Override
80+
protected CompletionStage<Void> cleanDb() {
81+
return loop( annotatedEntities(), aClass -> getSessionFactory()
82+
.withTransaction( s -> s.createNativeQuery( "delete from " + aClass.getSimpleName() ).executeUpdate() )
83+
);
84+
}
85+
86+
@Test
87+
public void testActiveStrategyWithYesNoConverter(VertxTestContext context) {
88+
testSoftDelete( context, s -> s.equals( "N" ), "active", ActiveEntity.class, activeEntities,
89+
() -> getMutinySessionFactory().withTransaction( s -> s
90+
.remove( s.getReference( ActiveEntity.class, activeEntities[0].getId() ) )
91+
)
92+
);
93+
}
94+
95+
@Test
96+
public void testDeletedStrategyWithYesNoConverter(VertxTestContext context) {
97+
testSoftDelete( context, s -> s.equals( "Y" ), "deleted", DeletedEntity.class, deletedEntities,
98+
() -> getMutinySessionFactory().withTransaction( s -> s
99+
.remove( s.getReference( DeletedEntity.class, deletedEntities[0].getId() ) )
100+
)
101+
);
102+
}
103+
104+
@Test
105+
public void testDefaults(VertxTestContext context) {
106+
Predicate<Object> deleted = obj -> {
107+
switch ( dbType() ) {
108+
case DB2:
109+
return ( (short) obj ) == 1;
110+
case ORACLE:
111+
return ( (Number) obj ).intValue() == 1;
112+
default:
113+
return (boolean) obj;
114+
}
115+
};
116+
testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities,
117+
() -> getMutinySessionFactory().withTransaction( s -> s
118+
.remove( s.getReference( ImplicitEntity.class, implicitEntities[0].getId() ) )
119+
)
120+
);
121+
}
122+
123+
@Test
124+
public void testDeletionWithHQLQuery(VertxTestContext context) {
125+
Predicate<Object> deleted = obj -> {
126+
switch ( dbType() ) {
127+
case DB2:
128+
return ( (short) obj ) == 1;
129+
case ORACLE:
130+
return ( (Number) obj ).intValue() == 1;
131+
default:
132+
return (boolean) obj;
133+
}
134+
};
135+
testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities,
136+
() -> getMutinySessionFactory().withTransaction( s -> s
137+
.createMutationQuery( "delete from ImplicitEntity where name = :name" )
138+
.setParameter( "name", implicitEntities[0].getName() )
139+
.executeUpdate()
140+
)
141+
);
142+
}
143+
144+
@Test
145+
public void testDeletionWithCriteria(VertxTestContext context) {
146+
Predicate<Object> deleted = obj -> {
147+
switch ( dbType() ) {
148+
case DB2:
149+
return ( (short) obj ) == 1;
150+
case ORACLE:
151+
return ( (Number) obj ).intValue() == 1;
152+
default:
153+
return (boolean) obj;
154+
}
155+
};
156+
testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities,
157+
() -> getMutinySessionFactory().withTransaction( s -> {
158+
CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder();
159+
CriteriaDelete<ImplicitEntity> delete = cb.createCriteriaDelete( ImplicitEntity.class );
160+
Root<ImplicitEntity> root = delete.from( ImplicitEntity.class );
161+
delete.where( cb.equal( root.get( "name" ), implicitEntities[0].getName() ) );
162+
return s.createQuery( delete ).executeUpdate();
163+
} )
164+
);
165+
}
166+
167+
private void testSoftDelete(
168+
VertxTestContext context,
169+
Predicate<Object> deleted,
170+
String deletedColumn,
171+
Class<?> entityClass,
172+
Deletable[] entities, Supplier<Uni<?>> deleteEntity) {
173+
test( context, getMutinySessionFactory()
174+
// Check that the soft delete column exists and has the expected initial value
175+
.withSession( s -> s
176+
// This SQL query should be compatible with all databases
177+
.createNativeQuery( "select id, name, " + deletedColumn + " from " + entityClass.getSimpleName() + " order by id" )
178+
.getResultList()
179+
.invoke( rows -> {
180+
assertThat( rows ).hasSize( entities.length );
181+
for ( int i = 0; i < entities.length; i++ ) {
182+
Object[] row = (Object[]) rows.get( i );
183+
Integer actualId = ( (Number) row[0] ).intValue();
184+
assertThat( actualId ).isEqualTo( entities[i].getId() );
185+
assertThat( row[1] ).isEqualTo( entities[i].getName() );
186+
// Only the first element should be deleted
187+
assertThat( deleted.test( row[2] ) ).isFalse();
188+
}
189+
} )
190+
)
191+
// Delete an entity
192+
.call( deleteEntity::get )
193+
// Test select all
194+
.call( () -> getMutinySessionFactory().withTransaction( s -> s
195+
.createSelectionQuery( "from " + entityClass.getSimpleName() + " order by id", Object.class )
196+
.getResultList()
197+
.invoke( list -> assertThat( list ).containsExactly( entities[1], entities[2] ) )
198+
) )
199+
// Test find
200+
.call( () -> getMutinySessionFactory().withTransaction( s -> s
201+
.find( entityClass, entities[0].getId() )
202+
.invoke( entity -> assertThat( entity ).isNull() )
203+
) )
204+
// Test table content with a native query
205+
.call( () -> getMutinySessionFactory().withSession( s -> s
206+
// This SQL query should be compatible with all databases
207+
.createNativeQuery( "select id, name, " + deletedColumn + " from " + entityClass.getSimpleName() + " order by id" )
208+
.getResultList()
209+
.invoke( rows -> {
210+
assertThat( rows ).hasSize( entities.length );
211+
for ( int i = 0; i < entities.length; i++ ) {
212+
Object[] row = (Object[]) rows.get( i );
213+
Integer actualId = ( (Number) row[0] ).intValue();
214+
assertThat( actualId ).isEqualTo( entities[i].getId() );
215+
assertThat( row[1] ).isEqualTo( entities[i].getName() );
216+
// Only the first element should have been deleted
217+
System.out.println( Arrays.toString( row ) );
218+
System.out.println( "Index: " + i + ", Actual: " + deleted.test( row[2] ) + " Expected: " + ( i == 0 ) );
219+
assertThat( deleted.test( row[2] ) ).isEqualTo( i == 0 );
220+
}
221+
} )
222+
) )
223+
);
224+
}
225+
226+
// The interface helps with simplifying the code for the test
227+
private interface Deletable {
228+
Integer getId();
229+
230+
String getName();
231+
}
232+
233+
@Entity(name = "ActiveEntity")
234+
@Table(name = "ActiveEntity")
235+
@SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
236+
public static class ActiveEntity implements Deletable {
237+
@Id
238+
private Integer id;
239+
private String name;
240+
241+
public ActiveEntity() {
242+
}
243+
244+
public ActiveEntity(Integer id, String name) {
245+
this.id = id;
246+
this.name = name;
247+
}
248+
249+
public Integer getId() {
250+
return id;
251+
}
252+
253+
public void setId(Integer id) {
254+
this.id = id;
255+
}
256+
257+
public String getName() {
258+
return name;
259+
}
260+
261+
public void setName(String name) {
262+
this.name = name;
263+
}
264+
265+
@Override
266+
public boolean equals(Object o) {
267+
if ( this == o ) {
268+
return true;
269+
}
270+
if ( o == null || getClass() != o.getClass() ) {
271+
return false;
272+
}
273+
ActiveEntity that = (ActiveEntity) o;
274+
return Objects.equals( name, that.name );
275+
}
276+
277+
@Override
278+
public int hashCode() {
279+
return Objects.hashCode( name );
280+
}
281+
282+
@Override
283+
public String toString() {
284+
return this.getClass() + ":" + id + ":" + name;
285+
}
286+
}
287+
288+
@Entity(name = "DeletedEntity")
289+
@Table(name = "DeletedEntity")
290+
@SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.DELETED)
291+
public static class DeletedEntity implements Deletable {
292+
@Id
293+
private Integer id;
294+
private String name;
295+
296+
public DeletedEntity() {
297+
}
298+
299+
public DeletedEntity(Integer id, String name) {
300+
this.id = id;
301+
this.name = name;
302+
}
303+
304+
public Integer getId() {
305+
return id;
306+
}
307+
308+
public void setId(Integer id) {
309+
this.id = id;
310+
}
311+
312+
public String getName() {
313+
return name;
314+
}
315+
316+
public void setName(String name) {
317+
this.name = name;
318+
}
319+
320+
@Override
321+
public boolean equals(Object o) {
322+
if ( this == o ) {
323+
return true;
324+
}
325+
if ( o == null || getClass() != o.getClass() ) {
326+
return false;
327+
}
328+
DeletedEntity that = (DeletedEntity) o;
329+
return Objects.equals( name, that.name );
330+
}
331+
332+
@Override
333+
public int hashCode() {
334+
return Objects.hashCode( name );
335+
}
336+
337+
@Override
338+
public String toString() {
339+
return this.getClass() + ":" + id + ":" + name;
340+
}
341+
}
342+
343+
@Entity(name = "ImplicitEntity")
344+
@Table(name = "ImplicitEntity")
345+
@SoftDelete
346+
public static class ImplicitEntity implements Deletable {
347+
@Id
348+
private Integer id;
349+
private String name;
350+
351+
public ImplicitEntity() {
352+
}
353+
354+
public ImplicitEntity(Integer id, String name) {
355+
this.id = id;
356+
this.name = name;
357+
}
358+
359+
public Integer getId() {
360+
return id;
361+
}
362+
363+
public void setId(Integer id) {
364+
this.id = id;
365+
}
366+
367+
public String getName() {
368+
return name;
369+
}
370+
371+
public void setName(String name) {
372+
this.name = name;
373+
}
374+
375+
@Override
376+
public boolean equals(Object o) {
377+
if ( this == o ) {
378+
return true;
379+
}
380+
if ( o == null || getClass() != o.getClass() ) {
381+
return false;
382+
}
383+
ImplicitEntity that = (ImplicitEntity) o;
384+
return Objects.equals( name, that.name );
385+
}
386+
387+
@Override
388+
public int hashCode() {
389+
return Objects.hashCode( name );
390+
}
391+
392+
@Override
393+
public String toString() {
394+
return this.getClass() + ":" + id + ":" + name;
395+
}
396+
}
397+
}

0 commit comments

Comments
 (0)