A minimal Core Data Stack
Let’s build a type-safe Core Data stack that’s both powerful and pleasant to use. We’ll add features incrementally, starting with fetching, and adding declarative queries and domain separation.
This will be the result:
Fetching Entities
This is the simplest API possible without macros or property wrappers. Can you guess the implementation?
fetch() implementation
This is how you fetch a specific entity in Core Data:
It works for any type so we can make it generic. The name should match the class name.
And we arrive at our goal.
A query can also have a predicate, sort, and a limit. A problem with Core Data is that it lacks a declarative API, let’s fix that.
Integrating this query on our fetch method doesn’t change its signature, but enables query modification with a fluent API.
Convenience queries are also possible with minimal additions to the API surface.
Implementation
Data First
Let’s reflect on the benefits of a declarative approach:
Gone are the underlying mechanics of NSFetchRequest
. Clients are now closer to what they want to achieve, not how. Additional benefits:
- Queries become composable and reusable
- The structure prevents invalid states
- Declarative queries are easier to inspect and test, compared to procedural code
- Separation of concerns between how and what to fetch
- Fetch mechanics are centralize in a single point that interprets data, decreasing the code and its possible mistakes
Through the brief history of computing, many have noted the convenience and power of shifting procedural complexity to declarative data.
Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious. –The Mythical Man-Month (1975)
Rule of Representation: Fold knowledge into data, so program logic can be stupid and robust. Even the simplest procedural logic is hard for humans to verify, but quite complex data structures are fairly easy to model and reason about. –Basics of the Unix Philosophy
Bad programmers worry about the code. Good programmers worry about data structures and their relationships. –Linus Torvalds
Smart data structures and dumb code works a lot better than the other way around. –Eric S. Raymond, The Cathedral and The Bazaar.Or a similar argument before computers existed:
Have the argument clear in your mind, the words will follow naturally. –Cato the Elder
Fetching Domain
This would be nice to have:
To implement this we know
- managed objects need to map to domain,
- both the fetch and the mapping needs to be tied to the generic type, which leads to the following solution:
Persisting Domain
A similar protocol is used to map back from model to database:
For updates create a User.Update
that has the same properties, but all except the ID are optional. When we submit an update, it looks for the object by ID and updates any non-nil property. For objects with relations this requires some effort, since we have to upsert the objects.
An interesting case is bidirectional relationships, for instance,
Dog.owner <--n 1--> Person.dogs
There are two solutions to avoid an infinite loop:
- change
dog.owner
from person to UUID - or remove the
dog.owner
property
Both are valid. Prefer removing the dog.owner
if you primarily access dogs through their owners.
Benefits
This architecture provides several advantages:
- Type-safe queries prevent runtime errors
- Mapping between domain and persistence
- Testable code through protocol abstractions
- Bidirectional conversion between domain models and entities
- Easier thread-safety thanks to value objects
Thread-Safety in Core Data
These are the rules:
- For light tasks use main thread.
- For heavy tasks use performBackgroundTask.
- For complex operations child contexts with type NSPrivateQueueConcurrencyType that merges changes back to the main context. This also lets you discard the child context to rollback changes.
- To send objects across threads use domain structs or NSManagedObjectID, never managed objects.
The cost is minimal - just a thin layer over Core Data that makes it significantly more robust and maintainable. The source code for the article is in GitHub.