MENU

SwiftDataの@Relationshipマクロを徹底解説:モデル間の関連を自在に操る

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を使うことで:

  1. 削除時の挙動を制御できる:親を削除したとき、子も削除するか?関連だけ外すか?
  2. データの整合性を保てる:最小・最大数の制限などを設定できる
  3. コードの意図が明確になる:「これは重要な関連です」というメッセージを伝えられる
  4. 双方向の関連を明示できる:逆方向の参照を指定できる

つまり、より堅牢で保守しやすいアプリを作るために必要なのです。

基本的な使い方

まずは、シンプルな一対多の関係から見ていきましょう。

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)

関連の数を制限する

minimumModelCountmaximumModelCountで、関連の数に制約を設けられます。

@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アプリを作りましょう!

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次