使用 Core Data 插入/更新记录的最有效方法?
Posted
技术标签:
【中文标题】使用 Core Data 插入/更新记录的最有效方法?【英文标题】:Most efficient way to insert/update records with Core Data? 【发布时间】:2017-12-11 23:46:53 【问题描述】:我正在开发一个定期从服务器下载数据的应用程序。如果需要更新数据,我会使用类似以下的方法来更新记录或插入新记录(如果它们不存在)。
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Trip")
for csvTrip in csvTrips
var trip: NSManagedObject!
let tripId = Int(csvTrip[0])!
fetchRequest.predicate = NSPredicate(format: "id = %d", tripId)
if (context.count(for: fetch) == 0)
trip = NSEntityDescription.insertNewObject(forEntityName: "Trip", into: context)
trip.setValue(tripId, forKey: "id")
else
tripObject = (context.fetch(fetch) as! [NSManagedObject])[0]
// Set other properties
检查实体是否已经存在于每个循环中,比仅插入而不检查实体要慢大约 100 倍,这对于数千个实体来说是个大问题。我已经尝试先获取所有实体,但我仍然必须遍历每个实体并将 id 添加到数组或其他东西中,这并没有快多少。我知道核心数据与 mysql 不同,但我很难相信没有类似于 INSERT ... ON DUPLICATE KEY UPDATE 的功能,这在 MYSQL 中非常快。我错过了什么吗?
【问题讨论】:
获取所有 id 并将它们加载到Set
应该很快
有没有办法专门获取 ID?我知道的唯一方法是获取每个对象并循环遍历它们以将它们添加到集合中,当我尝试时并没有更快。
您可以将获取请求的propertiesToFetch
属性设置为只返回id
。还将结果类型设置为dictionaryResultType
。然后,您可以获取所有当前对象并使用 map
操作将 ID 快速加载到集合中
【参考方案1】:
如果获取几千个实体并将 ID 加载到 Set
中花费了特别长的时间,我会感到惊讶。
你可以使用类似的东西:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Trip")
fetchRequest.resultType = .dictionaryResultType
fetchRequest.propertiesToFetch = ["id"]
do
if let results = try self.moc.fetch(fetchRequest) as? [[String:Any]]
let idSet = Set<Int32>(results.flatMap( (dict) -> Int32? in
return dict["id"] as? Int32
))
catch
print("Error reading trips")
现在您可以轻松检查给定 ID 是否为新 ID,并在需要时插入新行程:
for csvTrip in csvTrips
if let tripId = Int(csvTrip[0])
if !idSet.contains(tripId)
trip = NSEntityDescription.insertNewObject(forEntityName: "Trip", into: context)
trip.setValue(tripId, forKey: "id")
在我的测试中,将 320,000 个行程 ID 加载到集合中需要 1.35 秒,创建 10,000 个新行程需要 0.08 秒,同时检查行程 ID 是否包含在集合中。
【讨论】:
【参考方案2】:您可以使用 Core Data 的独特约束技术。
-
告诉 Core Data 您的 id 是唯一标识符。
为此,请选择您的数据模型 (Trip.xcdatamodeld) 并确保选择了 Trip 实体而不是其属性之一。
在数据模型检查器中,查看“约束”字段,然后单击该字段底部的 + 按钮。
将出现一个新行,上面写着“逗号,分隔,属性”。单击它,按 Enter 使其可编辑,然后键入 id 并再次按 Enter。按 Cmd+S 保存更改。
-
修改
loadPersistentStores()
方法调用以允许Core Data 更新您的对象:
container.loadPersistentStores storeDescription, error in
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
if let error = error
print("Unresolved error \(error)")
注意:
使用属性约束可能会导致NSFetchedResultsController
出现问题:属性约束仅在保存发生时强制为唯一,这意味着如果您要插入数据,则NSFetchedResultsController
可能包含重复项,直到发生保存。您可以通过在加载前执行保存来避免它。只需知道做出此类更改取决于您。
您可以阅读有关此技术的更多信息here。
【讨论】:
如果你想写一个关于唯一约束的答案,那么就这样做,但只有链接的答案会过时和改变。 @WarrenBurton 我已经更新了一个答案,如果有人对这个理论感到好奇,请保留链接的详细信息【参考方案3】:加快插入/更新的一种方法是将输入数组切成相当小的“桶”并在 NSPredicate 中使用IN
运算符。使用IN
运算符,您可以通过single 查询检查桶的所有 元素是否已经存在于数据库中。让我用一些代码来说明这一点。
let bucketSize = 10
let bucketStart = 0
let bucketEnd = bucketSize
while bucketStart < csvTrips.count
let tripBucket = csvTrips[bucketStart..<bucketEnd]
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Trip")
fetchRequest.predicate = NSPredicate(format: "id in %@", tripBucket.map Int($0[0]))
// count == bucketSize would imply that all elements in the bucket are also in the db, in which case we simply move on to the next bucket
if context.count(for: fetch) != bucketSize
// some of the elements in the bucket are not in the db,
// now use your existing code to update the missing ones
for csvTrip in tripBucket
// ...
// update bucketStart and bucketEnd here
您可以通过更改存储桶大小来调整此算法的效率。您必须选择一个考虑输入数据中新记录的概率的大小,以便最大没有。的桶不要输入以下代码块。
if context.count(for: fetch) != bucketSize ...
桶太大意味着几乎所有的桶都会从数据库中丢失至少一个元素;这反过来又意味着您将比现有方法几乎没有优势。另一方面,桶太小意味着额外的获取请求(id in %@
)的开销太大。
【讨论】:
以上是关于使用 Core Data 插入/更新记录的最有效方法?的主要内容,如果未能解决你的问题,请参考以下文章
NSSortDescriptor 对 Core Data NSSet 对象进行排序的最有效方法