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