如何允许 CoreData 实体列表中的重复条目

How to allow duplicate entries in a list of CoreData entities

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

的列表

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

示例应用程序

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

上午巡回赛:

  1. 开合跳:60 秒
  2. 弓步:60 秒
  3. 仰卧起坐:60 秒
  4. 开合跳:60 秒
  5. 仰卧起坐:60 秒
  6. 开合跳:60 秒

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

上午巡回赛:

  1. ListItem -> 开合跳: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 来获取例程,并用其活动填充 table。要添加 activity,我只需创建一个新列表项并为其分配一个随机 activity.

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. 重复活动显示为“未知 Activity”。核心数据不允许它们,即使它们连接到唯一的列表项。
  2. 无论何时执行此操作,它都会将列表项插入到列表中的随机索引中。否则,它会按预期附加到列表中。

如有任何帮助,我们将不胜感激。 干杯

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

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

这是我刚刚在 Playgrounds 中拼凑的一些示例代码。我使用您的 Core Data 对象模型的简化版本。第一部分只是我用代码构建模型,因为 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 seconds”项都是存储在 Core Data 中的不同对象。这是可行的,因为我没有为活动设置任何唯一设置,只为电路设置。

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

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

作为一般规则,每 ?和 !应该在 if elseif letguard 之前,以便您可以检测到这些东西并对其做出反应。