マイグレーションとは何か
データベースのスキーマ(構造や形式)をより最新のバージョンに変更する過程を、マイグレーション といいます。
要件の変更に対応するために、データベースの構造を更新し、既存のデータを新しい形式に変換する手順です。
SwiftDataにおけるマイグレーション
SwiftDataでは、VersionedSchema
とSchemaMigrationPlan
を使用することでマイグレーションを行うことができます。
マイグレーションは二段階あり、それぞれ「 lightweight migration 」と「complex migration 」と言います。
あるモデルにおいて、以前は一意でなかったプロパティにユニーク制約をつける際、「 lightweight migration 」を使用することができます。
ここで、新しいプロパティを追加したり、プロパティ名を変更する場合、「complex migration」を使用します。
「complex migration」においては、SwiftDataにモデルの古いバージョンと新しいバージョンの整合性を保つために、カスタムロジックを提供します。
マイグレーションの手順
手順は簡単です。
- マイグレーション対象のモデルのtypealiasとして最新のモデルを指定する
- 特定のバージョン間でどのようにマイグレーションするかを定義するためのインターフェースである、MigrationPlanを作成する
- マイグレーションの移行手法を定義する
上記の手順は、「 lightweight migration 」においても「complex migration 」においても同様です。
マイグレーションの実装
lightweight migration
例としてItemモデルを作成しました。プロパティとして <code>timestamp
を持っています。
<code>VersionedSchema
に準拠させます。
スキーマバージョンは 「1, 0, 0」です。
struct ItemSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
}
Itemの <code>timestamp
をユニークにしましょう。
ユニークな制約が適用された V2のバージョン化スキーマ を作成してみました。
struct ItemSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
// timestampに一意制約を付与する
@Attribute(.unique) var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
}
この場合、lightweight migration
で実現できます。
先ほど示した手順に則ってマイグレーションを行います。
まずはItemのtypealiasとして、ItemSchemaV2を指定します。
typealias Item = ItemSchemaV2.Item
次に、MigrationPlanを作成します。
schemas
にて、データベーススキーマの異なるバージョンをリストすることで、スキーマのバージョン管理を行います。
MigrationStage
にて、マイグレーションプロセスの具体的な手順を定義します。
enum ItemMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
// バージョンの順番に配置することで、SwiftDataが順序正しくバージョン間を移行できるようにする
[ItemSchemaV1.self, ItemSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
// 簡易的なマイグレーション。V1toV2の時は、一意制約をつけただけなので、lightweightで十分。
static let migrateV1toV2: MigrationStage = MigrationStage.lightweight(
fromVersion: ItemSchemaV1.self,
toVersion: ItemSchemaV2.self
)
}
complex migration
Imageに新しいプロパティmessage
と image
を追加します。
struct ItemSchemaV3: VersionedSchema {
static var versionIdentifier = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
@Attribute(.unique) var timestamp: Date
// メッセージ 写真を追加。
var message: String = ""
var image: String = ""
init(timestamp: Date, message: String = "", image: String = "") {
self.timestamp = timestamp
self.message = message
self.image = image
}
}
}
大筋は先ほど共有したlightweight migrationの手順と同一です。
違いは、MigrationStageにてデータの整合性を保つためのカスタムメソッドを実装する必要がある点です。
// カスタムマイグレーション。V2toV3時は messageとitemが追加されているため、旧バージョンの際の扱いを定義する。
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: ItemSchemaV2.self,
toVersion: ItemSchemaV3.self,
willMigrate: nil
) { context in
let items = try? context.fetch(FetchDescriptor<ItemSchemaV3.Item>())
items?.forEach { item in
item.message = ""
item.image = ""
}
try? context.save()
}
ここでは、新しいバージョン(ItemSchemaV3
)において追加された message
と image
フィールドが旧バージョン(ItemSchemaV2
)には存在しないため、各アイテムの message
と image
プロパティを空文字列(””)で初期化しています。
ここまでのコードをまとめると、以下の状態になります。コピペすればマイグレーションの挙動を確認できます。
import Foundation
import SwiftData
struct ItemSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
}
struct ItemSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
// timestampに一意制約を付与する
@Attribute(.unique) var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
}
struct ItemSchemaV3: VersionedSchema {
static var versionIdentifier = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
@Attribute(.unique) var timestamp: Date
// メッセージ 写真カラムを追加。
var message: String = ""
var image: String = ""
init(timestamp: Date, message: String = "", image: String = "") {
self.timestamp = timestamp
self.message = message
self.image = image
}
}
}
// Itemが常に最新バージョンを指すように型エイリアスを追加する
typealias Item = ItemSchemaV3.Item
// マイグレーションプランを作成する。
// 一つの特定のバージョンから別のバージョンに移動する方法を定義する
enum ItemMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
// バージョンの順番に配置することで、SwiftDataが順序正しくバージョン間を移行できるようにする
[ItemSchemaV1.self, ItemSchemaV2.self, ItemSchemaV3.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
// 簡易的なマイグレーション。V1toV2の時は、一意制約をつけただけなので、lightweightで十分。
static let migrateV1toV2: MigrationStage = MigrationStage.lightweight(
fromVersion: ItemSchemaV1.self,
toVersion: ItemSchemaV2.self
)
// カスタムマイグレーション。V2toV3時は messageとitemが追加されているため、旧バージョンの際の扱いを定義する。
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: ItemSchemaV2.self,
toVersion: ItemSchemaV3.self,
willMigrate: nil
) { context in
let items = try? context.fetch(FetchDescriptor<ItemSchemaV3.Item>())
items?.forEach { item in
item.message = ""
item.image = ""
}
try? context.save()
}
}