Using Realm with Value Types
In this post I’d like to explore how to build a Data Persistence Layer on top of Realm, using struct
based models and type-safe queries.
In case you have been frozen in carbonite for the past couple of years, Realm is a database technology built from scratch for mobile devices, and the de facto alternative to Core Data in the Apple world.
Like Core Data, Realm requires subclassing to define model objects and allows using them only on the thread which they were created. These and other requirements often add complexity and usually increase the coupling with Realm everywhere in your app.
That’s why it is usually a good idea to treat these frameworks as an implementation detail, abstracting them away under a persistence layer.
Models
As an example, let’s create a persistence layer to store comic-book superhero characters with their publishers. We can use a couple of structs to define our models:
public struct Publisher {
public let identifier: Int
public let name: String
}
public struct Character {
public let identifier: Int
public let name: String
public let realName: String
public let publisher: Publisher?
}
We still need to create the corresponding RealmSwift.Object
subclasses, although we will only use them as a mere transportation mechanism and hide them from the rest of the app.
final class PublisherObject: Object {
dynamic var identifier = 0
dynamic var name = ""
override static func primaryKey() -> String? {
return "identifier"
}
}
final class CharacterObject: Object {
dynamic var identifier = 0
dynamic var name = ""
dynamic var realName = ""
dynamic var publisher: PublisherObject?
override static func primaryKey() -> String? {
return "identifier"
}
}
Mapping
We need a mechanism to transform our struct
based models into their corresponding Realm objects. Let’s define a protocol for that:
public protocol Persistable {
associatedtype ManagedObject: RealmSwift.Object
init(managedObject: ManagedObject)
func managedObject() -> ManagedObject
}
Any type conforming to Persistable
will be able to be serialized to and from the corresponding RealmSwift.Object
subclass.
Here is the code that makes our Character
model conform to Persistable
:
extension Character: Persistable {
public init(managedObject: CharacterObject) {
identifier = managedObject.identifier
name = managedObject.name
realName = managedObject.realName
publisher = managedObject.publisher
.flatMap(Publisher.init(managedObject:))
}
public func managedObject() -> CharacterObject {
let character = CharacterObject()
character.identifier = identifier
character.name = name
character.realName = realName
character.publisher = publisher?.managedObject()
return character
}
}
Creation
With these tools in place, we are ready to implement the insertion methods of our persistence layer.
But first, let’s review how objects are inserted using Realm:
// Create a Publisher object
let publisher = PublisherObject()
publisher.id = 1
publisher.name = "Marvel"
// Get the default Realm
let realm = try! Realm()
// Add to the Realm
try! realm.write {
realm.add(publisher)
}
There are a couple of things we can abstract here. On the one hand, we have the database itself, and on the other the operations that we can perform inside a write transaction.
Even though both concepts are provided by the Realm
object, we are going to encapsulate them into separate types: Container
and WriteTransaction
respectively.
Let’s start with the WriteTransaction
type:
public final class WriteTransaction {
private let realm: Realm
internal init(realm: Realm) {
self.realm = realm
}
public func add<T: Persistable>(_ value: T, update: Bool) {
realm.add(value.managedObject(), update: update)
}
}
Notice that we made the constructor internal
, as only the Container
will create instances of this class. Other operations we can add to the WriteTransaction
interface are delete
and update
.
Implementing the Container
is pretty straightforward:
public final class Container {
private let realm: Realm
public convenience init() throws {
try self.init(realm: Realm())
}
internal init(realm: Realm) {
self.realm = realm
}
public func write(_ block: (WriteTransaction) throws -> Void) throws {
let transaction = WriteTransaction(realm: realm)
try realm.write {
try block(transaction)
}
}
}
Now we have the minimal infrastructure to insert Character
and Publisher
values:
let character = Character(
identifier: 1455,
name: "Iron Man",
realName: "Tony Stark",
publisher: Publisher(identifier: 1, name: "Marvel")
)
let container = try! Container()
try! container.write { transaction in
transaction.add(character)
}
So far this is looking good. We are successfully persisting our struct
based models, and there is no coupling with Realm on the call site.
Partial Updates
With what we have implemented so far, if we want to update a character in the database, we have to provide the full set of values:
// Updating Iron Man's real name
let updatedCharacter = Character(
identifier: 1455,
name: "Iron Man",
realName: "Anthony Edward Stark",
publisher: Publisher(identifier: 1, name: "Marvel")
)
try! container.write { transaction in
transaction.add(updatedCharacter, update: true)
}
We could improve this situation by making the Character
struct mutable, but that will still require fetching the model before updating it.
Fortunately, Realm provides a way to partially update objects by passing a subset of the values, along with the primary key:
try! realm.write {
realm.create(
CharacterObject.self,
value: [
"identifier": 1455,
"realName": "Anthony Edward Stark"
],
update: true
)
}
How can we pass a subset of values in a type-safe manner?
We could use an enum
with associated values to model the different properties and their values. By using that approach, a partial update will look like this:
try! container.write { transaction in
transaction.update(Character.self, properties: [
.identifier(1455),
.realName("Anthony Edward Stark")
])
}
It seems like a good idea, and it will bring auto-completion as a bonus. We will need to add a few things to make this work.
Let’s start by creating a protocol that provides the internal representation of a property value:
public typealias PropertyValuePair = (name: String, value: Any)
public protocol PropertyValueType {
var propertyValuePair: PropertyValuePair { get }
}
We will use this protocol to implement WriteTransaction.update
later.
It makes sense to add an associated type to Persistable
for the type providing the enum
, as it will be tightly related to the model itself.
public protocol Persistable {
associatedtype ManagedObject: Object
associatedtype PropertyValue: PropertyValueType
...
}
Now we are ready to implement the update
method in WriteTransaction
:
public final class WriteTransaction {
...
public func update<T: Persistable>(_ type: T.Type, values: [T.PropertyValue]) {
var dictionary: [String: Any] = [:]
values.forEach {
let pair = $0.propertyValuePair
dictionary[pair.name] = pair.value
}
realm.create(T.ManagedObject.self, value: dictionary, update: true)
}
}
The implementation simply iterates over the values, querying for the internal representation and adding it to a dictionary that is passed to Realm.create
.
Finally, we need to implement the enum
for the Character
property values:
extension Character {
public enum PropertyValue: PropertyValueType {
case identifier(Int)
case realName(String)
...
public var propertyValuePair: PropertyValuePair {
switch self {
case .identifier(let id):
return ("identifier", id)
case .realName(let realName):
return ("realName", realName)
...
}
}
}
}
Now we have an elegant and type-safe way for partially updating objects in our Persistence Layer.
Querying
We still lack an essential functionality in our Persistence Layer: fetching data.
Let’s review how data is fetched using Realm:
// All the Marvel characters sorted by name
let results = realm.objects(CharacterObject.self)
.filter("publisher.name == Marvel")
.sorted(byProperty: "name")
This is very similar to the problem we were facing when dealing with partial updates. We need to find a way to pass a predicate and a set of sort descriptors in a type-safe manner.
We could go crazy and implement a DSL for fully featured type-safe queries, but let’s take a more pragmatic approach and implement something simpler.
As we did with the partial updates, we will use an enum
with associated values to express the different queries that we can apply to a particular model in our Persistence Layer.
First, we create the protocol that will provide the internal representation of a query:
public protocol QueryType {
var predicate: NSPredicate? { get }
var sortDescriptors: [SortDescriptor] { get }
}
Then we add an associated type to Persistable
that must conform to that protocol:
public protocol Persistable {
associatedtype ManagedObject: Object
associatedtype PropertyValue: PropertyValueType
associatedtype Query: QueryType
...
}
Recall that Realm will return Results<Object>
as the result of a query. We need to create a new type that wraps those results and maps them to our struct
based models:
public final class FetchedResults<T: Persistable> {
internal let results: Results<T.ManagedObject>
public var count: Int {
return results.count
}
internal init(results: Results<T.ManagedObject>) {
self.results = results
}
public func value(at index: Int) -> T {
return T(managedObject: results[index])
}
}
Now we are ready to add a method in Container
that returns the resulting values of a query:
public func values<T: Persistable> (_ type: T.Type, matching query: T.Query) -> FetchedResults<T> {
var results = realm.objects(T.ManagedObject.self)
if let predicate = query.predicate {
results = results.filter(predicate)
}
results = results.sorted(by: query.sortDescriptors)
return FetchedResults(results: results)
}
Let’s create a type for the Character
queries and implement one to filter by publisher name:
extension Character {
public enum Query: QueryType {
case publisherName(String)
public var predicate: NSPredicate? {
switch self {
case .publisherName(let value):
return NSPredicate(format: "publisher.name == %@", value)
}
}
public var sortDescriptors: [SortDescriptor] {
return [SortDescriptor(property: "name")]
}
}
}
With all of that in place, here’s how it looks querying characters by publisher name:
let results = container.values(
Character.self,
matching: .publisherName("marvel")
)
I would say that gets the job done in a very elegant way.
Conclusion
We have successfully implemented a simple Persistence Layer using structs and decoupled the underlying persistence technology (Realm) from the rest of the app.
Please note that mapping to value types might not be the best solution if you have a complex object graph and depend on live objects. In those cases, it is better to use managed objects directly and process or transform them for presentation.
The code in this post is available here.