Skip to content

Commit

Permalink
jakartaee/persistence#431 - FindOption/LockOption/RefreshOption suppo…
Browse files Browse the repository at this point in the history
…rt in EntityManager

Signed-off-by: Tomáš Kraus <tomas.kraus@oracle.com>
  • Loading branch information
Tomas-Kraus authored and lukasj committed Dec 20, 2023
1 parent 05df5c0 commit e9e8ae1
Show file tree
Hide file tree
Showing 14 changed files with 661 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,10 @@ public class ExceptionLocalizationResource extends ListResourceBundle {
{ "schema_validation_missing_table", "The {0} table vas not found in the schema"},
{ "schema_validation_table_surplus_columns", "The {0} table has surplus columns in the schema"},
{ "schema_validation_table_missing_columns", "The {0} table has missing columns in the schema"},
{ "schema_validation_table_different_columns", "The {0} table has different columns in the schema"}
{ "schema_validation_table_different_columns", "The {0} table has different columns in the schema"},
{ "find_option_class_unknown", "The FindOption implementing class {0} is not supported"},
{ "refresh_option_class_unknown", "The RefreshOption implementing class {0} is not supported"},
{ "lock_option_class_unknown", "The LockOption implementing class {0} is not supported"}
};
/**
* Return the lookup table.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ public class MySQLPlatform extends DatabasePlatform {
/** Support fractional seconds in time values since MySQL v. 5.6.4. */
private boolean isFractionalTimeSupported;
private boolean isConnectionDataInitialized;
private boolean supportsForUpdateNoWait;

public MySQLPlatform(){
super();
this.pingSQL = "SELECT 1";
this.startDelimiter = "`";
this.endDelimiter = "`";
this.supportsReturnGeneratedKeys = true;
this.supportsForUpdateNoWait = false;
}

@Override
Expand All @@ -106,13 +108,26 @@ public void initializeConnectionData(Connection connection) throws SQLException
this.isFractionalTimeSupported = Helper.compareVersions(databaseVersion, "5.6.4") >= 0;
// Driver 5.1 supports NVARCHAR
this.driverSupportsNationalCharacterVarying = Helper.compareVersions(dmd.getDriverVersion(), "5.1.0") >= 0;
// Database supports NOWAIT since 8.0.1
this.supportsForUpdateNoWait = Helper.compareVersions(databaseVersion, "8.0.1") >= 0;
this.isConnectionDataInitialized = true;
}

public boolean isFractionalTimeSupported() {
return isFractionalTimeSupported;
}

/**
* Whether locking read concurrency with {@code NOWAIT} is supported.
* Requires MySQL 8.0.1 or later.
*
* @return value of {@code true} when locking read concurrency with {@code NOWAIT}
* is supported or {@code false} otherwise
*/
public boolean supportsForUpdateNoWait() {
return supportsForUpdateNoWait;
}

/**
* Appends an MySQL specific date if usesNativeSQL is true otherwise use the ODBC format.
* Native FORMAT: 'YYYY-MM-DD'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
// Markus KARG - Added methods allowing to support stored procedure creation on SQLAnywherePlatform.
// 12/14/2023: Tomas Kraus
// - New Jakarta Persistence 3.2 Features
package org.eclipse.persistence.tools.schemaframework;

import org.eclipse.persistence.exceptions.ValidationException;
Expand All @@ -23,14 +25,16 @@

import java.io.IOException;
import java.io.Writer;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

/**
* <b>Purpose</b>: Allow a semi-generic way of creating stored procedures.
*/
public class StoredProcedureDefinition extends DatabaseObjectDefinition {
protected List<FieldDefinition> variables;
// Function/procedure characteristic, e.g. DETERMINISTIC, NO SQL, ...
protected List<String> characteristics;
protected List<String> statements;
protected List<FieldDefinition> arguments;
protected List<Integer> argumentTypes;
Expand All @@ -39,10 +43,11 @@ public class StoredProcedureDefinition extends DatabaseObjectDefinition {
protected static final Integer INOUT = 3;

public StoredProcedureDefinition() {
this.statements = new Vector<>();
this.variables = new Vector<>();
this.arguments = new Vector<>();
this.argumentTypes = new Vector<>();
this.statements = new LinkedList<>();
this.variables = new LinkedList<>();
this.characteristics = new LinkedList<>();
this.arguments = new LinkedList<>();
this.argumentTypes = new LinkedList<>();
}

/**
Expand Down Expand Up @@ -125,6 +130,15 @@ public void addStatement(String statement) {
getStatements().add(statement);
}

/**
* Add characteristic into the characteristics section of function/procedure.
*
* @param characteristic the function/procedure characteristic
*/
public void addCharacteristic(String characteristic) {
getCharacteristics().add(characteristic);
}

/**
* The variables are the names of the declared variables used in the procedure.
*/
Expand Down Expand Up @@ -171,6 +185,13 @@ public Writer buildCreationWriter(AbstractSession session, Writer writer) throws
}

printReturn(writer, session);

// Function characteristics, e.g. DETERMINISTIC, NO SQL, ...
for (String characteristic : getCharacteristics()) {
writer.write(characteristic);
writer.write("\n");
}

writer.write(platform.getProcedureAsString());
writer.write("\n");

Expand Down Expand Up @@ -267,6 +288,14 @@ public List<String> getStatements() {
return statements;
}

/**
* The characteristic section in procedure.
* E.g. {@code COMMENT <string>}, {@code DETERMINISTIC}, {@code MODIFIES SQL DATA}, {@code NO SQL}, ... .
*/
public List<String> getCharacteristics() {
return characteristics;
}

/**
* The variables are the names of the declared variables used in the procedure.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@
import java.util.Set;
import java.util.Vector;


/**
* This test suite tests EclipseLink JPA annotations extensions.
*/
Expand Down Expand Up @@ -327,6 +326,10 @@ public StoredFunctionDefinition buildStoredFunction() {
func.setName("StoredFunction_In");
func.addArgument("P_IN", Long.class);
func.setReturnType(Long.class);
// Works for Oracle and MySQL, MS SQL should use SCHEMABINDING keyword.
if (getPlatform().isOracle() || getPlatform().isMySQL()) {
func.addCharacteristic("DETERMINISTIC");
}
func.addStatement("RETURN P_IN * 1000");
return func;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ public StoredFunctionDefinition buildStoredFunction() {
func.setName("StoredFunction_In");
func.addArgument("P_IN", Long.class);
func.setReturnType(Long.class);
// Works for Oracle and MySQL, MS SQL should use SCHEMABINDING keyword.
if (getPlatform().isOracle() || getPlatform().isMySQL()) {
func.addCharacteristic("DETERMINISTIC");
}
func.addStatement("RETURN P_IN * 1000");
return func;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ public void testCreateConflictingCustomEntityManagerFactory() {
fail("Persistence.createEntityManagerFactory(PersistenceConfiguration) with already existing PU name shall throw PersistenceException");
} catch (PersistenceException pe) {
assertTrue(
"Unexpected exception message: " + pe.getLocalizedMessage(),
pe.getLocalizedMessage().contains("Cannot create custom persistence unit with name"));
"Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("Cannot create custom persistence unit with name"));
assertTrue(
"Unexpected exception message: " + pe.getLocalizedMessage(),
pe.getLocalizedMessage().contains("This name was found in xml configuration."));
"Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("This name was found in xml configuration."));
}
}

Expand All @@ -224,11 +224,11 @@ public void testCreateConflictingConfiguredEntityManagerFactory() {
fail("Persistence.createEntityManagerFactory(String, Map) with already existing PU name shall throw PersistenceException");
} catch (PersistenceException pe) {
assertTrue(
"Unexpected exception message: " + pe.getLocalizedMessage(),
pe.getLocalizedMessage().contains("Cannot create configured persistence unit with name"));
"Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("Cannot create configured persistence unit with name"));
assertTrue(
"Unexpected exception message: " + pe.getLocalizedMessage(),
pe.getLocalizedMessage().contains("This name was found in custom persistence units."));
"Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("This name was found in custom persistence units."));
}
}

Expand Down Expand Up @@ -365,7 +365,7 @@ public void testGetVersionOnEntityWithoutVersion() {
fail("PersistenceUnitUtil#getVersion(Entity) on entity without version shall throw IllegalArgumentException");
} catch (IllegalArgumentException iae) {
assertTrue(
"Unexpected exception message: " + iae.getLocalizedMessage(),
"Unexpected exception message: " + iae.getMessage(),
iae.getMessage().contains("which has no version attribute"));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
package org.eclipse.persistence.testing.tests.jpa.persistence32;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.LockModeType;
import jakarta.persistence.LockOption;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.Timeout;
import junit.framework.Test;
import org.eclipse.persistence.internal.descriptors.PersistenceEntity;
import org.eclipse.persistence.testing.models.jpa.persistence32.Pokemon;
Expand All @@ -30,13 +35,18 @@ public class EntityManagerTest extends AbstractPokemon {
static final Pokemon[] POKEMONS = new Pokemon[] {
null, // Skip array index 0
new Pokemon(1, "Pidgey", List.of(TYPES[1], TYPES[3])),
null, // Array index 2 is reserved for testGetReferenceForNotExistingEntity
new Pokemon(3, "Squirtle", List.of(TYPES[11])),
new Pokemon(4, "Caterpie", List.of(TYPES[7]))
};

public static Test suite() {
return suite(
"EntityManagerTest",
new EntityManagerTest("testGetReferenceForExistingEntity"),
new EntityManagerTest("testGetReferenceForNotExistingEntity")
new EntityManagerTest("testGetReferenceForNotExistingEntity"),
new EntityManagerTest("testLockOptionUtilsUnknownClass"),
new EntityManagerTest("testLockPessimisticWriteWithTimeout")
);
}

Expand All @@ -54,7 +64,9 @@ protected void suiteSetUp() {
super.suiteSetUp();
emf.runInTransaction(em -> {
for (int i = 1; i < POKEMONS.length; i++) {
em.persist(POKEMONS[i]);
if (POKEMONS[i] != null) {
em.persist(POKEMONS[i]);
}
}
});
}
Expand All @@ -68,6 +80,7 @@ public void testGetReferenceForExistingEntity() {
Pokemon pokemon = new Pokemon(1, "Pidgey", List.of(TYPES[1], TYPES[3]));
Pokemon reference = em.getReference(pokemon);
assertTrue(reference instanceof PersistenceEntity);
// Verify that access to entity attribute works
reference.getName();
et.commit();
} catch (Exception e) {
Expand All @@ -87,12 +100,13 @@ public void testGetReferenceForNotExistingEntity() {
Pokemon reference = em.getReference(pokemon);
assertTrue(reference instanceof PersistenceEntity);
try {
// Verify that access to entity attribute fails
reference.getName();
fail("Accessing non-existing entity shall throw EntityNotFoundException");
// EntityNotFoundException shall be thrown on non-existing entity access
} catch (EntityNotFoundException enfe) {
assertTrue("Unexpected exception message: " + enfe.getLocalizedMessage(),
enfe.getLocalizedMessage().contains("Could not find Entity"));
assertTrue("Unexpected exception message: " + enfe.getMessage(),
enfe.getMessage().contains("Could not find Entity"));
}
et.commit();
} catch (Exception e) {
Expand All @@ -102,4 +116,57 @@ public void testGetReferenceForNotExistingEntity() {
}
}

// Call lock(Object, LockModeType, LockOption...) with unsupported LockOption instance
// Shall throw PersistenceException
public void testLockOptionUtilsUnknownClass() {
emf.runInTransaction(em -> {
Pokemon pokemon = em.find(Pokemon.class, POKEMONS[4].getId());
try {
em.lock(pokemon, LockModeType.PESSIMISTIC_WRITE, new LockOption() { });
fail("Calling lock(Object, LockModeType, LockOption...) with unsupported LockOption shall throw PersistenceException");
} catch (PersistenceException pe) {
assertTrue("Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("The LockOption implementing class"));
assertTrue("Unexpected exception message: " + pe.getMessage(),
pe.getMessage().contains("is not supported"));
}
});
}

// Test lock(Object, LockModeType, LockOption...) with LockModeType.PESSIMISTIC_WRITE and specific timeout
// Parallel attempt to lock the entity shall fail.
public void testLockPessimisticWriteWithTimeout() {
if (isSelectForUpateNoWaitSupported()) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
emf.runInTransaction(em -> {
Pokemon pokemon = em.find(Pokemon.class, POKEMONS[3].getId());
em.lock(pokemon, LockModeType.PESSIMISTIC_WRITE, Timeout.ms(0));
});
}
});
AtomicBoolean hanging = new AtomicBoolean(true);
emf.runInTransaction(em -> {
Pokemon pokemon = em.find(Pokemon.class, POKEMONS[3].getId());
em.lock(pokemon, LockModeType.PESSIMISTIC_WRITE, Timeout.s(60));
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Thread should have failed to get a lock with NOWAIT and hence should have finished by now
hanging.set(thread.isAlive());
if (hanging.get()) {
thread.interrupt();
}
});
assertFalse("Pessimistic lock with NOWAIT on entity causes concurrent thread to wait", hanging.get());
} else {
warning("Skipping testLockPessimisticWriteWithTimeout because SELECT FOR UPDATE NO WAIT is not supported on "
+ getPlatform().getClass().getSimpleName());
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
Expand Down Expand Up @@ -34,6 +34,7 @@
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.DefaultSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.platform.database.MySQLPlatform;
import org.eclipse.persistence.sessions.Connector;
import org.eclipse.persistence.sessions.DefaultConnector;
import org.eclipse.persistence.sessions.JNDIConnector;
Expand Down Expand Up @@ -1031,7 +1032,8 @@ public boolean isSelectForUpateNoWaitSupported(){
}

public static boolean isSelectForUpateNoWaitSupported(Platform platform) {
if (platform.isOracle() || platform.isSQLServer() || platform.isMariaDB()) {
if (platform.isOracle() || platform.isSQLServer() || platform.isMariaDB()
|| (platform.isMySQL() && ((MySQLPlatform)platform).supportsForUpdateNoWait())) {
return true;
}
warning("This database does not support NOWAIT.");
Expand Down

0 comments on commit e9e8ae1

Please sign in to comment.