From eea7f20177241e8de0ce08621eb4ae8778b3eb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Fri, 16 Jun 2023 13:30:23 +0200 Subject: [PATCH] EntityManagerFactory.withTransaction() as default implementation. (#1884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus --- .../jpa.test.persistence32/pom.xml | 86 ++++++++ .../Persistence32TableCreator.java | 51 +++++ .../models/jpa/persistence32/Pokemon.java | 128 ++++++++++++ .../models/jpa/persistence32/Type.java | 81 ++++++++ .../main/resources/META-INF/persistence.xml | 35 ++++ .../EntityManagerFactoryTest.java | 183 ++++++++++++++++++ jpa/eclipselink.jpa.testapps/pom.xml | 1 + .../jpa/JpaEntityManagerFactory.java | 125 +++++++++++- 8 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/pom.xml create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Pokemon.java create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Type.java create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml create mode 100644 jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/pom.xml b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/pom.xml new file mode 100644 index 00000000000..a7617914a93 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/pom.xml @@ -0,0 +1,86 @@ + + + + + + org.eclipse.persistence.jpa.testapps + org.eclipse.persistence + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + org.eclipse.persistence.jpa.testapps.persistence32 + + Test - Jakarta Persistence 3.2 + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + get-test-classpath-to-properties + process-test-classes + + + + + org.carlspring.maven + derby-maven-plugin + + + start-derby + process-test-classes + + + stop-derby + prepare-package + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + -javaagent:${org.eclipse.persistence:org.eclipse.persistence.jpa:jar} @{argLine} + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + package-model + package + + + + + + + diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java new file mode 100644 index 00000000000..8b1d2f6387b --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.persistence.testing.models.jpa.persistence32; + +import org.eclipse.persistence.testing.framework.TogglingFastTableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; + +public class Persistence32TableCreator extends TogglingFastTableCreator { + + public Persistence32TableCreator() { + setName("Persistence32Project"); + addTableDefinition(buildTypeTable()); + addTableDefinition(buildPokemonTable()); + addTableDefinition(buildPokemonTypeTable()); + } + + public static TableDefinition buildTypeTable() { + TableDefinition table = new TableDefinition(); + table.setName("PERSISTENCE32_TYPE"); + table.addField(createNumericPk("ID")); + table.addField(createStringColumn("NAME", 64, false)); + return table; + } + + public static TableDefinition buildPokemonTable() { + TableDefinition table = new TableDefinition(); + table.setName("PERSISTENCE32_POKEMON"); + table.addField(createNumericPk("ID")); + table.addField(createStringColumn("NAME", 64, false)); + return table; + } + + public static TableDefinition buildPokemonTypeTable() { + TableDefinition table = new TableDefinition(); + table.setName("PERSISTENCE32_POKEMON_TYPE"); + table.addField(createNumericFk("POKEMON_ID", "PERSISTENCE32_POKEMON.ID")); + table.addField(createNumericFk("TYPE_ID", "PERSISTENCE32_TYPE.ID")); + return table; + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Pokemon.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Pokemon.java new file mode 100644 index 00000000000..c34d84a33d8 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Pokemon.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.persistence.testing.models.jpa.persistence32; + +import java.util.Collection; +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +@Entity +@Table(name="PERSISTENCE32_POKEMON") +@NamedQuery(name="Pokemon.get", query="SELECT p FROM Pokemon p WHERE p.id = :id") +public class Pokemon { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + private String name; + + @ManyToMany + @JoinTable(name = "PERSISTENCE32_POKEMON_TYPE", + joinColumns = @JoinColumn( + name = "POKEMON_ID", + referencedColumnName = "ID" + ), + inverseJoinColumns = @JoinColumn( + name = "TYPE_ID", + referencedColumnName = "ID" + )) + private Collection types; + + public Pokemon() { + } + + public Pokemon(String name, Collection types) { + this.name = name; + this.types = types; + } + + public Pokemon(int id, String name, Collection types) { + this(name, types); + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Collection getTypes() { + return types; + } + + public void setTypes(Collection types) { + this.types = types; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return id == ((Pokemon) obj).id + && Objects.equals(name, ((Pokemon) obj).name) + && Objects.deepEquals(types, ((Pokemon) obj).types); + } + + @Override + public int hashCode() { + int result = Objects.hash(id, name); + for (Type type : types) { + result = 31 * result + type.hashCode(); + } + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Pokemon {id="); + sb.append(id); + sb.append(", name="); + sb.append(name); + sb.append(", types=["); + boolean first = true; + for (Type type : types) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(type.toString()); + } + sb.append("]}"); + return sb.toString(); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Type.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Type.java new file mode 100644 index 00000000000..ce11207e001 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Type.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.persistence.testing.models.jpa.persistence32; + +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +@Entity +@Table(name="PERSISTENCE32_TYPE") +@NamedQuery(name="Type.all", query="SELECT t FROM Type t") +public class Type { + + @Id + private int id; + + private String name; + + public Type() { + } + + public Type(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return id == ((Type) obj).id + && Objects.equals(name, ((Type) obj).name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Type {id="); + sb.append(id); + sb.append(", name="); + sb.append(name); + sb.append("}"); + return sb.toString(); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..bb963cf1140 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,35 @@ + + + + + + + org.eclipse.persistence.jpa.PersistenceProvider + org.eclipse.persistence.testing.models.jpa.persistence32.Pokemon + org.eclipse.persistence.testing.models.jpa.persistence32.Type + true + + + + + + + + + diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java new file mode 100644 index 00000000000..89095c53eb7 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import junit.framework.Test; +import junit.framework.TestSuite; +import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; +import org.eclipse.persistence.jpa.JpaEntityManagerFactory; +import org.eclipse.persistence.testing.framework.jpa.junit.JUnitTestCase; +import org.eclipse.persistence.testing.models.jpa.persistence32.Persistence32TableCreator; +import org.eclipse.persistence.testing.models.jpa.persistence32.Pokemon; +import org.eclipse.persistence.testing.models.jpa.persistence32.Type; + +public class EntityManagerFactoryTest extends JUnitTestCase { + + // Pokemon types. Array index is ID value. Value of ID = 0 does not exist, + // so it's array instance is set to null. + private static final Type[] TYPES = new Type[] { + null, + new Type( 1, "Normal"), + new Type( 2, "Fighting"), + new Type( 3, "Flying"), + new Type( 4, "Poison"), + new Type( 5, "Ground"), + new Type( 6, "Rock"), + new Type( 7, "Bug"), + new Type( 8, "Ghost"), + new Type( 9, "Steel"), + new Type(10, "Fire"), + new Type(11, "Water"), + new Type(12, "Grass"), + new Type(13, "Electric"), + new Type(14, "Psychic"), + new Type(15, "Ice"), + new Type(16, "Dragon"), + new Type(17, "Dark"), + new Type(18, "Fairy") + } ; + + private JpaEntityManagerFactory emf = null; + + public static Test suite() { + TestSuite suite = new TestSuite(); + suite.setName("EntityManagerFactoryTest"); + suite.addTest(new EntityManagerFactoryTest("testSetup")); + suite.addTest(new EntityManagerFactoryTest("testWithApplicationManagedTransaction")); + suite.addTest(new EntityManagerFactoryTest("testWithApplicationManagedTransactionVoid")); + suite.addTest(new EntityManagerFactoryTest("testWithUserManagedTransaction")); + suite.addTest(new EntityManagerFactoryTest("testWithUserManagedTransactionVoid")); + return suite; + } + + public EntityManagerFactoryTest() {} + + public EntityManagerFactoryTest(String name) { + super(name); + setPuName(getPersistenceUnitName()); + } + + + @Override + public String getPersistenceUnitName() { + return "persistence32"; + } + + @Override + public void setUp () { + super.setUp(); + emf = getEntityManagerFactory(getPersistenceUnitName()).unwrap(EntityManagerFactoryImpl.class); + } + + @Override + public void tearDown () { + super.tearDown(); + } + + /** + * The setup is done as a test, both to record its failure, and to allow + * execution in the server. + */ + public void testSetup() { + new Persistence32TableCreator().replaceTables(JUnitTestCase.getServerSession(getPersistenceUnitName())); + clearCache(); + // emf = getEntityManagerFactory(getPersistenceUnitName()); + try (EntityManager em = emf.createEntityManager()) { + EntityTransaction et = em.getTransaction(); + try { + et.begin(); + + for (int i = 1; i < TYPES.length; i++) { + em.persist(TYPES[i]); + } + et.commit(); + } catch (Exception e) { + et.rollback(); + } + } + } + + public void testWithApplicationManagedTransaction() { + Pokemon pokemon = emf.withTransaction((em -> { + Map types = new HashMap<>(24); + em.createNamedQuery("Type.all", Type.class) + .getResultList() + .forEach(type -> types.put(type.getId(), type)); + Pokemon newPokemon = new Pokemon("Pidgey", List.of(types.get(1), types.get(3))); + em.persist(newPokemon); + return newPokemon; + })); + verifyObjectInEntityManager(pokemon, getPersistenceUnitName()); + } + + public void testWithApplicationManagedTransactionVoid() { + Pokemon[] pokemon = new Pokemon[1]; + emf.withTransaction((em -> { + Map types = new HashMap<>(24); + em.createNamedQuery("Type.all", Type.class) + .getResultList() + .forEach(type -> types.put(type.getId(), type)); + Pokemon newPokemon = new Pokemon("Beedrill", List.of(types.get(7), types.get(4))); + em.persist(newPokemon); + pokemon[0] = newPokemon; + })); + verifyObjectInEntityManager(pokemon[0], getPersistenceUnitName()); + } + + public void testWithUserManagedTransaction() { + Pokemon pokemon = emf.withTransaction(((em, et) -> { + try { + Map types = new HashMap<>(24); + em.createNamedQuery("Type.all", Type.class) + .getResultList() + .forEach(type -> types.put(type.getId(), type)); + Pokemon newPokemon = new Pokemon("Caterpie", List.of(types.get(7))); + em.persist(newPokemon); + et.commit(); + return newPokemon; + } catch (Exception e) { + et.rollback(); + throw e; + } + })); + verifyObjectInEntityManager(pokemon, getPersistenceUnitName()); + } + + + public void testWithUserManagedTransactionVoid() { + Pokemon[] pokemon = new Pokemon[1]; + emf.withTransaction(((em, et) -> { + try { + Map types = new HashMap<>(24); + em.createNamedQuery("Type.all", Type.class) + .getResultList() + .forEach(type -> types.put(type.getId(), type)); + Pokemon newPokemon = new Pokemon("Squirtle", List.of(types.get(11))); + em.persist(newPokemon); + et.commit(); + pokemon[0] = newPokemon; + } catch (Exception e) { + et.rollback(); + throw e; + } + })); + verifyObjectInEntityManager(pokemon[0], getPersistenceUnitName()); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/pom.xml b/jpa/eclipselink.jpa.testapps/pom.xml index e339e9c83ff..4067f1b4cdd 100644 --- a/jpa/eclipselink.jpa.testapps/pom.xml +++ b/jpa/eclipselink.jpa.testapps/pom.xml @@ -324,6 +324,7 @@ jpa.test.partitioned jpa.test.performance jpa.test.performance2 + jpa.test.persistence32 jpa.test.privateowned jpa.test.pu with spaces jpa.test.relationships diff --git a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/jpa/JpaEntityManagerFactory.java b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/jpa/JpaEntityManagerFactory.java index 4969912a4f1..f27d4c97261 100644 --- a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/jpa/JpaEntityManagerFactory.java +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/jpa/JpaEntityManagerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -16,9 +16,14 @@ // - 494610: Session Properties map should be Map package org.eclipse.persistence.jpa; +import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.PersistenceException; import org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate; import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl; import org.eclipse.persistence.sessions.broker.SessionBroker; @@ -68,5 +73,123 @@ public interface JpaEntityManagerFactory extends EntityManagerFactory, AutoClose */ void refreshMetadata(Map properties); + /** + * Create a new application-managed {@code EntityManager}, start a resource-local + * transaction, and call the given function, passing both the {@code EntityManager} + * and the {@code EntityTransaction}. + * The given function is responsible for finishing the transaction by calling {@code commit} + * or {@code rollback} method. + * + * @param work a function to be called in the scope of the transaction + */ + default void withTransaction(BiConsumer work) { + try (EntityManager em = this.createEntityManager()) { + EntityTransaction t = em.getTransaction(); + t.begin(); + work.accept(em, t); + } + } + + /** + * Create a new application-managed {@code EntityManager}, start a resource-local + * transaction, and call the given function, passing both the {@code EntityManager} + * and the {@code EntityTransaction}. + * The given function is responsible for finishing the transaction by calling {@code commit} + * or {@code rollback} method. + * + * @param work a function to be called in the scope of the transaction + * @return the value returned by the given function + */ + default R withTransaction(BiFunction work) { + try (EntityManager em = this.createEntityManager()) { + EntityTransaction t = em.getTransaction(); + t.begin(); + return work.apply(em, t); + } + } + + /** + * Create a new application-managed {@code EntityManager}, start a resource-local + * transaction, and call the given function, passing the {@code EntityManager}. + * If the given function does not throw an exception, commit the transaction and + * return the result of the function. If the function does throw an exception, + * roll back the transaction and rethrow the exception. Finally, close the + * {@code EntityManager}. + * + * @param work a function to be called in the scope of the transaction + */ + default void withTransaction(TransactionVoidWork work) { + try (EntityManager em = this.createEntityManager()) { + EntityTransaction t = em.getTransaction(); + try { + t.begin(); + work.work(em); + t.commit(); + } catch (Exception e) { + t.rollback(); + throw new PersistenceException("Application-managed transaction failed", e); + } + } + } + + /** + * Create a new application-managed {@code EntityManager}, start a resource-local + * transaction, and call the given function, passing the {@code EntityManager}. + * If the given function does not throw an exception, commit the transaction and + * return the result of the function. If the function does throw an exception, + * roll back the transaction and rethrow the exception. Finally, close the + * {@code EntityManager}. + * + * @param work a function to be called in the scope of the transaction + * @return the value returned by the given function + */ + default R withTransaction(TransactionWork work) { + try (EntityManager em = this.createEntityManager()) { + EntityTransaction t = em.getTransaction(); + try { + t.begin(); + R result = work.work(em); + t.commit(); + return result; + } catch (Exception e) { + t.rollback(); + throw new PersistenceException("Application-managed transaction failed", e); + } + } + } + + /** + * A task that runs in a transaction, returns no result and may throw an exception. + * Implementors define a single method with {@link EntityManager} argument called {@code work}. + */ + @FunctionalInterface + interface TransactionVoidWork { + /** + * Executes the function. Throws an exception if unable to do so. + * + * @param em the application-managed {@link EntityManager} instance + * @throws Exception when unable to compute a result + */ + void work(EntityManager em) throws Exception; + } + + /** + * A task that runs in a transaction, returns result and may throw an exception. + * Implementors define a single method with {@link EntityManager} argument called {@code work}. + * + * @param the result type of method {@code work} + */ + @FunctionalInterface + interface TransactionWork { + /** + * Executes the function. Throws an exception if unable to do so. + * + * @param em the application-managed {@link EntityManager} instance + * @return the computed result + * @throws Exception when unable to compute a result + */ + R work(EntityManager em) throws Exception; + } + }