Search code examples
grailsinheritanceforeign-keysgrails-ormtable-per-class

How to ensure data integrity when using Table Per Subclass?


I am using the table per subclass strategy in Grails by setting the tablePerHierarchy property of the static mapping field in my superclass to false. This way, Grails creates one table for my superclass and one additional table for each of my subclasses.

However, while the superclass and subclass records share the same ID (primary key), there are no foreign key constraints to keep them consistent, i.e. it is possible to delete the superclass record, leaving the subclass record in an invalid state. I want to know if there is a setting/property to make GORM address this in some way, e.g. through constraints. Or is my only option to add foreign keys manually?


For example, given the following domain class as superclass:

class Product {
    String productCode

    static mapping = {
        tablePerHierarchy false
    }
}

And the following domain class as subclass:

class Book extends Product {
    String isbn
}

This results in the creation of two tables, the Product table and the Book table. When creating a Book – through scaffolded pages, for instance – a record is inserted into each table, their only link being the fact that the ID value is the same for each. Specifically, the data might look like this:

PRODUCT
Id      Version     ProductCode
1       1           BLAH-02X1

BOOK
Id      ISBN
1       123-4-56-7891011-1

Because there is no formal relationship defined at the database level for these tables, it is possible to delete one of the records and leave the other, which results in invalid data. Obviously I can use SQL to manually create a foreign key constraint on the two ID fields, but I was hoping to let Grails handle that. Is this possible?


Using Grails 2.2.1


Solution

  • Solved!

    The following solution fixed this issue for me. Add the class below to src/java (this class cannot be written in Groovy)

    package org.example;
    
    import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration;
    import org.hibernate.MappingException;
    import org.hibernate.mapping.JoinedSubclass;
    import org.hibernate.mapping.PersistentClass;
    import org.hibernate.mapping.RootClass;
    
    import java.util.Iterator;
    
    public class TablePerSubclassConfiguration extends GrailsAnnotationConfiguration {
    
        private static final long serialVersionUID = 1;
    
        private boolean alreadyProcessed = false;
    
        @Override
        protected void secondPassCompile() throws MappingException {
            super.secondPassCompile();
    
            if (alreadyProcessed) {
                return;
            }
    
            for (PersistentClass persistentClass : classes.values()) {
                if (persistentClass instanceof RootClass) {
                    RootClass rootClass = (RootClass) persistentClass;
    
                    if (rootClass.hasSubclasses()) {
                        Iterator subclasses = rootClass.getSubclassIterator();
    
                        while (subclasses.hasNext()) {
    
                            Object subclass = subclasses.next();
    
                            // This test ensures that foreign keys will only be created for subclasses that are
                            // mapped using "table per subclass"
                            if (subclass instanceof JoinedSubclass) {
                                JoinedSubclass joinedSubclass = (JoinedSubclass) subclass;
                                joinedSubclass.createForeignKey();
                            }
                        }
                    }
                }
            }
    
            alreadyProcessed = true;
        }
    }
    

    Then in DataSource.groovy set this as the configuration class

    dataSource {
        configClass = 'org.example.TablePerSubclassConfiguration'
        pooled = true
        driverClassName = "org.h2.Driver"
        username = "sa"
        password = ""
        dbCreate = "update"
        url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
    }
    

    Update

    I've submitted a pull request to Grails for this issue. The fix was included in Grails 2.3.8 or 2.3.9 (can't remember which).