一对一关系意味着每个某类型的对象都存在另一个不同类型的对象。例如,每个职工都有一家公司,或者每个宠物都有一个主人。
虽然在 SwiftData 中可以实现一对一的关系,但这种情况相当罕见,主要是因为它们在项目需求中很少见。比如上面给出的两个例子乍一看似乎都是合理的,但如果你仔细思考,它们也都在现实逻辑中站不住脚:
- 虽然每个职工都在一家公司工作,但他们可能会换好多份工作,或者职工可能都没有工作,
- 虽然大多数宠物都只有一个主人,但真正的一对一关系意味着每个主人只能养一只宠物,这显然是无稽之谈。
在实践中,我们主要在两个地方使用一对一关系:
-
我们拆分数据是为了减少冗余或更好地组织代码结构,因为编写
Country.capitalcity.name这样的代码显然比使用一个包含City模型的所有属性的庞大Country模型更简单、更容易。代码也更好维护。 -
我们实际上是在尝试建模一个可选的一对一关系,即可能只有一个匹配的数据,但也可能什么都没有。例如,任何应用的用户可能会去设置用户头像而不是用默认的配置头像,但并不是所有用户都会设置自己的头像,这并不是必须得。
从编码的角度来看,Swift 要求我们将一对一关系声明为0对1,即使它们实际上永远不会是0对1。这是一个非常容易理解的事实:如果我们试图使两个属性都是非可选的,那么我们就会遇到龟兔赛跑的问题,即我们不能先创建一个属性而不创建另一个属性,这两个属性是绑定在一起的。
例如,如果我们有 Country 和 City 两个模型,它们之间有真正的一对一关系。如果要创建一个城市,我们需要指定它属于哪个国家,如果要创建一个国家,我们需要指定它都包含哪些城市。
所以,为了能够正确的创建你的对象并让 SwiftData 正确地推断关系,你应该总是让关系的两边对应的参数都是可选的,像这样:
@Model
class Country {
var name: String
var capitalCity: City?
init(name: String, capitalCity: City? = nil) {
self.name = name
self.capitalCity = capitalCity
}
}
@Model
class City {
var name: String
var latitude: Double
var longitude: Double
var country: Country?
init(name: String, latitude: Double, longitude: Double, country: Country? = nil) {
self.name = name
self.latitude = latitude
self.longitude = longitude
self.country = country
}
}
重要提醒:不要尝试同时插入城市和国家对象,因为在我们插入一个的时候, SwiftData 会自动插入另一个,SwiftData 会默认这么做的原因是因为这两个对象有一对一的关系。
事实上,如果我们尝试同时插入它们,Xcode 很可能会抛出一个 fatal 类型的错误消息:Duplicate registration attempt for object。