I finally solved the problem by using a custom MappingDefaults class which 
extends the default PersistenceMappingDefaults and creates identifiers for the 
primary and foreign keys.
Unfortunately I encountered a few technical problems along the way which I 
could only figure out by using Java debugging with the OpenJPA's source code.

By default the mapping defaults uses a setting of DefaultMissingInfo = true 
which somehow causes the MappingDefaults.getPrimaryKeyIdentifier method to be 
ignored. (And it takes quite some time to figure this out...) On the other hand 
when using DefaultMissingInfo = false I get an exception where it cannot handle 
a @MappedSuperclass that has a generated primary key column:

org.apache.openjpa.util.MetaDataException: For 
"nl.hm.olga.core.entity.AbstractEntity.id", expected 1 column(s), but found 0.

So I am using DefaultMissingInfo = true again and work around the issue by 
nothing that the Primary key is created inside ClassStrategy.map which is 
pluggable. So I created an extension of FullClassStrategy where the only way to 
get what I wanted was to copy/paste OpenJPA internal code which uses 
package-private elements so I have to use the 
org.apache.openjpa.jdbc.meta.strats package for my own class.

Anyway, here is the code:


Persistence.xml:

                        <property name="openjpa.jdbc.MappingDefaults"
                                
value="nl.hm.olga.core.dao.openjpa.KeyConstraintNamesMappingDefaults(ForeignKeyDeleteAction=restrict,
 JoinForeignKeyDeleteAction=restrict, 
                                
BaseClassStrategy=org.apache.openjpa.jdbc.meta.strats.PrimaryKeyConstraintNameFullClassStrategy,
 
SubclassStrategy=org.apache.openjpa.jdbc.meta.strats.PrimaryKeyConstraintNameFullClassStrategy)"
 />




KeyConstraintNamesMappingDefaults:


package nl.hm.olga.core.dao.openjpa;

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.MappingDefaults;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import 
org.apache.openjpa.jdbc.meta.strats.PrimaryKeyConstraintNameFullClassStrategy;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.persistence.jdbc.PersistenceMappingDefaults;

/**
 * A {@link MappingDefaults} implementation which extends the
 * {@link PersistenceMappingDefaults} and generates predictable primary key and
 * foreign key constraint names. By default OpenJPA uses the
 * {@link PersistenceMappingDefaults} which delegates creating the constraint
 * names to the database. The names generated on Microsoft SQL Server are not
 * predictable because they contain a seemingly random number.
 * 
 * <p>
 * Note: {@link #getPrimaryKeyIdentifier(ClassMapping, Table)} is normally
 * ignored when running with the default MappingTool option of
 * <code>DefaultMissingInfo=true</code>. This can be solved by using the
 * {@link PrimaryKeyConstraintNameFullClassStrategy}.
 * 
 * @author Henno Vermeulen
 */
public class KeyConstraintNamesMappingDefaults extends
                PersistenceMappingDefaults {

        @Override
        public DBIdentifier getPrimaryKeyIdentifier(ClassMapping cm, Table 
table) {
                return DBIdentifier.preCombine(table.getIdentifier(), "PK");
        }

        @Override
        public ForeignKey getForeignKey(ValueMapping vm, DBIdentifier name,
                        Table local, Table foreign, boolean inverse) {
                return setForeignKeyIdentifier(
                                super.getForeignKey(vm, name, local, foreign, 
inverse), local,
                                name);
        }

        /**
         * {@inheritDoc}
         * 
         * <p>
         * Handles (at least) inverse foreign keys of join tables, i.e. it 
refers
         * from the join table back to the owner table. In this case
         * <code>local</code> is the join table and <code>foreign</code> is the
         * owner table.
         */
        @Override
        public ForeignKey getJoinForeignKey(FieldMapping fm, Table local,
                        Table foreign) {
                return setForeignKeyIdentifier(
                                super.getJoinForeignKey(fm, local, foreign), 
local,
                                foreign.getIdentifier());
        }

        private ForeignKey setForeignKeyIdentifier(ForeignKey foreignKey,
                        Table table, DBIdentifier refersTo) {
                foreignKey.setIdentifier(getForeignKeyIdentifier(table, 
refersTo));
                return foreignKey;
        }

        private DBIdentifier getForeignKeyIdentifier(Table table,
                        DBIdentifier refersTo) {
                DBIdentifier identifier =
                                DBIdentifier.combine(table.getIdentifier(), 
refersTo.getName());
                return DBIdentifier.preCombine(identifier, "FK");
        }

}



PrimaryKeyConstraintNameFullClassStrategy:

package org.apache.openjpa.jdbc.meta.strats;

import static java.util.Arrays.asList;

/**
 * Custom {@link ClassStrategy} which ensures the {@link MappingDefaults} is
 * called to generate a name for the primary key.
 * 
 * <p>
 * Note: we had to copy/paste OpenJPA code so this is not guaranteed to work for
 * later versions unless we do the copy/paste/adjust again. See
 * {@link #checkCompatibility()} and <a href=
 * 
"http://svn.apache.org/viewvc/openjpa/trunk/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/FullClassStrategy.java?view=log";
 * >the source code</a> of {@link FullClassStrategy}.
 * 
 * @author Henno Vermeulen
 */
public class PrimaryKeyConstraintNameFullClassStrategy extends
                FullClassStrategy {

        private static final long serialVersionUID = 1L;

        private static final List<String> COMPATIBLE_OPENJPA_VERSION_NUMBERS =
                        asList("2.2.0", "2.2.1");

        private static final Localizer _loc = Localizer
                        .forPackage(FullClassStrategy.class);

        public PrimaryKeyConstraintNameFullClassStrategy() {
                super();
                checkCompatibility();
        }

        private void checkCompatibility() {
                if (!COMPATIBLE_OPENJPA_VERSION_NUMBERS
                                .contains(OpenJPAVersion.VERSION_NUMBER)) {
                        throw new IllegalStateException(
                                        "Please verify compatibility of this 
class with OpenJPA "
                                                        + 
OpenJPAVersion.VERSION_NUMBER
                                                        + " (see Javadoc). 
Known compatible versions are: "
                                                        + 
COMPATIBLE_OPENJPA_VERSION_NUMBERS);
                }
        }

        /**
         * Overridden to replace <code>adapt</code> in the following code with 
true
         * so that the MappingDefaults is always checked to create the primary 
key
         * name regardless of the value of <code>adapt</code>. 
(<code>adapt</code>
         * is set to false by default and will be set to true by using the
         * <code>DefaultMissing=true</code> option but this gives another
         * exception.)
         * 
         * <pre>
         * DBIdentifier pkname = DBIdentifier.NULL;
         * if (adapt)
         *      pkname =
         *                      cls.getMappingRepository().getMappingDefaults()
         *                                      .getPrimaryKeyIdentifier(cls, 
table);
         * </pre>
         */
        @Override
        public void map(boolean adapt) {
                if (cls.getEmbeddingMetaData() != null)
                        throw new MetaDataException(_loc.get("not-full", cls));

                ClassMapping sup = cls.getMappedPCSuperclassMapping();
                ClassMappingInfo info = cls.getMappingInfo();
                if (sup != null && info.isJoinedSubclass())
                        throw new MetaDataException(_loc.get("not-full", cls));

                info.assertNoJoin(cls, true);
                info.assertNoForeignKey(cls, !adapt);
                info.assertNoIndex(cls, false);
                info.assertNoUnique(cls, false);

                // find class table
                Table table = info.getTable(cls, adapt);

                // find primary key column
                Column[] pkCols = null;
                if (cls.getIdentityType() == cls.ID_DATASTORE) {
                        Column id = new Column();
                        DBDictionary dict = 
cls.getMappingRepository().getDBDictionary();
                        DBIdentifier idName =
                                        DBIdentifier.newColumn("id",
                                                        dict != null ? 
dict.delimitAll() : false);
                        id.setIdentifier(idName);
                        id.setJavaType(JavaTypes.LONG);
                        id.setComment("datastore id");
                        if (cls.getIdentityStrategy() == 
ValueStrategies.AUTOASSIGN)
                                id.setAutoAssigned(true);
                        id.setNotNull(true);
                        pkCols =
                                        info.getDataStoreIdColumns(cls, new 
Column[] { id }, table,
                                                        adapt);
                        cls.setPrimaryKeyColumns(pkCols);
                        cls.setColumnIO(info.getColumnIO());
                }
                cls.setTable(table);

                // add a primary key if we don't have one already
                PrimaryKey pk = table.getPrimaryKey();
                if (pk == null) {
                        DBIdentifier pkname = DBIdentifier.NULL;
                        if (true)
                                pkname =
                                                
cls.getMappingRepository().getMappingDefaults()
                                                                
.getPrimaryKeyIdentifier(cls, table);
                        pk = table.addPrimaryKey(pkname);
                        pk.setLogical(!adapt);
                        if (pkCols != null)
                                pk.setColumns(pkCols);
                }

                // set joinable
                if (cls.getIdentityType() == ClassMapping.ID_DATASTORE)
                        cls.setJoinable(cls.getPrimaryKeyColumns()[0],
                                        new IdentityJoinable(cls));
        }

}



-----Oorspronkelijk bericht-----
Van: Henno Vermeulen [mailto:[email protected]] 
Verzonden: maandag 22 april 2013 14:56
Aan: '[email protected]'
Onderwerp: RE: repeatable autogenerate names for key constraints

Another issue I found with the @ForeignKey annotation: it doesn't work with 
inheritance when using InheritanceType.TABLE_PER_CLASS because this would 
generate the same foreign key name for each subclass but they must be unique in 
the database!

-----Oorspronkelijk bericht-----
Van: Henno Vermeulen [mailto:[email protected]] 
Verzonden: maandag 22 april 2013 13:28
Aan: '[email protected]'
Onderwerp: repeatable autogenerate names for key constraints

When I let OpenJPA generate my database schema on sql server, the generated 
foreign keys have names such as

PK__booking___3213E83F5626D20A
FK__booking_A__ACTIV__6E886B80

When I later generate the schema again I get different names, e.g.

PK__booking___3213E83F7C97F46A
FK__booking_A__EVENT__1311456E

I use a database schema comparison tool to create a migration script for a 
newer version of my application. Because of the different names it lists very 
many changes that are not really changes at all.

Is it possible to let OpenJPA automatically generate the same names for the 
same primary/foreign keys? Perhaps it is possible to override the default 
strategy with a few lines of Java code? Is there another approach you can 
suggest?

All of my entities inherit an autogenerated primary key from a single mapped 
superclass.

PS. I know of the @ForeignKey annotation but it has a few drawbacks:

-          adds unnecessary boilerplate code (name can be autogenerated without 
numbers) that takes time to maintain.

-          Not sure if it works for mappings such as element collections and 
Maps

-          Obviously won't work for primary keys, especially not if it is 
defined in one place in a mapped superclass

Reply via email to