-
-
Notifications
You must be signed in to change notification settings - Fork 255
Saving and processing transactions
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.
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
.
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.
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.
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.
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 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()
}