🔬

SwiftData 1:N で削除の挙動観測隊

2024/10/08に公開

構造

次のようなアプリがきっかけでこの観測を行ったので、記事全体でこの構造を基本にしている。

モデルはA、B、C

複数のA、複数のCがあり、AとCを結びつけるBがあります。結びつき方の情報が必要なので間にBが必要です。結びつき方を具体的にイメージしたい場合は「AとCの友好度4」とか「敵対度5」のようなものでよいでしょう。Bは頻繁に作成と削除が行われ、AとCは、Bと比べると作成と削除の頻度は低いものです。

  • Aは、0か1かいくつかのBを配列で持つ
  • Bは、nilか1つのAを持つ
  • Bは、nilか1つのCを持つ
  • Cは、0か1かいくつかのBを配列で持つ

Aを削除した時のB削除のルールは .cascade 、連鎖してBを削除します。
Cを削除した時のB削除のルールは .cascade 、連鎖してBを削除します。

Bを削除した時のA削除のルールは .nullify 、AはBの保持を捨てます。
Bを削除した時のC削除のルールは .nullify 、AはBの保持を捨てます。

コード
import SwiftUI
import SwiftData

@Model
class A {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String

    @Relationship(deleteRule: .cascade)
    var b: [B] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
class B {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String

    @Relationship(deleteRule: .nullify, inverse: \A.b)
    var a: A?
    
    @Relationship(deleteRule: .nullify, inverse: \C.b)
    var c: C?
    
    init(name: String) {
        self.name = name
    }
}


@Model
class C {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String

    @Relationship(deleteRule: .cascade)
    var b: [B] = []
    
    init(name: String) {
        self.name = name
    }
}

あるbに関連するものを取り出すと基本の状態は次のようになります。

【テーマ1】 本体削除と配列の中身削除は挙動が違うか

aを削除したとき

modelContext.delete(a)

bがmodelContextから削除される
cのB配列中のbが削除される

bを削除したとき

modelContext.delete(b)

aのB配列中のbが削除される
cのB配列中のbが削除される

cを削除したとき

aと同様

aのbを空配列にする

a.b = []

bはmodelContextから削除されず
b.aがnilになる

cのbを空配列にする

aと同様

aのbからremoveAllで排除する

a.b.removeAll()

bはmodelContextから削除されず
そのb.aがnilになる

cのbからremoveAllで排除する

aと同様

【テーマ2】 a.bをc.bにコピー後の挙動

aのbをcのbにコピーしたとき

c.b = a.b

コピー前

コピー後
ちゃんとbからcが見えてます。

aのbをcのbにコピーしたあと、aを削除したとき

modelContext.delete(a)

bがmodelContextから削除される
cのB配列中のbが削除される

aのbをcのbにコピーしたあと、bをmodelContextから削除したとき

modelContext.delete(b)

aのB配列中のbが削除される
cのB配列中のbが削除される

aのbをcのbにコピーしたあと、aのbを空配列にしたとき

a.b = []

bはmodelContextから削除されず
b.aがnilになる

a.bをc.bにコピーすることに関して、何か特別なことが起きるかなと思って観測したが、特に何もなかった。

【テーマ3】 a1.bをa2.bにコピーしたとき

a2.b = a1.b

コピー前

コピー後

  • a1.bはクリア
  • a2.bは意図通り
  • b.aはa2

なんと a1.bがクリアされる (B配列中のbが削除される)。

【テーマ4】 a1のbをa2のbにも登録したとき

a1が関係していることはコードに書かれていない。

if let b = listB.first {
    a2.b.append(b)
}

a2のb登録前

a2のb登録後

  • a1.bはクリア
  • a2.bは意図通り
  • b.aはa2

こちらも a1.bがクリアされる

【テーマ5】 すでにbが設定されているa2のbにa1のbをコピーしたとき

a2.b = a1.b

代入前

代入後

  • a1.bはクリア
  • a2.bは意図通り
  • b1.aはa2
  • b2.aはnil

結構複雑ですけど やってくれます

他の記事

デリートルールを調査しました。
https://zenn.dev/samekard_dev/articles/c1bdeb2ae2b095

Discussion