自己参照っぽいツリーぽいあれをGORMで表現する

こういうやつです。それぞれがひとつのNodeというインスタンスで表される。

f:id:yamkazu:20120701155941p:image

class Node {

    String name

    static belongsTo = [parent: Node]

    static hasMany = [children: Node]
    static mappedBy = [children: "parent"]

}

belongsToとhasManyしてしてマッピングテーブルが出来なようにしつつ、mappedByでparent指定してhasManyが機能するようにしている。

schema-exportするとこんなかんじになっている。

    create table node (
        id bigint generated by default as identity,
        version bigint not null,
        name varchar(255) not null,
        parent_id bigint,
        primary key (id)
    );
    alter table node 
        add constraint FK33AE026EFF8DB4 
        foreign key (parent_id) 
        references node

テストを書いてみる。

class NodeSpec extends IntegrationSpec {

    void "テスト"() {
        when:
        new Node(name: 'A')
                .addToChildren(new Node(name: 'B')
                    .addToChildren(new Node(name: 'D'))
                    .addToChildren(new Node(name: 'E')))
                .addToChildren(new Node(name: 'C')
                    .addToChildren(new Node(name: 'F'))
                    .addToChildren(new Node(name: 'G')))
                .save(flush: true)

        then: "カスケードされるのでAの保存ですべて保存されている"
        assert Node.count() == 7

        and: "ノードAの状態確認"
        def a = Node.findByName('A')
        a.parent == null
        a.children*.name as Set == ['B', 'C'] as Set

        and: "ノードBの状態確認"
        def b = Node.findByName('B')
        b.parent == a
        b.children*.name as Set == ['D', 'E'] as Set

        and: "ノードDの状態確認"
        def d = Node.findByName('D')
        d.parent == b
        d.children == null
    }

}

ここまでうまくいったのですが、消そうとしたら想定外の動き。

class NodeSpec extends IntegrationSpec {

    void "テスト"() {
        when:
        new Node(name: 'A')
                .addToChildren(new Node(name: 'B')
                    .addToChildren(new Node(name: 'D'))
                    .addToChildren(new Node(name: 'E')))
                .addToChildren(new Node(name: 'C')
                    .addToChildren(new Node(name: 'F'))
                    .addToChildren(new Node(name: 'G')))
                .save(flush: true)

        then: "カスケードされるのでAの保存ですべて保存されている"
        assert Node.count() == 7

        and: "ノードAの状態確認"
        def a = Node.findByName('A')
        a.parent == null
        a.children*.name as Set == ['B', 'C'] as Set

        and: "ノードBの状態確認"
        def b = Node.findByName('B')
        b.parent == a
        b.children*.name as Set == ['D', 'E'] as Set

        and: "ノードDの状態確認"
        def d = Node.findByName('D')
        d.parent == b
        d.children == null

        when: "ノードBを消してみる"
        b.delete()

        then: "子供ノードも消えている"
        assert Node.count() == 4
        assert Node.exists(b.id) == false
        assert Node.exists(d.id) == false
    }

}

belongsToとhasManyあるから双方向でカスケードデリートされかと思いきや

[408/1802]|  org.springframework.dao.InvalidDataAccessApiUsageException: deleted object would be re-saved by cascade (remove deleted object from associations): 
[org.yamkazu.Node#2]; nested exception is org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): 
[org.yamkazu.Node#2]        at org.yamkazu.NodeSpec.テスト(NodeSpec.groovy:40)Caused by: org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): 
[org.yamkazu.Node#2]        ... 1 more| Completed 1 spock test, 1 failed in 1280ms

ふむー。よくわらない。

http://blog.springsource.com/2010/07/02/gorm-gotchas-part-2/

このへん見ながら色々やってみたが、とりあえず以下のようにな感じにしてみたら上手く言ったけど、これでいいのか、こういうものなのかわからない。

class Node {

    String name

    static belongsTo = [parent: Node]

    static hasMany = [children: Node]
    static mappedBy = [children: "parent"]

    void deleteWithChildren() {
        deleteWithChildren(this)
    }

    static void deleteWithChildren(Node node) {
        if (node.children) {
            def children = []
            children += node.children
            children.each { deleteWithChildren(it) }
        }
        node.parent?.removeFromChildren(node)
        node.delete()
    }

}
class NodeSpec extends IntegrationSpec {

    void "テスト"() {
        when:
        new Node(name: 'A')
                .addToChildren(new Node(name: 'B')
                    .addToChildren(new Node(name: 'D'))
                    .addToChildren(new Node(name: 'E')))
                .addToChildren(new Node(name: 'C')
                    .addToChildren(new Node(name: 'F'))
                    .addToChildren(new Node(name: 'G')))
                .save(flush: true)

        then: "カスケードされるのでAの保存ですべて保存されている"
        assert Node.count() == 7

        and: "ノードAの状態確認"
        def a = Node.findByName('A')
        a.parent == null
        a.children*.name as Set == ['B', 'C'] as Set

        and: "ノードBの状態確認"
        def b = Node.findByName('B')
        b.parent == a
        b.children*.name as Set == ['D', 'E'] as Set

        and: "ノードDの状態確認"
        def d = Node.findByName('D')
        d.parent == b
        d.children == null

        when: "ノードBを消してみる"
        b.deleteWithChildren()

        then: "子供ノードも消えている"
        assert Node.count() == 4
        assert Node.exists(b.id) == false
        assert Node.exists(d.id) == false
    }

}