CloudKit - 具有依赖关系的 CKQueryOperation
Posted
技术标签:
【中文标题】CloudKit - 具有依赖关系的 CKQueryOperation【英文标题】:CloudKit - CKQueryOperation with dependency 【发布时间】:2015-09-02 14:00:43 【问题描述】:我刚刚开始使用 CloudKit,所以请耐心等待。
背景信息
在 WWDC 2015 上,苹果发表了关于 CloudKit 的演讲https://developer.apple.com/videos/wwdc/2015/?id=715
在本次演讲中,他们警告不要创建链接查询,而是推荐这种策略:
let firstFetch = CKFetchRecordsOperation(...)
let secondFetch = CKFetchRecordsOperation(...)
...
secondFetch.addDependency(firstFetch)
letQueue = NSOperationQueue()
queue.addOperations([firstFetch, secondFetch], waitUntilFinished: false)
示例结构
测试项目数据库包含宠物及其主人,如下所示:
|Pets | |Owners |
|-name | |-firstName |
|-birthdate | |-lastName |
|-owner (Reference) | | |
我的问题
我正在尝试查找属于某个所有者的所有宠物,但我担心我正在创建链式苹果警告。请参阅下面的两种方法,它们做同样的事情,但有两种方式。哪个更正确或两者都错?我觉得我在做同样的事情,但只是使用了完成块。
我很困惑如何更改 otherSearchBtnClick: 以使用依赖项。我需要在哪里添加
ownerQueryOp.addDependency(queryOp)
在 otherSearchBtnClick 中:?
@IBAction func searchBtnClick(sender: AnyObject)
var petString = ""
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let privateDatabase = container.privateCloudDatabase
let predicate = NSPredicate(format: "lastName == '\(ownerLastNameTxt.text)'")
let ckQuery = CKQuery(recordType: "Owner", predicate: predicate)
publicDatabase.performQuery(ckQuery, inZoneWithID: nil)
record, error in
if error != nil
println(error.localizedDescription)
else
if record != nil
for owner in record
let myRecord = owner as! CKRecord
let myReference = CKReference(record: myRecord, action: CKReferenceAction.None)
let myPredicate = NSPredicate(format: "owner == %@", myReference)
let petQuery = CKQuery(recordType: "Pet", predicate: myPredicate)
publicDatabase.performQuery(petQuery, inZoneWithID: nil)
record, error in
if error != nil
println(error.localizedDescription)
else
if record != nil
for pet in record
println(pet.objectForKey("name") as! String)
@IBAction func otherSearchBtnClick (sender: AnyObject)
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let privateDatabase = container.privateCloudDatabase
let queue = NSOperationQueue()
let petPredicate = NSPredicate(format: "lastName == '\(ownerLastNameTxt.text)'")
let petQuery = CKQuery(recordType: "Owner", predicate: petPredicate)
let queryOp = CKQueryOperation(query: petQuery)
queryOp.recordFetchedBlock = (record: CKRecord!) in
println("recordFetchedBlock: \(record)")
self.matchingOwners.append(record)
queryOp.queryCompletionBlock = (cursor: CKQueryCursor!, error: NSError!) in
if error != nil
println(error.localizedDescription)
else
println("queryCompletionBlock: \(cursor)")
println("ALL RECORDS ARE: \(self.matchingOwners)")
for owner in self.matchingOwners
let ownerReference = CKReference(record: owner, action: CKReferenceAction.None)
let ownerPredicate = NSPredicate(format: "owner == %@", ownerReference)
let ownerQuery = CKQuery(recordType: "Pet", predicate: ownerPredicate)
let ownerQueryOp = CKQueryOperation(query: ownerQuery)
ownerQueryOp.recordFetchedBlock = (record: CKRecord!) in
println("recordFetchedBlock (pet values): \(record)")
self.matchingPets.append(record)
ownerQueryOp.queryCompletionBlock = (cursor: CKQueryCursor!, error: NSError!) in
if error != nil
println(error.localizedDescription)
else
println("queryCompletionBlock (pet values)")
for pet in self.matchingPets
println(pet.objectForKey("name") as! String)
publicDatabase.addOperation(ownerQueryOp)
publicDatabase.addOperation(queryOp)
【问题讨论】:
没有回复是因为我离基地太远了我绝望了吗?! 【参考方案1】:如果您不需要取消并且不担心重试网络错误,那么我认为您可以很好地链接查询。
我知道我知道,在 WWDC 2015 中,Nihar Sharma 推荐了添加依赖项方法,但看起来他只是在最后没有多想就将其加入。您会看到无法重试 NSOperation,因为它们无论如何都是一次性的,并且他没有提供取消队列中已经存在的操作的示例,或者如何从下一个操作传递数据。鉴于这 3 个可能需要数周时间才能解决的复杂问题,请坚持您的工作并等待下一个 WWDC 的解决方案。此外,块的全部意义在于让您调用内联方法并能够访问上述方法中的参数,因此如果您转向操作,您可能无法充分利用这一优势。
他不使用链接的主要原因是荒谬的一个,他无法分辨哪个错误是针对哪个请求的,他将他的错误命名为 someError 然后 otherError 等等。没有人在他们的正确头脑中命名错误参数在块内不同所以只需为所有这些名称使用相同的名称,然后您就会知道在一个块内您总是使用正确的错误。因此,他是创建混乱场景并为其提供解决方案的人,但最好的解决方案是首先不要创建多个错误参数名称的混乱场景!
话虽如此,如果您仍然想尝试使用操作依赖项,这里有一个示例说明如何完成:
__block CKRecord* venueRecord;
CKRecordID* venueRecordID = [[CKRecordID alloc] initWithRecordName:@"4c31ee5416adc9282343c19c"];
CKFetchRecordsOperation* fetchVenue = [[CKFetchRecordsOperation alloc] initWithRecordIDs:@[venueRecordID]];
fetchVenue.database = [CKContainer defaultContainer].publicCloudDatabase;
// init a fetch for the category, it's just a placeholder just now to go in the operation queue and will be configured once we have the venue.
CKFetchRecordsOperation* fetchCategory = [[CKFetchRecordsOperation alloc] init];
[fetchVenue setFetchRecordsCompletionBlock:^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable error)
venueRecord = recordsByRecordID.allValues.firstObject;
CKReference* ref = [venueRecord valueForKey:@"category"];
// configure the category fetch
fetchCategory.recordIDs = @[ref.recordID];
fetchCategory.database = [CKContainer defaultContainer].publicCloudDatabase;
];
[fetchCategory setFetchRecordsCompletionBlock:^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable error)
CKRecord* categoryRecord = recordsByRecordID.allValues.firstObject;
// here we have a venue and a category so we could call a completion handler with both.
];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[fetchCategory addDependency:fetchVenue];
[queue addOperations:@[fetchVenue, fetchCategory] waitUntilFinished:NO];
它的工作原理是首先检查场地记录,然后获取其类别。
抱歉,没有错误处理,但正如您所见,已经有大量的代码来做某事,可以通过链接在几行代码中完成。而且我个人觉得这个结果比简单地将便利方法链接在一起更令人费解和困惑。
【讨论】:
【参考方案2】:理论上你可以有多个所有者,因此可以有多个依赖项。此外,内部查询将在外部查询已经执行后创建。您将太迟来创建依赖项。在您的情况下,将内部查询强制执行到单独的队列可能更容易,如下所示:
if record != nil
for owner in record
NSOperationQueue.mainQueue().addOperationWithBlock
这样您将确保每个内部查询都将在新队列上执行,同时父查询可以完成。
还有一点:为了让你的代码更简洁,如果 for 循环中的所有代码都在一个单独的函数中,并以 CKReference 作为参数会更好。
【讨论】:
【参考方案3】:我最近遇到了同样的问题,最终使用 NSBlockOperation 来准备第二个查询并添加了一个依赖项以使其全部工作:
let container = CKContainer.defaultContainer()
let publicDB = container.publicCloudDatabase
let operationqueue = NSOperationQueue.mainQueue()
let familyPredicate = NSPredicate(format: "name == %@", argumentArray: [familyName])
let familyQuery = CKQuery(recordType: "Familias", predicate: familyPredicate)
let fetchFamilyRecordOp = CKQueryOperation(query: familyQuery)
fetchFamilyRecordOp.recordFetchedBlock = record in
familyRecord = record
let fetchMembersOP = CKQueryOperation()
// Once we have the familyRecord, we prepare the PersonsFetch
let prepareFamilyRef = NSBlockOperation()
let familyRef = CKReference(record: familyRecord!, action: CKReferenceAction.None)
let familyRecordID = familyRef?.recordID
let membersPredicate = NSPredicate(format: "familia == %@", argumentArray: [familyRecordID!])
let membersQuery = CKQuery(recordType: "Personas", predicate: membersPredicate)
fetchMembersOP.query = membersQuery
prepareFamilyRef.addDependency(fetchFamilyRecordOp)
fetchMembersOP.recordFetchedBlock = record in
members.append(record)
fetchMembersOP.addDependency(prepareFamilyRef)
fetchMembersOP.database = publicDB
fetchFamilyRecordOp.database = publicDB
operationqueue.addOperations([fetchFamilyRecordOp, fetchMembersOP, prepareFamilyRef], waitUntilFinished: false)
现在它正在按我的预期工作,因为您可以以非常精细的方式设置您的操作,并且它们以正确的顺序执行^.^
在你的情况下,我会这样构造它:
let predicate = NSPredicate(format: "lastName == '\(ownerLastNameTxt.text)'")
let ckQuery = CKQuery(recordType: "Owner", predicate: predicate)
let getOwnerOperation = CKQueryOperation(query: ckQuery)
getOwnerOperation.recordFetchedBlock = record in
let name = record.valueForKey("name") as! String
if name == myOwnerName
ownerRecord = record
//now we have and operation that will save in our var OwnerRecord the record that is exactly our owner
//now we create another that will fetch our pets
let queryPetsForOurOwner = CKQueryOperation()
queryPetsForOurOwner.recordFetchedBlock = record in
results.append(record)
//That's all this op has to do, BUT it needs the owner operation to be completed first, but not inmediately, we need to prepare it's query first so:
var fetchPetsQuery : CKQuery?
let preparePetsForOwnerQuery = NSBlockOperation()
let myOwnerRecord = ownerRecord!
let ownerRef = CKReference(record: myOwnerRecord, action: CKReferenceAction.None)
let myPredicate = NSPredicate(format: "owner == %@", myReference)
fetchPetsQuery = CKQuery(recordType: "Pet", predicate: myPredicate)
queryPetsForOurOwner.query = fetchPetsQuery
preparePetsForOwnerQuery.addDependency(getOwnerOperation)
queryPetsForOurOwner.addDependency(preparePetsForOwnerQuery)
现在需要做的就是在我们将它们定向到我们的数据库后将它们添加到新创建的操作队列中
getOwnerOperation.database = publicDB
queryPetsForOurOwner.database = publicDB
let operationqueue = NSOperationQueue.mainQueue()
operationqueue.addOperations([getOwnerOperation, queryPetsForOurOwner, preparePetsForOwnerQuery], waitUntilFinished: false)
P.S:我知道我说的是 Family 和 Person,名字不是那样的,但我是西班牙人,正在测试一些 cloudkit 操作,所以我还没有标准化为英文记录类型名称;)
【讨论】:
我认为您可以使用 getOwnerOperation 上的 queryCompletionBlock 来设置 queryPetsForOurOwner 上的 fetchPetsQuery。在 WWDC 2015 的演讲中,他说这些自定义完成块发生在操作结束之前,他特别说那是为下一个操作设置数据的时间。这样可以节省您创建块操作的时间。这是我还没有尝试过的所有理论。以上是关于CloudKit - 具有依赖关系的 CKQueryOperation的主要内容,如果未能解决你的问题,请参考以下文章
swift Swift - CloudKit - 关系中的引用类型