Skip to content

Commit cb1082f

Browse files
samebGuice Team
authored and
Guice Team
committed
Don't automatically create an EntityManager outside of a UnitOfWork, because it leads to leaks. Fixes #739, fixes #997, fixes #730, fixes #985, fixes #959, fixes #731. This also adds an optional Options to the JpaPersistModule constructor, if users wish to use the legacy behavior. This is a breaking change, but to a better default value. Users who want to keep the dangerous form can opt back in with the options.
PiperOrigin-RevId: 525852009
1 parent d882d5e commit cb1082f

15 files changed

+149
-50
lines changed

extensions/persist/src/com/google/inject/persist/jpa/JpaPersistModule.java

+7
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@
4444
*/
4545
public final class JpaPersistModule extends PersistModule {
4646
private final String jpaUnit;
47+
private final JpaPersistOptions options;
4748

4849
public JpaPersistModule(String jpaUnit) {
50+
this(jpaUnit, JpaPersistOptions.builder().build());
51+
}
52+
53+
public JpaPersistModule(String jpaUnit, JpaPersistOptions options) {
4954
Preconditions.checkArgument(
5055
null != jpaUnit && jpaUnit.length() > 0, "JPA unit name must be a non-empty string.");
5156
this.jpaUnit = jpaUnit;
57+
this.options = options;
5258
}
5359

5460
private Map<?, ?> properties;
@@ -57,6 +63,7 @@ public JpaPersistModule(String jpaUnit) {
5763
@Override
5864
protected void configurePersistence() {
5965
bindConstant().annotatedWith(Jpa.class).to(jpaUnit);
66+
bind(JpaPersistOptions.class).annotatedWith(Jpa.class).toInstance(options);
6067

6168
bind(JpaPersistService.class).in(Singleton.class);
6269

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2023 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.inject.persist.jpa;
18+
19+
/** Options that configure how the JPA persist service will work. */
20+
public final class JpaPersistOptions {
21+
22+
private final boolean autoBeginWorkOnEntityManagerCreation;
23+
24+
private JpaPersistOptions(JpaPersistOptions.Builder builder) {
25+
this.autoBeginWorkOnEntityManagerCreation = builder.autoBeginWorkOnEntityManagerCreation;
26+
}
27+
28+
/**
29+
* Returns true if the work unit should automatically begin when the EntityManager is created, if
30+
* it hasn't already begun.
31+
*
32+
* <p>This defaults to <b>false</b> because it's not safe, as careless usage can lead to leaking
33+
* sessions.
34+
*/
35+
public boolean getAutoBeginWorkOnEntityManagerCreation() {
36+
return autoBeginWorkOnEntityManagerCreation;
37+
}
38+
39+
/** Returns a builder to set options. */
40+
public static Builder builder() {
41+
return new Builder();
42+
}
43+
44+
/** A builder to create the options. */
45+
public static final class Builder {
46+
private boolean autoBeginWorkOnEntityManagerCreation;
47+
48+
private Builder() {}
49+
50+
public JpaPersistOptions build() {
51+
return new JpaPersistOptions(this);
52+
}
53+
54+
/** Sets the {@link JpaPersistOptions#getAutoBeginWorkOnEntityManagerCreation} property. */
55+
public Builder setAutoBeginWorkOnEntityManagerCreation(
56+
boolean autoBeginWorkOnEntityManagerCreation) {
57+
this.autoBeginWorkOnEntityManagerCreation = autoBeginWorkOnEntityManagerCreation;
58+
return this;
59+
}
60+
}
61+
}

extensions/persist/src/com/google/inject/persist/jpa/JpaPersistService.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,30 @@
3333
import javax.persistence.EntityManagerFactory;
3434
import javax.persistence.Persistence;
3535

36-
/** @author Dhanji R. Prasanna ([email protected]) */
36+
/**
37+
* @author Dhanji R. Prasanna ([email protected])
38+
*/
3739
@Singleton
3840
class JpaPersistService implements Provider<EntityManager>, UnitOfWork, PersistService {
3941
private final ThreadLocal<EntityManager> entityManager = new ThreadLocal<>();
4042

4143
private final String persistenceUnitName;
4244
private final Map<?, ?> persistenceProperties;
45+
private final JpaPersistOptions options;
4346

4447
@Inject
4548
public JpaPersistService(
46-
@Jpa String persistenceUnitName, @Nullable @Jpa Map<?, ?> persistenceProperties) {
49+
@Jpa JpaPersistOptions options,
50+
@Jpa String persistenceUnitName,
51+
@Nullable @Jpa Map<?, ?> persistenceProperties) {
52+
this.options = options;
4753
this.persistenceUnitName = persistenceUnitName;
4854
this.persistenceProperties = persistenceProperties;
4955
}
5056

5157
@Override
5258
public EntityManager get() {
53-
if (!isWorking()) {
59+
if (options.getAutoBeginWorkOnEntityManagerCreation() && !isWorking()) {
5460
begin();
5561
}
5662

extensions/persist/test/com/google/inject/persist/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ java_library(
2727
"//third_party/java/hibernate:hibernate5",
2828
"//third_party/java/hsqldb:hsqldb2",
2929
"//third_party/java/javax_persistence",
30+
"//third_party/java/jsr330_inject",
3031
"//third_party/java/junit",
3132
"//third_party/java/mockito",
3233
],

extensions/persist/test/com/google/inject/persist/jpa/ClassLevelManagedLocalTransactionsTest.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import com.google.inject.Injector;
2222
import com.google.inject.persist.PersistService;
2323
import com.google.inject.persist.Transactional;
24+
import com.google.inject.persist.UnitOfWork;
2425
import java.io.FileNotFoundException;
2526
import java.io.IOException;
2627
import java.util.Date;
2728
import java.util.List;
29+
import javax.inject.Provider;
2830
import javax.persistence.EntityManager;
2931
import javax.persistence.EntityManagerFactory;
3032
import junit.framework.TestCase;
@@ -62,6 +64,7 @@ public void tearDown() {
6264
public void testSimpleTransaction() {
6365
injector.getInstance(TransactionalObject.class).runOperationInTxn();
6466

67+
injector.getInstance(UnitOfWork.class).begin();
6568
EntityManager session = injector.getInstance(EntityManager.class);
6669
assertFalse(
6770
"EntityManager was not closed by transactional service",
@@ -92,6 +95,7 @@ public void testSimpleTransactionRollbackOnChecked() {
9295
// ignore
9396
}
9497

98+
injector.getInstance(UnitOfWork.class).begin();
9599
EntityManager session = injector.getInstance(EntityManager.class);
96100
assertFalse(
97101
"EntityManager was not closed by transactional service (rollback didnt happen?)",
@@ -119,6 +123,7 @@ public void testSimpleTransactionRollbackOnCheckedExcepting() {
119123
// ignored
120124
}
121125

126+
injector.getInstance(UnitOfWork.class).begin();
122127
EntityManager session = injector.getInstance(EntityManager.class);
123128
assertFalse(
124129
"Txn was not closed by transactional service (commit didnt happen?)",
@@ -144,6 +149,7 @@ public void testSimpleTransactionRollbackOnUnchecked() {
144149
// ignore
145150
}
146151

152+
injector.getInstance(UnitOfWork.class).begin();
147153
EntityManager session = injector.getInstance(EntityManager.class);
148154
assertFalse(
149155
"EntityManager was not closed by transactional service (rollback didnt happen?)",
@@ -184,9 +190,10 @@ public void testTransactionalDoesntAffectObjectMethods() {
184190

185191
@Transactional
186192
public static class TransactionalObject {
187-
@Inject EntityManager session;
193+
@Inject Provider<EntityManager> sessionProvider;
188194

189195
public void runOperationInTxn() {
196+
EntityManager session = sessionProvider.get();
190197
assertTrue(session.getTransaction().isActive());
191198
JpaTestEntity entity = new JpaTestEntity();
192199
entity.setText(UNIQUE_TEXT);
@@ -196,10 +203,11 @@ public void runOperationInTxn() {
196203

197204
@Transactional
198205
public static class TransactionalObject4 {
199-
@Inject EntityManager session;
206+
@Inject Provider<EntityManager> sessionProvider;
200207

201208
@Transactional
202209
public void runOperationInTxnThrowingUnchecked() {
210+
EntityManager session = sessionProvider.get();
203211
assertTrue(session.getTransaction().isActive());
204212
JpaTestEntity entity = new JpaTestEntity();
205213
entity.setText(TRANSIENT_UNIQUE_TEXT);
@@ -211,9 +219,10 @@ public void runOperationInTxnThrowingUnchecked() {
211219

212220
@Transactional(rollbackOn = IOException.class, ignore = FileNotFoundException.class)
213221
public static class TransactionalObject3 {
214-
@Inject EntityManager session;
222+
@Inject Provider<EntityManager> sessionProvider;
215223

216224
public void runOperationInTxnThrowingCheckedExcepting() throws IOException {
225+
EntityManager session = sessionProvider.get();
217226
assertTrue(session.getTransaction().isActive());
218227
JpaTestEntity entity = new JpaTestEntity();
219228
entity.setText(UNIQUE_TEXT_2);
@@ -225,9 +234,10 @@ public void runOperationInTxnThrowingCheckedExcepting() throws IOException {
225234

226235
@Transactional(rollbackOn = IOException.class)
227236
public static class TransactionalObject2 {
228-
@Inject EntityManager session;
237+
@Inject Provider<EntityManager> sessionProvider;
229238

230239
public void runOperationInTxnThrowingChecked() throws IOException {
240+
EntityManager session = sessionProvider.get();
231241
assertTrue(session.getTransaction().isActive());
232242
JpaTestEntity entity = new JpaTestEntity();
233243
entity.setText(TRANSIENT_UNIQUE_TEXT);

extensions/persist/test/com/google/inject/persist/jpa/CustomPropsEntityManagerFactoryProvisionTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public void testSessionCreateOnInjection() {
5454
//startup persistence
5555
injector.getInstance(PersistService.class).start();
5656

57+
injector.getInstance(UnitOfWork.class).begin();
58+
5759
//obtain em
5860
assertTrue(injector.getInstance(EntityManager.class).isOpen());
5961
}

extensions/persist/test/com/google/inject/persist/jpa/DynamicFinderTest.java

+2-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.inject.Provider;
2323
import com.google.inject.persist.PersistService;
2424
import com.google.inject.persist.Transactional;
25+
import com.google.inject.persist.UnitOfWork;
2526
import com.google.inject.persist.finder.Finder;
2627
import java.util.ArrayList;
2728
import java.util.Date;
@@ -62,11 +63,7 @@ public void testDynamicFinderListAll() {
6263

6364
dao.persist(te);
6465

65-
//im not sure this hack works...
66-
assertFalse(
67-
"Duplicate entity managers crossing-scope",
68-
dao.lastEm.equals(injector.getInstance(EntityManager.class)));
69-
66+
injector.getInstance(UnitOfWork.class).begin();
7067
List<JpaTestEntity> list = injector.getInstance(JpaFinder.class).listAll();
7168
assertNotNull(list);
7269
assertFalse(list.isEmpty());

extensions/persist/test/com/google/inject/persist/jpa/EntityManagerFactoryProvisionTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public void testSessionCreateOnInjection() {
5050
//startup persistence
5151
injector.getInstance(PersistService.class).start();
5252

53+
injector.getInstance(UnitOfWork.class).begin();
54+
5355
//obtain em
5456
assertTrue(injector.getInstance(EntityManager.class).isOpen());
5557
}

extensions/persist/test/com/google/inject/persist/jpa/EntityManagerProvisionTest.java

-10
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ public void testEntityManagerLifecyclePerTxn() {
5757

5858
dao.persist(te);
5959

60-
//im not sure this hack works...
61-
assertFalse(
62-
"Duplicate entity managers crossing-scope",
63-
dao.lastEm.equals(injector.getInstance(EntityManager.class)));
64-
6560
//try to start a new em in a new txn
6661
dao = injector.getInstance(JpaDao.class);
6762

@@ -80,11 +75,6 @@ public void testEntityManagerLifecyclePerTxn2() {
8075

8176
dao.persist(te);
8277

83-
//im not sure this hack works...
84-
assertFalse(
85-
"Duplicate entity managers crossing-scope",
86-
dao.lastEm.equals(injector.getInstance(EntityManager.class)));
87-
8878
//try to start a new em in a new txn
8979
dao = injector.getInstance(JpaDao.class);
9080

extensions/persist/test/com/google/inject/persist/jpa/JoiningLocalTransactionsTest.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.inject.persist.UnitOfWork;
2525
import java.io.IOException;
2626
import java.util.Date;
27+
import javax.inject.Provider;
2728
import javax.persistence.EntityManager;
2829
import javax.persistence.EntityManagerFactory;
2930
import javax.persistence.NoResultException;
@@ -58,6 +59,7 @@ public void testSimpleTransaction() {
5859
.getInstance(JoiningLocalTransactionsTest.TransactionalObject.class)
5960
.runOperationInTxn();
6061

62+
injector.getInstance(UnitOfWork.class).begin();
6163
EntityManager em = injector.getInstance(EntityManager.class);
6264
assertFalse("txn was not closed by transactional service", em.getTransaction().isActive());
6365

@@ -86,6 +88,7 @@ public void testSimpleTransactionRollbackOnChecked() {
8688
injector.getInstance(UnitOfWork.class).end();
8789
}
8890

91+
injector.getInstance(UnitOfWork.class).begin();
8992
EntityManager em = injector.getInstance(EntityManager.class);
9093

9194
assertFalse(
@@ -114,6 +117,7 @@ public void testSimpleTransactionRollbackOnUnchecked() {
114117
injector.getInstance(UnitOfWork.class).end();
115118
}
116119

120+
injector.getInstance(UnitOfWork.class).begin();
117121
EntityManager em = injector.getInstance(EntityManager.class);
118122
assertFalse(
119123
"Session was not closed by transactional service (rollback didnt happen?)",
@@ -131,11 +135,11 @@ public void testSimpleTransactionRollbackOnUnchecked() {
131135
}
132136

133137
public static class TransactionalObject {
134-
private final EntityManager em;
138+
private final Provider<EntityManager> emProvider;
135139

136140
@Inject
137-
public TransactionalObject(EntityManager em) {
138-
this.em = em;
141+
public TransactionalObject(Provider<EntityManager> emProvider) {
142+
this.emProvider = emProvider;
139143
}
140144

141145
@Transactional
@@ -145,6 +149,7 @@ public void runOperationInTxn() {
145149

146150
@Transactional(rollbackOn = IOException.class)
147151
public void runOperationInTxnInternal() {
152+
EntityManager em = emProvider.get();
148153
JpaTestEntity entity = new JpaTestEntity();
149154
entity.setText(UNIQUE_TEXT);
150155
em.persist(entity);
@@ -159,7 +164,7 @@ public void runOperationInTxnThrowingChecked() throws IOException {
159164
private void runOperationInTxnThrowingCheckedInternal() throws IOException {
160165
JpaTestEntity entity = new JpaTestEntity();
161166
entity.setText(TRANSIENT_UNIQUE_TEXT);
162-
em.persist(entity);
167+
emProvider.get().persist(entity);
163168

164169
throw new IOException();
165170
}
@@ -173,7 +178,7 @@ public void runOperationInTxnThrowingUnchecked() {
173178
public void runOperationInTxnThrowingUncheckedInternal() {
174179
JpaTestEntity entity = new JpaTestEntity();
175180
entity.setText(TRANSIENT_UNIQUE_TEXT);
176-
em.persist(entity);
181+
emProvider.get().persist(entity);
177182

178183
throw new IllegalStateException();
179184
}

extensions/persist/test/com/google/inject/persist/jpa/JpaPersistServiceTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public class JpaPersistServiceTest extends TestCase {
3434
private static final Properties PERSISTENCE_PROPERTIES = new Properties();
3535

3636
private final JpaPersistService sut =
37-
new JpaPersistService(PERSISTENCE_UNIT_NAME, PERSISTENCE_PROPERTIES);
37+
new JpaPersistService(
38+
JpaPersistOptions.builder().build(), PERSISTENCE_UNIT_NAME, PERSISTENCE_PROPERTIES);
3839
private final PersistenceProvider provider = mock(PersistenceProvider.class);
3940
private final EntityManagerFactory factory = mock(EntityManagerFactory.class);
4041
private final EntityManager entityManager = mock(EntityManager.class);

0 commit comments

Comments
 (0)