自己参照っぽいツリーぽいあれをGORMで表現する
こういうやつです。それぞれがひとつのNodeというインスタンスで表される。
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 } }