Swift Core Data 分阶段迁移

news2024/11/22 18:58:37

在这里插入图片描述

在这里插入图片描述

文章目录

    • 前言
    • 什么是分阶段迁移?
    • 提供一些背景信息
    • 创建迁移管理器
    • 设置使用 Core Data 栈。
    • 总结

前言

在这之前,我发布了一篇文章,在其中解释了如何使用映射模型和自定义迁移策略执行复杂的 Core Data 迁移。虽然这种方法性能良好且运行良好,但很难维护,不适用于应用程序扩展,并且存在高度的错误风险。

例如,对于每个需要自定义迁移的新模型,你需要定义一个映射模型,以定义如何将每个模型的现有版本迁移到新版本。与你可能认为的相反(以及我所认为的),Core Data 在跨多个版本进行迁移时并不会按顺序迭代映射模型,相反,它需要从当前版本到新版本的精确模型。

除此之外,你需要使用 Xcode 的 UI 和映射模型来定义所有这些内容,这使得 PR 难以审查,错误难以发现。出于这些原因,我最近重新设计了我们的迁移流程,改用分阶段迁移,对开发者体验产生了巨大的影响!

什么是分阶段迁移?

正如在 WWDC23 中宣布的那样,与在 Swift 数据模型之间执行迁移的方式非常相似,你现在可以使用 NSStagedMigrationManager 实例以编程方式定义 Core Data 迁移。

该方法通过定义一系列迁移步骤(称为阶段),描述了如何在模型的不同版本之间进行迁移。

例如,假设你的应用程序当前正在使用数据模型的第 1 版,你想要迁移到第 3 版。迁移管理器将顺序应用所有必要的阶段,以从第 1 版迁移到第 2 版,然后从第 2 版迁移到第 3 版。

提供一些背景信息

为了演示 Core Data 分阶段迁移的工作原理,我将使用我之前在有关使用映射模型进行自定义 Core Data 迁移的文章中使用的相同示例。

与之前的文章一样,我们想要将 Track 模型中的 json 属性转换为一个单独的实体,该实体将为每个曲目保存所有相关的艺术家信息。将此属性转换也将使模型更灵活、更易于维护,因为我们将能够删除 json 属性本身和 artistName,而使用新的关系。

让我们比较一下我们的 Track 模型之前和之后的情况,CoreData.swift 文件代码如下:

Copy code
CoreData.swift
// Before
import Foundation
import CoreData

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var json: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artistName: String?
}

// After

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artists: NSSet?

    @objc(addArtistsObject:)
    @NSManaged public func addToArtists(_ value: Artist)

    @objc(removeArtistsObject:)
    @NSManaged public func removeFromArtists(_ value: Artist)

    @objc(addArtists:)
    @NSManaged public func addToArtists(_ values: NSSet)

    @objc(removeArtists:)
    @NSManaged public func removeFromArtists(_ values: NSSet)
}

@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
        return NSFetchRequest<Artist>(entityName: "Artist")
    }

    @NSManaged public var name: String?
    @NSManaged public var id: String?
    @NSManaged public var imageURL: String?
    @NSManaged public var tracks: NSSet?

    @objc(addTracksObject:)
    @NSManaged public func addToTracks(_ value: Track)

    @objc(removeTracksObject:)
    @NSManaged public func removeFromTracks(_ value: Track)

    @objc(addTracks:)
    @NSManaged public func addToTracks(_ values: NSSet)

    @objc(removeTracks:)
    @NSManaged public func removeFromTracks(_ values: NSSet)
}

从上面的代码中可以看出,迁移并不是微不足道的,而且,对我们来说,Core Data 不能自动推断它。让我们看看如何使用分阶段迁移以代码形式定义迁移步骤。

创建迁移管理器

要定义我们的阶段,我们需要将我们的模型拆分为三个不同的模型版本和迁移:

  1. 保持原始模型版本不变。
  2. 第二个模型版本包含所有属性,并添加 Artist 实体和关系。这将是一个自定义阶段。
  3. 第三个模型版本删除了 jsonartistName 属性。这将是一个轻量级的阶段。

我们需要将迁移分解为三个阶段的原因是,就目前而言,我们不能在同一个阶段中使用并删除属性。

让我们从创建一个负责创建 NSStagedMigrationManager 实例并定义所有阶段的工厂类开始。StagedMigrationFactory.swift 文件代码如下:

import Foundation
import CoreData
import OSLog

// 1
extension Logger {
    private static var subsystem = "dev.polpiella.CustomMigration"
    
    static let storage = Logger(subsystem: subsystem, category: "Storage")
}

// 2
extension NSManagedObjectModelReference {
    convenience init(in database: URL, modelName: String) {
        let modelURL = database.appending(component: "\(modelName).mom")
        guard let model = NSManagedObjectModel(contentsOf: modelURL) else { fatalError() }
        
        self.init(model: model, versionChecksum: model.versionChecksum)
    }
}

// 3
final class StagedMigrationFactory {
    private let databaseURL: URL
    private let jsonDecoder: JSONDecoder
    private let logger: Logger
    
    init?(
        bundle: Bundle = .main,
        jsonDecoder: JSONDecoder = JSONDecoder(),
        logger: Logger = .storage
    ) {
        // 4
        guard let databaseURL = bundle.url(forResource: "CustomMigration", withExtension: "momd") else { return nil }
        self.databaseURL = databaseURL
        self.jsonDecoder = jsonDecoder
        self.logger = logger
    }
    
    // 5
    func create() -> NSStagedMigrationManager {
        let allStages = [
            v1toV2(),
            v2toV3()
        ]
        
        return NSStagedMigrationManager(allStages)
    }

    // 6
    private func v1toV2() -> NSCustomMigrationStage {
        struct Song: Decodable {
            let artists: [Artist]
            
            struct Artist: Decodable {
                let id: String
                let name: String
                let imageURL: String
            }
        }
        
        // 7
        let customMigrationStage = NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2")
        )
        
        // 8
        customMigrationStage.didMigrateHandler = { migrationManager, currentStage in
            guard let container = migrationManager.container else {
                return
            }
            
            // 9
            let context = container.newBackgroundContext()
            context.performAndWait {
                let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Track")
                fetchRequest.predicate = NSPredicate(format: "json != nil")
                
                do {
                    let allTracks = try context.fetch(fetchRequest)
                    let addedArtists = [String: NSManagedObject]()
                    for track in allTracks {
                        if let jsonString = track.value(forKey: "json") as? String {
                            let jsonData = Data(jsonString.utf8)
                            let object = try? self.jsonDecoder.decode(Song.self, from: jsonData)
                            let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
                                if let matchedArtist = addedArtists[jsonArtist.id] {
                                    return matchedArtist
                                }
                                let artist = NSEntityDescription
                                    .insertNewObject(
                                        forEntityName: "Artist",
                                        into: context
                                    )
                                
                                artist.setValue(jsonArtist.name, forKey: "name")
                                artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
                                artist.setValue(jsonArtist.id, forKey: "id")
                                
                                return artist
                            } ?? []
                            
                            track.setValue(Set<NSManagedObject>(artists), forKey: "artists")
                        }
                    }
                    try context.save()
                } catch {
                    logger.error("\(error.localizedDescription)")
                }
            }
        }
        
        return customMigrationStage
    }
    
    // 10
    private func v2toV3() -> NSCustomMigrationStage {
        NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 3")
        )
    }
}

回到上面的代码,让我们逐步分解:

  1. 我们定义了一个自定义记录器,以将迁移过程中发生的任何错误报告到控制台。
  2. 我们扩展了 NSManagedObjectModelReference,创建了一个方便的初始化方法,它接受数据库 URL 和模型名称,并返回一个新的 NSManagedObjectModelReference 实例。
  3. 我们定义了一个工厂类,负责创建 NSStagedMigrationManager 实例并定义所有阶段。
  4. 我们使用 bundle 初始化工厂,并检索数据库的 URL、JSON 解码器和记录器。
  5. 我们创建了 NSStagedMigrationManager 实例,并定义了所有阶段。
  6. 我们定义了一个方法,该方法将返回从我们模型的第 1 版迁移到第 2 版的迁移阶段。
  7. 我们创建了一个 NSCustomMigrationStage 实例,并传递我们要从何处迁移和迁移到的对象模型引用。文件名需要与包中的 .mom 文件的名称匹配。
  8. 我们定义了 didMigrateHandler 闭包,在模型迁移后调用。此时,新的模型版本可在上下文中使用,你可以填充其属性。你必须知道,还有一个在先前模型版本上执行的单独处理程序,称为 willMigrateHandler,但我们在这种情况下不会使用它。
  9. 我们创建了一个新的后台上下文,并获取所有具有 json 属性的曲目。然后,我们将 JSON 字符串解码为 Song 对象,并为 JSON 中的每个艺术家创建一个新的 Artist 实体。然后,我们将 Track 实体的 artists 关系设置为新的 Artist 实体。
  10. 我们定义了一个方法,该方法将返回从我们模型的第 2 版迁移到第 3 版的迁移阶段。这个迁移非常简单,事实上,它应该是一个轻量级的迁移。然而,我找不到一个能够在所有情况下使用的 NSLightweightMigrationStage 实例的方法。如果你知道如何做,请告诉我!

设置使用 Core Data 栈。

设置使用分阶段迁移的 Core Data 栈。

现在我们有了创建 NSStagedMigrationManager 实例的方法,我们需要设置我们的 Core Data 栈以使用它。PersistenceController.swift 文件代码如下:

PersistenceController.swift
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CustomMigration")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        if let description = container.persistentStoreDescriptions.first {
            if let migrationFactory = StagedMigrationFactory() {
                description.setOption(migrationFactory.create(), forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
            }
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

这部分非常简单,你只需要将 NSStagedMigrationManager 实例设置为持久化存储描述的选项。

总结

这篇文章介绍了使用分阶段迁移来改进 Core Data 迁移流程的重要性和方法。传统的迁移方法使用映射模型,但这种方法不易维护,扩展性差且容易出错。分阶段迁移通过定义一系列迁移步骤,使得在不同模型版本之间进行迁移变得更加简单和可控。文章以一个示例来说明分阶段迁移的工作原理,以及如何以代码形式定义迁移步骤。最后,文章展示了如何设置使用分阶段迁移的 Core Data 栈。通过使用分阶段迁移,可以显著提高开发者体验,简化迁移流程,并降低错误风险。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1894465.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙小案例-自定义键盘

一个自定义键盘 效果 完成简单的26键中英文输入 使用&#xff1a; Entry Component struct IndexInput {State text: string inputController: TextInputController new TextInputController()//自定义键盘关闭事件hideClick(){this.inputController.stopEditing()}//自定义…

自动化设备上位机设计 一

目录 一 设计原型 二 后台代码 一 设计原型 二 后台代码 namespace 自动化上位机设计 {public partial class Form1 : Form{public Form1(){InitializeComponent();}private void Form1_Load(object sender, EventArgs e){}} }namespace 自动化上位机设计 {partial class Fo…

GPU相关的一些截图(备查,待整理)

GPU相关的一些截图 这里记录一些与GPU相关的截图,方便查阅

线段树求区间最值问题

引言 今天主要还是练了两道题&#xff0c;是有关线段树如何去求一个区间内的最值问题的&#xff0c;我们可以用线段树来解决。 对应一个无法改变顺序的数组&#xff0c;我们想要去求一个区间内的最值&#xff0c;假设有n个结点&#xff0c;m次询问&#xff0c;暴力的解决办法…

【高校科研前沿】南京地理与湖泊研究所博士后夏凡为第一作者在环境科学与水资源领域Top期刊发文:钙对云南洱海溶解有机质与浮游细菌相互作用的调控作用

文章简介 论文名称&#xff1a;Calcium regulates the interactions between dissolved organic matter and planktonic bacteria in Erhai Lake, Yunnan Province, China 第一作者及单位&#xff1a;夏凡&#xff08;博士后|中国科学院南京地理与湖泊研究所&#xff09; 通讯…

关于虚拟机CentOS 7使用ssh无法连接(详细)

虚拟机CentOS 7使用ssh无法连接 猜测&#xff1a;可能是虚拟机软件的网关和和centos7的网关不同导致的问题。 首先打开CentOS7的终端, 输入ifconfig&#xff0c;查看一下系统的ip 打开虚拟机的虚拟网络编辑器, 查看一下网关, 发现确实不一样. 这里有两种方式, 要么修改虚…

fluwx插件实现微信支付

Flutter开发使用fluwx插件实现微信支付&#xff0c;代码量不多&#xff0c;复杂的是安卓和iOS的各种配置。 在 pubspec.yaml 文件中添加fluwx依赖 fluwx: ^4.5.5 使用方法 通过fluwx注册微信Api await Fluwx().registerApi(appId: wxea7a1c53d9e5849d, universalLink: htt…

Android系统集成和使用FFmpeg

文章目录 前言FFmpeg源码下载交叉编译NDK下载x264编译源码下载编译 FFmpeg编译脚本 AOSP继承FFmpeg 前言 原生AOSP中并未继承FFmpeg&#xff0c;所以要想在android上使用&#xff0c;需要自己编译集成。 FFmpeg源码下载 git clone https://git.ffmpeg.org/ffmpeg.git目前最新…

Java [ 基础 ] Stream流 ✨

✨探索Java基础Stream流✨ 在现代Java编程中&#xff0c;Stream是一个非常强大的工具&#xff0c;它提供了一种更高效和简洁的方式来处理集合数据。在这篇博客中&#xff0c;我们将深入探讨Java中的Stream流&#xff0c;介绍它的基础知识、常见操作和一些实用示例。 什么是Str…

暗潮短视频:成都柏煜文化传媒有限公司

暗潮短视频&#xff1a;涌动的新媒体力量 在数字化时代的浪潮中&#xff0c;短视频以其独特的魅力和无限的潜力&#xff0c;迅速成为新媒体领域的一股强大力量。而在这片繁荣的短视频领域中&#xff0c;成都柏煜文化传媒有限公司“暗潮短视频”以其独特的定位和深邃的内容&…

解决mysql数据库连接报错:Authentication plugin ‘caching_sha2_password‘ cannot be loaded

解决mysql数据库连接报错&#xff1a;Authentication plugin ‘caching_sha2_password’ cannot be loaded OperationalError: (2059, “Authentication plugin ‘caching_sha2_password’ cannot be loaded: /usr/lib/mysql/plugin/caching_sha2_password.so: cannot open sha…

虚拟机与主机的联通

本地光纤分配地址给路由器--》连结路由器是连结局域网--》由路由器分配IP地址 因此在网站上搜索的IP与本机的IP是不一样的 1.windows查看主机IP地址 在终端输入 2.linux虚拟机查看ip 3.主机是否联通虚拟机ping加ip

【AI学习】无线AI的问题和挑战

无线AI&#xff0c;即无线人工智能&#xff0c;是指内生于未来&#xff08;6G&#xff09;无线通信系统并通过无线架构、无线数据、无线算法和无线应用所呈现出来的新的人工智能技术体系。 最近一直在进行无线AI的调研&#xff0c;感觉真的是路漫漫其修远兮。业界有一些探索&a…

数学建模------Matlab数据可视化

目录 1.plot函数 &#xff08;1&#xff09;函数介绍 &#xff08;2&#xff09;参数介绍 &#xff08;3&#xff09;图形美化 &#xff08;4&#xff09;背景更改 &#xff08;5&#xff09;多组绘制 &#xff08;6&#xff09;图形叠加 &#xff08;7&#xff09;添加…

Matplotlib 简介

import matplotlib.pyplot as plt plt.plot([1, 2, 3, 4]) plt.ylabel(some numbers) plt.show() 当使用plot只传入单个数组时&#xff0c;matplotlib会认为这是y的值&#xff0c;并自动生成长度相同&#xff0c;但是从0开始的x值&#xff0c;所以这里的x会自动生成为 [0,1,2,…

Vue.js 案例——商品管理

一.需要做出的效果图&#xff1a; 二.实现的步骤 首先&#xff0c;先建一个项目&#xff0c;命名Table&#xff0c;在Table项目中的components里新建一个MyTable.vue文件。 第二步&#xff0c;在原有的 HelloWorld.vue中写入代码。 HelloWorld.vue代码如下&#xff1a; <…

土地规划与文化遗产保护:在发展浪潮中守护历史的脉络

在这个日新月异的时代&#xff0c;城市化进程如火如荼&#xff0c;土地规划作为引导城市发展方向的关键&#xff0c;承载着平衡发展与保护的重任。在追求现代化的同时&#xff0c;保护文化遗产不仅是对过去的尊重&#xff0c;更是对未来负责。本文旨在深入探讨如何在土地规划实…

云桌面运维工程师

一 深信服驻场工程师 1 深信服AC、AF、AD、NGAF、WOC Atrust、WAF项目实施经验者优先考虑。 负责云桌面POC测试 部署和配置&#xff1a;设置云桌面基础设施&#xff0c;包括虚拟化平台、云桌面管理软件和相关组件。确保正确配置网络、存储和安全设置。 用户体验&#xff1…

7.3数据库第一次作业

安装MySQL 1.打开安装包 2.选择自定义安装&#xff08;custom&#xff09;并点击下一步 3.自定义安装路径 4.点击执行 5.执行成功 6.默认选项点击下一步 7.选择新的授权方式并点击下一步 8.配置密码 9.默认配置并点击下一步 10.点击执行&#xff08;Execute&#xff09; 11.执…

bcc python开发示例

文章目录 1. hello_world.py2. hello_fields.py3. sync_timing.py4. disksnoop.py5. hello_perf_output.py6. bitehist.py7. vfsreadlat.py8. urandomread.py9. strlen_count.py10. nodejs_http_server.py11. task_switch.c12. 监视 do_execve 1. hello_world.py from bcc imp…