Skip to content

Saving and processing transactions

John Estropia edited this page Jul 18, 2015 · 4 revisions

To ensure deterministic state for objects in the read-only NSManagedObjectContext, CoreStore does not expose API's for updating and saving directly from the main context (or any other context for that matter.) Instead, you spawn transactions from DataStack instances:

let dataStack = self.dataStack
dataStack.beginAsynchronous { (transaction) -> Void in
    // make changes
    transaction.commit()
}

or for the default stack, directly from CoreStore:

CoreStore.beginAsynchronous { (transaction) -> Void in
    // make changes
    transaction.commit()
}

The commit() method saves the changes to the persistent store. If commit() is not called when the transaction block completes, all changes within the transaction is discarded.

The examples above use beginAsynchronous(...), but there are actually 3 types of transactions at your disposal: asynchronous, synchronous, and detached.

Transaction types

Asynchronous transactions

are spawned from beginAsynchronous(...). This method returns immediately and executes its closure from a background serial queue:

CoreStore.beginAsynchronous { (transaction) -> Void in
    // make changes
    transaction.commit()
}

Transactions created from beginAsynchronous(...) are instances of AsynchronousDataTransaction.

Synchronous transactions

are created from beginSynchronous(...). While the syntax is similar to its asynchronous counterpart, beginSynchronous(...) waits for its transaction block to complete before returning:

CoreStore.beginSynchronous { (transaction) -> Void in
    // make changes
    transaction.commit()
} 

transaction above is a SynchronousDataTransaction instance.

Since beginSynchronous(...) technically blocks two queues (the caller's queue and the transaction's background queue), it is considered less safe as it's more prone to deadlock. Take special care that the closure does not block on any other external queues.

Detached transactions

are special in that they do not enclose updates within a closure:

let transaction = CoreStore.beginDetached()
// make changes
downloadJSONWithCompletion({ (json) -> Void in

    // make other changes
    transaction.commit()
})
downloadAnotherJSONWithCompletion({ (json) -> Void in

    // make some other changes
    transaction.commit()
})

This allows for non-contiguous updates. Do note that this flexibility comes with a price: you are now responsible for managing concurrency for the transaction. As uncle Ben said, "with great power comes great race conditions."

As the above example also shows, only detached transactions are allowed to call commit() multiple times; doing so with synchronous and asynchronous transactions will trigger an assert.

You've seen how to create transactions, but we have yet to see how to make creates, updates, and deletes. The 3 types of transactions above are all subclasses of BaseDataTransaction, which implements the methods shown below.

Creating objects

The create(...) method accepts an Into clause which specifies the entity for the object you want to create:

let person = transaction.create(Into(MyPersonEntity))

While the syntax is straightforward, CoreStore does not just naively insert a new object. This single line does the following:

  • Checks that the entity type exists in any of the transaction's parent persistent store
  • If the entity belongs to only one persistent store, a new object is inserted into that store and returned from create(...)
  • If the entity does not belong to any store, an assert will be triggered. This is a programmer error and should never occur in production code.
  • If the entity belongs to multiple stores, an assert will be triggered. This is also a programmer error and should never occur in production code. Normally, with Core Data you can insert an object in this state but saving the NSManagedObjectContext will always fail. CoreStore checks this for you at creation time when it makes sense (not during save).

If the entity exists in multiple configurations, you need to provide the configuration name for the destination persistent store:

let person = transaction.create(Into<MyPersonEntity>("Config1"))

or if the persistent store is the auto-generated "Default" configuration, specify nil:

let person = transaction.create(Into<MyPersonEntity>(nil))

Note that if you do explicitly specify the configuration name, CoreStore will only try to insert the created object to that particular store and will fail if that store is not found; it will not fall back to any other configuration that the entity belongs to.

Updating objects

After creating an object from the transaction, you can simply update its properties as normal:

CoreStore.beginAsynchronous { (transaction) -> Void in
    let person = transaction.create(Into(MyPersonEntity))
    person.name = "John Smith"
    person.age = 30
    transaction.commit()
}

To update an existing object, fetch the object's instance from the transaction:

CoreStore.beginAsynchronous { (transaction) -> Void in
    let person = transaction.fetchOne(
        From(MyPersonEntity),
        Where("name", isEqualTo: "Jane Smith")
    )
    person.age = person.age + 1
    transaction.commit()
}

(For more about fetching, see Fetching and querying)

Do not update an instance that was not created/fetched from the transaction. If you have a reference to the object already, use the transaction's edit(...) method to get an editable proxy instance for that object:

let jane: MyPersonEntity = // ...

CoreStore.beginAsynchronous { (transaction) -> Void in
    // WRONG: jane.age = jane.age + 1
    // RIGHT:
    let jane = transaction.edit(jane) // using the same variable name protects us from misusing the non-transaction instance
    jane.age = jane.age + 1
    transaction.commit()
}

This is also true when updating an object's relationships. Make sure that the object assigned to the relationship is also created/fetched from the transaction:

let jane: MyPersonEntity = // ...
let john: MyPersonEntity = // ...

CoreStore.beginAsynchronous { (transaction) -> Void in
    // WRONG: jane.friends = [john]
    // RIGHT:
    let jane = transaction.edit(jane)
    let john = transaction.edit(john)
    jane.friends = [john]
    transaction.commit()
}

Deleting objects

Deleting an object is simpler because you can tell a transaction to delete an object directly without fetching an editable proxy (CoreStore does that for you):

let john: MyPersonEntity = // ...

CoreStore.beginAsynchronous { (transaction) -> Void in
    transaction.delete(john)
    transaction.commit()
}

or several objects at once:

let john: MyPersonEntity = // ...
let jane: MyPersonEntity = // ...

CoreStore.beginAsynchronous { (transaction) -> Void in
    transaction.delete(john, jane)
    // transaction.delete([john, jane]) is also allowed
    transaction.commit()
}

If you do not have references yet to the objects to be deleted, transactions have a deleteAll(...) method you can pass a query to:

CoreStore.beginAsynchronous { (transaction) -> Void in
    transaction.deleteAll(
        From(MyPersonEntity)
        Where("age > 30")
    )
    transaction.commit()
}

Contents