diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java index d2c092ac719..6f09ab6484a 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2021 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -1868,6 +1868,13 @@ public class PersistenceUnitProperties { * * {@code * } + *

+ * Example 2 : To change the value of + * DatabasePlatform.supportsReturnGeneratedKeys via configuration, provide the + * following :

+ * + * {@code + * } * @see TargetDatabase * @see DatabasePlatform */ diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseAccessor.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseAccessor.java index 8b0077bcc21..73baf1fd1e1 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseAccessor.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseAccessor.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2018 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -644,6 +644,15 @@ public Object basicExecuteCall(Call call, AbstractRecord translationRow, Abstrac // Bug 2804663 - LOBValueWriter is no longer a singleton getLOBWriter().addCall(dbCall); } + + if(dbCall.shouldReturnGeneratedKeys()) { + resultSet = statement.getGeneratedKeys(); + + dbCall.setStatement(statement); + dbCall.setGeneratedKeys(resultSet); + this.possibleFailure = false; + return dbCall; + } } else if ((!dbCall.getReturnsResultSet() || (dbCall.getReturnsResultSet() && dbCall.shouldBuildOutputRow()))) { result = session.getPlatform().executeStoredProcedure(dbCall, (PreparedStatement)statement, this, session); this.storedProcedureStatementsCount++; @@ -883,7 +892,7 @@ public void run() { /** * Execute the statement. */ - public Integer executeDirectNoSelect(Statement statement, DatabaseCall call, AbstractSession session) throws DatabaseException { + public Object executeDirectNoSelect(Statement statement, DatabaseCall call, AbstractSession session) throws DatabaseException { int rowCount = 0; try { @@ -966,8 +975,8 @@ protected int executeJDK12BatchStatement(Statement statement, DatabaseCall dbCal /** * Execute the statement. */ - protected Integer executeNoSelect(DatabaseCall call, Statement statement, AbstractSession session) throws DatabaseException { - Integer rowCount = executeDirectNoSelect(statement, call, session); + protected Object executeNoSelect(DatabaseCall call, Statement statement, AbstractSession session) throws DatabaseException { + Object rowCount = executeDirectNoSelect(statement, call, session); // Allow for procs with outputs to be raised as events for error handling. if (call.shouldBuildOutputRow()) { @@ -1594,6 +1603,8 @@ public Statement prepareStatement(DatabaseCall call, AbstractSession session, bo } else if (call.isDynamicCall(session)) { // PERF: Dynamic statements are used for dynamic SQL. statement = allocateDynamicStatement(nativeConnection); + } else if(call.shouldReturnGeneratedKeys()) { + statement = nativeConnection.prepareStatement(call.getSQLString(), Statement.RETURN_GENERATED_KEYS); } else { statement = nativeConnection.prepareStatement(call.getSQLString()); } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseCall.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseCall.java index fab262aa5df..d7370ca243c 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseCall.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabaseCall.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2019, 2020 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2022 IBM Corporation. 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 @@ -98,12 +98,20 @@ public abstract class DatabaseCall extends DatasourceCall { transient protected Statement statement; transient protected ResultSet result; + // The generated keys are cached for lookup later + transient protected ResultSet generatedKeys; + // The call may specify that its parameters should be bound. protected Boolean usesBinding; // Bound calls can use prepared statement caching. protected Boolean shouldCacheStatement; + /* + * Indicate this call should return generated keys. Only supported for INSERT calls. + */ + protected boolean shouldReturnGeneratedKeys; + // The returned fields. transient protected Vector fields; // PERF: fields array @@ -547,6 +555,13 @@ public int getQueryTimeout() { return this.queryTimeout; } + /** + * The result set that stores the generated keys from the Statement + */ + public ResultSet getGeneratedKeys() { + return this.generatedKeys; + } + /** * The result set is stored for the return value of cursor selects. */ @@ -792,13 +807,12 @@ protected void prepareInternalParameters(AbstractSession session) { if (this.returnsResultSet == null) { setReturnsResultSet(!isCallableStatementRequired()); } - // if there is nothing returned and we are not using optimistic locking then batch - //if it is a StoredProcedure with in/out or out parameters then do not batch - //logic may be weird but we must not batch if we are not using JDBC batchwriting and we have parameters - // we may want to refactor this some day + // if it is a StoredProcedure with in/out or out parameters then do not batch + // (DatasourceCallQueryMechanism.executeCall() will return an AbstractRecord) + // logic may be weird but we must not batch if we are not using JDBC batchwriting and we have parameters this.isBatchExecutionSupported = (isNothingReturned() && (!hasOptimisticLock() || session.getPlatform().canBatchWriteWithOptimisticLocking(this)) - && (!shouldBuildOutputRow()) + && (!shouldBuildOutputRow() && !shouldReturnGeneratedKeys()) && (session.getPlatform().usesJDBCBatchWriting() || (!hasParameters())) && (!isLOBLocatorNeeded())) && (getQuery().isModifyQuery() && ((ModifyQuery)getQuery()).isBatchExecutionSupported()); @@ -905,6 +919,15 @@ public void setIgnoreMaxResultsSetting(boolean ignoreMaxResultsSetting){ this.ignoreMaxResultsSetting = ignoreMaxResultsSetting; } + /** + * Indicate that this call should set {@link java.sql.Statement#RETURN_GENERATED_KEYS} when executing + *

+ * Only set to true if {@link DatabasePlatform#supportsReturnGeneratedKeys()} + */ + public boolean setShouldReturnGeneratedKeys(boolean shouldReturnGeneratedKeys) { + return this.shouldReturnGeneratedKeys = shouldReturnGeneratedKeys; + } + /** * Callable statement is required if there is an output parameter. */ @@ -967,6 +990,13 @@ public void setQueryTimeoutUnit(TimeUnit queryTimeoutUnit) { this.queryTimeoutUnit = queryTimeoutUnit; } + /** + * The result set that stores the generated keys from the Statement + */ + public void setGeneratedKeys(ResultSet generatedKeys) { + this.generatedKeys = generatedKeys; + } + /** * The result set is stored for the return value of cursor selects. */ @@ -1085,6 +1115,13 @@ public boolean shouldIgnoreMaxResultsSetting(){ return this.ignoreMaxResultsSetting; } + /** + * Indicate that this call should set {@link java.sql.Statement#RETURN_GENERATED_KEYS} when executing + */ + public boolean shouldReturnGeneratedKeys() { + return this.shouldReturnGeneratedKeys; + } + /** * INTERNAL: * Print the SQL string. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatasourcePlatform.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatasourcePlatform.java index c374f6a2c06..3e6131bf49c 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatasourcePlatform.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatasourcePlatform.java @@ -1,6 +1,6 @@ /* * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2019, 2020 IBM Corporation. All rights reserved. + * Copyright (c) 2019, 2022 IBM Corporation. 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 @@ -98,10 +98,28 @@ public class DatasourcePlatform implements Platform { /** If sequences should start at Next Value */ protected boolean defaultSeqenceAtNextValue; + /** + * This property configures if the database platform will use {@link java.sql.Statement#getGeneratedKeys()}, + * or a separate query, in order to obtain javax.persistence.GenerationType.IDENTITY generated values. + *

+ * Allowed Values: + *

+ *

+ * See: + *

+ */ + protected boolean supportsReturnGeneratedKeys; + public DatasourcePlatform() { this.tableQualifier = ""; this.startDelimiter = ""; this.endDelimiter = ""; + this.supportsReturnGeneratedKeys = false; } /** @@ -776,6 +794,15 @@ public void addSequence(Sequence sequence) { addSequence(sequence, false); } + /** + * Indicates whether the platform supports the use of {@link java.sql.Statement#RETURN_GENERATED_KEYS}. + * If supported, IDENTITY values will be obtained through {@link java.sql.Statement#getGeneratedKeys()} + * and will replace usage of {@link #buildSelectQueryForIdentity()} + */ + public void setSupportsReturnGeneratedKeys(boolean supportsReturnGeneratedKeys) { + this.supportsReturnGeneratedKeys = supportsReturnGeneratedKeys; + } + /** * Add sequence corresponding to the name. * Use this method with isSessionConnected parameter set to true @@ -978,6 +1005,15 @@ public boolean supportsSequenceObjects() { return false; } + /** + * Indicates whether the platform supports the use of {@link java.sql.Statement#RETURN_GENERATED_KEYS}. + * If supported, IDENTITY values will be obtained through {@link java.sql.Statement#getGeneratedKeys()} + * and will replace usage of {@link #buildSelectQueryForIdentity()} + */ + public boolean supportsReturnGeneratedKeys() { + return supportsReturnGeneratedKeys; + } + /** * INTERNAL: * Returns query used to read value generated by sequence object (like Oracle sequence). @@ -1009,6 +1045,9 @@ public ValueReadQuery buildSelectQueryForSequenceObject(String qualifiedSeqName, * the returned query used until the sequence is disconnected. * If the platform supportsIdentity then (at least) one of buildSelectQueryForIdentity * methods should return non-null query. + *

+ * Alternatively, if the platform supports {@link java.sql.Statement#getGeneratedKeys()}, + * see {@link DatabasePlatform#supportsReturnGeneratedKeys()} */ public ValueReadQuery buildSelectQueryForIdentity() { return null; diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DynamicSQLBatchWritingMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DynamicSQLBatchWritingMechanism.java index 26115d2ebb4..1edd1c21956 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DynamicSQLBatchWritingMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DynamicSQLBatchWritingMechanism.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -150,9 +151,9 @@ private void executeBatch(AbstractSession session) { if (this.sqlStrings.size() == 1) { // If only one call, just execute normally. try { - int rowCount = (Integer)this.databaseAccessor.basicExecuteCall(this.lastCallAppended, null, session, false); - if (this.usesOptimisticLocking) { - if (rowCount != 1) { + Object rowCount = this.databaseAccessor.basicExecuteCall(this.lastCallAppended, null, session, false); + if (this.usesOptimisticLocking && rowCount instanceof Integer) { + if ((Integer)rowCount != 1) { throw OptimisticLockException.batchStatementExecutionFailure(); } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/ParameterizedSQLBatchWritingMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/ParameterizedSQLBatchWritingMechanism.java index ecae2073806..e3203fba880 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/ParameterizedSQLBatchWritingMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/ParameterizedSQLBatchWritingMechanism.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -151,9 +152,9 @@ private void executeBatch(AbstractSession session) { if (this.parameters.size() == 1) { // If only one call, just execute normally. try { - int rowCount = (Integer)this.databaseAccessor.basicExecuteCall(this.previousCall, null, session, false); - if (this.previousCall.hasOptimisticLock()) { - if (rowCount != 1) { + Object rowCount = this.databaseAccessor.basicExecuteCall(this.previousCall, null, session, false); + if (this.previousCall.hasOptimisticLock() && rowCount instanceof Integer) { + if ((Integer)rowCount != 1) { throw OptimisticLockException.batchStatementExecutionFailure(); } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java index 88694f8f074..6dfc8b76588 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java @@ -1,6 +1,6 @@ /* * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2021 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -390,7 +390,7 @@ protected void assignReturnValueToMapping(Object object, ReadObjectQuery query, * @exception DatabaseException - an error has occurred on the database. */ public Object assignSequenceNumber(Object object, AbstractSession writeSession) throws DatabaseException { - return assignSequenceNumber(object, writeSession, null); + return assignSequenceNumber(object, null, writeSession, null); } /** @@ -404,7 +404,21 @@ public Object assignSequenceNumber(Object object, AbstractSession writeSession) * @exception DatabaseException - an error has occurred on the database. */ public Object assignSequenceNumber(WriteObjectQuery writeQuery) throws DatabaseException { - return assignSequenceNumber(writeQuery.getObject(), writeQuery.getSession(), writeQuery); + return assignSequenceNumber(writeQuery.getObject(), null, writeQuery.getSession(), writeQuery); + } + + /** + * INTERNAL: + * Update the writeQuery's object primary key by fetching a new sequence number from the accessor. + * This assume the uses sequence numbers check has already been done. + * Adds the assigned sequence value to writeQuery's modify row. + * If object has a changeSet then sets sequence value into change set as an Id + * adds it also to object's change set in a ChangeRecord if required. + * @return the sequence value or null if not assigned. + * @exception DatabaseException - an error has occurred on the database. + */ + public Object assignSequenceNumber(WriteObjectQuery writeQuery, Object sequenceValue) throws DatabaseException { + return assignSequenceNumber(writeQuery.getObject(), sequenceValue, writeQuery.getSession(), writeQuery); } /** @@ -417,7 +431,7 @@ public Object assignSequenceNumber(WriteObjectQuery writeQuery) throws DatabaseE * @return the sequence value or null if not assigned. * @exception DatabaseException - an error has occurred on the database. */ - protected Object assignSequenceNumber(Object object, AbstractSession writeSession, WriteObjectQuery writeQuery) throws DatabaseException { + protected Object assignSequenceNumber(Object object, Object sequenceValue, AbstractSession writeSession, WriteObjectQuery writeQuery) throws DatabaseException { DatabaseField sequenceNumberField = this.descriptor.getSequenceNumberField(); Object existingValue = null; if (this.sequenceMapping != null) { @@ -428,12 +442,12 @@ protected Object assignSequenceNumber(Object object, AbstractSession writeSessio // PERF: The (internal) support for letting the sequence decide this was removed, // as anything other than primitive should allow null and default as such. - Object sequenceValue; int index = this.descriptor.getPrimaryKeyFields().indexOf(sequenceNumberField); if (isPrimaryKeyComponentInvalid(existingValue, index) || this.descriptor.getSequence().shouldAlwaysOverrideExistingValue()) { - sequenceValue = writeSession.getSequencing().getNextValue(this.descriptor.getJavaClass()); - } else { - return null; + // If no sequence value was passed, obtain one from the Sequence + if(sequenceValue == null) { + sequenceValue = writeSession.getSequencing().getNextValue(this.descriptor.getJavaClass()); + } } // Check that the value is not null, this occurs on any databases using IDENTITY type sequencing. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/CallQueryMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/CallQueryMechanism.java index 15282e40d4b..c81cbc7bdcf 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/CallQueryMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/CallQueryMechanism.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -153,11 +154,18 @@ protected void configureDatabaseCall(DatabaseCall call) { if (!this.query.shouldIgnoreCacheStatement()) { call.setShouldCacheStatement(this.query.shouldCacheStatement()); } + call.setQueryTimeout(this.query.getQueryTimeout()); call.setQueryTimeoutUnit(this.query.getQueryTimeoutUnit()); + if (this.query.isNativeConnectionRequired()) { call.setIsNativeConnectionRequired(true); } + + if (this.query.isInsertObjectQuery()) { + call.setShouldReturnGeneratedKeys(this.query.shouldReturnGeneratedKeys()); + } + if (this.query.isReadQuery()) { ReadQuery readQuery = (ReadQuery)this.query; // Some DB don't support FirstRow in SELECT statements in spite of supporting MaxResults(Symfoware). diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatabaseQueryMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatabaseQueryMechanism.java index ac466bfc9b9..7a6717d1a0d 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatabaseQueryMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatabaseQueryMechanism.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -21,6 +22,8 @@ package org.eclipse.persistence.internal.queries; import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -35,6 +38,7 @@ import org.eclipse.persistence.exceptions.OptimisticLockException; import org.eclipse.persistence.exceptions.QueryException; import org.eclipse.persistence.expressions.Expression; +import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor; import org.eclipse.persistence.internal.databaseaccess.DatabaseCall; import org.eclipse.persistence.internal.databaseaccess.DatasourceCall; import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy; @@ -218,7 +222,14 @@ public boolean isJPQLCallQueryMechanism() { * @exception DatabaseException * @return the row count. */ - public abstract Integer executeNoSelect() throws DatabaseException; + public abstract Object executeNoSelect() throws DatabaseException; + + /** + * Execute a non selecting SQL call, that returns the generated keys + * This should be overridden by subclasses. + * @exception DatabaseException + */ + public abstract DatabaseCall generateKeysExecuteNoSelect() throws DatabaseException; /** * Execute a select SQL call and return the rows. @@ -877,6 +888,64 @@ protected void updateObjectAndRowWithSequenceNumber() throws DatabaseException { writeQuery.getDescriptor().getObjectBuilder().assignSequenceNumber(writeQuery); } + /** + * Update the object's primary key by getting the generated keys from the call + * If there are no generated keys or the value is NULL, then default back to the {@link #updateObjectAndRowWithSequenceNumber()} + */ + protected void updateObjectAndRowWithSequenceNumber(DatabaseCall call) throws DatabaseException { + WriteObjectQuery writeQuery = getWriteObjectQuery(); + AbstractSession session = writeQuery.getSession(); + DatabaseAccessor dbAccessor = (DatabaseAccessor)writeQuery.getAccessor(); + + Object sequenceValue = null; + boolean exceptionOccured = false; + ResultSet resultSet = call.getGeneratedKeys(); + try { + if(resultSet.next()) { + sequenceValue = resultSet.getObject(1); + } + + if(sequenceValue != null) { + writeQuery.getDescriptor().getObjectBuilder().assignSequenceNumber(writeQuery, sequenceValue); + } + } catch (SQLException exception) { + exceptionOccured = true; + DatabaseException commException = dbAccessor.processExceptionForCommError(session, exception, call); + if (commException != null) { + throw commException; + } + throw DatabaseException.sqlException(exception, call, dbAccessor, session, false); + } catch (RuntimeException exception) { + exceptionOccured = true; + if (exception instanceof DatabaseException) { + ((DatabaseException)exception).setCall(call); + if(((DatabaseException)exception).getAccessor() == null) { + ((DatabaseException)exception).setAccessor(dbAccessor); + } + } + throw exception; + } finally { + try { + if (resultSet != null) { + resultSet.close(); + } + } catch (SQLException cleanupSQLException) { + if (!exceptionOccured) { + throw DatabaseException.sqlException(cleanupSQLException, call, dbAccessor, session, false); + } + } catch (RuntimeException cleanupException) { + if (!exceptionOccured) { + throw cleanupException; + } + } + } + + // Fallback on original implementation if no value was found in the generated keys + if(sequenceValue == null) { + updateObjectAndRowWithSequenceNumber(); + } + } + /** * Update the object. * This is only used for non-unit-of-work updates. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatasourceCallQueryMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatasourceCallQueryMechanism.java index 8658ff162c6..19f20ffa054 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatasourceCallQueryMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/DatasourceCallQueryMechanism.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2019 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -37,10 +37,8 @@ import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.internal.helper.DatabaseTable; import org.eclipse.persistence.internal.helper.Helper; -import org.eclipse.persistence.internal.helper.NonSynchronizedVector; import org.eclipse.persistence.internal.sessions.AbstractRecord; import org.eclipse.persistence.internal.sessions.AbstractSession; -import org.eclipse.persistence.mappings.DatabaseMapping; import org.eclipse.persistence.mappings.DatabaseMapping.WriteType; import org.eclipse.persistence.queries.ConstructorReportItem; import org.eclipse.persistence.queries.DatabaseQuery; @@ -285,13 +283,26 @@ protected Object executeCall(DatasourceCall databaseCall) throws DatabaseExcepti /** * Execute a non selecting call. * @exception DatabaseException - an error has occurred on the database. - * @return the row count. + * @return Returns either a {@link DatabaseCall} or Integer value, + * depending on if this INSERT call needs to return generated keys */ @Override - public Integer executeNoSelect() throws DatabaseException { + public Object executeNoSelect() throws DatabaseException { + if(((DatabaseCall)this.call).shouldReturnGeneratedKeys()) { + return generateKeysExecuteNoSelect(); + } return executeNoSelectCall(); } + /** + * Execute a non selecting call. + * @exception DatabaseException - an error has occurred on the database. + * @return the row count. + */ + public DatabaseCall generateKeysExecuteNoSelect() throws DatabaseException { + return (DatabaseCall)executeCall(); + } + /** * Execute a non selecting call. * @exception DatabaseException - an error has occurred on the database. @@ -413,7 +424,11 @@ public void insertObject() throws DatabaseException { updateObjectAndRowWithReturnRow(returnFields, index == 0); } if ((index == 0) && usesSequencing && shouldAcquireValueAfterInsert) { - updateObjectAndRowWithSequenceNumber(); + if(result instanceof DatabaseCall) { + updateObjectAndRowWithSequenceNumber((DatabaseCall) result); + } else { + updateObjectAndRowWithSequenceNumber(); + } } } } @@ -427,7 +442,11 @@ public void insertObject() throws DatabaseException { updateObjectAndRowWithReturnRow(returnFields, true); } if (usesSequencing && shouldAcquireValueAfterInsert) { - updateObjectAndRowWithSequenceNumber(); + if(result instanceof DatabaseCall) { + updateObjectAndRowWithSequenceNumber((DatabaseCall) result); + } else { + updateObjectAndRowWithSequenceNumber(); + } } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/StatementQueryMechanism.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/StatementQueryMechanism.java index 524d4dac3f5..5a00eff033d 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/StatementQueryMechanism.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/queries/StatementQueryMechanism.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -117,7 +118,7 @@ public Integer deleteObject() throws DatabaseException { * @return the row count. */ @Override - public Integer executeNoSelect() throws DatabaseException { + public Object executeNoSelect() throws DatabaseException { // Prepare the calls if not already set (prepare may not have had the modify row). if ((this.call == null) && (!hasMultipleCalls())) { prepareExecuteNoSelect(); diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/sessions/DatabaseSessionImpl.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/sessions/DatabaseSessionImpl.java index b0becb868e5..80414be96ff 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/sessions/DatabaseSessionImpl.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/sessions/DatabaseSessionImpl.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2015, 2020 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2022 IBM Corporation. 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 @@ -47,6 +47,7 @@ import java.util.Vector; import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.eclipse.persistence.config.PropertiesUtils; import org.eclipse.persistence.descriptors.ClassDescriptor; import org.eclipse.persistence.descriptors.partitioning.PartitioningPolicy; import org.eclipse.persistence.exceptions.DatabaseException; @@ -862,6 +863,13 @@ protected void preConnectDatasource(){ * is connected to. */ protected void postConnectDatasource(){ + // Initialize the Platform properties now that the datasource is connected and the Platform should be initialized + if((getDatasourcePlatform() instanceof DatabasePlatform)) { + final Platform platform = getDatasourcePlatform(); + String dbProperties = (String) getProperties().get(PersistenceUnitProperties.TARGET_DATABASE_PROPERTIES); + PropertiesUtils.set(platform, PersistenceUnitProperties.TARGET_DATABASE_PROPERTIES, dbProperties); + } + if (!hasBroker()) { initializeDescriptors(); diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/DatabaseQuery.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/DatabaseQuery.java index 36c5d35448d..c2b140c1826 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/DatabaseQuery.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/DatabaseQuery.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2019 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2018 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -268,6 +268,8 @@ public enum ParameterType {POSITIONAL, NAMED} protected TimeUnit queryTimeoutUnit; + protected boolean shouldReturnGeneratedKeys; + /* Used as default for read, means shallow write for modify. */ public static final int NoCascading = 1; @@ -2423,6 +2425,18 @@ public void setShouldRetrieveBypassCache(boolean shouldRetrieveBypassCache) { this.shouldRetrieveBypassCache = shouldRetrieveBypassCache; } + /** + * ADVANCED: JPA flag used to control the behavior of IDENTITY generation. This + * flag specifies the behavior when data is retrieved by the find methods + * and by the execution of queries. + *

+ * This flag is only applicable to Insert queries and will only apply if the database + * platform supports {@link java.sql.Statement#RETURN_GENERATED_KEYS} + */ + public void setShouldReturnGeneratedKeys(boolean shouldReturnGeneratedKeys) { + this.shouldReturnGeneratedKeys = shouldReturnGeneratedKeys; + } + /** * ADVANCED: JPA flag used to control the behavior of the shared cache. This * flag specifies the behavior when data is read from the database and when @@ -2634,6 +2648,18 @@ public boolean shouldRetrieveBypassCache() { return this.shouldRetrieveBypassCache; } + /** + * ADVANCED: JPA flag used to control the behavior of IDENTITY generation. This + * flag specifies the behavior when data is retrieved by the find methods + * and by the execution of queries. + *

+ * This flag is only applicable to Insert queries and will only apply if the database + * platform supports {@link java.sql.Statement#RETURN_GENERATED_KEYS} + */ + public boolean shouldReturnGeneratedKeys() { + return this.shouldReturnGeneratedKeys; + } + /** * ADVANCED: JPA flag used to control the behavior of the shared cache. This * flag specifies the behavior when data is read from the database and when diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/InsertObjectQuery.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/InsertObjectQuery.java index d28adab1b9a..e07c28c1dc7 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/InsertObjectQuery.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/queries/InsertObjectQuery.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -18,6 +19,8 @@ import org.eclipse.persistence.exceptions.*; import org.eclipse.persistence.internal.sessions.AbstractRecord; import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.sequencing.NativeSequence; +import org.eclipse.persistence.sequencing.Sequence; /** *

Purpose: @@ -114,6 +117,15 @@ protected void prepare() { if (this.name == null) { this.name = "insert" + this.descriptor.getJavaClass().getSimpleName(); } + + // Insert queries may need to use generated keys. Determine here if the underlying sequence for the object will want to. + if(this.descriptor.usesSequenceNumbers()) { + Sequence sequence = this.descriptor.getSequence(); + if(sequence.isNative() && ((NativeSequence)sequence).shouldUseGeneratedKeysIfPlatformSupports()) { + setShouldReturnGeneratedKeys(true); + } + } + getQueryMechanism().prepareInsertObject(); } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/sequencing/NativeSequence.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/sequencing/NativeSequence.java index 038f0de4ad6..6ddb4849d62 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/sequencing/NativeSequence.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/sequencing/NativeSequence.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 IBM Corporation. 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 @@ -36,6 +37,12 @@ public class NativeSequence extends QuerySequence { */ protected boolean shouldUseIdentityIfPlatformSupports = true; + /** + * true indicates that identity is used and generatedKeys should be used - if the platform supports generatedKeys + * false indicates that identity is used and generatedKeys should not be used - if the platform does not support generatedKeys + */ + protected boolean shouldUseGeneratedKeysIfPlatformSupports = false; + /** * Allow sequencing to be delegated to another sequence if native sequencing is not supported. */ @@ -107,6 +114,14 @@ public boolean shouldUseIdentityIfPlatformSupports() { return shouldUseIdentityIfPlatformSupports; } + public void setShouldUseGeneratedKeysIfPlatformSupports(boolean shouldUseGeneratedKeysIfPlatformSupports) { + this.shouldUseGeneratedKeysIfPlatformSupports = shouldUseGeneratedKeysIfPlatformSupports; + } + + public boolean shouldUseGeneratedKeysIfPlatformSupports() { + return shouldUseGeneratedKeysIfPlatformSupports; + } + @Override public boolean equals(Object obj) { if (obj instanceof NativeSequence) { @@ -183,9 +198,18 @@ public void setDelegateSequence(QuerySequence delegateSequence) { public void onConnect() { DatasourcePlatform platform = (DatasourcePlatform)getDatasourcePlatform(); // Set shouldAcquireValueAfterInsert flag: identity -> true; sequence objects -> false. - if (platform.supportsIdentity() && shouldUseIdentityIfPlatformSupports()) { + if ((platform.supportsIdentity() || platform.supportsReturnGeneratedKeys()) && shouldUseIdentityIfPlatformSupports()) { // identity is both supported by platform and desired by the NativeSequence setShouldAcquireValueAfterInsert(true); + + //Here we can ask the platform if it supports generatedKeys + // We know that it's IDENTITY and not some other generation type + // I should set here that generated keys will be used and Sequence can expect a ResultSet from the InsertQuery later + + // set a flag so that Sequence knows how to obtain values later + if(platform.supportsReturnGeneratedKeys()) { + setShouldUseGeneratedKeysIfPlatformSupports(true); + } } else if (platform.supportsSequenceObjects() && !shouldUseIdentityIfPlatformSupports()) { // sequence objects is both supported by platform and desired by the NativeSequence setShouldAcquireValueAfterInsert(false); diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/TestIdentityGeneration.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/TestIdentityGeneration.java new file mode 100644 index 00000000000..6e15bbad8a5 --- /dev/null +++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/TestIdentityGeneration.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2022 IBM Corporation and/or 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 + */ +// Contributors: +// IBM - Bug 579409: Add support for accurate IDENTITY generation when the database contains separate TRIGGER objects +package org.eclipse.persistence.jpa.test.sequence; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; + +import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; +import org.eclipse.persistence.jpa.test.framework.DDLGen; +import org.eclipse.persistence.jpa.test.framework.Emf; +import org.eclipse.persistence.jpa.test.framework.EmfRunner; +import org.eclipse.persistence.jpa.test.framework.Property; +import org.eclipse.persistence.jpa.test.sequence.model.Coffee; +import org.eclipse.persistence.jpa.test.sequence.model.Tea; +import org.eclipse.persistence.jpa.test.sequence.model.TeaShop; +import org.eclipse.persistence.platform.database.DatabasePlatform; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(EmfRunner.class) +public class TestIdentityGeneration { + @Emf(createTables = DDLGen.DROP_CREATE, classes = { Coffee.class, Tea.class, TeaShop.class }, + properties = { + @Property(name="eclipselink.logging.level", value="FINE")}) + private EntityManagerFactory emf; + + @Test + public void testPersistWithSecondaryTables() { + if (emf == null) + return; + + EntityManager em = emf.createEntityManager(); + try { + Set teas = new HashSet(); + + Tea tea1 = new Tea("Earl Grey", 3.29, "Lipton", 1.29); + teas.add(tea1); + Tea tea2 = new Tea("Oolong", 2.34, "Lipton", 0.75); + teas.add(tea2); + + em.getTransaction().begin(); + for(Tea tea : teas) + em.persist(tea); + em.getTransaction().commit(); + + for(Tea tea : teas) + Assert.assertNotNull(tea.getName() + " 'id' is null", tea.getId()); + + Long id = tea2.getId() + 11; + TeaShop teaShop = new TeaShop(id, "MyShop"); + teaShop.setTeas(teas); + + em.getTransaction().begin(); + em.persist(teaShop); + em.getTransaction().commit(); + + Tea findTea = em.find(Tea.class, tea1.getId()); + Assert.assertEquals("wrong object name", tea1.getName(), findTea.getName()); + Assert.assertEquals("wrong object color", tea1.getManufacturer(), findTea.getManufacturer()); + + findTea = em.find(Tea.class, tea2.getId()); + Assert.assertEquals("wrong object name", tea2.getName(), findTea.getName()); + Assert.assertEquals("wrong object color", tea2.getManufacturer(), findTea.getManufacturer()); + + TeaShop referenceCave = em.find(TeaShop.class, id); + Set referenceSet = referenceCave.getTeas(); + Assert.assertEquals("wrong set size", teas.size(), referenceSet.size()); + Assert.assertTrue("missing element", referenceSet.contains(tea1)); + Assert.assertTrue("missing element", referenceSet.contains(tea2)); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + if(em.isOpen()) { + em.close(); + } + } + } + + @Test + public void testRefreshWithTriggers() { + if (emf == null) + return; + + DatabasePlatform platform = getPlatform(emf); + Assume.assumeTrue(platform.supportsReturnGeneratedKeys()); + + createNonJPAObjects(emf); + + EntityManager em = emf.createEntityManager(); + + try { + Coffee coffee = new Coffee("Capa", 4.2); + + // persist and commit to initiate an INSERT into COFFEE and trigger a COFFEE_AUDIT INSERT + em.getTransaction().begin(); + em.persist(coffee); + em.getTransaction().commit(); + + // refreshing the entity will trigger EclipseLink to attempt a SELECT with the ID they have + // If it isn't correct, it will fail + em.getTransaction().begin(); + em.refresh(coffee); + em.getTransaction().commit(); + + em.getTransaction().begin(); + List result = em.createNativeQuery("SELECT AUDIT_ID, COFFEE_ID FROM COFFEE_AUDIT WHERE NAME = 'Capa' AND PRICE = 4.2").getResultList(); + em.getTransaction().commit(); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(2, result.get(0).length); + + if(platform.isSQLServer()) { + Assert.assertEquals(new BigDecimal(4), result.get(0)[0]); + Assert.assertEquals(new BigDecimal(1), result.get(0)[1]); + } else { + Assert.assertEquals(4l, result.get(0)[0]); + Assert.assertEquals(1l, result.get(0)[1]); + } + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + if(em.isOpen()) { + em.close(); + } + } + } + + private void createNonJPAObjects(EntityManagerFactory emf) { + EntityManager em = emf.createEntityManager(); + DatabasePlatform platform = getPlatform(emf); + try { + // Drop the audit table if it exists + try { + em.getTransaction().begin(); + em.createNativeQuery("DROP TABLE COFFEE_AUDIT").executeUpdate(); + em.getTransaction().commit(); + } catch (Exception e) { + em.getTransaction().rollback(); + } + + // Create an audit table outside of JPA + em.getTransaction().begin(); + if(platform.isMySQL()) { + em.createNativeQuery("CREATE TABLE COFFEE_AUDIT (AUDIT_ID BIGINT AUTO_INCREMENT NOT NULL, COFFEE_ID BIGINT NOT NULL, NAME VARCHAR(255), PRICE DOUBLE, PRIMARY KEY (AUDIT_ID))").executeUpdate(); + } else if(platform.isSQLServer()) { + em.createNativeQuery("CREATE TABLE COFFEE_AUDIT (AUDIT_ID NUMERIC(19) IDENTITY NOT NULL, COFFEE_ID NUMERIC(19) NOT NULL, NAME VARCHAR(255) NULL, PRICE FLOAT(32) NULL, PRIMARY KEY (AUDIT_ID))").executeUpdate(); + } else if (platform.isDB2() || platform.isDerby()) { + em.createNativeQuery("CREATE TABLE COFFEE_AUDIT (AUDIT_ID BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL, COFFEE_ID BIGINT NOT NULL, NAME VARCHAR(255), PRICE FLOAT, PRIMARY KEY (AUDIT_ID))").executeUpdate(); + } else { + Assume.assumeTrue("The database [" + platform + "] supports ReturnGeneratedKeys [" + platform.supportsReturnGeneratedKeys() + "], but this test does not support this database. Add support.", false); + } + em.getTransaction().commit(); + + // Drop the audit table trigger if it exists + try { + em.getTransaction().begin(); + em.createNativeQuery("DROP TRIGGER coffeetrigger").executeUpdate(); + em.getTransaction().commit(); + } catch (Exception e) { + em.getTransaction().rollback(); + } + + // Create an audit table trigger outside JPA + em.getTransaction().begin(); + if(platform.isMySQL()) { + em.createNativeQuery("CREATE TRIGGER coffeetrigger AFTER INSERT ON COFFEE FOR EACH ROW INSERT INTO COFFEE_AUDIT( COFFEE_ID, NAME, PRICE ) VALUES (NEW.ID, NEW.NAME, NEW.PRICE)").executeUpdate(); + } else if(platform.isSQLServer()) { + em.createNativeQuery("CREATE TRIGGER coffeetrigger on COFFEE AFTER INSERT as BEGIN SET NOCOUNT ON; INSERT INTO COFFEE_AUDIT( COFFEE_ID, NAME, PRICE ) SELECT i.ID, i.NAME, i.PRICE FROM inserted i END").executeUpdate(); + } else if (platform.isDB2()) { + em.createNativeQuery("CREATE TRIGGER coffeetrigger AFTER INSERT ON COFFEE REFERENCING NEW_TABLE AS NEW_COFFEE FOR EACH STATEMENT MODE DB2SQL BEGIN ATOMIC INSERT INTO COFFEE_AUDIT( COFFEE_ID, NAME, PRICE ) SELECT i.ID, i.NAME, i.PRICE FROM NEW_COFFEE i; END").executeUpdate(); + } else if (platform.isDerby()) { + em.createNativeQuery("CREATE TRIGGER coffeetrigger AFTER INSERT ON COFFEE REFERENCING NEW_TABLE AS NEW_COFFEE FOR EACH STATEMENT MODE DB2SQL INSERT INTO COFFEE_AUDIT( COFFEE_ID, NAME, PRICE ) SELECT i.ID, i.NAME, i.PRICE FROM NEW_COFFEE i").executeUpdate(); + } else { + Assume.assumeTrue("The database [" + platform + "] supports ReturnGeneratedKeys [" + platform.supportsReturnGeneratedKeys() + "], but this test does not support this database. Add support.", false); + } + em.getTransaction().commit(); + + // Add values to the audit table so that the audit table identity generated will be out of sync with the entity table + em.getTransaction().begin(); + em.createNativeQuery("INSERT INTO COFFEE_AUDIT (COFFEE_ID, NAME, PRICE) VALUES (1, 'COFFEE1', 11.1)").executeUpdate(); + em.createNativeQuery("INSERT INTO COFFEE_AUDIT (COFFEE_ID, NAME, PRICE) VALUES (2, 'COFFEE2', 22.2)").executeUpdate(); + em.createNativeQuery("INSERT INTO COFFEE_AUDIT (COFFEE_ID, NAME, PRICE) VALUES (3, 'COFFEE3', 33.3)").executeUpdate(); + em.getTransaction().commit(); + } finally { + if(em.isOpen()) { + em.close(); + } + } + } + + private DatabasePlatform getPlatform(EntityManagerFactory emf) { + return ((EntityManagerFactoryImpl)emf).getServerSession().getPlatform(); + } +} diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Coffee.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Coffee.java new file mode 100644 index 00000000000..8243d06b202 --- /dev/null +++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Coffee.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 IBM Corporation, Oracle, and/or 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 + */ +// Contributors: +// IBM - Bug 579409: Add support for accurate IDENTITY generation when the database contains separate TRIGGER objects +package org.eclipse.persistence.jpa.test.sequence.model; + +import java.io.Serializable; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Coffee implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + protected String name; + protected Double price; + + public Coffee() { } + + public Coffee(String name, Double price) { + this.name = name; + this.price = price; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof Coffee)) { + return false; + } + Coffee other = (Coffee) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "org.eclipse.persistence.jpa.test.sequence.model.Coffee[id=" + id + ", name=" + name + ", price=" + price + "]"; + } +} \ No newline at end of file diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Tea.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Tea.java new file mode 100644 index 00000000000..4a657eb99a5 --- /dev/null +++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/Tea.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 IBM Corporation, Oracle, and/or 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 + */ +// Contributors: +// IBM - Bug 579409: Add support for accurate IDENTITY generation when the database contains separate TRIGGER objects +package org.eclipse.persistence.jpa.test.sequence.model; + +import java.io.Serializable; +import java.util.Set; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.SecondaryTable; + +@Entity +@SecondaryTable(name = "TEA_DETAILS", pkJoinColumns = @PrimaryKeyJoinColumn(name = "SECONDTABLE_ID", referencedColumnName = "ID")) +public class Tea implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + protected String name; + protected Double price; + + @Column(table = "TEA_DETAILS") + protected String manufacturer; + @Column(table = "TEA_DETAILS") + protected Double cost; + + private Set teaShops; + + public Tea() { } + + public Long getId() { + return id; + } + + public Tea(String name, Double price, String manufacturer, Double cost) { + this.name = name; + this.price = price; + this.manufacturer = manufacturer; + this.cost = cost; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public Double getCost() { + return cost; + } + + public void setCost(Double cost) { + this.cost = cost; + } + + @ManyToMany(mappedBy = "teas", targetEntity = TeaShop.class) + public Set getTeaShops() { + return this.teaShops; + } + + public void setCaves(Set teaShops) { + this.teaShops = teaShops; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Tea) { + Tea other = (Tea) obj; + return other.name.equals(name); + } + return false; + } + + @Override + public int hashCode() { + if (name == null) { + return 0; + } + return name.hashCode(); + } + +} diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/TeaShop.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/TeaShop.java new file mode 100644 index 00000000000..25fdf037115 --- /dev/null +++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/sequence/model/TeaShop.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 IBM Corporation, Oracle, and/or 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 + */ +// Contributors: +// IBM - Bug 579409: Add support for accurate IDENTITY generation when the database contains separate TRIGGER objects +package org.eclipse.persistence.jpa.test.sequence.model; + +import java.util.Set; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinTable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; + +@Entity +public class TeaShop { + + @Id + private long id; + private String name; + + @ManyToMany + @JoinTable(name = "TEASHOP_TEA", + joinColumns = { @JoinColumn(name = "TEASHOP_ID") }, + inverseJoinColumns = { @JoinColumn(name = "TEA_ID") }) + private Set teas; + + public TeaShop() { + } + + public TeaShop(long id, String name) { + this.id = id; + } + + public long getId() { + return this.id; + } + + public Set getTeas() { + return this.teas; + } + + public void setTeas(Set teas) { + this.teas = teas; + } + +} diff --git a/jpa/eclipselink.jpa.wdf.test/src/it/java/org/eclipse/persistence/testing/tests/wdf/jpa1/generator/TestIdentity.java b/jpa/eclipselink.jpa.wdf.test/src/it/java/org/eclipse/persistence/testing/tests/wdf/jpa1/generator/TestIdentity.java index b9caab4c1e8..509b621540f 100644 --- a/jpa/eclipselink.jpa.wdf.test/src/it/java/org/eclipse/persistence/testing/tests/wdf/jpa1/generator/TestIdentity.java +++ b/jpa/eclipselink.jpa.wdf.test/src/it/java/org/eclipse/persistence/testing/tests/wdf/jpa1/generator/TestIdentity.java @@ -81,6 +81,8 @@ public void testPersist() { Assert.assertEquals("missing element", referenceSet.contains(fluppi1), true); Assert.assertEquals("missing element", referenceSet.contains(fluppi2), true); + } catch (Exception e) { + e.printStackTrace(); } finally { if (env.isTransactionActive(em)) { env.rollbackTransactionAndClear(em); @@ -110,6 +112,8 @@ public void testMerge() { Assert.assertEquals("wrong object color", schnappi1.getColor(), schnappi2.getColor()); em.merge(schnappi2); Assert.assertEquals("id change", schnappi1.getId(), schnappi2.getId()); + } catch (Exception e) { + e.printStackTrace(); } finally { if (env.isTransactionActive(em)) { env.rollbackTransactionAndClear(em); @@ -165,6 +169,8 @@ public void testJoinedSubclass() { Assert.assertEquals("wrong object color", unicorn.getColor(), duocorn.getColor()); Assert.assertEquals("wrong object story", unicorn.getStory(), duocorn.getStory()); + } catch (Exception e) { + e.printStackTrace(); } finally { if (env.isTransactionActive(em)) { env.rollbackTransactionAndClear(em); @@ -201,6 +207,8 @@ public void testPersistNoTx() { em.persist(fluppi1); env.beginTransaction(em); env.commitTransaction(em); + } catch (Exception e) { + e.printStackTrace(); } finally { if (em.getTransaction().isActive()) { em.getTransaction().rollback(); diff --git a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerSetupImpl.java b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerSetupImpl.java index 98dd233004c..9bac79b4d34 100644 --- a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerSetupImpl.java +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerSetupImpl.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2021 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2022 IBM Corporation. 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 @@ -182,7 +182,6 @@ import org.eclipse.persistence.config.ParserType; import org.eclipse.persistence.config.PersistenceUnitProperties; import org.eclipse.persistence.config.ProfilerType; -import org.eclipse.persistence.config.PropertiesUtils; import org.eclipse.persistence.config.RemoteProtocol; import org.eclipse.persistence.config.SessionCustomizer; import org.eclipse.persistence.descriptors.ClassDescriptor; @@ -206,7 +205,6 @@ import org.eclipse.persistence.internal.databaseaccess.BatchWritingMechanism; import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor; import org.eclipse.persistence.internal.databaseaccess.DatasourcePlatform; -import org.eclipse.persistence.internal.databaseaccess.Platform; import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy; import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy.LockOnChange; import org.eclipse.persistence.internal.helper.ClassConstants; @@ -256,7 +254,6 @@ import org.eclipse.persistence.jpa.metadata.ProjectCache; import org.eclipse.persistence.jpa.metadata.XMLMetadataSource; import org.eclipse.persistence.logging.AbstractSessionLog; -import org.eclipse.persistence.logging.DefaultSessionLog; import org.eclipse.persistence.logging.SessionLog; import org.eclipse.persistence.platform.database.converters.StructConverter; import org.eclipse.persistence.platform.database.events.DatabaseEventListener; @@ -771,10 +768,6 @@ public AbstractSession deploy(ClassLoader realClassLoader, Map additionalPropert login(getDatabaseSession(), deployProperties, requiresConnection); } - final Platform platform = getDatabaseSession().getDatasourcePlatform(); - String dbProperties = getConfigPropertyAsStringLogDebug(PersistenceUnitProperties.TARGET_DATABASE_PROPERTIES, deployProperties, this.session); - PropertiesUtils.set(platform, PersistenceUnitProperties.TARGET_DATABASE_PROPERTIES, dbProperties); - // Make JTA integration throw JPA exceptions. if (this.session.hasExternalTransactionController()) { if (this.session.getExternalTransactionController().getExceptionHandler() == null) {