简体   繁体   中英

CloudKit - CKQueryOperation with dependency

I'm just beginning working with CloudKit, so bear with me.

Background info

At WWDC 2015, apple gave a talk about CloudKit https://developer.apple.com/videos/wwdc/2015/?id=715

In this talk, they warn against creating chaining queries and instead recommend this tactic:

let firstFetch = CKFetchRecordsOperation(...)
let secondFetch = CKFetchRecordsOperation(...)
...
secondFetch.addDependency(firstFetch)

letQueue = NSOperationQueue()
queue.addOperations([firstFetch, secondFetch], waitUntilFinished: false)

Example structure

The test project database contains pets and their owners, it looks like this:

|Pets               |   |Owners     |
|-name              |   |-firstName |
|-birthdate         |   |-lastName  |
|-owner (Reference) |   |           |

My Question

I am trying to find all pets that belong to an owner, and I'm worried I'm creating the chain apple warns against. See below for two methods that do the same thing, but two ways. Which is more correct or are both wrong? I feel like I'm doing the same thing but just using completion blocks instead.

I'm confused about how to change otherSearchBtnClick: to use dependency. Where would I need to add

ownerQueryOp.addDependency(queryOp)

in 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)
}

If you don't need cancellation and aren't bothered about retrying on a network error then I think you are fine chaining the queries.

I know I know, in WWDC 2015 Nihar Sharma recommended the add dependency approach but it would appear he just threw that in at the end without much thought. You see it isn't possible to retry a NSOperation because they are one-shot anyway, and he offered no example for cancelling operations already in the queue, or how to pass data from one operation from the next. Given these 3 complications that could take you weeks to solve, just stick with what you have working and wait for the next WWDC for their solution. Plus the whole point of blocks is to let you call inline methods and be able to access the params in the method above, so if you move to operations you kind of don't get full advantage of that benefit.

His main reason for not using chaining is the ridiculous one that he couldn't tell which error is for which request, he had names his errors someError then otherError etc. No one in their right mind names error params different inside blocks so just use the same name for all of them and then you know inside a block you are always using the right error. Thus he was the one that created his messy scenario and offered a solution for it, however the best solution is just don't create the messy scenario of multiple error param names in the first place!

With all that being said, in case you still want to try to use operation dependencies here is an example of how it could be done:

__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];

How it works is first it vetches a Venue record, then it fetches its Category.

Sorry there is no error handling but as you can see it was already a ton of code to do something can could be done in a couple of lines with chaining. And personally I find this result more convoluted and confusing than simply chaining together the convenience methods.

in theory you could have multiple owners and therefore multiple dependencies. Also the inner queries will be created after the outer query is already executed. You will be too late to create a dependency. In your case it's probably easier to force the execution of the inner queries to a separate queue like this:

if record != nil {
    for owner in record {
        NSOperationQueue.mainQueue().addOperationWithBlock {

This way you will make sure that every inner query will be executed on a new queue and in the mean time that parent query can finish.

Something else: to make your code cleaner, it would be better if all the code inside the for loop was in a separate function with a CKReference as a parameter.

I had the same problem recently and ended up using a NSBlockOperation to prepare the second query and added a dependency to make it all work:

    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)

And now it's working as i expected, because you can set up your operations in a very granular way and they execute in the correct order ^.^

in your case i would structure it like this:

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)

and now all it needs to be done is to add them to the newly created operation queue after we direct them to our database

getOwnerOperation.database = publicDB
queryPetsForOurOwner.database = publicDB
let operationqueue = NSOperationQueue.mainQueue()
operationqueue.addOperations([getOwnerOperation, queryPetsForOurOwner, preparePetsForOwnerQuery], waitUntilFinished: false)

PS: i know i said Family and Person and the names are not like that, but i'm spanish and testing some cloudkit operations, so i haven't standardized to english recor type names yet ;)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM