肘子的 Swift 记事本

How to use Derived and Transient Properties in Core Data

Published on

Get weekly handpicked updates on Swift and SwiftUI!

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).

https://cdn.fatbobman.com/image-20211025175712406.png

Before, if we wanted to see how many items were in each TodoList, we could use the following code:

Swift
let count = todolist.items.count

After using the Derived attribute, we can obtain the number of items using the following code:

Swift
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.

https://cdn.fatbobman.com/image-20211025183247335.png

  • 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:

Swift
@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:

SQL
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:

Swift
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.

Swift
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:

Swift
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:

Bash
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.

Swift
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.

https://cdn.fatbobman.com/image-20211025201846952.png

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:

Swift
@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:

Swift
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.
Swift
    @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:

Swift
    @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.

I'm really looking forward to hearing your thoughts! Please Leave Your Comments Below to share your views and insights.

Fatbobman(东坡肘子)

I'm passionate about life and sharing knowledge. My blog focuses on Swift, SwiftUI, Core Data, and Swift Data. Follow my social media for the latest updates.

You can support me in the following ways