Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dkunzler/d8bf1402cf2d5848b4c1e4fc29bffd94 to your computer and use it in GitHub Desktop.
Save dkunzler/d8bf1402cf2d5848b4c1e4fc29bffd94 to your computer and use it in GitHub Desktop.
generate onDelete actions in Foreign Key constraints with liquibase-hibernate

I created a changelog-generator to be used with hibernate and liquibase.

Calling the liquibase commandline from a project including these classes will generate FK constraints with the respective onDelete action. However deleting an @OnDelete annotation will still not result in a change. But adding @OnDelete will.

package liquibase.ext.hibernate.diff;
import liquibase.change.Change;
import liquibase.change.core.AddForeignKeyConstraintChange;
import liquibase.database.Database;
import liquibase.diff.Difference;
import liquibase.diff.ObjectDifferences;
import liquibase.diff.output.DiffOutputControl;
import liquibase.diff.output.changelog.ChangeGeneratorChain;
import liquibase.ext.hibernate.database.HibernateDatabase;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.ForeignKey;
import liquibase.structure.core.ForeignKeyConstraintType;
/**
* Hibernate doesn't know about all the variations that occur with foreign keys but just whether the FK exists or not.
* To prevent changing customized foreign keys, we suppress all foreign key changes from hibernate.
*/
public class MdmChangedForeignKeyChangeGenerator extends liquibase.diff.output.changelog.core.ChangedForeignKeyChangeGenerator {
@Override
public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
if (ForeignKey.class.isAssignableFrom(objectType)) {
return PRIORITY_ADDITIONAL + 1;
}
return PRIORITY_NONE;
}
@Override
public Change[] fixChanged(DatabaseObject changedObject, ObjectDifferences differences, DiffOutputControl control, Database referenceDatabase, Database comparisonDatabase, ChangeGeneratorChain chain) {
if (referenceDatabase instanceof HibernateDatabase || comparisonDatabase instanceof HibernateDatabase) {
Change[] changes = super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain);
if (changes != null && changes.length == 2) {
AddForeignKeyConstraintChange addFkChange = (AddForeignKeyConstraintChange) changes[1];
if (differences.isDifferent("deleteRule")) {
Difference deleteRule = differences.getDifference("deleteRule");
ForeignKeyConstraintType hibernateDeleteAction = (ForeignKeyConstraintType) deleteRule.getReferenceValue();
ForeignKeyConstraintType actualDeleteAction = (ForeignKeyConstraintType) deleteRule.getComparedValue();
if (hibernateDeleteAction == null || hibernateDeleteAction == ForeignKeyConstraintType.importedKeyNoAction) {
if (actualDeleteAction != null && actualDeleteAction != ForeignKeyConstraintType.importedKeyNoAction) {
// changed from onDelete to doNothing, most likely added ondelete by hand
differences.removeDifference("deleteRule");
} else {
// no real change -> remove it
differences.removeDifference("deleteRule");
}
} else {
addFkChange.setOnDelete(hibernateDeleteAction);
}
}
if (differences.isDifferent("updateRule")) {
Difference updateRule = differences.getDifference("updateRule");
ForeignKeyConstraintType hibernateUpdateAction = (ForeignKeyConstraintType) updateRule.getReferenceValue();
ForeignKeyConstraintType actualUpdateAction = (ForeignKeyConstraintType) updateRule.getComparedValue();
if (hibernateUpdateAction == null || hibernateUpdateAction == ForeignKeyConstraintType.importedKeyNoAction) {
if (actualUpdateAction != null && actualUpdateAction != ForeignKeyConstraintType.importedKeyNoAction) {
// changed from onDelete to doNothing, most likely added onupdate by hand
differences.removeDifference("updateRule");
} else {
// no real change -> remove it
differences.removeDifference("updateRule");
}
} else {
addFkChange.setOnUpdate(hibernateUpdateAction);
}
}
}
if (!differences.hasDifferences()) {
return null;
}
return changes;
}
return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain);
}
}
package liquibase.ext.hibernate.snapshot;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.DatabaseException;
import liquibase.ext.hibernate.database.HibernateDatabase;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.ForeignKey;
import liquibase.structure.core.ForeignKeyConstraintType;
import liquibase.structure.core.Table;
import org.hibernate.boot.spi.MetadataImplementor;
import java.util.Collection;
import java.util.Iterator;
public class MdmForeignKeySnapshotGenerator extends HibernateSnapshotGenerator {
@SuppressWarnings("unchecked")
public MdmForeignKeySnapshotGenerator() {
super(ForeignKey.class, new Class[]{Table.class});
}
@Override
protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
return example;
}
@Override
protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
if (!snapshot.getSnapshotControl().shouldInclude(ForeignKey.class)) {
return;
}
if (foundObject instanceof Table) {
Table table = (Table) foundObject;
HibernateDatabase database = (HibernateDatabase) snapshot.getDatabase();
MetadataImplementor metadata = (MetadataImplementor) database.getMetadata();
Collection<org.hibernate.mapping.Table> tmapp = metadata.collectTableMappings();
Iterator<org.hibernate.mapping.Table> tableMappings = tmapp.iterator();
while (tableMappings.hasNext()) {
org.hibernate.mapping.Table hibernateTable = (org.hibernate.mapping.Table) tableMappings.next();
Iterator fkIterator = hibernateTable.getForeignKeyIterator();
while (fkIterator.hasNext()) {
org.hibernate.mapping.ForeignKey hibernateForeignKey = (org.hibernate.mapping.ForeignKey) fkIterator.next();
Table currentTable = new Table().setName(hibernateTable.getName());
currentTable.setSchema(hibernateTable.getCatalog(), hibernateTable.getSchema());
org.hibernate.mapping.Table hibernateReferencedTable = hibernateForeignKey.getReferencedTable();
Table referencedTable = new Table().setName(hibernateReferencedTable.getName());
referencedTable.setSchema(hibernateReferencedTable.getCatalog(), hibernateReferencedTable.getSchema());
if (hibernateForeignKey.isPhysicalConstraint()) {
ForeignKey fk = new ForeignKey();
fk.setName(hibernateForeignKey.getName());
fk.setPrimaryKeyTable(referencedTable);
fk.setForeignKeyTable(currentTable);
for (Object column : hibernateForeignKey.getColumns()) {
fk.addForeignKeyColumn(new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName()));
}
for (Object column : hibernateForeignKey.getReferencedColumns()) {
fk.addPrimaryKeyColumn(new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName()));
}
if (fk.getPrimaryKeyColumns() == null || fk.getPrimaryKeyColumns().isEmpty()) {
for (Object column : hibernateReferencedTable.getPrimaryKey().getColumns()) {
fk.addPrimaryKeyColumn(new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName()));
}
}
if (hibernateForeignKey.isCascadeDeleteEnabled()) {
fk.setDeleteRule(ForeignKeyConstraintType.importedKeyCascade);
}
fk.setDeferrable(false);
fk.setInitiallyDeferred(false);
// Index index = new Index();
// index.setName("IX_" + fk.getName());
// index.setTable(fk.getForeignKeyTable());
// index.setColumns(fk.getForeignKeyColumns());
// fk.setBackingIndex(index);
// table.getIndexes().add(index);
if (DatabaseObjectComparatorFactory.getInstance().isSameObject(currentTable, table, null, database)) {
table.getOutgoingForeignKeys().add(fk);
table.getSchema().addDatabaseObject(fk);
}
}
}
}
}
}
}
@stefpiatek
Copy link

Thanks again for this, was really helpful.

Interestingly - I don't think the MdmChangedForeignKeyChangeGenerator is ever used. At least when I added a similar class to the classpath and tried to get it to be used in unit tests I never managed a pointbreak to be hit. ¯_(ツ)_/¯

@dkunzler
Copy link
Author

dkunzler commented Jun 8, 2020

I could at least verify - by commenting out the code - that the behaviour changes when it's not there and the onDelete would not get picked up. 🤷

@stefpiatek
Copy link

That's fun, maybe theres differences when used as a the command line tool, or changes with versions. Either way thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment