Bug 323023 - Enabling history support (HistoryPolicy) results in wrong assignment of database generated primary keys since version 2.0.1 inclusive
Summary: Enabling history support (HistoryPolicy) results in wrong assignment of datab...
Status: NEW
Alias: None
Product: z_Archived
Classification: Eclipse Foundation
Component: Eclipselink (show other bugs)
Version: unspecified   Edit
Hardware: PC Windows Vista
: P2 major with 4 votes (vote)
Target Milestone: ---   Edit
Assignee: Project Inbox CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2010-08-18 09:45 EDT by bertram.walter CLA
Modified: 2022-06-09 10:05 EDT (History)
8 users (show)

See Also:


Attachments
A HistoryPolicy that do not fetch the last inserted id (33.21 KB, text/x-java)
2014-10-31 14:11 EDT, Nicolas Marcotte CLA
no flags Details
A HistoryPolicy that do not fetch the last inserted id (The correct file) (33.28 KB, text/x-java)
2014-10-31 14:43 EDT, Nicolas Marcotte CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description bertram.walter CLA 2010-08-18 09:45:50 EDT
Build Identifier: 2.2.0-20100817.072156-45

Since version 2.0.1 I have this problem tested also with 2.1.0, 2.2.0 and 2.2.0-SNAPSHOT (18.08.):

Activating history creation on a SQLServer 2008 database results in a wrong assignment of primary keys, when generated by the database.

Assuming one entity with an primary key marked with the following annotations:
        @Column(name = "id")
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)

and a HistoryPolicy results in the following steps during addition of a new entity (verified with a debugger):

1) The original entity is added to the database and the correct primary key is assigned.
2) The corresponding history object is added to the database. 
3) The primary key of the history object is assigned to the original entity overwriting the already assigned correct primary key. This happens because the orignal and the history object are the same as there is no special entity for the history object. In 2.0.0 this step never happened and everything worked fine.

The problem is in org.eclipse.persistence.internal.descriptors.ObjectBuilder.assignSequenceNumber():

The working version 2.0.0 used the following code to prevent step 3 to happen (return of null value as the new sequence key):

  // If the value is null or zero (int/long) return.
  // PERF: The (internal) support for letting the sequence decide this was removed,
  // as anything other than primitive should allow null and default as such.
  if (existingValue != null && !Helper.isEquivalentToNull(existingValue)) {
     return null;
  }

In 2.2.0-SNAPSHOT this looks like (haven't looked in the code of older versions if they are identical):

  // 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 = his.descriptor.getPrimaryKeyFields().indexOf(sequenceNumberField);
  if (isPrimaryKeyComponentInvalid(existingValue, index) || 
      this.descriptor.getSequence().shouldAlwaysOverrideExistingValue()) {
     sequenceValue= writeSession.getSequencing().getNextValue(this.descriptor.getJavaClass());
  } else {
     return null;
  }

In the working version null is returned but in the newer version a new sequence value is retrieved.

History support is working but it breaks everything else as wrong identifiers are assigned to the original entities.

Currently I cannot provide steps to reproduce this feature but I hope the above gives information where the execution chain during adding a new object with history enabled differs between the working version 2.0.0 and non-working versions 2.0.1+

Reproducible: Always
Comment 1 Tom Ware CLA 2010-08-24 13:33:06 EDT
Setting target and priority see: http://wiki.eclipse.org/EclipseLink/Development/Bugs/Guidelines for more information on the meanings of these fields.
Comment 2 Stephen CLA 2011-06-15 10:01:57 EDT
I can confirm that this bug is also reproduce-able on OSX 10.6 with a MySQL database.
Comment 3 Nicolas Marcotte CLA 2014-10-31 14:11:28 EDT
Created attachment 248311 [details]
A HistoryPolicy that do not fetch the last inserted id

The patch is quite simple.
at initialization clone the descriptor and remove the sequence field. 
----Patch start below ----
@@ -311,7 +311,8 @@
      */
     public void initialize(AbstractSession session) {
         if (getMapping() != null) {
-            setDescriptor(getMapping().getDescriptor());
+            setDescriptor((ClassDescriptor) getMapping().getDescriptor().clone());
+            getDescriptor().setSequenceNumberField(null);
             if (getMapping().isDirectCollectionMapping()) {
                 DatabaseTable refTable = ((DirectCollectionMapping)getMapping()).getReferenceTable();
                 DatabaseTable histTable = getHistoricalTables().get(0);
----Patch ends here----
Comment 4 Nicolas Marcotte CLA 2014-10-31 14:37:03 EDT
Please ignore the preceding post...
I have diff and uploaded the wrong version, here is the correct patch:
----Patch start below ----
@@ -733,7 +733,9 @@
      * of the previous version).
      */
     public void logicalInsert(ObjectLevelModifyQuery writeQuery, boolean isUpdate) {
-        ClassDescriptor descriptor = getDescriptor();
+        ClassDescriptor descriptor =(ClassDescriptor) getDescriptor().clone();
+        descriptor.setSequenceNumberField(null);//Do not retrive the sequence number
+        writeQuery.setDescriptor(descriptor);
         AbstractRecord modifyRow = null;
         AbstractRecord originalModifyRow = writeQuery.getModifyRow();
         Object currentTime = null;
----Patch ends above----
Comment 5 Nicolas Marcotte CLA 2014-10-31 14:39:05 EDT
Comment on attachment 248311 [details]
A HistoryPolicy that do not fetch the last inserted id

/*******************************************************************************
 * Copyright (c) 1998, 2013 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 v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/  
package org.eclipse.persistence.history;

import java.io.Serializable;
import java.util.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.expressions.*;
import org.eclipse.persistence.internal.history.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.queries.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.queries.*;

/**
 * <b>Purpose:</b>Expresses how historical data is saved on the data store.
 * <p>This information is used to both maintain a history of all objects
 * modified through TopLink and to enable point in time querying.
 * <p>If Oracle 9R2 or later Flashback is used this policy is not required, as
 * the preservation of history is automatic.
 * <p>Descriptors, ManyToManyMappings, DirectCollectionMappings,
 * and DirectMapMappings only can have a history policy, as only they have associated
 * database tables.
 * @author Stephen McRitchie
 * @since 10
 */
public class HistoryPolicy implements Cloneable, Serializable {
    protected ClassDescriptor descriptor;
    protected DatabaseMapping mapping;
    protected List<DatabaseTable> historicalTables;
    protected List<DatabaseField> startFields;
    protected List<DatabaseField> endFields;
    protected boolean shouldHandleWrites = true;
    protected boolean usesLocalTime = true;

    public HistoryPolicy() {
    }

    /**
     * INTERNAL:
     * Add any temporal querying conditions to this object expression.
     */
    public Expression additionalHistoryExpression(Expression context, Expression base) {
        return additionalHistoryExpression(context, base, null);
    }

    /**
     * INTERNAL:
     * Add any temporal querying conditions to this object expression.
     * @parameter Integer tableIndex not null indicates that only expression for a single table should be returned.
     */
    public Expression additionalHistoryExpression(Expression context, Expression base, Integer tableIndex) {
        //
        AsOfClause clause = base.getAsOfClause();
        Object value = clause.getValue();
        Expression join = null;
        Expression subJoin = null;
        Expression start = null;
        Expression end = null;
        if (value == null) {
            return null;
            // for now nothing as assume mirroring historical tables.
        } else {
            if (value instanceof Expression) {
                // Sort of an implementation of native sql.
                // Print AS OF TIMESTAMP (SYSDATE - 1000*60*10) not AS OF ('SYSDATE - 1000*60*10').
                if ((value instanceof ConstantExpression) && (((ConstantExpression)value).getValue() instanceof String)) {
                    value = (((ConstantExpression)value).getValue());
                }
            } else {
                ConversionManager converter = ConversionManager.getDefaultManager();
                value = converter.convertObject(value, ClassConstants.TIMESTAMP);
            }

            if (getMapping() != null) {
                if (tableIndex != null && tableIndex.intValue() > 0) {
                    return null;
                }
                TableExpression tableExp = null;
                DatabaseTable historicalTable = getHistoricalTables().get(0);
                tableExp = (TableExpression)((ObjectExpression)base).existingDerivedTable(historicalTable);

                start = tableExp.getField(getStart());
                end = tableExp.getField(getEnd());

                join = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value)));

                // We also need to do step two here in advance.	
                tableExp.setTable(historicalTable);

                return join;
            }
            int iFirst, iLast;
            if (tableIndex == null) {
                // loop through all history tables
                iFirst = 0;
                iLast = getHistoricalTables().size() - 1;
            } else {
                // only return expression for the specified table
                iFirst = tableIndex.intValue();
                iLast = iFirst;
            }
            for (int i = iFirst; i <= iLast ; i++) {
                start = base.getField(getStart(i));
                end = base.getField(getEnd(i));

                subJoin = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value)));
                join = ((join == null) ? subJoin : join.and(subJoin));
            }
            return join;
        }
    }

    /**
     * PUBLIC:
     * Performs a sufficiently deep clone.
     * Use to quickly setup standard policies on multiple descriptors.
     */
    public Object clone() {
        HistoryPolicy clone = null;
        try {
            clone = (HistoryPolicy)super.clone();
        } catch (CloneNotSupportedException ignore) {
        }
        if (startFields != null) {
            clone.setStartFields(new ArrayList(startFields.size()));
            for (DatabaseField field : startFields) {
                clone.getStartFields().add(field.clone());
            }
        }
        if (endFields != null) {
            clone.setEndFields(new ArrayList(endFields.size()));
            for (DatabaseField field : endFields) {
                clone.getEndFields().add(field.clone());
            }
        }
        if (historicalTables != null) {
            clone.setHistoricalTables(new ArrayList(historicalTables));
        }
        return clone;
    }

    /**
     * PUBLIC:
     * Whenever a historical record is logically deleted (updated) or inserted,
     * the end and start fields respectively will be set to this value.
     */
    public Object getCurrentTime(AbstractSession session) {
        if (shouldUseLocalTime()) {
            return new java.sql.Timestamp(System.currentTimeMillis());
        }
        if (shouldUseDatabaseTime()) {
            AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass());
            while (readSession.isUnitOfWork()) {
                readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass());
            }
            return readSession.getDatasourceLogin().getDatasourcePlatform().getTimestampFromServer(session, readSession.getName());
        }
        return null;
    }

    /**
     * INTERNAL:
     * Return a minimal time increment supported by the platform.
     */
    public long getMinimumTimeIncrement(AbstractSession session) {
        AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass());
        while (readSession.isUnitOfWork()) {
            readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass());
        }
        return readSession.getPlatform().minimumTimeIncrement();
    }

    /**
     * PUBLIC:
     * Return the descriptor of the policy.
     */
    public ClassDescriptor getDescriptor() {
        return descriptor;
    }

    /**
     * INTERNAL:
     */
    public final List<DatabaseTable> getHistoricalTables() {
        if (historicalTables == null) {
            historicalTables = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(1);
        }
        return historicalTables;
    }

    /**
     * PUBLIC:
     */
    public List<String> getHistoryTableNames() {
        List<String> names = new ArrayList(getHistoricalTables().size());
        for (DatabaseTable table : getHistoricalTables()) {
            names.add(table.getQualifiedName());
        }
        return names;
    }

    /**
     * PUBLIC:
     */
    public DatabaseMapping getMapping() {
        return mapping;
    }

    /**
     * INTERNAL:
     */
    protected DatabaseField getStart() {
        if (startFields != null) {
            return startFields.get(0);
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     */
    protected DatabaseField getStart(int i) {
        return startFields.get(i);
    }

    /**
     * PUBLIC:
     * Answers the name of the start field.  Assumes that multiple tables
     * for a descriptor have the same field names.
     */
    public String getStartFieldName() {
        if (getStart() != null) {
            return getStart().getName();
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     */
    public List<DatabaseField> getStartFields() {
        return startFields;
    }

    /**
     * INTERNAL:
     */
    protected DatabaseField getEnd() {
        if (endFields != null) {
            return endFields.get(0);
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     */
    protected DatabaseField getEnd(int i) {
        return endFields.get(i);
    }

    /**
     * PUBLIC:
     */
    public String getEndFieldName() {
        if (getEnd() != null) {
            return getEnd().getName();
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     */
    public List<DatabaseField> getEndFields() {
        return endFields;
    }

    /**
     * PUBLIC:
     */
    public void setDescriptor(ClassDescriptor descriptor) {
        this.descriptor = descriptor;
    }

    /**
     * INTERNAL:
     * Initialize a HistoryPolicy.
     */
    public void initialize(AbstractSession session) {
        if (getMapping() != null) {
            setDescriptor(getMapping().getDescriptor());
            if (getMapping().isDirectCollectionMapping()) {
                DatabaseTable refTable = ((DirectCollectionMapping)getMapping()).getReferenceTable();
                DatabaseTable histTable = getHistoricalTables().get(0);
                histTable.setName(refTable.getName());
                histTable.setTableQualifier(refTable.getTableQualifier());
                getStart().setTable(histTable);
                getEnd().setTable(histTable);
            } else if (getMapping().isManyToManyMapping()) {
                DatabaseTable relationTable = ((ManyToManyMapping)getMapping()).getRelationTable();
                DatabaseTable histTable = getHistoricalTables().get(0);
                histTable.setName(relationTable.getName());
                histTable.setTableQualifier(relationTable.getTableQualifier());
                getStart().setTable(histTable);
                getEnd().setTable(histTable);
            }
            verifyTableQualifiers(session.getPlatform());
            return;
        }

        // Some historicalTables will be inherited from a parent policy.
        int offset = getDescriptor().getTables().size() - getHistoricalTables().size();

        // In this configuration descriptor tables, history tables, and start/end fields
        // are all in the same order.
        if (!getHistoricalTables().isEmpty() && getHistoricalTables().get(0).getName().equals("")) {
            for (int i = 0; i < getHistoricalTables().size(); i++) {
                DatabaseTable table = getHistoricalTables().get(i);
                if (table.getName().equals("")) {
                    DatabaseTable mirrored = getDescriptor().getTables().get(i + offset);
                    table.setName(mirrored.getName());
                    table.setTableQualifier(mirrored.getTableQualifier());
                }
                if (getStartFields().size() < (i + 1)) {
                    DatabaseField startField = getStart(0).clone();
                    startField.setTable(table);
                    getStartFields().add(startField);
                } else {
                    DatabaseField startField = getStart(i);
                    startField.setTable(table);
                }
                if (getEndFields().size() < (i + 1)) {
                    DatabaseField endField = getEnd(0).clone();
                    endField.setTable(table);
                    getEndFields().add(endField);
                } else {
                    DatabaseField endField = getEnd(i);
                    endField.setTable(table);
                }
            }
        } else {
            // The user did not specify history tables/fields in order, so
            // initialize will take a little longer.
            List<DatabaseTable> unsortedTables = getHistoricalTables();
            List<DatabaseTable> sortedTables = new ArrayList(unsortedTables.size());
            List<DatabaseField> sortedStartFields = new ArrayList(unsortedTables.size());
            List<DatabaseField> sortedEndFields = new ArrayList(unsortedTables.size());
            boolean universalStartField = ((getStartFields().size() == 1) && (!(getStartFields().get(0)).hasTableName()));
            boolean universalEndField = ((getEndFields().size() == 1) && (!(getEndFields().get(0)).hasTableName()));
            DatabaseTable descriptorTable = null;
            DatabaseTable historicalTable = null;
            DatabaseField historyField = null;

            List<DatabaseTable> descriptorTables = getDescriptor().getTables();
            for (int i = offset; i < descriptorTables.size(); i++) {
                descriptorTable = descriptorTables.get(i);

                int index = unsortedTables.indexOf(descriptorTable);
                if (index == -1) {
                    // this is a configuration error!
                }
                historicalTable = unsortedTables.get(index);
                historicalTable.setTableQualifier(descriptorTable.getTableQualifier());
                sortedTables.add(historicalTable);

                if (universalStartField) {
                    historyField = getStart(0).clone();
                    historyField.setTable(historicalTable);
                    sortedStartFields.add(historyField);
                } else {
                    for (DatabaseField field : getStartFields()) {
                        if (field.getTable().equals(historicalTable)) {
                            sortedStartFields.add(field);
                            break;
                        }
                    }
                }
                if (universalEndField) {
                    historyField = getEnd(0).clone();
                    historyField.setTable(historicalTable);
                    sortedEndFields.add(historyField);
                } else {
                    for (DatabaseField field : getEndFields()) {
                        if (field.getTable().equals(historicalTable)) {
                            sortedEndFields.add(field);
                            break;
                        }
                    }
                }
            }
            setHistoricalTables(sortedTables);
            setStartFields(sortedStartFields);
            setEndFields(sortedEndFields);
        }
        verifyTableQualifiers(session.getPlatform());

        // A user need not set a policy on every level of an inheritance, but
        // historic tables can be inherited.
        if (getDescriptor().hasInheritance()) {
            ClassDescriptor parentDescriptor = getDescriptor().getInheritancePolicy().getParentDescriptor();
            while ((parentDescriptor != null) && (parentDescriptor.getHistoryPolicy() == null)) {
                parentDescriptor = parentDescriptor.getInheritancePolicy().getParentDescriptor();
            }
            if (parentDescriptor != null) {
                // Unique is required because the builder can add the same table many times.
                // This is done after init properties to make sure the default table is the first local one.
                setHistoricalTables(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getHistoricalTables(), getHistoricalTables()));
                setStartFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getStartFields(), getStartFields()));
                setEndFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getEndFields(), getEndFields()));
            }
        }
    }

    /**
     * PUBLIC:
     * Use to specify the names of the mirroring historical tables.
     * <p>
     * Assumes that the order in which tables are added with descriptor.addTableName()
     * matches the order in which mirroring historical tables are added with
     * descriptor.addHistoryTableName().
     */
    public void addHistoryTableName(String name) {
        HistoricalDatabaseTable table = new HistoricalDatabaseTable("");
        table.setHistoricalName(name);
        getHistoricalTables().add(table);
    }

    /**
     * PUBLIC:
     * Use to specify the names of the mirroring historical tables.
     * <p>
     * Explicitly states that <code>sourceTableName</code> is mirrored by history table
     * <code>historyTableName</code>.
     * The order in which tables are added with descriptor.addTableName()
     * should still match the order in which mirroring historical tables are
     * added with descriptor.addMirroringHistoryTableName().
     */
    public void addHistoryTableName(String sourceTableName, String historyTableName) {
        if ((sourceTableName == null) || sourceTableName.equals("")) {
            addHistoryTableName(historyTableName);
        }
        HistoricalDatabaseTable table = new HistoricalDatabaseTable(sourceTableName);
        table.setHistoricalName(historyTableName);
        // Note that the equality check is only on sourceTableName, not historyTableName.
        int index = getHistoricalTables().indexOf(table);
        if (index == -1) {
            getHistoricalTables().add(table);
        } else {
            getHistoricalTables().set(index, table);
        }
    }

    /**
     * INTERNAL:
     */
    public void setHistoricalTables(List<DatabaseTable> historicalTables) {
        this.historicalTables = historicalTables;
    }

    /**
     * INTERNAL:
     */
    public void setMapping(DatabaseMapping mapping) {
        this.mapping = mapping;
    }

    /**
     * INTERNAL:
     */
    protected void setStartFields(List<DatabaseField> startFields) {
        this.startFields = startFields;
    }

    /**
     * PUBLIC:
     * Sets the name of the start field.
     * <p>
     * By default all tables belonging to a descriptor have the same primary
     * key field names, and so the same start field names also.
     * <p>
     * However, if <code>startFieldName</code> is qualified, i.e. of the form
     * "EMPLOYEE_HIST.EMP_START", then this call will only set the start field
     * name for a single historical table.
     */
    public void addStartFieldName(String startFieldName) {
        DatabaseField startField = new DatabaseField(startFieldName);
        startField.setType(ClassConstants.TIMESTAMP);

        if (startFields == null) {
            startFields = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance();
            startFields.add(startField);
            return;
        }

        for (DatabaseField existing : startFields) {
            if (startField.getTableName().equals(existing.getTableName())) {
                existing.setName(startField.getName());
                return;
            }
        }
        startFields.add(startField);
    }

    /**
     * ADVANCED:
     * Sets the type of all start fields.  Not required to be set as the default
     * of Timestamp is assumed.
     */
    public void setStartFieldType(Class type) {
        for (DatabaseField existing : startFields) {
            existing.setType(type);
        }
    }

    /**
     * INTERNAL:
     */
    protected void setEndFields(List<DatabaseField> endFields) {
        this.endFields = endFields;
    }

    /**
     * PUBLIC:
     * @see #addStartFieldName
     */
    public void addEndFieldName(String endFieldName) {
        DatabaseField endField = new DatabaseField(endFieldName);
        endField.setType(ClassConstants.TIMESTAMP);

        if (endFields == null) {
            endFields = new ArrayList();
            endFields.add(endField);
            return;
        }

        for (DatabaseField existing : endFields) {
            if (endField.getTableName().equals(existing.getTableName())) {
                existing.setName(endField.getName());
                return;
            }
        }
        endFields.add(endField);
    }

    /**
     * ADVANCED:
     * @see #setStartFieldType
     */
    public void setEndFieldType(String fieldName, Class type) {
        for (DatabaseField existing : endFields) {
            existing.setType(type);
        }
    }

    /**
     * Sets if TopLink is responsible for writing history.
     * <p>
     * If history is maintained via low level database triggers or application
     * logic a policy is still needed for point in time querying.
     * <p>
     * If Oracle flashback is used no HistoryPolicy is needed.
     * <p>
     * Setting this to false lets you use History for many other applications.
     * For instance a table that tracks available flights or hotel deals may
     * benefit from a HistoryPolicy just to simplify temporal querying.
     * <p>If all hotel discounts have a start and end date, you could query on
     * all discounts available at a certain date.
     */
    public void setShouldHandleWrites(boolean value) {
        this.shouldHandleWrites = value;
    }

    /**
     * Answers if TopLink is responsible for writing history.
     * <p>
     * If history is maintained via low level database triggers or application
     * logic a policy is still usefull for point in time querying.
     * <p>
     * If Oracle flashback is used no HistoryPolicy is needed.
     * @return true by default
     * @see #setShouldHandleWrites
     */
    public boolean shouldHandleWrites() {
        return shouldHandleWrites;
    }

    /**
     * Sets if the Timestamp used in maintainaing history should be the
     * current time according to the database.
     * @param value if false uses localTime (default) instead
     */
    public void setShouldUseDatabaseTime(boolean value) {
        usesLocalTime = !value;
    }

    /**
     * Answers if the Timestamp used in maintaining history should be
     * System.currentTimeMillis();
     * @see #shouldUseDatabaseTime
     * @see #useLocalTime
     * @return true by default
     */
    public boolean shouldUseLocalTime() {
        return usesLocalTime;
    }

    /**
     * Answers if the Timestamp used in maintaining history should be the
     * current time according to the database.
     * @see #shouldUseLocalTime
     * @see #useDatabaseTime
     * @return false by default
     */
    public boolean shouldUseDatabaseTime() {
        return !usesLocalTime;
    }

    /**
     * Answers if the Timestamp used in maintaining history should be
     * System.currentTimeMillis();
     * @see #useDatabaseTime
     * @see #shouldUseLocalTime
     */
    public void useLocalTime() {
        usesLocalTime = true;
    }

    /**
     * Answers if the Timestamp used in maintaining history should be the
     * current time according to the database.
     * @see #useLocalTime
     * @see #shouldUseDatabaseTime
     */
    public void useDatabaseTime() {
        usesLocalTime = false;
    }

    /**
     * INTERNAL: Check that the qualifiers on the historical tables are
     * properly set.
     * <p>A similar method exists on ClassDescriptor.
     */
    protected void verifyTableQualifiers(DatasourcePlatform platform) {
        String tableQualifier = platform.getTableQualifier();

        if (tableQualifier.length() == 0) {
            return;
        }

        for (DatabaseTable table : getHistoricalTables()) {
            // Build a scratch table to see if history table name has a qualifier.
            DatabaseTable scratchTable = new DatabaseTable(table.getQualifiedName());
            if (scratchTable.getTableQualifier().length() == 0) {
                scratchTable.setTableQualifier(tableQualifier);
                ((HistoricalDatabaseTable)table).setHistoricalName(scratchTable.getQualifiedNameDelimited(platform));
            }
        }
    }

    /**
     * INTERNAL:
     * Checks for the case where an object has multiple tables but only some
     * are part of a minimal update.
     */
    protected boolean checkWastedVersioning(AbstractRecord modifyRow, DatabaseTable table) {
        for (Enumeration fieldsEnum = modifyRow.keys(); fieldsEnum.hasMoreElements();) {
            DatabaseField field = (DatabaseField)fieldsEnum.nextElement();
            if (field.getTable().equals(table) || (!field.hasTableName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * INTERNAL:
     */
    public void postDelete(ModifyQuery deleteQuery) {
        logicalDelete(deleteQuery, false);
    }
    
    /**
     * INTERNAL:
     */
    // Bug 319276 - pass whether shallow insert/update
    public void postUpdate(ObjectLevelModifyQuery writeQuery) {
        postUpdate(writeQuery, false);
    }

    /**
     * INTERNAL:
     */
    public void postUpdate(ObjectLevelModifyQuery writeQuery, boolean isShallow) {
        logicalDelete(writeQuery, true, isShallow);
        logicalInsert(writeQuery, true);
    }

    /**
     * INTERNAL:
     */
    public void postInsert(ObjectLevelModifyQuery writeQuery) {
        logicalInsert(writeQuery, false);
    }

    /**
     * INTERNAL:
     * Perform a logical insert into the historical schema, creating a new version
     * of an object.
     * <p>Called by postInsert() and also postUpdate() (which first does a logicalDelete
     * of the previous version).
     */
    public void logicalInsert(ObjectLevelModifyQuery writeQuery, boolean isUpdate) {
        ClassDescriptor descriptor =(ClassDescriptor) getDescriptor().clone();
        descriptor.setSequenceNumberField(null);//Do not retrive the sequence number
        writeQuery.setDescriptor(descriptor);
        AbstractRecord modifyRow = null;
        AbstractRecord originalModifyRow = writeQuery.getModifyRow();
        Object currentTime = null;
        if (isUpdate) {
            modifyRow = descriptor.getObjectBuilder().buildRow(writeQuery.getObject(), writeQuery.getSession(), WriteType.UPDATE); // Bug 319276
            // If anyone added items to the modify row, then they should also be added here.
            modifyRow.putAll(originalModifyRow);
        } else {
            modifyRow = originalModifyRow;
            // If update would have already discovered timestamp to use.
            currentTime = getCurrentTime(writeQuery.getSession());
        }
        StatementQueryMechanism insertMechanism = new StatementQueryMechanism(writeQuery);

        for (int i = 0; i < getHistoricalTables().size(); i++) {
            DatabaseTable table = getHistoricalTables().get(i);
            if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) {
                continue;
            }
            if (!isUpdate) {
                modifyRow.add(getStart(i), currentTime);
            }
            SQLInsertStatement insertStatement = new SQLInsertStatement();
            insertStatement.setTable(table);
            insertMechanism.getSQLStatements().add(insertStatement);
        }
        if (insertMechanism.hasMultipleStatements()) {
            writeQuery.setTranslationRow(modifyRow);
            writeQuery.setModifyRow(modifyRow);
            insertMechanism.insertObject();
        }
    }

    /**
     * INTERNAL:
     * Performs a logical insert into the historical schema.  Direct
     * collections and many to many mappings are maintained through the session
     * events.
     */
    public void mappingLogicalInsert(DataModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) {
        DataModifyQuery historyQuery = new DataModifyQuery();
        SQLInsertStatement historyStatement = new SQLInsertStatement();
        DatabaseTable histTable = getHistoricalTables().get(0);

        historyStatement.setTable(histTable);
        AbstractRecord modifyRow = originalQuery.getModifyRow().clone();
        AbstractRecord translationRow = arguments.clone();

        // Start could be the version field in timestamp locking.
        if (!modifyRow.containsKey(getStart())) {
            Object time = getCurrentTime(session);
            modifyRow.add(getStart(), time);
            translationRow.add(getStart(), time);
        }
        historyQuery.setSQLStatement(historyStatement);
        historyQuery.setModifyRow(modifyRow);
        historyStatement.setModifyRow(modifyRow);
        session.executeQuery(historyQuery, translationRow);
    }
    
    /**
     * INTERNAL:
     * Performs a logical delete (update) on the historical schema.
     */
    // Bug 319276 - pass whether shallow insert/update
    public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate) {
        logicalDelete(writeQuery, isUpdate, false);
    }

    /**
     * INTERNAL:
     * Performs a logical delete (update) on the historical schema.
     */
    public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate, boolean isShallow) {
        ClassDescriptor descriptor = writeQuery.getDescriptor();
        AbstractRecord originalModifyRow = writeQuery.getModifyRow();
        AbstractRecord modifyRow = new DatabaseRecord();
        StatementQueryMechanism updateMechanism = new StatementQueryMechanism(writeQuery);
        Object currentTime = getCurrentTime(writeQuery.getSession());

        for (int i = 0; i < getHistoricalTables().size(); i++) {
            DatabaseTable table = getHistoricalTables().get(i);

            if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) {
                continue;
            }
            SQLUpdateStatement updateStatement = new SQLUpdateStatement();
            updateStatement.setTable(table);
            Expression whereClause = null;
            if (writeQuery instanceof DeleteAllQuery) {
                if (writeQuery.getSelectionCriteria() != null) {
                    whereClause = (Expression)writeQuery.getSelectionCriteria().clone();
                }
            } else {
                whereClause = descriptor.getObjectBuilder().buildPrimaryKeyExpression(table);
            }
            ExpressionBuilder builder = ((whereClause == null) ? new ExpressionBuilder() : whereClause.getBuilder());
            whereClause = builder.getField(getEnd(i)).isNull().and(whereClause);
            updateStatement.setWhereClause(whereClause);

            modifyRow.add(getEnd(i), currentTime); 

            // save a little time here and add the same timestamp value for
            // the start field in the logicalInsert.
            if (isUpdate) {
                if (isShallow) {
                    // Bug 319276 - increment the timestamp by 1 to avoid unique constraint violation potential 
                    java.sql.Timestamp  incrementedTime = (java.sql.Timestamp) currentTime;
                    incrementedTime.setTime(incrementedTime.getTime() + getMinimumTimeIncrement(writeQuery.getSession()));
                    originalModifyRow.add(getStart(i), incrementedTime); 
                } else {
                    originalModifyRow.add(getStart(i), currentTime);
                }
            }
            updateMechanism.getSQLStatements().add(updateStatement);
        }
        if (updateMechanism.hasMultipleStatements()) {
            writeQuery.setModifyRow(modifyRow);
            updateMechanism.updateObject();
            writeQuery.setModifyRow(originalModifyRow);
        }
    }

    /**
     * INTERNAL:
     * Performs a logical delete (update) on the historical schema.  Direct
     * collections and many to many mappings are maintained through the session
     * events.
     */
    public void mappingLogicalDelete(ModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) {
        SQLDeleteStatement originalStatement = (SQLDeleteStatement)originalQuery.getSQLStatement();

        DataModifyQuery historyQuery = new DataModifyQuery();
        SQLUpdateStatement historyStatement = new SQLUpdateStatement();
        DatabaseTable histTable = getHistoricalTables().get(0);

        historyStatement.setTable(histTable);
        Expression whereClause = (Expression)originalStatement.getWhereClause().clone();
        DatabaseField endField = getEnd();
        whereClause = whereClause.getBuilder().getField(endField).isNull().and(whereClause);
        historyStatement.setWhereClause(whereClause);
        AbstractRecord modifyRow = new DatabaseRecord();
        AbstractRecord translationRow = arguments.clone();
        Object time = getCurrentTime(session);
        modifyRow.add(getEnd(), time);
        translationRow.add(getEnd(), time);
        historyStatement.setModifyRow(modifyRow);
        historyQuery.setSQLStatement(historyStatement);
        historyQuery.setModifyRow(modifyRow);
        session.executeQuery(historyQuery, translationRow);
    }
}
Comment 6 Nicolas Marcotte CLA 2014-10-31 14:41:52 EDT
Comment on attachment 248311 [details]
A HistoryPolicy that do not fetch the last inserted id

>/*******************************************************************************
> * Copyright (c) 1998, 2013 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 v1.0 and Eclipse Distribution License v. 1.0 
> * which accompanies this distribution. 
> * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
> * and the Eclipse Distribution License is available at 
> * http://www.eclipse.org/org/documents/edl-v10.php.
> *
> * Contributors:
> *     Oracle - initial API and implementation from Oracle TopLink
> ******************************************************************************/  
>package org.eclipse.persistence.history;
>
>import java.io.Serializable;
>import java.util.*;
>import org.eclipse.persistence.descriptors.ClassDescriptor;
>import org.eclipse.persistence.expressions.*;
>import org.eclipse.persistence.internal.databaseaccess.*;
>import org.eclipse.persistence.internal.expressions.*;
>import org.eclipse.persistence.internal.history.*;
>import org.eclipse.persistence.internal.helper.*;
>import org.eclipse.persistence.internal.queries.*;
>import org.eclipse.persistence.internal.sessions.AbstractRecord;
>import org.eclipse.persistence.internal.sessions.AbstractSession;
>import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
>import org.eclipse.persistence.mappings.*;
>import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
>import org.eclipse.persistence.sessions.DatabaseRecord;
>import org.eclipse.persistence.queries.*;
>
>/**
> * <b>Purpose:</b>Expresses how historical data is saved on the data store.
> * <p>This information is used to both maintain a history of all objects
> * modified through TopLink and to enable point in time querying.
> * <p>If Oracle 9R2 or later Flashback is used this policy is not required, as
> * the preservation of history is automatic.
> * <p>Descriptors, ManyToManyMappings, DirectCollectionMappings,
> * and DirectMapMappings only can have a history policy, as only they have associated
> * database tables.
> * @author Stephen McRitchie
> * @since 10
> */
>public class HistoryPolicy implements Cloneable, Serializable {
>    protected ClassDescriptor descriptor;
>    protected DatabaseMapping mapping;
>    protected List<DatabaseTable> historicalTables;
>    protected List<DatabaseField> startFields;
>    protected List<DatabaseField> endFields;
>    protected boolean shouldHandleWrites = true;
>    protected boolean usesLocalTime = true;
>
>    public HistoryPolicy() {
>    }
>
>    /**
>     * INTERNAL:
>     * Add any temporal querying conditions to this object expression.
>     */
>    public Expression additionalHistoryExpression(Expression context, Expression base) {
>        return additionalHistoryExpression(context, base, null);
>    }
>
>    /**
>     * INTERNAL:
>     * Add any temporal querying conditions to this object expression.
>     * @parameter Integer tableIndex not null indicates that only expression for a single table should be returned.
>     */
>    public Expression additionalHistoryExpression(Expression context, Expression base, Integer tableIndex) {
>        //
>        AsOfClause clause = base.getAsOfClause();
>        Object value = clause.getValue();
>        Expression join = null;
>        Expression subJoin = null;
>        Expression start = null;
>        Expression end = null;
>        if (value == null) {
>            return null;
>            // for now nothing as assume mirroring historical tables.
>        } else {
>            if (value instanceof Expression) {
>                // Sort of an implementation of native sql.
>                // Print AS OF TIMESTAMP (SYSDATE - 1000*60*10) not AS OF ('SYSDATE - 1000*60*10').
>                if ((value instanceof ConstantExpression) && (((ConstantExpression)value).getValue() instanceof String)) {
>                    value = (((ConstantExpression)value).getValue());
>                }
>            } else {
>                ConversionManager converter = ConversionManager.getDefaultManager();
>                value = converter.convertObject(value, ClassConstants.TIMESTAMP);
>            }
>
>            if (getMapping() != null) {
>                if (tableIndex != null && tableIndex.intValue() > 0) {
>                    return null;
>                }
>                TableExpression tableExp = null;
>                DatabaseTable historicalTable = getHistoricalTables().get(0);
>                tableExp = (TableExpression)((ObjectExpression)base).existingDerivedTable(historicalTable);
>
>                start = tableExp.getField(getStart());
>                end = tableExp.getField(getEnd());
>
>                join = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value)));
>
>                // We also need to do step two here in advance.	
>                tableExp.setTable(historicalTable);
>
>                return join;
>            }
>            int iFirst, iLast;
>            if (tableIndex == null) {
>                // loop through all history tables
>                iFirst = 0;
>                iLast = getHistoricalTables().size() - 1;
>            } else {
>                // only return expression for the specified table
>                iFirst = tableIndex.intValue();
>                iLast = iFirst;
>            }
>            for (int i = iFirst; i <= iLast ; i++) {
>                start = base.getField(getStart(i));
>                end = base.getField(getEnd(i));
>
>                subJoin = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value)));
>                join = ((join == null) ? subJoin : join.and(subJoin));
>            }
>            return join;
>        }
>    }
>
>    /**
>     * PUBLIC:
>     * Performs a sufficiently deep clone.
>     * Use to quickly setup standard policies on multiple descriptors.
>     */
>    public Object clone() {
>        HistoryPolicy clone = null;
>        try {
>            clone = (HistoryPolicy)super.clone();
>        } catch (CloneNotSupportedException ignore) {
>        }
>        if (startFields != null) {
>            clone.setStartFields(new ArrayList(startFields.size()));
>            for (DatabaseField field : startFields) {
>                clone.getStartFields().add(field.clone());
>            }
>        }
>        if (endFields != null) {
>            clone.setEndFields(new ArrayList(endFields.size()));
>            for (DatabaseField field : endFields) {
>                clone.getEndFields().add(field.clone());
>            }
>        }
>        if (historicalTables != null) {
>            clone.setHistoricalTables(new ArrayList(historicalTables));
>        }
>        return clone;
>    }
>
>    /**
>     * PUBLIC:
>     * Whenever a historical record is logically deleted (updated) or inserted,
>     * the end and start fields respectively will be set to this value.
>     */
>    public Object getCurrentTime(AbstractSession session) {
>        if (shouldUseLocalTime()) {
>            return new java.sql.Timestamp(System.currentTimeMillis());
>        }
>        if (shouldUseDatabaseTime()) {
>            AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass());
>            while (readSession.isUnitOfWork()) {
>                readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass());
>            }
>            return readSession.getDatasourceLogin().getDatasourcePlatform().getTimestampFromServer(session, readSession.getName());
>        }
>        return null;
>    }
>
>    /**
>     * INTERNAL:
>     * Return a minimal time increment supported by the platform.
>     */
>    public long getMinimumTimeIncrement(AbstractSession session) {
>        AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass());
>        while (readSession.isUnitOfWork()) {
>            readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass());
>        }
>        return readSession.getPlatform().minimumTimeIncrement();
>    }
>
>    /**
>     * PUBLIC:
>     * Return the descriptor of the policy.
>     */
>    public ClassDescriptor getDescriptor() {
>        return descriptor;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public final List<DatabaseTable> getHistoricalTables() {
>        if (historicalTables == null) {
>            historicalTables = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(1);
>        }
>        return historicalTables;
>    }
>
>    /**
>     * PUBLIC:
>     */
>    public List<String> getHistoryTableNames() {
>        List<String> names = new ArrayList(getHistoricalTables().size());
>        for (DatabaseTable table : getHistoricalTables()) {
>            names.add(table.getQualifiedName());
>        }
>        return names;
>    }
>
>    /**
>     * PUBLIC:
>     */
>    public DatabaseMapping getMapping() {
>        return mapping;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected DatabaseField getStart() {
>        if (startFields != null) {
>            return startFields.get(0);
>        } else {
>            return null;
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected DatabaseField getStart(int i) {
>        return startFields.get(i);
>    }
>
>    /**
>     * PUBLIC:
>     * Answers the name of the start field.  Assumes that multiple tables
>     * for a descriptor have the same field names.
>     */
>    public String getStartFieldName() {
>        if (getStart() != null) {
>            return getStart().getName();
>        } else {
>            return null;
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public List<DatabaseField> getStartFields() {
>        return startFields;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected DatabaseField getEnd() {
>        if (endFields != null) {
>            return endFields.get(0);
>        } else {
>            return null;
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected DatabaseField getEnd(int i) {
>        return endFields.get(i);
>    }
>
>    /**
>     * PUBLIC:
>     */
>    public String getEndFieldName() {
>        if (getEnd() != null) {
>            return getEnd().getName();
>        } else {
>            return null;
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public List<DatabaseField> getEndFields() {
>        return endFields;
>    }
>
>    /**
>     * PUBLIC:
>     */
>    public void setDescriptor(ClassDescriptor descriptor) {
>        this.descriptor = descriptor;
>    }
>
>    /**
>     * INTERNAL:
>     * Initialize a HistoryPolicy.
>     */
>    public void initialize(AbstractSession session) {
>        if (getMapping() != null) {
>            setDescriptor(getMapping().getDescriptor());
>            if (getMapping().isDirectCollectionMapping()) {
>                DatabaseTable refTable = ((DirectCollectionMapping)getMapping()).getReferenceTable();
>                DatabaseTable histTable = getHistoricalTables().get(0);
>                histTable.setName(refTable.getName());
>                histTable.setTableQualifier(refTable.getTableQualifier());
>                getStart().setTable(histTable);
>                getEnd().setTable(histTable);
>            } else if (getMapping().isManyToManyMapping()) {
>                DatabaseTable relationTable = ((ManyToManyMapping)getMapping()).getRelationTable();
>                DatabaseTable histTable = getHistoricalTables().get(0);
>                histTable.setName(relationTable.getName());
>                histTable.setTableQualifier(relationTable.getTableQualifier());
>                getStart().setTable(histTable);
>                getEnd().setTable(histTable);
>            }
>            verifyTableQualifiers(session.getPlatform());
>            return;
>        }
>
>        // Some historicalTables will be inherited from a parent policy.
>        int offset = getDescriptor().getTables().size() - getHistoricalTables().size();
>
>        // In this configuration descriptor tables, history tables, and start/end fields
>        // are all in the same order.
>        if (!getHistoricalTables().isEmpty() && getHistoricalTables().get(0).getName().equals("")) {
>            for (int i = 0; i < getHistoricalTables().size(); i++) {
>                DatabaseTable table = getHistoricalTables().get(i);
>                if (table.getName().equals("")) {
>                    DatabaseTable mirrored = getDescriptor().getTables().get(i + offset);
>                    table.setName(mirrored.getName());
>                    table.setTableQualifier(mirrored.getTableQualifier());
>                }
>                if (getStartFields().size() < (i + 1)) {
>                    DatabaseField startField = getStart(0).clone();
>                    startField.setTable(table);
>                    getStartFields().add(startField);
>                } else {
>                    DatabaseField startField = getStart(i);
>                    startField.setTable(table);
>                }
>                if (getEndFields().size() < (i + 1)) {
>                    DatabaseField endField = getEnd(0).clone();
>                    endField.setTable(table);
>                    getEndFields().add(endField);
>                } else {
>                    DatabaseField endField = getEnd(i);
>                    endField.setTable(table);
>                }
>            }
>        } else {
>            // The user did not specify history tables/fields in order, so
>            // initialize will take a little longer.
>            List<DatabaseTable> unsortedTables = getHistoricalTables();
>            List<DatabaseTable> sortedTables = new ArrayList(unsortedTables.size());
>            List<DatabaseField> sortedStartFields = new ArrayList(unsortedTables.size());
>            List<DatabaseField> sortedEndFields = new ArrayList(unsortedTables.size());
>            boolean universalStartField = ((getStartFields().size() == 1) && (!(getStartFields().get(0)).hasTableName()));
>            boolean universalEndField = ((getEndFields().size() == 1) && (!(getEndFields().get(0)).hasTableName()));
>            DatabaseTable descriptorTable = null;
>            DatabaseTable historicalTable = null;
>            DatabaseField historyField = null;
>
>            List<DatabaseTable> descriptorTables = getDescriptor().getTables();
>            for (int i = offset; i < descriptorTables.size(); i++) {
>                descriptorTable = descriptorTables.get(i);
>
>                int index = unsortedTables.indexOf(descriptorTable);
>                if (index == -1) {
>                    // this is a configuration error!
>                }
>                historicalTable = unsortedTables.get(index);
>                historicalTable.setTableQualifier(descriptorTable.getTableQualifier());
>                sortedTables.add(historicalTable);
>
>                if (universalStartField) {
>                    historyField = getStart(0).clone();
>                    historyField.setTable(historicalTable);
>                    sortedStartFields.add(historyField);
>                } else {
>                    for (DatabaseField field : getStartFields()) {
>                        if (field.getTable().equals(historicalTable)) {
>                            sortedStartFields.add(field);
>                            break;
>                        }
>                    }
>                }
>                if (universalEndField) {
>                    historyField = getEnd(0).clone();
>                    historyField.setTable(historicalTable);
>                    sortedEndFields.add(historyField);
>                } else {
>                    for (DatabaseField field : getEndFields()) {
>                        if (field.getTable().equals(historicalTable)) {
>                            sortedEndFields.add(field);
>                            break;
>                        }
>                    }
>                }
>            }
>            setHistoricalTables(sortedTables);
>            setStartFields(sortedStartFields);
>            setEndFields(sortedEndFields);
>        }
>        verifyTableQualifiers(session.getPlatform());
>
>        // A user need not set a policy on every level of an inheritance, but
>        // historic tables can be inherited.
>        if (getDescriptor().hasInheritance()) {
>            ClassDescriptor parentDescriptor = getDescriptor().getInheritancePolicy().getParentDescriptor();
>            while ((parentDescriptor != null) && (parentDescriptor.getHistoryPolicy() == null)) {
>                parentDescriptor = parentDescriptor.getInheritancePolicy().getParentDescriptor();
>            }
>            if (parentDescriptor != null) {
>                // Unique is required because the builder can add the same table many times.
>                // This is done after init properties to make sure the default table is the first local one.
>                setHistoricalTables(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getHistoricalTables(), getHistoricalTables()));
>                setStartFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getStartFields(), getStartFields()));
>                setEndFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getEndFields(), getEndFields()));
>            }
>        }
>    }
>
>    /**
>     * PUBLIC:
>     * Use to specify the names of the mirroring historical tables.
>     * <p>
>     * Assumes that the order in which tables are added with descriptor.addTableName()
>     * matches the order in which mirroring historical tables are added with
>     * descriptor.addHistoryTableName().
>     */
>    public void addHistoryTableName(String name) {
>        HistoricalDatabaseTable table = new HistoricalDatabaseTable("");
>        table.setHistoricalName(name);
>        getHistoricalTables().add(table);
>    }
>
>    /**
>     * PUBLIC:
>     * Use to specify the names of the mirroring historical tables.
>     * <p>
>     * Explicitly states that <code>sourceTableName</code> is mirrored by history table
>     * <code>historyTableName</code>.
>     * The order in which tables are added with descriptor.addTableName()
>     * should still match the order in which mirroring historical tables are
>     * added with descriptor.addMirroringHistoryTableName().
>     */
>    public void addHistoryTableName(String sourceTableName, String historyTableName) {
>        if ((sourceTableName == null) || sourceTableName.equals("")) {
>            addHistoryTableName(historyTableName);
>        }
>        HistoricalDatabaseTable table = new HistoricalDatabaseTable(sourceTableName);
>        table.setHistoricalName(historyTableName);
>        // Note that the equality check is only on sourceTableName, not historyTableName.
>        int index = getHistoricalTables().indexOf(table);
>        if (index == -1) {
>            getHistoricalTables().add(table);
>        } else {
>            getHistoricalTables().set(index, table);
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public void setHistoricalTables(List<DatabaseTable> historicalTables) {
>        this.historicalTables = historicalTables;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public void setMapping(DatabaseMapping mapping) {
>        this.mapping = mapping;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected void setStartFields(List<DatabaseField> startFields) {
>        this.startFields = startFields;
>    }
>
>    /**
>     * PUBLIC:
>     * Sets the name of the start field.
>     * <p>
>     * By default all tables belonging to a descriptor have the same primary
>     * key field names, and so the same start field names also.
>     * <p>
>     * However, if <code>startFieldName</code> is qualified, i.e. of the form
>     * "EMPLOYEE_HIST.EMP_START", then this call will only set the start field
>     * name for a single historical table.
>     */
>    public void addStartFieldName(String startFieldName) {
>        DatabaseField startField = new DatabaseField(startFieldName);
>        startField.setType(ClassConstants.TIMESTAMP);
>
>        if (startFields == null) {
>            startFields = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance();
>            startFields.add(startField);
>            return;
>        }
>
>        for (DatabaseField existing : startFields) {
>            if (startField.getTableName().equals(existing.getTableName())) {
>                existing.setName(startField.getName());
>                return;
>            }
>        }
>        startFields.add(startField);
>    }
>
>    /**
>     * ADVANCED:
>     * Sets the type of all start fields.  Not required to be set as the default
>     * of Timestamp is assumed.
>     */
>    public void setStartFieldType(Class type) {
>        for (DatabaseField existing : startFields) {
>            existing.setType(type);
>        }
>    }
>
>    /**
>     * INTERNAL:
>     */
>    protected void setEndFields(List<DatabaseField> endFields) {
>        this.endFields = endFields;
>    }
>
>    /**
>     * PUBLIC:
>     * @see #addStartFieldName
>     */
>    public void addEndFieldName(String endFieldName) {
>        DatabaseField endField = new DatabaseField(endFieldName);
>        endField.setType(ClassConstants.TIMESTAMP);
>
>        if (endFields == null) {
>            endFields = new ArrayList();
>            endFields.add(endField);
>            return;
>        }
>
>        for (DatabaseField existing : endFields) {
>            if (endField.getTableName().equals(existing.getTableName())) {
>                existing.setName(endField.getName());
>                return;
>            }
>        }
>        endFields.add(endField);
>    }
>
>    /**
>     * ADVANCED:
>     * @see #setStartFieldType
>     */
>    public void setEndFieldType(String fieldName, Class type) {
>        for (DatabaseField existing : endFields) {
>            existing.setType(type);
>        }
>    }
>
>    /**
>     * Sets if TopLink is responsible for writing history.
>     * <p>
>     * If history is maintained via low level database triggers or application
>     * logic a policy is still needed for point in time querying.
>     * <p>
>     * If Oracle flashback is used no HistoryPolicy is needed.
>     * <p>
>     * Setting this to false lets you use History for many other applications.
>     * For instance a table that tracks available flights or hotel deals may
>     * benefit from a HistoryPolicy just to simplify temporal querying.
>     * <p>If all hotel discounts have a start and end date, you could query on
>     * all discounts available at a certain date.
>     */
>    public void setShouldHandleWrites(boolean value) {
>        this.shouldHandleWrites = value;
>    }
>
>    /**
>     * Answers if TopLink is responsible for writing history.
>     * <p>
>     * If history is maintained via low level database triggers or application
>     * logic a policy is still usefull for point in time querying.
>     * <p>
>     * If Oracle flashback is used no HistoryPolicy is needed.
>     * @return true by default
>     * @see #setShouldHandleWrites
>     */
>    public boolean shouldHandleWrites() {
>        return shouldHandleWrites;
>    }
>
>    /**
>     * Sets if the Timestamp used in maintainaing history should be the
>     * current time according to the database.
>     * @param value if false uses localTime (default) instead
>     */
>    public void setShouldUseDatabaseTime(boolean value) {
>        usesLocalTime = !value;
>    }
>
>    /**
>     * Answers if the Timestamp used in maintaining history should be
>     * System.currentTimeMillis();
>     * @see #shouldUseDatabaseTime
>     * @see #useLocalTime
>     * @return true by default
>     */
>    public boolean shouldUseLocalTime() {
>        return usesLocalTime;
>    }
>
>    /**
>     * Answers if the Timestamp used in maintaining history should be the
>     * current time according to the database.
>     * @see #shouldUseLocalTime
>     * @see #useDatabaseTime
>     * @return false by default
>     */
>    public boolean shouldUseDatabaseTime() {
>        return !usesLocalTime;
>    }
>
>    /**
>     * Answers if the Timestamp used in maintaining history should be
>     * System.currentTimeMillis();
>     * @see #useDatabaseTime
>     * @see #shouldUseLocalTime
>     */
>    public void useLocalTime() {
>        usesLocalTime = true;
>    }
>
>    /**
>     * Answers if the Timestamp used in maintaining history should be the
>     * current time according to the database.
>     * @see #useLocalTime
>     * @see #shouldUseDatabaseTime
>     */
>    public void useDatabaseTime() {
>        usesLocalTime = false;
>    }
>
>    /**
>     * INTERNAL: Check that the qualifiers on the historical tables are
>     * properly set.
>     * <p>A similar method exists on ClassDescriptor.
>     */
>    protected void verifyTableQualifiers(DatasourcePlatform platform) {
>        String tableQualifier = platform.getTableQualifier();
>
>        if (tableQualifier.length() == 0) {
>            return;
>        }
>
>        for (DatabaseTable table : getHistoricalTables()) {
>            // Build a scratch table to see if history table name has a qualifier.
>            DatabaseTable scratchTable = new DatabaseTable(table.getQualifiedName());
>            if (scratchTable.getTableQualifier().length() == 0) {
>                scratchTable.setTableQualifier(tableQualifier);
>                ((HistoricalDatabaseTable)table).setHistoricalName(scratchTable.getQualifiedNameDelimited(platform));
>            }
>        }
>    }
>
>    /**
>     * INTERNAL:
>     * Checks for the case where an object has multiple tables but only some
>     * are part of a minimal update.
>     */
>    protected boolean checkWastedVersioning(AbstractRecord modifyRow, DatabaseTable table) {
>        for (Enumeration fieldsEnum = modifyRow.keys(); fieldsEnum.hasMoreElements();) {
>            DatabaseField field = (DatabaseField)fieldsEnum.nextElement();
>            if (field.getTable().equals(table) || (!field.hasTableName())) {
>                return true;
>            }
>        }
>        return false;
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public void postDelete(ModifyQuery deleteQuery) {
>        logicalDelete(deleteQuery, false);
>    }
>    
>    /**
>     * INTERNAL:
>     */
>    // Bug 319276 - pass whether shallow insert/update
>    public void postUpdate(ObjectLevelModifyQuery writeQuery) {
>        postUpdate(writeQuery, false);
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public void postUpdate(ObjectLevelModifyQuery writeQuery, boolean isShallow) {
>        logicalDelete(writeQuery, true, isShallow);
>        logicalInsert(writeQuery, true);
>    }
>
>    /**
>     * INTERNAL:
>     */
>    public void postInsert(ObjectLevelModifyQuery writeQuery) {
>        logicalInsert(writeQuery, false);
>    }
>
>    /**
>     * INTERNAL:
>     * Perform a logical insert into the historical schema, creating a new version
>     * of an object.
>     * <p>Called by postInsert() and also postUpdate() (which first does a logicalDelete
>     * of the previous version).
>     */
>    public void logicalInsert(ObjectLevelModifyQuery writeQuery, boolean isUpdate) {
>        ClassDescriptor descriptor =(ClassDescriptor) getDescriptor().clone();
>        descriptor.setSequenceNumberField(null);//Do not retrive the sequence number
>        writeQuery.setDescriptor(descriptor);
>        AbstractRecord modifyRow = null;
>        AbstractRecord originalModifyRow = writeQuery.getModifyRow();
>        Object currentTime = null;
>        if (isUpdate) {
>            modifyRow = descriptor.getObjectBuilder().buildRow(writeQuery.getObject(), writeQuery.getSession(), WriteType.UPDATE); // Bug 319276
>            // If anyone added items to the modify row, then they should also be added here.
>            modifyRow.putAll(originalModifyRow);
>        } else {
>            modifyRow = originalModifyRow;
>            // If update would have already discovered timestamp to use.
>            currentTime = getCurrentTime(writeQuery.getSession());
>        }
>        StatementQueryMechanism insertMechanism = new StatementQueryMechanism(writeQuery);
>
>        for (int i = 0; i < getHistoricalTables().size(); i++) {
>            DatabaseTable table = getHistoricalTables().get(i);
>            if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) {
>                continue;
>            }
>            if (!isUpdate) {
>                modifyRow.add(getStart(i), currentTime);
>            }
>            SQLInsertStatement insertStatement = new SQLInsertStatement();
>            insertStatement.setTable(table);
>            insertMechanism.getSQLStatements().add(insertStatement);
>        }
>        if (insertMechanism.hasMultipleStatements()) {
>            writeQuery.setTranslationRow(modifyRow);
>            writeQuery.setModifyRow(modifyRow);
>            insertMechanism.insertObject();
>        }
>    }
>
>    /**
>     * INTERNAL:
>     * Performs a logical insert into the historical schema.  Direct
>     * collections and many to many mappings are maintained through the session
>     * events.
>     */
>    public void mappingLogicalInsert(DataModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) {
>        DataModifyQuery historyQuery = new DataModifyQuery();
>        SQLInsertStatement historyStatement = new SQLInsertStatement();
>        DatabaseTable histTable = getHistoricalTables().get(0);
>
>        historyStatement.setTable(histTable);
>        AbstractRecord modifyRow = originalQuery.getModifyRow().clone();
>        AbstractRecord translationRow = arguments.clone();
>
>        // Start could be the version field in timestamp locking.
>        if (!modifyRow.containsKey(getStart())) {
>            Object time = getCurrentTime(session);
>            modifyRow.add(getStart(), time);
>            translationRow.add(getStart(), time);
>        }
>        historyQuery.setSQLStatement(historyStatement);
>        historyQuery.setModifyRow(modifyRow);
>        historyStatement.setModifyRow(modifyRow);
>        session.executeQuery(historyQuery, translationRow);
>    }
>    
>    /**
>     * INTERNAL:
>     * Performs a logical delete (update) on the historical schema.
>     */
>    // Bug 319276 - pass whether shallow insert/update
>    public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate) {
>        logicalDelete(writeQuery, isUpdate, false);
>    }
>
>    /**
>     * INTERNAL:
>     * Performs a logical delete (update) on the historical schema.
>     */
>    public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate, boolean isShallow) {
>        ClassDescriptor descriptor = writeQuery.getDescriptor();
>        AbstractRecord originalModifyRow = writeQuery.getModifyRow();
>        AbstractRecord modifyRow = new DatabaseRecord();
>        StatementQueryMechanism updateMechanism = new StatementQueryMechanism(writeQuery);
>        Object currentTime = getCurrentTime(writeQuery.getSession());
>
>        for (int i = 0; i < getHistoricalTables().size(); i++) {
>            DatabaseTable table = getHistoricalTables().get(i);
>
>            if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) {
>                continue;
>            }
>            SQLUpdateStatement updateStatement = new SQLUpdateStatement();
>            updateStatement.setTable(table);
>            Expression whereClause = null;
>            if (writeQuery instanceof DeleteAllQuery) {
>                if (writeQuery.getSelectionCriteria() != null) {
>                    whereClause = (Expression)writeQuery.getSelectionCriteria().clone();
>                }
>            } else {
>                whereClause = descriptor.getObjectBuilder().buildPrimaryKeyExpression(table);
>            }
>            ExpressionBuilder builder = ((whereClause == null) ? new ExpressionBuilder() : whereClause.getBuilder());
>            whereClause = builder.getField(getEnd(i)).isNull().and(whereClause);
>            updateStatement.setWhereClause(whereClause);
>
>            modifyRow.add(getEnd(i), currentTime); 
>
>            // save a little time here and add the same timestamp value for
>            // the start field in the logicalInsert.
>            if (isUpdate) {
>                if (isShallow) {
>                    // Bug 319276 - increment the timestamp by 1 to avoid unique constraint violation potential 
>                    java.sql.Timestamp  incrementedTime = (java.sql.Timestamp) currentTime;
>                    incrementedTime.setTime(incrementedTime.getTime() + getMinimumTimeIncrement(writeQuery.getSession()));
>                    originalModifyRow.add(getStart(i), incrementedTime); 
>                } else {
>                    originalModifyRow.add(getStart(i), currentTime);
>                }
>            }
>            updateMechanism.getSQLStatements().add(updateStatement);
>        }
>        if (updateMechanism.hasMultipleStatements()) {
>            writeQuery.setModifyRow(modifyRow);
>            updateMechanism.updateObject();
>            writeQuery.setModifyRow(originalModifyRow);
>        }
>    }
>
>    /**
>     * INTERNAL:
>     * Performs a logical delete (update) on the historical schema.  Direct
>     * collections and many to many mappings are maintained through the session
>     * events.
>     */
>    public void mappingLogicalDelete(ModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) {
>        SQLDeleteStatement originalStatement = (SQLDeleteStatement)originalQuery.getSQLStatement();
>
>        DataModifyQuery historyQuery = new DataModifyQuery();
>        SQLUpdateStatement historyStatement = new SQLUpdateStatement();
>        DatabaseTable histTable = getHistoricalTables().get(0);
>
>        historyStatement.setTable(histTable);
>        Expression whereClause = (Expression)originalStatement.getWhereClause().clone();
>        DatabaseField endField = getEnd();
>        whereClause = whereClause.getBuilder().getField(endField).isNull().and(whereClause);
>        historyStatement.setWhereClause(whereClause);
>        AbstractRecord modifyRow = new DatabaseRecord();
>        AbstractRecord translationRow = arguments.clone();
>        Object time = getCurrentTime(session);
>        modifyRow.add(getEnd(), time);
>        translationRow.add(getEnd(), time);
>        historyStatement.setModifyRow(modifyRow);
>        historyQuery.setSQLStatement(historyStatement);
>        historyQuery.setModifyRow(modifyRow);
>        session.executeQuery(historyQuery, translationRow);
>    }
>}
>
Comment 7 Nicolas Marcotte CLA 2014-10-31 14:43:54 EDT
Created attachment 248313 [details]
A HistoryPolicy that do not fetch the last inserted id (The correct file)

Maybe there is a performance optimization to be made as I clone the descriptor on each logical-insert
Comment 8 Nicolas Marcotte CLA 2014-10-31 14:45:29 EDT
Sorry about the repeated file, I thought that edit attachment as comment would update the file, not post a comment. I will know better from now
Comment 9 Mark Richter CLA 2017-09-27 17:31:51 EDT
This problem still occurs in EclipseLink 2.6.4 with MySQL.  7 Years later any chance of getting this fixed?
Comment 10 ΝΕΚΤΑΡΙΟΣ ΑΡΑΚΑΣ CLA 2020-09-02 09:17:56 EDT
As in 02/09/2020 I can confirm that the bug is still active on mysql 8.0.21 and eclipseLink 2.7.6. However, I came across a work around. Instead of using GenerationType.IDENTITY used GenerationType.TABLE and so far it seems to work as expected.
Comment 11 Eclipse Webmaster CLA 2022-06-09 10:05:02 EDT
The Eclipselink project has moved to Github: https://github.com/eclipse-ee4j/eclipselink