Model Inheritance in Core Data

Published on

Get weekly handpicked updates on Swift and SwiftUI!

One of Core Data’s outstanding features is its ability to allow developers to declare data models in a manner closer to object-oriented programming without worrying about the underlying storage implementation details. Within this framework, model inheritance is a particularly important mechanism. This article delves into the core concepts of model inheritance, including Parent Entity, Sub Entity, and Abstract Entity. We will analyze their advantages and disadvantages and explore how to achieve similar effects without directly using these features.

Parent Entity and Sub Entity

When building models using the Xcode model editor, developers will notice that an Entity has a Parent Entity option. While in most cases we keep the No Parent Entity status, other entities can be selected as parent entities via the dropdown menu.

image-20241210144518284

After setting the Publication entity as the parent entity of the Book entity, we have declared the inheritance relationship between the two: Publication becomes the Parent Entity of Book, while Book is the Sub Entity of Publication.

Looking at the model code automatically generated by Xcode, we can see:

Swift
@objc(Book)
public class Book: Publication {}

In this declaration, Book exists as a subclass of Publication. Similar to standard Swift class inheritance, Book automatically inherits the properties declared in the Publication entity.

The Role of Model Inheritance

Imagine a scenario where we need to store various types of publications (such as Books, Academic Papers, Web Articles, Blog Posts, etc.) that share many common characteristics, and we frequently need to perform cross-type queries.

If different types of publications are defined as independent entities without a common parent entity, implementing requirements like finding publications with specific tags, counting the number of publications by an author, or searching publications by keywords becomes exceptionally complex. However, with model inheritance, these requirements become very straightforward.

image-20241210151035063

The image above was generated using CoreData Model Editor. Since Xcode has removed the graph-based modeling approach, CoreData Model Editor serves as an excellent alternative tool.

First, we declare a Publication entity, setting up the common properties and relationships for different types of publications. In this model, Publication includes two properties: publishDate and title, and establishes a many-to-many relationship with the Tag entity.

image-20241210151420355

Next, we declare the Book and AcademicPaper entities, each adding their unique properties and setting Publication as their parent entity. Although publishDate and title are not directly defined in Book, these properties are automatically inherited. More importantly, the many-to-many relationship with Tag is also inherited.

The biggest difference between model inheritance and protocol-based approaches is that model inheritance not only includes property declarations but also encompasses other Core Data model-specific information, such as inverse relationship definitions, delete rules, validation rules, indexes, predefined Fetch Requests, and constraints between entities.

After completing the above model setup, we can use Publication, Book, and AcademicPaper as independent model types within the project. However, since there is an inheritance relationship between them, when cross-publication type retrieval is needed, operations can be directly performed on Publication.

In the following code example, we create instances of Publication, Book, and AcademicPaper respectively. All data will uniformly appear in the retrieval results for Publication. In the PublicationView used to display data, each specific type displays its unique properties.

Swift
struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    // Fetch all Publications
    @FetchRequest(entity: Publication.entity(), sortDescriptors: [.init(key: "publishDate", ascending: false)])
    var Publications: FetchedResults<Publication>
  
    var body: some View {
        List {
            Button("New publication") {
                let publication = Publication(context: viewContext)
                publication.title = "\(Int.random(in: 0 ... 100))"
                publication.publishDate = .now
                try? viewContext.save()
            }
            Button("New book") {
                let book = Book(context: viewContext)
                book.title = "\(Int.random(in: 0 ... 100))"
                book.publishDate = .now
                book.isbn = "\(Int.random(in: 3000...4000))-\(Int.random(in: 6000...8000))"
                try? viewContext.save()
            }
            Button("New Paper") {
                let paper = AcademicPaper(context: viewContext)
                paper.title = "\(Int.random(in: 0 ... 100))"
                paper.publishDate = .now
                paper.paperType = Int16.random(in: 0 ..< 100)
                try? viewContext.save()
            }
            ForEach(Publications) { publication in
                PublicationView(publication: publication)
            }
        }
    }
}

struct PublicationView: View {
    @ObservedObject var publication: Publication
    var body: some View {
        // Display different content based on type
        switch publication {
        case is Book:
            if let book = publication as? Book, let isbn = book.isbn {
                Text("Base:\(isbn)").foregroundStyle(.red)
            }
        case is AcademicPaper:
            if let paper = publication as? AcademicPaper {
                Text("Publication:\(paper.paperType)").foregroundStyle(.blue)
            }
        default:
            Text(publication.publishDate!, format: .dateTime).foregroundStyle(.green)
        }
    }
}

As seen, model inheritance has at least the following advantages:

  • Simplified Model Declarations: Sub entities automatically inherit the parent entity’s properties, relationships, and configurations, reducing the need for repetitive definitions.
  • Unified Data Abstraction: By sharing a parent entity, it supports aggregated queries and holistic retrievals across different sub entities.
  • Enhanced Flexibility in Relationship Modeling: Allows for more semantic and structured associations between different subtype entities.

How Model Inheritance Works

So, how does Core Data implement model inheritance in SQLite? It’s actually not complicated. If we inspect the corresponding SQLite database of the project, we will find that there are no separate tables corresponding to Book and AcademicPaper. All data related to Publication is stored in the same table. This table contains properties for Publication as well as all its subentities and grandchild entities.

image-20241210161434306

image-20241210161515836

When performing data retrieval, Core Data first queries the declarations related to the entity in the Z_PRIMARYKEY table (corresponding to the Z_ENT value) and then sets the appropriate retrieval conditions based on the entity type being queried.

image-20241210161802140

For example, if we only retrieve data of type Book, Core Data will add a condition Z_ENT = 3 to the corresponding SQL statement. If retrieving Publication, no such limiting condition is added. Core Data achieves an abstract separation between the underlying storage and the model description in this clever way, easily supporting multi-layer inheritance structures.

This implementation approach offers the following advantages:

  • Simplified Query Logic: By storing all inherited entity data in a single table, the complexity of cross-table queries is reduced.
  • Efficient Data Management: A unified storage structure makes data insertion, updating, and deletion operations more efficient.
  • Flexible Inheritance Support: Supports multi-level inheritance, making model design more flexible and scalable.

However, this implementation also has its limitations, such as potential table structure complexity and performance bottlenecks when handling large amounts of data from different sub entities. Therefore, when designing Core Data models, it’s essential to weigh the convenience brought by inheritance against potential performance impacts and choose the most suitable approach for project needs.

Abstract Entity

In the model editor, there is another option for entities: Abstract Entity.

image-20241210171516653

The official Apple documentation describes it as follows:

Specify that an entity is abstract if you will not create any instances of that entity. You typically make an entity abstract if you have a number of entities that all represent specializations of (inherit from) a common entity that should not itself be instantiated. For example, in the Employee entity you could define Person as an abstract entity and specify that only concrete subentities (Employee and Customer) can be instantiated. By marking an entity as abstract in the Entity pane of the Data Model inspector, you are informing Core Data that it will never be instantiated directly.

According to the official documentation, an entity marked as abstract is more like a special base class. It can be inherited but cannot be instantiated. Ideally, in the earlier example, if Publication is marked as an Abstract Entity, directly creating instances of it would not be allowed. However, at least in Core Data’s Swift wrapper implementation, this rule is not strictly enforced. In practice, in most cases, even if an entity is set as abstract, instances of it can still be created.

However, I have indeed encountered situations where creating instances of an Abstract Entity caused the application to crash. Therefore, if you mark an entity as Abstract, it is best to follow the documentation and avoid writing code that instantiates that entity.

When an abstract entity has subentities and no parent entity itself, the table name in the database still uses the name of that entity. This is the same as when the entity is not marked as abstract.

Limitations of Model Inheritance

Although model inheritance brings many advantages, its underlying storage mechanism also introduces certain limitations, mainly in terms of data redundancy and potential performance degradation. Specifically, the degree of data waste depends on the number of unique properties each sub entity has. Therefore, many developers do not recommend using model inheritance when dealing with large amounts of data, as it can lead to wasted storage space and affect application performance.

Every tool has its suitable scenarios, and model inheritance is no exception. Without understanding the underlying storage mechanism of model inheritance, some developers might attempt to abstract common properties (such as creation time, UUID) from different entities as a parent entity, especially by declaring it as an abstract entity. This way, all sub entity data would be stored in the same table. If these sub entities themselves do not have many other common properties, this design would inevitably lead to significant performance loss.

Therefore, in most cases, the root cause lies in the improper use of model inheritance.

As shown by the advantages of model inheritance, using model inheritance in the following scenarios can yield good results:

  • Parent Entity Contains Numerous Common Properties and Relationships: When the parent entity has many shared properties and relationship configurations, model inheritance can effectively reduce repetitive definitions.
  • High Commonality Between Sub Entities: Sub entities with a common parent entity have significant similarities in properties and behaviors, making the inheritance relationship more reasonable.
  • Need for Aggregated Queries Across Different Entities: By sharing a parent entity, it becomes convenient to perform unified queries and processing on data from different sub entities.

Taking Apple’s Contacts app as an example, we can see that Apple extensively uses model inheritance in its data model. In the figure below, it shows a complex inheritance relationship built with an abstract entity ABCDRecord as the root.

image-20241210172904069

Considering the limited amount of data in the Contacts app and the aforementioned conditions, this model inheritance design brings significant advantages to code writing, such as simplifying data management and enhancing query efficiency.

The image above shows the result of viewing the database file using Core Data Lab. Compared to directly using an SQLite client, Core Data Lab presents more Core Data-specific model details, offering a more intuitive database view.

All Roads Lead to Rome

Although model inheritance has many advantages, if you were to ask me today whether you should use this feature, I would advise caution.

The main reason is that the model inheritance feature is not supported by SwiftData. Once you adopt model inheritance, the database cannot be migrated to the SwiftData framework.

However, if you can accept the similar storage efficiency sacrifices, we can still manually achieve similar effects.

Similar to the implementation of model inheritance, we can store different types with commonalities in the same table (i.e., declared as one entity) and distinguish them through specific properties or relationships.

It is important to note that in this case, developers are better off manually writing model code to achieve effects similar to model inheritance, where sub entities do not contain other sub entity properties. Of course, this also means that the model declaration code will be more complex.

For example, in my project, I constructed the following code, where the ItemData entity corresponds to seven different types. To create and display different types of data more safely and conveniently, I changed the presentation of specialized properties in the model to use an enumeration:

Swift
// Define the specialized content of different data types in the ItemData entity, distinguished by an enumeration
public enum ItemDataContent: Equatable, Sendable {
    case singleValue(eventDate: Date, value: Double)
    case singleOption(eventDate: Date, optionID: UUID)
    case valueWithOption(eventDate: Date, value: Double, optionID: UUID)
    case dualValues(eventDate: Date, pairValue: ValidatedPairDouble)
    case valueWithInterval(pairDate: ValidatedPairDate, value: Double)
    case optionWithInterval(pairDate: ValidatedPairDate, optionID: UUID)
    case interval(pairDate: ValidatedPairDate)
}

In this way, we only need to present this enumeration in the public properties and build initializer methods based on the enumeration:

Swift
@objc(ItemData)
public final class ItemData: NSManagedObject {}

// MARK: - Public Properties
extension ItemData: ItemDataBaseProtocol {
    @NSManaged public var createTimestamp: Date
    @NSManaged public var uid: UUID
    @NSManaged public var item: Item?
    @NSManaged public var memo: Memo?
    @NSManaged public var deletionLog: DeletionLog?

    // Specialized properties for different types
    public var dataContent: ItemDataContent? {
        get { dataContentGetter(type: type) }
        set { dateContentSetter(content: newValue) }
    }
}

extension ItemData {
    public convenience init(
        createTimestamp: Date,
        uid: UUID,
        dataContent: ItemDataContent
    ) {
        self.init(entity: Self.entity(), insertInto: nil)
        self.createTimestamp = createTimestamp
        self.uid = uid
        self.dataContent = dataContent
    }
}

Of course, to achieve this clean and easy-to-use public API, a lot of internal conversions are required (the complete code is too lengthy to display here). Some of these conversions would be automatically handled by Core Data if using model inheritance.

In summary, I currently recommend that developers manually implement effects similar to model inheritance. On one hand, this approach ensures better compatibility with SwiftData; on the other hand, it makes model declarations more aligned with Swift’s programming style, enhancing code controllability.

Conclusion

Whether or not to use model inheritance, developers should fully understand its advantages and disadvantages. In appropriate scenarios, model inheritance can significantly simplify data model design, improve code maintainability, and support complex query requirements.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed