SwiftDataで複数のモデルを扱う際、避けて通れないのが「モデル間の関連性」です。@Relationship
マクロを使えば、データベース上の複雑な関係性を、Swiftのコードでシンプルに表現できます。
今回は、@Relationship
の使い方を実例を交えながら、わかりやすく解説します。
@Relationshipマクロとは?
@Relationship
は、SwiftDataでモデル同士の関連性を定義するマクロです。
例えば、ブログアプリを作る場合:
- 1人のユーザーが複数の記事を書く
- 1つの記事が複数のコメントを持つ
- 1つの記事が複数のタグを持ち、1つのタグが複数の記事に付けられる
こういった関係性を、@Relationship
で表現できます。
なぜ@Relationshipが必要なの?
実は、単純な関連であれば@Relationship
を書かなくても動作します。
@Model
final class Author {
var name: String
var books: [Book] // これだけでも動く
}
しかし、@Relationship
を使うことで:
- 削除時の挙動を制御できる:親を削除したとき、子も削除するか?関連だけ外すか?
- データの整合性を保てる:最小・最大数の制限などを設定できる
- コードの意図が明確になる:「これは重要な関連です」というメッセージを伝えられる
- 双方向の関連を明示できる:逆方向の参照を指定できる
つまり、より堅牢で保守しやすいアプリを作るために必要なのです。
基本的な使い方
まずは、シンプルな一対多の関係から見ていきましょう。
import SwiftData
@Model
final class Author {
var name: String
var birthYear: Int
@Relationship(deleteRule: .cascade)
var books: [Book]
init(name: String, birthYear: Int) {
self.name = name
self.birthYear = birthYear
self.books = []
}
}
@Model
final class Book {
var title: String
var publishedYear: Int
var author: Author?
init(title: String, publishedYear: Int, author: Author? = nil) {
self.title = title
self.publishedYear = publishedYear
self.author = author
}
}
この例では:
- 1人の著者(Author)が複数の本(Book)を持つ
deleteRule: .cascade
により、著者を削除すると、その著者の本も全て削除される
削除ルール(deleteRule)を理解する
@Relationship
の最も重要な機能が、この削除ルールです。親データを削除したとき、関連する子データをどう扱うかを決めます。
.cascade:カスケード削除
親を削除すると、子も全て削除されます。
@Model
final class Folder {
var name: String
// フォルダを削除すると、中のファイルも全て削除
@Relationship(deleteRule: .cascade)
var files: [File]
init(name: String) {
self.name = name
self.files = []
}
}
@Model
final class File {
var name: String
var folder: Folder?
init(name: String, folder: Folder? = nil) {
self.name = name
self.folder = folder
}
}
使うべき場面:
- 親なしでは子が意味をなさない場合
- フォルダとファイル、注文と注文明細など
.nullify:関連を解除(デフォルト)
親を削除すると、子の関連がnil
になります。子自体は削除されません。
@Model
final class Department {
var name: String
// 部署を削除しても、社員は残る(部署がnilになる)
@Relationship(deleteRule: .nullify)
var employees: [Employee]
init(name: String) {
self.name = name
self.employees = []
}
}
@Model
final class Employee {
var name: String
var department: Department?
init(name: String, department: Department? = nil) {
self.name = name
self.department = department
}
}
使うべき場面:
- 子が独立して存在できる場合
- 部署と社員、カテゴリと商品など
.deny:削除を拒否
子が存在する場合、親を削除できません。
@Model
final class Project {
var name: String
// タスクが残っている場合、プロジェクトは削除できない
@Relationship(deleteRule: .deny)
var tasks: [Task]
init(name: String) {
self.name = name
self.tasks = []
}
}
@Model
final class Task {
var title: String
var project: Project?
init(title: String, project: Project? = nil) {
self.title = title
self.project = project
}
}
使うべき場面:
- データの整合性が重要な場合
- 誤削除を防ぎたい場合
双方向の関連を定義する:inverse
双方向の関連がある場合、inverse
パラメータで逆方向の参照を指定できます。
@Model
final class Author {
var name: String
// books配列の逆方向はBook.author
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book]
init(name: String) {
self.name = name
self.books = []
}
}
@Model
final class Book {
var title: String
var author: Author?
init(title: String, author: Author? = nil) {
self.title = title
self.author = author
}
}
inverse
を指定すると:
- SwiftDataが関連を適切に管理してくれる
- 片方を更新すると、もう片方も自動的に更新される
// 使用例
let author = Author(name: "夏目漱石")
let book = Book(title: "吾輩は猫である", author: author)
// authorのbooksにも自動的に追加される
modelContext.insert(author)
modelContext.insert(book)
関連の数を制限する
minimumModelCount
とmaximumModelCount
で、関連の数に制約を設けられます。
@Model
final class SoccerTeam {
var name: String
// 最低11人、最大25人
@Relationship(
minimumModelCount: 11,
maximumModelCount: 25,
deleteRule: .deny
)
var players: [Player]
init(name: String) {
self.name = name
self.players = []
}
}
@Model
final class Player {
var name: String
var number: Int
var team: SoccerTeam?
init(name: String, number: Int, team: SoccerTeam? = nil) {
self.name = name
self.number = number
self.team = team
}
}
これにより:
- プレイヤーが11人未満のチームは保存できない
- プレイヤーが25人を超えるチームは保存できない
- ビジネスルールをコードレベルで強制できる
実践的な使用例
実際のアプリでよく使うパターンを見ていきましょう。
例1:ブログシステム
@Model
final class User {
var username: String
var email: String
// ユーザーを削除すると、記事も削除
@Relationship(deleteRule: .cascade, inverse: \Post.author)
var posts: [Post]
// ユーザーを削除すると、コメントも削除
@Relationship(deleteRule: .cascade, inverse: \Comment.author)
var comments: [Comment]
init(username: String, email: String) {
self.username = username
self.email = email
self.posts = []
self.comments = []
}
}
@Model
final class Post {
var title: String
var content: String
var createdAt: Date
var author: User?
// 記事を削除すると、コメントも削除
@Relationship(deleteRule: .cascade, inverse: \Comment.post)
var comments: [Comment]
// 記事を削除しても、タグは残る
@Relationship(deleteRule: .nullify)
var tags: [Tag]
init(title: String, content: String, author: User? = nil) {
self.title = title
self.content = content
self.createdAt = Date()
self.author = author
self.comments = []
self.tags = []
}
}
@Model
final class Comment {
var content: String
var createdAt: Date
var author: User?
var post: Post?
init(content: String, author: User? = nil, post: Post? = nil) {
self.content = content
self.createdAt = Date()
self.author = author
self.post = post
}
}
@Model
final class Tag {
var name: String
@Relationship(deleteRule: .nullify)
var posts: [Post]
init(name: String) {
self.name = name
self.posts = []
}
}
例2:タスク管理アプリ
@Model
final class Project {
var name: String
var startDate: Date
var endDate: Date?
// プロジェクトを削除すると、タスクも削除
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task]
init(name: String, startDate: Date, endDate: Date? = nil) {
self.name = name
self.startDate = startDate
self.endDate = endDate
self.tasks = []
}
}
@Model
final class Task {
var title: String
var isCompleted: Bool
var dueDate: Date?
var priority: Int
var project: Project?
// タスクを削除すると、サブタスクも削除
@Relationship(deleteRule: .cascade, inverse: \SubTask.parentTask)
var subTasks: [SubTask]
// タスクを削除しても、担当者は残る
@Relationship(deleteRule: .nullify)
var assignees: [User]
init(title: String, priority: Int = 0, project: Project? = nil) {
self.title = title
self.isCompleted = false
self.priority = priority
self.project = project
self.subTasks = []
self.assignees = []
}
}
@Model
final class SubTask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String, parentTask: Task? = nil) {
self.title = title
self.isCompleted = false
self.parentTask = parentTask
}
}
例3:ECサイト
@Model
final class Customer {
var name: String
var email: String
// 顧客を削除すると、注文も削除(通常はしない方が良い)
@Relationship(deleteRule: .nullify, inverse: \Order.customer)
var orders: [Order]
init(name: String, email: String) {
self.name = name
self.email = email
self.orders = []
}
}
@Model
final class Order {
var orderNumber: String
var orderDate: Date
var totalAmount: Double
var customer: Customer?
// 注文を削除すると、注文明細も削除
@Relationship(deleteRule: .cascade, inverse: \OrderItem.order)
var items: [OrderItem]
init(orderNumber: String, customer: Customer? = nil) {
self.orderNumber = orderNumber
self.orderDate = Date()
self.totalAmount = 0
self.customer = customer
self.items = []
}
}
@Model
final class OrderItem {
var quantity: Int
var unitPrice: Double
var order: Order?
var product: Product?
init(quantity: Int, unitPrice: Double, order: Order? = nil, product: Product? = nil) {
self.quantity = quantity
self.unitPrice = unitPrice
self.order = order
self.product = product
}
}
@Model
final class Product {
var name: String
var price: Double
var stock: Int
// 商品を削除しても、過去の注文明細は残る
@Relationship(deleteRule: .nullify)
var orderItems: [OrderItem]
init(name: String, price: Double, stock: Int) {
self.name = name
self.price = price
self.stock = stock
self.orderItems = []
}
}
多対多の関係
多対多の関係も簡単に定義できます。
@Model
final class Student {
var name: String
var studentID: String
@Relationship(deleteRule: .nullify, inverse: \Course.students)
var courses: [Course]
init(name: String, studentID: String) {
self.name = name
self.studentID = studentID
self.courses = []
}
}
@Model
final class Course {
var title: String
var courseCode: String
@Relationship(deleteRule: .nullify, inverse: \Student.courses)
var students: [Student]
init(title: String, courseCode: String) {
self.title = title
self.courseCode = courseCode
self.students = []
}
}
// 使用例
let swift = Course(title: "Swift入門", courseCode: "CS101")
let student = Student(name: "田中太郎", studentID: "S001")
student.courses.append(swift)
// swift.studentsにも自動的に追加される
SwiftUIでの実装例
実際にSwiftUIで使う場合の例を見てみましょう。
import SwiftUI
import SwiftData
struct AuthorListView: View {
@Query private var authors: [Author]
@Environment(\.modelContext) private var modelContext
var body: some View {
NavigationStack {
List(authors) { author in
NavigationLink(destination: AuthorDetailView(author: author)) {
VStack(alignment: .leading) {
Text(author.name)
.font(.headline)
Text("\(author.books.count)冊の本")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("著者一覧")
.toolbar {
Button("追加") {
addAuthor()
}
}
}
}
func addAuthor() {
let author = Author(name: "新しい著者", birthYear: 2000)
modelContext.insert(author)
}
}
struct AuthorDetailView: View {
let author: Author
@Environment(\.modelContext) private var modelContext
var body: some View {
List {
Section("著者情報") {
Text("名前: \(author.name)")
Text("生年: \(author.birthYear)")
}
Section("著書") {
ForEach(author.books) { book in
VStack(alignment: .leading) {
Text(book.title)
Text("出版年: \(book.publishedYear)")
.font(.caption)
}
}
}
}
.navigationTitle(author.name)
.toolbar {
Button("本を追加") {
addBook()
}
}
}
func addBook() {
let book = Book(
title: "新しい本",
publishedYear: 2024,
author: author
)
modelContext.insert(book)
// author.booksにも自動的に追加される
}
}
よくある間違いと対処法
間違い1:循環参照を作ってしまう
// ❌ 両方でcascade削除すると問題になる可能性
@Model
final class Parent {
@Relationship(deleteRule: .cascade)
var children: [Child]
}
@Model
final class Child {
@Relationship(deleteRule: .cascade)
var parent: Parent? // これは危険
}
解決策: 一方は.nullify
または.deny
にする
// ✅ 片方だけcascade
@Model
final class Parent {
@Relationship(deleteRule: .cascade)
var children: [Child]
}
@Model
final class Child {
var parent: Parent? // こちらは通常の参照
}
間違い2:inverseの指定ミス
// ❌ 間違ったプロパティを指定
@Relationship(deleteRule: .cascade, inverse: \Book.title) // titleは関連ではない
var books: [Book]
解決策: 正しい関連プロパティを指定する
// ✅ 正しい指定
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book]
間違い3:必要のないところでdenyを使う
// ❌ これでは何も削除できなくなる
@Model
final class Category {
@Relationship(deleteRule: .deny)
var products: [Product]
}
解決策: ビジネスロジックに合った削除ルールを選ぶ
まとめ
@Relationship
マクロを使うことで、SwiftDataでのモデル間の関連を安全かつ明確に定義できます。
重要なポイント:
✅ 削除ルールを適切に選ぶ
.cascade
:親なしでは子が意味をなさない場合.nullify
:子が独立して存在できる場合.deny
:誤削除を防ぎたい場合
✅ 双方向の関連はinverseで明示する データの整合性が保たれ、コードが読みやすくなる
✅ 必要に応じて数の制限を設ける ビジネスルールをコードレベルで強制できる
✅ 実際のデータ構造に合わせて設計する 一対多、多対多など、現実のモデルを正しく反映させる
SwiftDataと@Relationship
を使いこなして、堅牢で保守しやすいiOSアプリを作りましょう!