【问题标题】:How to ensure data integrity when using Table Per Subclass?使用 Table Per Subclass 时如何确保数据完整性?
【发布时间】:2013-05-17 04:24:10
【问题描述】:

我在 Grails 中使用 table per subclass 策略,方法是将我的超类中的静态 mapping 字段的 tablePerHierarchy 属性设置为 false。这样,Grails 会为我的超类创建一个表,并为我的每个子类创建一个额外的表。

但是,虽然超类和子类记录共享相同的 ID(主键),但没有外键约束来保持它们的一致性,即可以删除超类记录,使子类记录处于无效状态。我想知道是否有一个设置/属性可以让 GORM 以某种方式解决这个问题,例如通过约束。还是我唯一的选择是手动添加外键?


例如,给定以下域类作为超类:

class Product {
    String productCode

    static mapping = {
        tablePerHierarchy false
    }
}

并将以下域类作为子类:

class Book extends Product {
    String isbn
}

这将创建两个表,Product 表和 Book 表。当创建一本书时——例如,通过脚手架页面——一条记录被插入到每个表中,它们唯一的链接是每个表的 ID 值是相同的这一事实。具体来说,数据可能如下所示:

PRODUCT
Id      Version     ProductCode
1       1           BLAH-02X1

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

由于这些表没有在数据库级别定义正式关系,因此可能会删除其中一条记录而留下另一条记录,从而导致数据无效。显然,我可以使用 SQL 在两个 ID 字段上手动创建外键约束,但我希望让 Grails 处理它。这可能吗?


使用 Grails 2.2.1

【问题讨论】:

    标签: grails inheritance foreign-keys grails-orm table-per-class


    【解决方案1】:

    解决了!

    以下解决方案为我解决了这个问题。将下面的类添加到src/java(这个类不能用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;
        }
    }
    

    然后在DataSource.groovy中设置这个为配置类

    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"
    }
    

    更新

    我已为此问题向 Grails 提交了pull request。该修复程序包含在 Grails 2.3.8 或 2.3.9 中(不记得是哪个)。

    【讨论】:

    • 使用 2.4.3 我仍然可以重现此问题。我应该在我的数据源中包含 configClass = 'org.example.TablePerSubclassConfiguration' 还是这已成为默认行为?
    【解决方案2】:

    Hibernate 确保每个子类的表的数据完整性。在每个子类的表的情况下,子类维护与超类的主键关联。看看Hibernate Table per subclass。为了验证事实,这里是您的测试用例:


    class Product {
        String productCode
    
        static mapping = {
            tablePerHierarchy false
        }
    }
    
    class Book extends Product{
        String isbn
    }
    
    //Test Case
    def testTablePerSubclass{
        def product = new Product(productCode: 'XYZ456')
        product.save(flush: true, failOnError: true)
    
        def book = new Book(isbn: '123456123', productCode: 'ABC123')
        book.save(flush: true, failOnError: true)
    
        assert Book.list().size() == 1 //One Book
        assert Book.list()*.id == [2] //Book id
        assert Product.list().size() == 2 //One Product, one Book (2 Products)
        assert Product.list()*.id == [1, 2] //Product id, Book Id
    
        //Grab the product (book) to delete
        def productToDelete = Product.get(book.id)
        productToDelete.delete(flush: true)
    
        assert Book.list().isEmpty() //Book deleted from Book table as well
        assert Product.list().size() == 1 //One Product remaining in Product table
        assert Product.list()*.id == [1] //Remaining Product Id
    }
    

    DataSource.groovy 中保持logSql 为真,以查看相应的sqls 得到执行。


    Log Sql Output:-
    
    Hibernate: insert into product (id, version, product_code) values (null, ?, ?)
    Hibernate: insert into product (id, version, product_code) values (null, ?, ?)
    Hibernate: insert into book (isbn, id) values (?, ?)
    Hibernate: select this_.id as id0_0_, this_1_.version as version0_0_, this_1_.product_code as product3_0_0_, this_.isbn as isbn1_0_ from book this_ inner join product this_1_ on this_.id=this_1_.id
    [com.example.Book : 2]
    Hibernate: select this_.id as id0_0_, this_.version as version0_0_, this_.product_code as product3_0_0_, this_1_.isbn as isbn1_0_, case when this_1_.id is not null then 1 when this_.id is not null then 0 end as clazz_0_ from product this_ left outer join book this_1_ on this_.id=this_1_.id
    [com.example.Product : 1, com.example.Book : 2]
    Hibernate: delete from book where id=?
    Hibernate: delete from product where id=? and version=?
    Hibernate: select this_.id as id0_0_, this_1_.version as version0_0_, this_1_.product_code as product3_0_0_, this_.isbn as isbn1_0_ from book this_ inner join product this_1_ on this_.id=this_1_.id
    []
    Hibernate: select this_.id as id0_0_, this_.version as version0_0_, this_.product_code as product3_0_0_, this_1_.isbn as isbn1_0_, case when this_1_.id is not null then 1 when this_.id is not null then 0 end as clazz_0_ from product this_ left outer join book this_1_ on this_.id=this_1_.id
    [com.example.Product : 1]
    

    使用 Grails 2.2.2

    【讨论】:

    • 我已经运行了您的测试,虽然您似乎已经切换了产品和书籍 ID(1 和 2),但这已向我证明 Hibernate 维护了这两个域类之间的关联。但是,在数据库级别上没有这样的关联。那么有没有办法让Hibernate在DB中为此定义一个约束?
    • 交换了产品和图书 ID?没收到? A 添加了 2 件产品和 1 本书。我删除了一个产品(一本书),它最终也删除了这本书。请参阅我对logSql 输出的更新答案。尝试记录 sql 以查看更改。抱歉,我没有“让 Hibernate 在数据库中为此定义一个约束”?
    • 就像我在问题中所说的那样,数据库仍然允许一本书存在而没有相应的产品记录。您的回答表明 Hibernate 有一些逻辑可以在 Java 层中关联这些表。我希望数据库层本身有一些东西,但这似乎不受支持。
    猜你喜欢
    • 2023-03-12
    • 2017-04-18
    • 1970-01-01
    • 2012-04-28
    • 1970-01-01
    • 1970-01-01
    • 2011-01-02
    • 2014-02-09
    • 1970-01-01
    相关资源
    最近更新 更多