Skip to content

Commit 65ff89a

Browse files
authored
Adds removal of orphaned entities (#714)
* Enable removal of orphaned entity * tests for removal of orphaned entity
1 parent 021cd60 commit 65ff89a

File tree

4 files changed

+240
-1
lines changed

4 files changed

+240
-1
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,9 @@ private void cascadeLogicalOneToOneOrphanRemoval(
356356
// If FK direction is to-parent, we must remove the orphan *before* the queued update(s)
357357
// occur. Otherwise, replacing the association on a managed entity, without manually
358358
// nulling and flushing, causes FK constraint violations.
359-
eventSource.removeOrphanBeforeUpdates( entityName, loadedValue );
359+
ReactiveSession session = (ReactiveSession) eventSource;
360+
final Object finalLoadedValue = loadedValue;
361+
stage = stage.thenCompose( v -> session.reactiveRemoveOrphanBeforeUpdates( entityName, finalLoadedValue ) );
360362
}
361363
else {
362364
// Else, we must delete after the updates.

hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveSession.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ <T> CompletionStage<T> reactiveFind(Class<T> entityClass, Object id,
101101

102102
CompletionStage<Void> reactiveInitializeCollection(PersistentCollection collection, boolean writing);
103103

104+
CompletionStage<Void> reactiveRemoveOrphanBeforeUpdates(String entityName, Object child);
105+
104106
void setHibernateFlushMode(FlushMode flushMode);
105107
FlushMode getHibernateFlushMode();
106108

hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.hibernate.graph.GraphSemantic;
5353
import org.hibernate.graph.RootGraph;
5454
import org.hibernate.graph.spi.RootGraphImplementor;
55+
import org.hibernate.internal.EntityManagerMessageLogger;
56+
import org.hibernate.internal.HEMLogging;
5557
import org.hibernate.internal.SessionCreationOptions;
5658
import org.hibernate.internal.SessionFactoryImpl;
5759
import org.hibernate.internal.SessionImpl;
@@ -62,6 +64,7 @@
6264
import org.hibernate.loader.custom.sql.SQLCustomQuery;
6365
import org.hibernate.persister.entity.EntityPersister;
6466
import org.hibernate.persister.entity.MultiLoadOptions;
67+
import org.hibernate.pretty.MessageHelper;
6568
import org.hibernate.proxy.HibernateProxy;
6669
import org.hibernate.proxy.LazyInitializer;
6770
import org.hibernate.query.ParameterMetadata;
@@ -120,6 +123,7 @@
120123
* Hibernate core compares the identity of session instances.
121124
*/
122125
public class ReactiveSessionImpl extends SessionImpl implements ReactiveSession, EventSource {
126+
private static final EntityManagerMessageLogger log = HEMLogging.messageLogger( ReactiveSessionImpl.class );
123127

124128
private transient ReactiveActionQueue reactiveActionQueue = new ReactiveActionQueue( this );
125129
private final ReactiveConnection reactiveConnection;
@@ -1566,4 +1570,34 @@ public void checkOpen() {
15661570
super.checkOpen();
15671571
}
15681572

1573+
@Override
1574+
public void removeOrphanBeforeUpdates(String entityName, Object child) {
1575+
throw new UnsupportedOperationException();
1576+
}
1577+
1578+
@Override
1579+
public CompletionStage<Void> reactiveRemoveOrphanBeforeUpdates(String entityName, Object child) {
1580+
// TODO: The removeOrphan concept is a temporary "hack" for HHH-6484. This should be removed once action/task
1581+
// ordering is improved.
1582+
final StatefulPersistenceContext persistenceContext = (StatefulPersistenceContext)getPersistenceContextInternal();
1583+
persistenceContext.beginRemoveOrphanBeforeUpdates();
1584+
return fireRemove( new DeleteEvent( entityName, child, false, true, this ) )
1585+
.thenAccept( v -> {
1586+
persistenceContext.endRemoveOrphanBeforeUpdates();
1587+
if ( log.isTraceEnabled() ) {
1588+
logRemoveOrphanBeforeUpdates( "end", entityName, child, persistenceContext );
1589+
}
1590+
});
1591+
}
1592+
1593+
private void logRemoveOrphanBeforeUpdates(String timing, String entityName, Object entity, StatefulPersistenceContext persistenceContext) {
1594+
if ( log.isTraceEnabled() ) {
1595+
final EntityEntry entityEntry = persistenceContext.getEntry( entity );
1596+
log.tracef(
1597+
"%s remove orphan before updates: [%s]",
1598+
timing,
1599+
entityEntry == null ? entityName : MessageHelper.infoString( entityName, entityEntry.getId() )
1600+
);
1601+
}
1602+
}
15691603
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/* Hibernate, Relational Persistence for Idiomatic Java
2+
*
3+
* SPDX-License-Identifier: LGPL-2.1-or-later
4+
* Copyright: Red Hat Inc. and Hibernate Authors
5+
*/
6+
package org.hibernate.reactive;
7+
8+
import java.io.Serializable;
9+
import java.time.OffsetDateTime;
10+
import java.util.UUID;
11+
import javax.persistence.CascadeType;
12+
import javax.persistence.Column;
13+
import javax.persistence.DiscriminatorColumn;
14+
import javax.persistence.DiscriminatorType;
15+
import javax.persistence.DiscriminatorValue;
16+
import javax.persistence.Entity;
17+
import javax.persistence.FetchType;
18+
import javax.persistence.GeneratedValue;
19+
import javax.persistence.Id;
20+
import javax.persistence.Inheritance;
21+
import javax.persistence.InheritanceType;
22+
import javax.persistence.JoinColumn;
23+
import javax.persistence.OneToOne;
24+
25+
import org.hibernate.cfg.Configuration;
26+
import org.hibernate.reactive.mutiny.Mutiny;
27+
28+
import org.junit.After;
29+
import org.junit.Before;
30+
import org.junit.Test;
31+
32+
import io.vertx.ext.unit.TestContext;
33+
import org.assertj.core.api.Assertions;
34+
35+
public class LazyReplaceOrphanedEntityTest extends BaseReactiveTest {
36+
37+
private Campaign theCampaign;
38+
39+
@Override
40+
protected Configuration constructConfiguration() {
41+
Configuration configuration = super.constructConfiguration();
42+
configuration.addAnnotatedClass( Campaign.class );
43+
configuration.addAnnotatedClass( ExecutionDate.class );
44+
configuration.addAnnotatedClass( Schedule.class );
45+
return configuration;
46+
}
47+
48+
@Before
49+
public void populateDb(TestContext context) {
50+
theCampaign = new Campaign();
51+
theCampaign.setSchedule( new ExecutionDate(OffsetDateTime.now(), "ALPHA") );
52+
53+
Mutiny.Session session = openMutinySession();
54+
test( context, session.persist( theCampaign ).call( session::flush ) );
55+
}
56+
57+
@After
58+
public void cleanDB(TestContext context) {
59+
test( context, deleteEntities( "Campaign", "Schedule" ) );
60+
}
61+
62+
@Test
63+
public void testUpdateScheduleChange(TestContext context) {
64+
test(
65+
context,
66+
getMutinySessionFactory().withSession(
67+
session -> session.find( Campaign.class, theCampaign.getId() )
68+
.invoke( foundCampaign -> foundCampaign.setSchedule( new ExecutionDate(
69+
OffsetDateTime.now(),
70+
"BETA"
71+
) ) )
72+
.call( session::flush )
73+
.chain( () -> openMutinySession().find( Campaign.class, theCampaign.getId() ) )
74+
.invoke( updatedCampaign -> Assertions.assertThat(
75+
updatedCampaign.getSchedule().getCodeName() )
76+
.isNotEqualTo( theCampaign.getSchedule().getCodeName() ) )
77+
)
78+
);
79+
}
80+
81+
@Test
82+
public void testUpdateWithMultipleScheduleChanges(TestContext context) {
83+
test(
84+
context,
85+
getMutinySessionFactory().withSession(
86+
session -> session.find( Campaign.class, theCampaign.getId() )
87+
.invoke( foundCampaign -> foundCampaign.setSchedule( new ExecutionDate(
88+
OffsetDateTime.now(),
89+
"BETA"
90+
) ) )
91+
.call( session::flush ) )
92+
.call( () -> getMutinySessionFactory().withSession(
93+
session -> session.find( Campaign.class, theCampaign.getId() )
94+
.invoke( foundCampaign -> foundCampaign.setSchedule( new ExecutionDate(
95+
OffsetDateTime.now(),
96+
"GAMMA"
97+
) ) )
98+
.call( session::flush )
99+
) )
100+
.chain( () -> openMutinySession().find( Campaign.class, theCampaign.getId() ) )
101+
.invoke( updatedCampaign -> Assertions.assertThat(
102+
updatedCampaign.getSchedule().getCodeName() )
103+
.isNotEqualTo( theCampaign.getSchedule().getCodeName() )
104+
)
105+
);
106+
}
107+
108+
@Entity (name="Campaign")
109+
public static class Campaign implements Serializable {
110+
111+
@Id @GeneratedValue
112+
private Integer id;
113+
114+
@OneToOne(mappedBy = "campaign", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
115+
public Schedule schedule;
116+
117+
public Campaign() {
118+
}
119+
120+
// Getters and setters
121+
public void setSchedule(Schedule schedule) {
122+
this.schedule = schedule;
123+
if( schedule != null ) {
124+
this.schedule.setCampaign( this );
125+
}
126+
}
127+
128+
public Schedule getSchedule() {
129+
return this.schedule;
130+
}
131+
132+
public Integer getId() {
133+
return id;
134+
}
135+
}
136+
137+
@Entity (name="Schedule")
138+
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
139+
@DiscriminatorColumn(name = "schedule_type", discriminatorType = DiscriminatorType.STRING)
140+
public static abstract class Schedule implements Serializable {
141+
@Id
142+
@Column(name = "id")
143+
private String id = UUID.randomUUID().toString();
144+
145+
@Column(name = "code_name")
146+
private String code_name;
147+
148+
@OneToOne
149+
@JoinColumn(name = "campaign_id")
150+
private Campaign campaign;
151+
152+
// Getters and setters
153+
public String getId() {
154+
return id;
155+
}
156+
157+
public void setCampaign(Campaign campaign) {
158+
this.campaign = campaign;
159+
}
160+
161+
public Campaign getCampaign() {
162+
return campaign;
163+
}
164+
165+
public void setCodeName(String code_name) {
166+
this.code_name = code_name;
167+
}
168+
169+
public String getCodeName() {
170+
return code_name;
171+
}
172+
}
173+
174+
@Entity (name="ExecutionDate")
175+
@DiscriminatorValue("EXECUTION_DATE")
176+
public static class ExecutionDate extends Schedule {
177+
178+
@Column(name = "start_date")
179+
private OffsetDateTime start;
180+
181+
public ExecutionDate() {
182+
}
183+
184+
public ExecutionDate( OffsetDateTime start, String code_name ) {
185+
this.start = start;
186+
setCodeName( code_name );
187+
}
188+
189+
// Getters and setters
190+
191+
public Schedule setStart(OffsetDateTime start) {
192+
this.start = start;
193+
return null;
194+
}
195+
196+
public OffsetDateTime getStart() {
197+
return start;
198+
}
199+
}
200+
201+
}

0 commit comments

Comments
 (0)