In the previous article, ”Mastering Relationships in Core Data: Fundamentals” we explored the basic concepts and principles of relationships in Core Data. Building on that foundation, this article aims to share practical experience and techniques for handling relationships in Core Data. The goal is to assist developers in more effectively utilizing the relational features of the Core Data framework, thereby enhancing development flexibility and efficiency.
This article is intended for readers who already have some knowledge and practical experience with Core Data relationships, providing an advanced understanding and application perspective, rather than offering a comprehensive tutorial.
Optional
When defining entity attributes in the Xcode model editor, developers should differentiate between the Optional
option in the editor and the Optional
type in Swift, as they are not the same. In Core Data, the Optional
option means that the corresponding SQLite field can accept NULL values. In contrast, the Optional
type in Swift is a language-level feature that indicates a variable can be nil
. In Core Data models, the use of these two types of Optional
depends on the specific scenario and the developer’s needs, and they do not necessarily correspond directly.
In Core Data, if an attribute of a model is marked as Optional
, it can be defined as Non-Optional
in the corresponding Swift code. This approach offers more flexibility, allowing developers to decide whether to use Swift’s Optional
type in the code based on the actual application context.
For more detailed information on the
Optional
values in Core Data, please read Ask Apple 2022 Q&A Related on Core Data (Part 2).
For instance, consider Item
and Tag
, two entities with a One-to-One
relationship. When using Core Data with CloudKit, these relationships must be marked as Optional
in the model editor. However, in practical application, if these two entity instances are always related to each other, meaning their relationship always has a value, they can be adjusted to be non-optional in the Swift code. The benefit of this adjustment is more convenient access to these properties in the code, eliminating the need for frequent unwrapping.
The default code generated by Core Data is as follows:
extension Item {
@NSManaged public var timestamp: Date?
@NSManaged public var tag: Tag? // Optional
}
extension Tag {
@NSManaged public var name: String?
@NSManaged public var item: Item? // Optional
}
However, you can adjust them to be non-optional based on the actual situation:
extension Item {
@NSManaged public var timestamp: Date
@NSManaged public var tag: Tag // Non-Optional
}
extension Tag {
@NSManaged public var name: String // None-Optional
@NSManaged public var item: Item // Non-Optional
}
This allows for more convenient data retrieval in the code, provided that developers ensure that the properties have been assigned values before they are accessed:
Text(item.tag.name)
Swiftifying Core Data Collection Types
When dealing with to-Many
relationships in Core Data, especially those involving ordered relationships, adjusting their representation in Swift code can offer significant benefits.
For instance, consider changing tag
to an ordered to-Many
relationship tags
:
The default code generated by Core Data is as follows:
extension Item {
@NSManaged public var timestamp: Date?
@NSManaged public var tags: NSOrderedSet?
}
To enhance readability and usability of the code, we can consider converting the NSOrderedSet?
type to Array<Tag>
. This adjustment not only reduces the need for unwrapping but also aligns the tags
property more closely with Swift language conventions, such as using subscripting and iterators.
extension Item {
@NSManaged public var timestamp: Date?
@NSManaged public var tags: Array<Tag>
}
After this adjustment, we can more conveniently manipulate these data in Swift, for example (as Array conforms to the RandomAccessCollection protocol):
ForEach(item.tags){ tag in
Text(tag.name ?? "")
}
However, it’s worth noting that converting a non-ordered to-Many
relationship to an Array
type may not always be the best choice. This is mainly due to the intrinsic characteristics of non-ordered collections and their management in Core Data. In Core Data, non-ordered relationships are typically represented as NSSet
, intuitively reflecting the unordered nature and uniqueness of the elements in the collection. Converting this to an Array
type might cause a loss of these key characteristics at face value. Therefore, for non-ordered relationships, using Swift’s Set
type is often a more appropriate choice.
For example, for the tags
attribute of the Item
entity, if it is a non-ordered, optional to-Many
relationship, it can be represented in Swift as follows:
extension Item {
@NSManaged public var timestamp: Date?
@NSManaged public var tags: Set<Tag>
}
This approach maintains the unordered nature and uniqueness of the collection while aligning the code more closely with Swift usage habits, enhancing its readability.
Count
When dealing with to-Many
relationships in Core Data, it’s often necessary to obtain the count of associated objects. While directly using the .count
property is a common method, developers can also consider using a derived attribute (Derived Attribute) for a more efficient way to obtain this count.
For example, in the situation shown below, we have created a derived attribute named count
for the TodoList
entity. This allows developers to simply access todolist.count
to directly obtain the number of items
objects associated with the TodoList
. This method makes retrieving the count of associated objects both intuitive and efficient.
Compared to directly calling the .count
property of a relationship, using a derived attribute for counting is generally more efficient. This is because derived attributes employ a different counting mechanism—they calculate and save the count value when data is written, and use this pre-calculated value when data is read. This mechanism is particularly suited to scenarios where read operations significantly outnumber write operations.
However, an important limitation of derived attributes is that they can only count data that has been persisted. This means if there are data that have not yet been saved to persistent storage, i.e., in a transient state, these data will not be included in the count by the derived attribute. Therefore, when using derived attributes, developers need to be mindful of this limitation and ensure that their data handling logic takes this counting method into consideration.
For a deeper understanding of how to use derived attributes, it’s recommended to read “How to use Derived and Transient Properties in Core Data”, which provides a detailed introduction to the application of derived attributes.
Managing Non-Ordered to-Many Relationships
In many practical application scenarios, to-Many
relationships are often non-ordered. This is especially evident when using Core Data with CloudKit, as it does not support ordered relationships.
When data is directly retrieved through relationship properties, as shown in the example code below, Core Data cannot guarantee the order of the returned data:
let tags = Array(items.tags)
In most cases, Core Data uses a SQLite database for data storage at the backend. In the database, unless a specific sort order is explicitly defined, the retrieval order of records is indeterminate.
Therefore, to ensure consistency when fetching non-ordered to-Many
data, it is advised not to rely solely on direct use of relationship properties. Instead, create an NSFetchRequest
that includes predicates and sort criteria to perform the query, as shown below:
func fetchTagsBy(item:Item) -> [Tag] {
let request = NSFetchRequest<Tag>(entityName: "Tag")
request.predicate = NSPredicate(format: "item = %@", item)
request.sortDescriptors = [NSSortDescriptor(keyPath: \Tag.name, ascending: true)]
return (try? viewContext.fetch(request)) ?? []
}
In SwiftUI development, it’s recommended to encapsulate the interface displaying to-Many
data into a separate view and fetch data using @FetchRequest
. This approach not only ensures the stability of the data retrieval order but also promptly responds to data changes, making view updates more efficient:
struct TagsList: View {
@FetchRequest var tags: FetchedResults<Tag>
init(item: Item) {
let request = NSFetchRequest<Tag>(entityName: "Tag")
request.predicate = NSPredicate(format: "item = %@", item.objectID) // Using NSManagedObject and NSManagedObjectID generates the same SQL commands
request.sortDescriptors = [NSSortDescriptor(keyPath: \Tag.name, ascending: true)]
_tags = FetchRequest(fetchRequest: request)
}
var body: some View {
List(tags) { tag in
TagDetail(tag: tag)
}
}
}
struct TagDetail: View {
@ObservedObject var tag: Tag
var body: some View {
Text(tag.name)
}
}
Many-to-Many Relationships and Subqueries
In our previous article, we discussed how relationships can enhance query efficiency and expand querying capabilities in certain scenarios. Subqueries in Core Data are a prime example of this in action.
A subquery is an efficient querying technique within the Core Data framework, allowing developers to perform more complex queries on an existing set of results. This is particularly useful when dealing with complex data models, especially when filtering based on attributes of related objects.
The basic format of a subquery is as follows:
SUBQUERY(collection, $x, condition)
collection
refers to the set to be queried, typically a to-many relationship property.$x
is a variable representing each element in the set (the name can be arbitrarily set).condition
is the criterion applied to each element in the collection.
For example, suppose we want to retrieve all Item
instances that have at least one Tag
with a name starting with “A”. The following NSPredicate expression can be used:
NSPredicate(format: "SUBQUERY(tags, $tag, $tag.name BEGINSWITH 'A').@count > 0")
The corresponding operation using Swift’s higher-order functions in memory would be:
let result = items.filter { item in
item.tags.contains { tag in
tag.name.hasPrefix("A")
}
}
Subqueries are executed directly at the SQLite level, meaning they are more efficient both in terms of performance and memory usage compared to filtering in memory. Additionally, it is recommended to perform all filtering and sorting operations at the SQLite level, using well-designed predicates and sorting conditions. This approach not only improves data processing efficiency but also helps reduce the memory load on the application, especially when dealing with large data sets.
What’s Next
In this article, we have explored a series of techniques for applying relationships in Core Data within real-world development scenarios. Indeed, once developers grasp the fundamental theories and internal mechanisms of relationships, they can continuously summarize and discover methods and experiences that are more suitable for their own projects.
The upcoming article will concentrate on SwiftData, the successor framework to Core Data. We will explore the changes in how SwiftData manages data relationships and critically assess the applicability of these changes. Particular attention will be given to how potential performance issues in relational operations can be effectively avoided in its initial version.