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

Posted

技术标签:

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

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

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

示例应用

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

早间巡回赛:

    Jumping Jacks:60 秒 弓步:60 秒 仰卧起坐:60 秒 Jumping Jacks:60 秒 仰卧起坐:60 秒 Jumping Jacks:60 秒

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

早间巡回赛:

    ListItem -> Jumping Jacks:60 秒 ListItem -> 无 ListItem -> 弓步:60 秒 ListItem -> 仰卧起坐:60 秒 ListItem -> 无 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]
    
    


这是我得到的结果:


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

    重复活动显示为“未知活动”。 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。

以上是关于如何允许 CoreData 实体列表中的重复条目的主要内容,如果未能解决你的问题,请参考以下文章

使用 NSFetchedResultsController 在 TableView 上显示的元素列表与底层 CoreData 实体不匹配

SwiftUI - 如何在 CoreData 中删除实体中的行

如何以某种通用方式删除有序对多 CoreData 关系中的对象?

使用通过 React GUI 触发的实体框架向 DB 添加新条目时出现重复条目

如何防止 Jquery 可排序连接列表中的重复条目?

从出现在 NSPopUpButton 列表中的核心数据实体中过滤条目