Model separation: parsing with Codable and persisting with Core Data
It’s been a while since my previous post on Core Data. And, as often happens, my approach to the problem evolved (hopefully for the better). So, I decided to write about what I changed and why.
Separating the domain model from the persistence model
The main change I made over the past two years has been to separate the domain model from the persistence model. There are a few reasons behind this choice.
First, this helps with having a better separation of concerns. The domain model is defined by the API and represented by means of JSON. This means that we can just use barebones structs to represent the entities returned by the API. The persistence model, instead, is heavily dependent on the particular persistent storage of choice. In our case the chosen persistent storage is Core Data which, for the purposes of this post, we can consider as an ORM layer on top of a SQLite database. Of course, we want to make sure we have a 1:1 relationship between the domain model and the persistence model representing the same entity.
Among other things, this separation allows us, to some degree, to change each representation independently. For instance, we could change the details of one of entities persisted in Core Data without having to touch the corresponding domain model representation. This could be useful, for instance, if we wanted to use ad hoc types other than the primitive ones from JSON (bool
, number
, string
) for our persistence model. On the other hand, though, the model representation is usually changed because of the need to comply with some API update.
A second important advantage of model separation is better testability. Keeping the domain model separated from the persistence model allow us to test the parsing logic independently from the persistence logic.
Let’s see how we can apply model separation to the User
class from my previous post.
Domain model handling: User and Codable
Since the domain model will be only used to represent API entities, we can implement it as a simple Codable struct
:
struct User: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case avatarUrl = "avatar"
case username
case role
}
let avatarUrl: String?
let username: String?
let role: String?
}
Persistence model handling: UserManagedObject and Core Data
Since we’ll be using Core Data, the persistence model needs to inherit from NSManagedObject
:
class UserManagedObject: NSManagedObject {
@NSManaged var avatarUrl: String?
@NSManaged var username: String?
@NSManaged var role: String
}
Now, since there are only a few valid roles, we could make it easier to handle them by encapsulating them into a UserRole
enum:
enum UserRole: String {
case unknown = "Unknown"
case admin = "Admin"
case owner = "Owner"
case user = "User"
static func fromString(_ value: String?) -> UserRole {
guard let value = value else { return .unknown }
return UserRole(rawValue: value) ?? .unknown
}
}
class UserManagedObject: NSManagedObject {
@NSManaged var avatarUrl: String?
@NSManaged var username: String?
@NSManaged private var roleValue: String
var role: UserRole {
get {
return UserRole.fromString(roleValue)
}
set {
roleValue = newValue.rawValue
}
}
}
NOTE: There may be different ways to achieve this using Core Data specific functionality. But I personally prefer a more programmatic approach to transforming values.
Conversion across domain and persistence models
There are mainly two situations where we need to convert across domain and persistence models:
- Retrieving data from the API and persisting it in Core Data: In this case, we will be creating domain objects, as we parse the JSON response content, and then converting them to their corresponding persistence objects (
User
->UserManagedObject
) - Uploading Core Data updates to the backend: In case we have updated persistence objects, we likely need to communicate such updates to the backend through the API (usually by means of a
PUT
call). This requires converting one or more existing persistence objects to their corresponding domain objects that can be handled by the API (UserManagedObject
->User
).
Since I always find useful to express requirements through an interface, I decided to create a couple of protocols to serve this purpose.
Converting from domain to persistence model: ManagedObjectConvertible
In order to express the need to be able to convert from the domain model to the persistence model, we can rely on the ManagedObjectConvertible protocol:
/// Protocol to provide functionality for Core Data managed object conversion.
protocol ManagedObjectConvertible {
associatedtype ManagedObject
/// Converts a conforming instance to a managed object instance.
///
/// - Parameter context: The managed object context to use.
/// - Returns: The converted managed object instance.
func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject?
}
Now, to convert a User
object to its corresponding UserManagedObject
one, we just need to conform to the ManagedObjectConvertible
protocol:
extension User: ManagedObjectConvertible {
func toManagedObject(in context: NSManagedObjectContext) -> UserManagedObject? {
let entityName = UserManagedObject.entityName
guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: context) else {
NSLog("Can't create entity \(entityName)")
return nil
}
let object = UserManagedObject.init(entity: entityDescription, insertInto: context)
object.avatarUrl = avatarUrl
object.username = username
object.role = UserRole.fromString(role)
return object
}
}
The above code is just creating the appropriate type of Core Data entity and assigning its property values from the current User
object. Just a quick note about the context
(NSManagedObjectContext
) parameter: Since the method is creating a new Core Data object, the context
we use when calling it should be the appropriate context to be able to write to Core Data: In particular it must support writing to Core Data main context from a background thread.
Converting from persistence to domain model: ModelConvertible
Similarly to what we saw for the previous conversion, to express the need to be able to convert from the persistence model to the domain model, we can rely on the ModelConvertible protocol:
/// Protocol to provide functionality for data model conversion.
protocol ModelConvertible {
associatedtype Model
/// Converts a conforming instance to a data model instance.
///
/// - Returns: The converted data model instance.
func toModel() -> Model?
}
To convert a UserManagedObject
object to its corresponding User
one, we just need to conform to the ModelConvertible
protocol:
extension UserManagedObject: ModelConvertible {
// MARK: - ModelConvertible
/// Converts a UserManagedObject instance to a User instance.
///
/// - Returns: The converted User instance.
func toModel() -> User? {
return User(avatarUrl: avatarUrl,
username: username,
role: role.rawValue)
}
}
Parsing the JSON response and storing users in Core Data
Now that we have fully separated the domain model from the persistence model, let’s see how we can refactor the existing UserController
(which is responsible for retrieving users data from the API and persisting it in Core Data). The required changes are rather simple:
func parse(_ jsonData: Data) -> Bool {
do {
[...]
// Parse JSON data
let users = try JSONDecoder().decode([User].self, from: jsonData)
// Update Core Data
let managedObjectContext = persistentContainer.viewContext
_ = users.map { $0.toManagedObject(in: managedObjectContext) }
try managedObjectContext.save()
return true
} catch let error {
print(error)
return false
}
}
As mentioned earlier, we just need to make sure we’re using an appropriate context (one that supports writing to Core Data main context from a background thread) when calling the toManagedObject
method.
Conclusion
In this post, I illustrated my most recent approach in regards to separating the domain model from the persistence model when working with Core Data. This should allow better separation of concerns and enhanced testability.
The updated code is available on GitHub.
As briefly touched upon throughout this post, working with Core Data can be tricky. Especially when trying to leverage background threads/queues to interact with it. In the next post I’m going to describe some of the approaches I found useful to deal with some of Core Data rough edges.