This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
I love and hate relationship at the same time! They are so useful and so stressful! In the database world!
(In real life, I hate relationships of any kind! Too complex for my tiny brain to figure it out!)
Anyway, let’s take a look at how to implement the following relationship in SwiftData.
- One-To-One
- One-To-Many
- Many-To-Many
I will also be sharing with the pitfalls I get myself into while trying to implement a Many-To-Many relationship!
Let’s get started!
One-To-One
Honestly speaking, super rare cases! I cannot even think of a strict one-to-one relationship in real life! I was thinking of Countries and capital cities, but then I realize that depending on the definition of the country, you get Nicosia, the capital of both Cyprus and of Northern Cyprus.
Anyway, Let’s see it a little more detail!
A one-to-one relationship means that a record in one table is associated with exactly one record in another table. That is every X has exactly one Y and every Y has exactly one X.
However, this is really not the real world case. In practice, we mostly use one-to-one relationships for modeling an optional one-to-one relationship, i.e. zero-to-one.
For example, a person might have a left hand thumb fingerprint or he/she may not!
To model this with SwiftData
@Model
class Person {
var name: String
var age: Int
var leftThumbFingerPrint: FingerPrint?
init(name: String, age: Int, leftThumbFingerPrint: FingerPrint? = nil) {
self.name = name
self.age = age
self.leftThumbFingerPrint = leftThumbFingerPrint
}
}
@Model
class FingerPrint {
var finger: String
var fingerPrint: Data
var belongsTo: Person?
init(finger: String, fingerPrint: Data, belongsTo: Person? = nil) {
self.finger = finger
self.fingerPrint = fingerPrint
self.belongsTo = belongsTo
}
}
Couple important points here!
Two Sides relationship
That is keep a reference to the Person within the FingerPrint as well. This is needed so that when we insert or delete data from our database, the other side can be insert or removed automatically. We will see it in an example shortly.
Both Side of the relationship optional
Even you are 100% sure that your relationship will always be one-to-one and you will never have an optional value, you will have to declare it as an optional type.
If we make both properties non-optional, then we will be getting into the problem where we can’t create one without creating the other first.
Insert
let itsuki = Person(name: "itsuki", age: 100000)
let fingerPrint = FingerPrint(finger: "thumb", fingerPrint: Data(), belongsTo: itsuki)
modelContext.insert(fingerPrint)
ONLY insert one object. In this case, the fingerPrint. Since the relationship to itsuki is declared, inserting fingerPrint will automatically have itsuki inserted as well.
By that means, since within itsuki, we don’t have any reference to any fingerPrint at this point in time, inserting itsuki will NOT cause the fingerPrint to be inserted.
Delete
We can either call modelContext.delete(itsuki) or modelContext.delete(fingerPrint). Both of the options will have both itsuki and fingerPrint deleted.
(I really don’t want to delete myself so I guess I will have to use another name as my example next time!)
One-To-Many
This is created automatically when one side of the relationship has an array of data, and the other side is either optional or doesn’t even hold an inverse reference.
(Okay, if your other side doesn't even need a reference, you probably should consider declaring it as a struct and don’t even bother making it a database model because you don’t need it!)
Let’s extend what we have above, our Person and FingerPrint , but this time our Person will hold all their fingerPrints as an array.
@Model
class Person {
var name: String
var age: Int
@Relationship(deleteRule: .cascade, inverse: \FingerPrint.belongsTo) var fingerPrints: [FingerPrint] = []
init(name: String, age: Int, fingerPrints: [FingerPrint]) {
self.name = name
self.age = age
self.fingerPrints = fingerPrints
}
}
@Model
class FingerPrint {
var finger: String
var fingerPrint: Data
var belongsTo: Person
init(finger: String, fingerPrint: Data, belongsTo: Person) {
self.finger = finger
self.fingerPrint = fingerPrint
self.belongsTo = belongsTo
}
}
Set DeleteRule
A fingerPrint has to belong to someone so I have made belongsTo to be non-optional value. By doing this, we need to be really careful when deleting objects and that’s why I have the @Relationship(deleteRule: .cascade, inverse: \FingerPrint.belongsTo) specified for fingerPrints.
By default, SwiftData uses the .nullify, that is setting the related model’s reference to the deleted model to nil. Since we are not allowing belongsTo to be nil, we will have to set it to .cascade, deletes any related models on object deletion.
Insert
let itsuki = Person(name: "itsuki", age: 100000, fingerPrint: [])
let fingerPrint = FingerPrint(finger: "thumb", fingerPrint: Data(), belongsTo: itsuki)
modelContext.insert(fingerPrint)
Similar to above, since our fingerPrint holds a reference to itsuki, itsuki will also be inserted automatically for us!
Delete
Deleting itsuki and deleting the fingerPrint will behave a little differently.
If we delete itsuki by modelContext.delete(itsuki), the fingerPrint will also be deleted, just like what the .cascade rule suggested.
If we delete fingerPrint instead, obviously the fingerPrint will be deleted as well as the reference to that fingerPrint within itsuki. That is if we print out itsuki.fingerPrints, we will get an empty array.
Many-To-Many
I really want to say last but not least, but it is actually last and the MOST(at least for me), we have our Many-To-Many relationship.
The reason I said this is because I, again, as you might know from my previous article, always fall into random pitfalls, fall into those holes again!
Anyway, let’s start with a Todo App example where we have a TodoModel and a Tag.
@Model
class TodoModel {
var title: String
var isDone: Bool
var tags: [Tag]
init(title: String, isDone: Bool, tags: [Tag]) {
self.title = title
self.isDone = isDone
self.tags = tags
}
}
@Model
class Tag {
var name: String
@Relationship(inverse: \TodoModel.tags) var todos: [TodoModel]
init(name: String, todos: [TodoModel]) {
self.name = name
self.todos = todos
}
}
Each TodoModel can have 0 or more Tags, and each Tag can belong to 0 or more TodoModels. Since we don’t want to delete TodoModels on Tags deletion nor vice versa, the default deleteRule: nullify will work perfect.
Be Explicit!
SwiftData will not infer many-to-many relationships and that’s why we have added @Relationship(inverse: \TodoModel.tags) to our todos!
Here is where I am tripped over so I would like to spend couple minutes sharing with you what I did wrong so that you don’t make similar (stupid) mistakes!
Honestly speaking, at first, I didn’t even include a reverse reference, that is that var todos in the Tag struct does not exist! And what SwiftData assumes is that the relationship between our TodoModel and Tag is One-To-Many.
How would that mess things up?
Let’s say we have Tag1. We originally have it attached to TodoModel1 and we created another TodoModel, let’s say TodoModel2 and added Tag1 as its tag as well.
You might expect both TodoModel1 and TodoModel2 will have Tag1 as its tag but unfortunately, SwiftData will remove Tag1 from TodoModel1 automatically. That is TodoModel1.tags = [], while TodoModel1.tags = [Tag1].
Insert
First of all, inserting things separately works totally fine if you are not trying to specify any relationship, as you would expect.
let newTodo = TodoModel(title: "\(date)", isDone: false, tags: [])
modelContext.insert(newTodo)
let newTag = Tag(name: "Tag \(tags.count + 1)", todos: [])
modelContext.insert(newTag)
We can then modify the reference by updating the object directly.
newTodo.tags.append(newTag)
We can also insert objects WITH relationship pre-defined!
For example, let’s insert a TodoModel with a new Tag.
let newTag = Tag(name: "Tag \(tags.count + 1)", todos: [])
let date = Date()
let newTodo = TodoModel(title: "\(date)", isDone: false, tags: [newTag])
modelContext.insert(newTodo)
Just like above, since our newTodo holds a reference to newTag, newTag will also be inserted automatically for us!
I have seen articles saying that
if you attempt to manipulate the xxx property of an actor before inserting them, you’ll get a hard crash.
However, not true for me! That is the following will work equally as well!
let date = Date()
let newTodo = TodoModel(title: "\(date)", isDone: false, tags: [])
let newTag = Tag(name: "Tag \(tags.count + 1)", todos: [])
newTodo.tags.append(newTag)
modelContext.insert(newTodo)
modelContext.insert(newTag)
However, here is what will NOT work! Not like some of the article suggests, crashing with illegal attempt to establish a relationship, it did NOT crash on me! However, the newTodo created will not have the newTag attached!
let date = Date()
let newTodo = TodoModel(title: "\(date)", content: "Todo Created on \(Date())", icon: UIImage(systemName: "hare.fill")!, createDate: date, isDone: false, tags: [])
modelContext.insert(newTodo)
let newTag = Tag(name: "Tag \(tags.count + 1)", color: .blue, todos: [newTodo])
modelContext.insert(newTag)
To make it work, we can either simply remove the modelContext.insert(newTodo) or call try? modelContext.save() right after inserting newTodo like following.
let date = Date()
let newTodo = TodoModel(title: "\(date)", content: "Todo Created on \(Date())", icon: UIImage(systemName: "hare.fill")!, createDate: date, isDone: false, tags: [])
modelContext.insert(newTodo)
try? modelContext.save()
let newTag = Tag(name: "Tag \(tags.count + 1)", color: .blue, todos: [newTodo])
modelContext.insert(newTag)
Delete
Nothing really special here, simply call modelContext.delete(tag) or modelContext.delete(todoModel) to delete it from database!
Thank you for reading!
That’s all I have for today!
Happy relating!
SwiftData: Relationships was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Itsuki | Sciencx (2024-07-13T18:43:02+00:00) SwiftData: Relationships. Retrieved from https://www.scien.cx/2024/07/13/swiftdata-relationships/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.