Open3

SwiftData: .unique属性とはなんだ?

kabeyakabeya

SwiftDataで、@Attribute(.unique)をつけた場合の動作が思ってたのと違ってて混乱しています。

↓のような感じでデータクラスを用意します。

import SwiftData

@Model
class TestData {
    @Attribute(.unique) var name: String
    @Relationship(inverse: \TestValue.parent) var values: [TestValue]
    
    init(name: String, values: [TestValue]) {
        self.name = name
        self.values = values
    }
}

@Model
class TestValue {
    var value: Int
    var parent: TestData? = nil
    
    init(value: Int) {
        self.value = value
    }
}

TestDatanameはユニーク属性が付けてあるので、追加なのか保存なのか、そういうタイミングで一意性のチェックがかかって保存できないのだろう、どこかでthrowされてくるのだろう、と思ってたんですね。

do {
    let container = try ModelContainer(for: TestData.self, TestValue.self)
    let context = ModelContext(container)
    
    try context.delete(model: TestData.self)
    try context.delete(model: TestValue.self)
    
    let value1 = TestValue(value: 1)
    let value2 = TestValue(value: 2)
    let value3 = TestValue(value: 3)
    let value4 = TestValue(value: 4)
    
    context.insert(value1)
    context.insert(value2)
    context.insert(value3)
    context.insert(value4)
    
    let data1 = TestData(name: "a", values: [value1])
    let data2 = TestData(name: "b", values: [value2])
    let data3 = TestData(name: "c", values: [value3])
    let data4 = TestData(name: "a", values: [value4])
    
    context.insert(data1)
    context.insert(data2)
    context.insert(data3)
//    do {   // ↓ ※これ
//        try context.save()
//    }
//    catch {
//        print("error3: \(error.localizedDescription)")
//    }
    context.insert(data4)
    
    do {
        try context.save()
    }
    catch {
        print("error2: \(error.localizedDescription)")
    }
    
    let fetchDesc = FetchDescriptor<TestData>(sortBy: [.init(\.name)])
    let results = try context.fetch(fetchDesc)
    for result in results {
        print("data.name: \(result.name), data.values: \(result.values.map(\.value))")
    }
}
catch {
    print("error1: \(error.localizedDescription)")
}

コードの内容としては、最初に既存データを削除して、TestValuesの1〜4を追加、その後、TestData"a""b""c""a"の順で追加します。"a"がダブりです。

実際、このコードを実行してみると、name="a"TestDataレコードは確かに一つなんですが、エラーも起きずにvaluesに2つの値が入ってしまう(=マージされてしまう)のです。

data.name: a, data.values: [1, 4]
data.name: b, data.values: [2]
data.name: c, data.values: [3]

ちなみにコメントアウトしてあるの箇所のsave()がないと、

のようなメッセージがログに出ます。「保存する前の一時データ内でユニーク識別子がダブってるよ!」みたいなことなんでしょうかね。
の箇所でsave()すると出なくなります)

マージされるのはかなりキモい動きのような気がしますね。

Xcode 16.1/macOS 15.1.1です。

kabeyakabeya

iOS 18/macOS 15から入ったマクロ#Uniqueでも同じですね。

@Model
class TestData {
    //@Attribute(.unique) var name: String
    #Unique<TestData>([\.name])
    var name: String
    @Relationship(inverse: \TestValue.parent) var values: [TestValue]
    
    init(name: String, values: [TestValue]) {
        self.name = name
        self.values = values
    }
}

そういうもんなんでしょうか。

kabeyakabeya

ちなみにinverseをなくすと、2個目の"a"を追加後save()した段階で、fatalErrorにより止まります。

@Model
class TestData {
    //@Attribute(.unique) var name: String
    #Unique<TestData>([\.name])
    var name: String
    //@Relationship(inverse: \TestValue.parent) var values: [TestValue]
    var values: [TestValue]
    
    init(name: String, values: [TestValue]) {
        self.name = name
        self.values = values
    }
}

@Model
class TestValue {
    var value: Int
    //var parent: TestData? = nil
    
    init(value: Int) {
        self.value = value
    }
}