如何允许 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】:您的Activity
与ListItem
的关系是一对一的。
但它应该是一对多的。当您将activity
重新分配给“最新”练习时,它会生成之前的关系nil
,因为它只能附加到一个ListItem
。
作为一般规则,每个 ?和 !应该以if else
、if let
或guard
开头,以便您可以检测到这些内容并做出反应。
【讨论】:
是的!感谢您抽出宝贵时间回复。我现在明白了。当然。这个逻辑对我来说不是很明显,但我现在看到,要使这成为可能,必须允许活动与许多列表项有关系。我通过添加一些代码来删除活动并将删除规则设置为“级联”来确认这一点。所有列表项都随之消失。是的,可以肯定的是,强制展开并且没有错误处理是自找麻烦。干杯。【参考方案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 关系中的对象?