【问题标题】:How to allow duplicate entries in a list of CoreData entities如何允许 CoreData 实体列表中的重复条目
【发布时间】:2021-12-25 22:47:43
【问题描述】:

在 CoreData 中,NSManagedObject 的每个实例都是唯一的。这就是 CoreData 使用 NSSet(及其有序对应物 NSOrderedSet)来表示集合的原因。但是,我需要一个允许项目多次出现的列表。

我的直觉是将每个对象包装在ListItem 实体中,并使用NSOrderedSet 生成列表。由于列表项本身是唯一的,因此对象可以根据需要在列表中出现多次。然而,这会产生意想不到的结果。

示例应用

在此示例应用程序 iFitnessRoutine 中,用户可以从一系列活动中进行选择,例如起重跳、仰卧起坐和弓步。然后,他们可以构建一个FitnessCircuit 来创建一个活动列表并在一定时间内执行每个活动。例如:

早间巡回赛:

  1. Jumping Jacks:60 秒
  2. 弓步:60 秒
  3. 仰卧起坐:60 秒
  4. Jumping Jacks:60 秒
  5. 仰卧起坐:60 秒
  6. Jumping Jacks:60 秒

在我的实现中,每个Activity 都包装在ListItem 中,但是结果会产生如下内容:

早间巡回赛:

  1. ListItem -> Jumping Jacks:60 秒
  2. ListItem -> 无
  3. ListItem -> 弓步:60 秒
  4. ListItem -> 仰卧起坐:60 秒
  5. ListItem -> 无
  6. ListItem -> 无

我可以添加多个列表项,但不会设置重复的活动。


我的数据模型如下所示,listItems 关系定义为NSOrderedSet。对于CodeGen,我使用class definition 让Core Data 自动生成NSManagedObject 子类。

iFitnessRoutine.xcdatamodeld


我像往常一样设置我的核心数据堆栈,并在必要时使用种子数据填充它。

AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.addSeedDataIfNecessary()
    return true
}

lazy var persistentContainer: NSPersistentContainer { ... }

func addSeedDataIfNecessary() {
    
    // 1. Check if there are fitness circuits.
    // Otherwise create "MorningRoutine"
    let fitnessCircuitRequest = FitnessCircuit.fetchRequest()
    fitnessCircuitRequest.sortDescriptors = []
    let fitnessCircuits = try! self.persistentContainer.viewContext.fetch(fitnessCircuitRequest)
    if fitnessCircuits.isEmpty {
        let fitnessCircuit = FitnessCircuit(context: self.persistentContainer.viewContext)
        fitnessCircuit.name = "Morning Routine"
        try! self.persistentContainer.viewContext.save()
    } else {
        print("Fitness Circuits already seeded")
    }
    
    // 2. Check if there are activities
    // Otherwise create "Jumping Jacks", "Sit-up", and "Lunges"
    let activitiesRequest = Activity.fetchRequest()
    activitiesRequest.sortDescriptors = []
    let activities = try! self.persistentContainer.viewContext.fetch(activitiesRequest)
    if activities.isEmpty {
        let activityNames = ["Jumping Jacks", "Sit-Ups", "Lunges"]
        for activityName in activityNames {
            let activity = Activity(context: self.persistentContainer.viewContext)
            activity.name = activityName
        }
        try! self.persistentContainer.viewContext.save()
    } else {
        print("Activities already seeded")
    }
    
}

RoutineTableViewController 中,我创建FetchedResultsController 来获取例程,并用它的活动填充表。要添加活动,我只需创建一个新列表项并为其分配一个随机活动。

RoutineTableViewController.swift

class FitnessCircuitTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    
    var fetchedResultsController: NSFetchedResultsController<FitnessCircuit>!
    var persistentContainer: NSPersistentContainer!
    
    var fitnessCircuit: FitnessCircuit! {
        return self.fetchedResultsController!.fetchedObjects!.first!
    }
    
    //MARK: - Configuration
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. Grab the persistent container from AppDelegate
        self.persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
        
        // 2. Configure FetchedResultsController
        let fetchRequest = FitnessCircuit.fetchRequest()
        fetchRequest.sortDescriptors = []
        self.fetchedResultsController = .init(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        self.fetchedResultsController.delegate = self
        
        // 3. Perform initial fetch
        try! self.fetchedResultsController.performFetch()
        
        // 4. Update the title with the circuit's name.
        self.navigationItem.title = self.fitnessCircuit!.name
    }
    
    
    //MARK: - FetchedResultsControllerDelegate
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        self.tableView.reloadData()
    }

    
    //MARK: - IBActions
    
    @IBAction func addButtonPressed(_ sender: Any) {
        // 1. Get all activities
        let activityRequest = Activity.fetchRequest()
        activityRequest.sortDescriptors = []
        let activities = try! self.persistentContainer.viewContext.fetch(activityRequest)
        
        // 2. Create a new list item with a random activity, and save.
        let newListItem = ListItem(context: self.persistentContainer.viewContext)
        newListItem.activity = activities.randomElement()!
        self.fitnessCircuit.addToListItems(newListItem)
        
        try! self.persistentContainer.viewContext.save()
    }
    
    //MARK: - TableView
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.fitnessCircuit.listItems?.count ?? 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Create a table view cell with index path and activity name
        let cell = UITableViewCell()
        let listItem = self.listItemForIndexPath(indexPath)
        var contentConfig = cell.defaultContentConfiguration()
        let activityName = listItem.activity?.name ?? "Unknown Activity"
        contentConfig.text = "\(indexPath.row). " + activityName
        cell.contentConfiguration = contentConfig
        return cell
        
    }
 
    private func listItemForIndexPath(_ indexPath: IndexPath) -> ListItem {
        let listItems = self.fitnessCircuit.listItems!.array as! [ListItem]
        return listItems[indexPath.row]
    }
    
}

这是我得到的结果:


如您所见,这会产生奇怪的结果。

  1. 重复活动显示为“未知活动”。 Core Data 不允许它们,即使它们连接到唯一的列表项。
  2. 无论何时执行此操作,它都会将列表项插入到列表中的随机索引中。否则,它会按预期附加到列表中。

任何帮助将不胜感激。 干杯

【问题讨论】:

    标签: ios swift core-data


    【解决方案1】:

    您的ActivityListItem 的关系是一对一的。

    但它应该是一对多的。当您将activity 重新分配给“最新”练习时,它会生成之前的关系nil,因为它只能附加到一个ListItem

    作为一般规则,每个 ?和 !应该以if elseif letguard 开头,以便您可以检测到这些内容并做出反应。

    【讨论】:

    • 是的!感谢您抽出宝贵时间回复。我现在明白了。当然。这个逻辑对我来说不是很明显,但我现在看到,要使这成为可能,必须允许活动与许多列表项有关系。我通过添加一些代码来删除活动并将删除规则设置为“级联”来确认这一点。所有列表项都随之消失。是的,可以肯定的是,强制展开并且没有错误处理是自找麻烦。干杯。
    【解决方案2】:

    我认为您在 Activity 实体中设置了唯一约束。在您的代码中看不到它,但如果您在可视化模型编辑器中查看实体,我敢打赌它就在那里。

    NSSet 允许您拥有多个具有相同值的项目,如果它们是不同的项目。也就是说,您可以拥有多个同名的活动,只是不能为同一个活动添加多个引用。

    这是我刚刚在 Playgrounds 中汇总的一些示例代码。我使用您的核心数据对象模型的简化版本。第一部分只是我在代码中构建模型,因为 Playgrounds 没有托管对象模型的可视化编辑器:

    import CoreData
    
    // MARK: - Core Data MOM
    /// This is a Managed Object Model built in code rather than with the visual editor. The code here corresponds pretty directly to the settings in the visual editor.
    let FitnessCircuitDescription:NSEntityDescription = {
        let entity = NSEntityDescription()
        entity.name = "FitnessCircuit"
        entity.managedObjectClassName = "FitnessCircuit"
        entity.properties.append({
            let property = NSAttributeDescription()
            property.name = "name"
            property.attributeType = .stringAttributeType
            return property
        }())
        entity.properties.append({
            let relationship = NSRelationshipDescription()
            relationship.name = "activities"
            relationship.isOrdered = true
            relationship.deleteRule = .cascadeDeleteRule
            return relationship
        }())
        entity.uniquenessConstraints = [[entity.propertiesByName["name"]!]]
        return entity
    }()
    let FitnessActivityDescription:NSEntityDescription = {
        let entity = NSEntityDescription()
        entity.name = "FitnessActivity"
        entity.managedObjectClassName = "FitnessActivity"
        entity.properties.append({
            let property = NSAttributeDescription()
            property.name = "name"
            property.attributeType = .stringAttributeType
            return property
        }())
        entity.properties.append({
            let relationship = NSRelationshipDescription()
            relationship.name = "fitnessCircuit"
            relationship.deleteRule = .nullifyDeleteRule
            relationship.maxCount = 1
            return relationship
        }())
        return entity
    }()
    
    FitnessCircuitDescription.relationshipsByName["activities"]!.destinationEntity = FitnessActivityDescription
    FitnessCircuitDescription.relationshipsByName["activities"]!.inverseRelationship = FitnessActivityDescription.relationshipsByName["fitnessCircuit"]!
    FitnessActivityDescription.relationshipsByName["fitnessCircuit"]!.destinationEntity = FitnessCircuitDescription
    FitnessActivityDescription.relationshipsByName["fitnessCircuit"]!.inverseRelationship = FitnessCircuitDescription.relationshipsByName["activities"]!
    
    let iFitnessRoutineModel = NSManagedObjectModel()
    iFitnessRoutineModel.entities.append(FitnessCircuitDescription)
    iFitnessRoutineModel.entities.append(FitnessActivityDescription)
    
    
    
    // MARK: - Core Data Classes
    /// This stuff is handled for you if you have Codegen set to Class Definition. Don't have that option in Playgrounds.
    @objc(FitnessCircuit)
    public class FitnessCircuit: NSManagedObject {
        @NSManaged var name:String
        @NSManaged var activities:NSOrderedSet
        
        @objc(insertObject:inActivitiesAtIndex:)
        @NSManaged public func insertIntoActivities(_ value: FitnessActivity, at idx: Int)
        @objc(removeObjectFromActivitiesAtIndex:)
        @NSManaged public func removeFromActivities(at idx: Int)
        @objc(insertActivities:atIndexes:)
        @NSManaged public func insertIntoActivities(_ values: [FitnessActivity], at indexes: NSIndexSet)
        @objc(removeActivitiesAtIndexes:)
        @NSManaged public func removeFromActivities(at indexes: NSIndexSet)
        @objc(replaceObjectInActivitiesAtIndex:withObject:)
        @NSManaged public func replaceActivities(at idx: Int, with value: FitnessActivity)
        @objc(replaceActivitiesAtIndexes:withActivities:)
        @NSManaged public func replaceActivities(at indexes: NSIndexSet, with values: [FitnessActivity])
        @objc(addActivitiesObject:)
        @NSManaged public func addToActivities(_ value: FitnessActivity)
        @objc(removeActivitiesObject:)
        @NSManaged public func removeFromActivities(_ value: FitnessActivity)
        @objc(addActivities:)
        @NSManaged public func addToActivities(_ values: NSOrderedSet)
        @objc(removeActivities:)
        @NSManaged public func removeFromActivities(_ values: NSOrderedSet)
        
        @nonobjc func fetchRequest() -> NSFetchRequest<FitnessCircuit> {
            return NSFetchRequest<FitnessCircuit>(entityName: "FitnessCircuit")
        }
    }
    
    @objc(FitnessActivity)
    public class FitnessActivity: NSManagedObject {
        @NSManaged var name:String
        @NSManaged var fitnessCircuit:FitnessCircuit?
        
        @nonobjc func fetchRequest() -> NSFetchRequest<FitnessActivity> {
            return NSFetchRequest<FitnessActivity>(entityName: "FitnessActivity")
        }
    }
    
    
    
    // MARK: - Core Data Extensions
    /// Simple extension to give us a typed array to deal with rather than an ordered set.
    extension FitnessCircuit {
        public dynamic var activityArray: [FitnessActivity] {
            return self.activities.array as? [FitnessActivity] ?? []
        }
    }
    
    
    
    // MARK: - Core Data Stack
    /// I set this to go to /dev/null so things aren't actually saved to disk. You can set the path to /tmp/iFitnessRoutine if you want to see how it writes data to storage.
    let container = NSPersistentContainer(name: "iFitnessRoutine Container", managedObjectModel: iFitnessRoutineModel)
    container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
    })
    container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    
    
    
    // MARK: - Application logic
    /// Here's where we start building the objects and connecting them to each other.
    let circuit1 = FitnessCircuit(context: container.viewContext)
    circuit1.name = "Morning Circuit"
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Jumping Jacks: 60 seconds"
        return activity}())
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Lunges: 60 seconds"
        return activity}())
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Sit-ups: 60 seconds"
        return activity}())
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Jumping Jacks: 60 seconds"
        return activity}())
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Sit-ups: 60 seconds"
        return activity}())
    circuit1.addToActivities({ () -> FitnessActivity in
        let activity = FitnessActivity(context: container.viewContext)
        activity.name = "Jumping Jacks: 60 seconds"
        return activity}())
    
    try! container.viewContext.save()
    
    /// Now, to prove there's nothing up my sleeves, let's pull the data back out of the database and work solely with that, rather than the objects we built above.
    let circuitFetch = FitnessCircuit.fetchRequest()
    let circuits = try! container.viewContext.fetch(circuitFetch) as! [FitnessCircuit]
    for circuit in circuits {
        print("Circuit name: \(circuit.name)")
        for activity in circuit.activityArray {
            print(activity.name)
        }
    }
    

    当我在 macOS 11.6 上使用 Xcode 13.2.1 运行它时,我得到以下输出:

    Circuit name: Morning Circuit
    Jumping Jacks: 60 seconds
    Lunges: 60 seconds
    Sit-ups: 60 seconds
    Jumping Jacks: 60 seconds
    Sit-ups: 60 seconds
    Jumping Jacks: 60 seconds
    

    “Jumping Jacks:60 秒”项目都是存储在 Core Data 中的不同对象。这是可行的,因为我没有为活动设置任何独特的设置,仅用于电路。

    【讨论】:

    • 感谢您抽出宝贵时间回复!我检查了唯一的约束,但没有发现。如果我理解正确,在这个实现中,Jumping Jacks 的每个实例都是一个独特的对象。然而,我的目标是让 Jumping Jacks 的每次出现都指向同一个实例。想象一下,有一个包含所有已知活动的单独数据库。从源数据库中删除 Jumping Jacks 会在所有地方删除 Jumping Jacks。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-25
    • 2021-08-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多