How to use Derived and Transient Properties in Core Data
Introduction
Developers who have used Core Data must have seen the Derived and Transient properties in the attribute panel on the right-hand side when editing the Data Model. There is not much documentation available on these two properties, and most developers are not sure how or when to use them. This text will provide an introduction to the functionality, usage, and precautions of the Derived and Transient properties based on my experience of using them.
Derived
What is a Derived attribute
Starting from iOS 13, Apple has added the Derived attribute to Core Data. Its name already indicates the meaning of this attribute - the value of this attribute is derived from one or more other attributes.
In simple terms, when creating or modifying a managed object instance, Core Data will automatically generate a value for the derived attribute. The value is calculated based on the preset derived expression, which is derived from other attribute values.
The function of the Derived attribute
The following is a specific example to facilitate understanding of the function of the derived attribute.
There are two entities in the project, TodoList and Item. Todolist and Item have a one-to-many relationship (To Many), and Item and TodoList have a one-to-one relationship (To One).
Before, if we wanted to see how many items were in each TodoList, we could use the following code:
let count = todolist.items.count
After using the Derived attribute, we can obtain the number of items using the following code:
let count = todolist.count
How to set the Derived attribute
Usually, we need to set the derived attributes in the Core Data Data Model Editor, as shown in the following figure. We create a derived attribute “count” for the TodoList in the example above.
- Create an attribute named count for TodoList
- Choose Derived
- Set Derivation to items.@count (calculate the number of data corresponding to the items relationship)
Developers can set the type and expression of the derived attribute as needed. The following forms of derived expressions are currently supported:
- Simply copying content
Usually used in to-one relationships. For example, in the example above, we can use the derived expression todolist.name
to set a Derived attribute of todolistName for Item, which is used to save the name of the TodoList corresponding to the Item. There are no special restrictions on the type of copied attribute.
- Transforming a string property and saving it
Only supports string-type properties and can be used in different properties of the same Entity or to-one Entity properties. Supports three methods: uppercase:
, lowercase:
, and canonical:
. By saving variants of the string, search efficiency is improved. For example, the derived expression for saving TodoList’s name in lowercase is lowercase:(todolist.name)
.
- Calculating the count and sum of to-many relationships
Calculate the number of objects in the to-many relationship or calculate the sum of a specified property. When using @sum, the corresponding property must be a computable value type. For example, the expression for calculating the total sum value of an Entity named Student and a property named age is student.age.@sum
.
- Current time
Save the operation date of updating the corresponding data record of a managed object by SQLite. Usually used for timestamps like lastModifiedDate. The derived expression is now()
.
Usually we use Derived with Optional. If Optional is not selected, some special processing is required to make the program run correctly. Specific instructions are provided in the notes below.
If NSManagedObject code is manually written, the syntax for Derived attributes is identical to other attributes (still need to be set in Data Model Editor). For example, the count mentioned above can be defined with the following code:
@NSManaged var count: Int
Mechanism for Updating Derived Data
Who Calculates the Values of Derived Data
The values of derived data are calculated and updated directly by Sqlite.
The calculation of derived values is one of the few operations in Core Data that uses Sqlite’s built-in mechanisms directly, rather than being calculated by Swift (or Objective-C) code.
For example, the expression now()
, when used in Core Data, will generate Sql code similar to the following when creating a data table:
CREATE TRIGGER IF NOT EXISTS Z_DA_ZITEM_Item_update_UPDATE AFTER UPDATE OF Z_OPT ON ZITEM FOR EACH ROW BEGIN UPDATE ZITEM SET ZUPDATE = NSCoreDataNow() WHERE Z_PK = NEW.Z_PK; SELECT NSCoreDataDATriggerUpdatedAffectedObjectValue('ZITEM', Z_ENT, Z_PK, 'update', ZUPDATE) FROM ZITEM WHERE Z_PK = NEW.Z_PK; END'
Code for @count:
UPDATE ZITEM SET ZCOUNT = (SELECT IFNULL(COUNT(ZITEM), 0) FROM ZATTACHEMENT WHERE ZITEM = ZITEM.Z_PK);
Therefore, in cases with the same functionality, the efficiency of using Sql is higher than that of Swift (or Objective-C).
In Core Data, it is usually necessary to retrieve results from persistent storage, return to the context, perform calculations, and then persist. There are multiple IO processes in between, which affects efficiency.
When is derived data updated?
Since it is directly processed by Sqlite, the corresponding derived data will only be updated when the data is persisted. If it is only processed in the context without being persisted, the correct derived value will not be obtained. Persistence can be triggered by using the code viewcontext.save()
, or by syncing over the network, etc.
Advantages and Disadvantages of Derived
Advantages
-
High efficiency
Due to its unique update mechanism, the efficiency of processing values is higher, and there are no extra processing actions (updates are only performed during persistence).
-
Simple and clear logic
With proper use, less code is needed for configuration, and the expression is clearer. For example,
now()
.
Disadvantages
-
Limited supported expressions
Sqlite’s supported expressions are very limited and cannot meet more complex business needs.
-
For developers who are not familiar with Derived, the code is harder to read
The configuration of Derived is done in the Data Model Editor, and simply reading the code will not reveal the source and processing method of the data.
Alternatives to Derived
Computed Properties
For infrequently used property values, creating computed properties for managed objects may be a better choice, such as calculating the number of items in a TodoList as mentioned above.
extension TodoList {
var count:Int { items.count }
}
willSave
Use the willSave method of NSManagedObject to set values for specified properties before data persistence. For example:
extension Item {
override func willSave() {
super.willSave()
setPrimitiveValue(Date(), forKey: #keyPath(Item.lastModifiedDate))
}
}
Derived has its own advantages and disadvantages for each of the above two methods, so choose the appropriate solution based on the specific usage scenario.
Notes on Derived
When configuring the Derived property, if Optional is not selected and the code is executed directly, an error similar to the following will be obtained when adding data:
Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=1570 "count is a required value."
This is because, since the attribute is not an optional value, Core Data requires us to provide a default value for the derived attribute. However, since the derived attribute is read-only, we cannot directly assign a value to the derived attribute of the managed object instance in the code.
The solution is to set an initial value for the derived attribute in awakeFromInsert, which allows us to pass Core Data’s property validation check.
extension TodoList {
override func awakeFromInset(){
super.awakeFromInser()
setPrimitiveValue(0, forKey: #keyPath(Todolist.count))
}
}
The value set can be any value (as long as it meets the type requirements), because when persisted, Sqlite will generate a new value and overwrite our initial value.
Transient
What is a Transient attribute
A Transient attribute is a non-persistent attribute. As part of the managed object definition, Core Data tracks changes to Transient attributes and sets the corresponding managed object and managed object context states, but the contents of the attribute will not be saved to the persistent store and no corresponding field will be created in the persistent store.
Apart from not being persistent, Transient attributes are no different from other Core Data attributes and support all available data types as well as optional and default value options.
How to set a Transient attribute
Compared to Derived, setting a Transient attribute is very simple, just check the Transient option.
Why use Transient?
Since Transient is not persistent, why do we use Data Model Editor to create it?
We can directly create storage properties for managed objects through code, for example:
@objc(Test)
public class ITEM: NSManagedObject {
var temp:Int = 0
}
extension Item
@NSManaged public var createDate: Date?
@NSManaged public var title: String?
}
let item = Item(context:viewContext)
item.temp = 100
Regardless of how we modify the temp attribute of an item, Core Data will not be aware of it.
The managed attributes of a managed object (indicated by @NSManaged) are managed by Core Data, which continuously tracks them to set the corresponding states. Using a Transient attribute, Core Data sets the hasChanges of the managed object instance and the managed object context to true when the content of the attribute changes. This way, whether it is @FetchRequest or NSFetchedResultsController, data changes will be automatically reflected.
Therefore, when we do not need persistence but still need to track the dirty state, Transient becomes the only choice.
Initialization of Transient Values
Since the Transient attribute is not persistent, every time a managed object instance containing Transient property appears in the context (such as Fetch, Create, Undo, etc.), its transient property will be restored to its initial value.
Although we can set default values for Transient in the Data Model Editor, in many cases, we need to calculate and create the initial value of Transient based on the situation or other data. We can choose to set it at the following times:
- awakeFromFetch
When filling data for instances in a lazy state (Fault)
- awakeFromInsert
When creating a managed object instance
- awake(fromSnapshotEvents:NSSnapshotEventType)
When loading an instance from a snapshot
When setting Transient or other properties in these methods, we should use the original accessor methods to set data to avoid triggering KVO observer notifications. For example:
setPrimitiveValue("hello",forKey:#keyPath(Movie.title))
Example usage of the Transient attribute
Most Core Data books usually just briefly mention Transient attributes, with the authors often saying they haven’t encountered suitable use cases. I myself only recently came across a scenario that fit the Transient feature.
In developing Health Notes, I needed to Deep Copy (copy all related data) a managed object instance containing many relationships and records, with the copied instance replacing the original one after copying (to address a special need in network data sharing). Since @FetchRequest was used, during the 1-2 seconds of copying, two identical data records would appear in the UI list, causing confusion for users.
If a persistence scheme were used, I could create an attribute to represent whether it should be displayed or not, such as “visible.” By setting this attribute before and after the copy operation and configuring a predicate, I could solve the duplicate listing problem.
However, since this scenario is very rare (many users may never use it), creating a persistent field would be very wasteful.
Therefore, I created a Transient attribute called “visible” for this managed object, which avoids duplicate display while not wasting storage space.
Other Notes about Transients
- NSManagedObjectContext’s refreshAllObjects will reset Transient Content.
- If you only need to check if the persistent properties of a managed object have changed, use hasPersistentChangedValues.
- Do not use transient attributes as limiting criteria in NSPredicate.
@FetchRequest(entity: Test.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Test.title, ascending: true)],
predicate: NSPredicate(format: "visible = true"))
var tests: FetchedResults<Test>
The usage of the code above is incorrect. If you only want to display data where visible == true, you can use the following method:
@FetchRequest(entity: Test.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Test.title, ascending: true)])
ForEach(tests) { test in
if test.visible {
row(test: test)
}
}
Summary
As a well-established framework, Core Data includes many useful but less well-known features. Even just having a general understanding of these features can broaden our horizons and perhaps become a powerful tool to solve problems in certain situations.