openjpa-users mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Henno Vermeulen <he...@huizemolenaar.nl>
Subject RE: repeatable autogenerate names for key constraints
Date Tue, 23 Apr 2013 15:02:05 GMT
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:henno@huizemolenaar.nl] 
Verzonden: maandag 22 april 2013 14:56
Aan: 'users@openjpa.apache.org'
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:henno@huizemolenaar.nl] 
Verzonden: maandag 22 april 2013 13:28
Aan: 'users@openjpa.apache.org'
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


Mime
View raw message