This is something which is potentially interesting for Realm, although I think we'd need some additional pieces to actually use it.
Currently, users define model classes by inheriting from a base class we provide, and then marking properties with a property wrapper we provide:
class Document: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var title: String
@Persisted var content: String?
}
Object instances of these classes can either be "unmanaged", in which case they behave exactly like ordinary objects and @Persisted
is just pass-through storage, or they can be "managed", which means that the object is an accessor for data in the database, and the property getters read from the database and the setters write to the database. The normal initializer (i.e. Document()
) creates unmanaged objects, reading objects from the Realm produces managed objects (e.g. realm.objects(Document.self).first!
), and unmanaged objects can be turned into managed objects with realm.add(document)
.
The current property wrapper based design has a few drawbacks:
- It requires subclassing a base class. This means that it can't be a struct (which may or may not be a good idea even if it was possible), and it means that we don't play well with other libraries which want you to subclass something.
- There's some minor performance issues with initializing managed objects due to that we need to loop over each of the properties to initialize the property wrapper. This isn't a big deal, but given a model class with a large number of properties, something like
realm.objects(Document.self).map { $0.title }
can spend most of its runtime in initializing property wrappers. - The implementation is horribly complicated, and I'm not fully confident that everything we're doing is actually legal and not just things that happen to work. Also it relies on
_enclosingInstance
. - The complexity of the implementation has forced us to make the API worse in places. For example, we would have preferred
@PrimaryKey var _id: ObjectId
, but I couldn't figure out how to make it work without duplicating hundreds of lines of code. - The managed or unmanaged enum discriminant is stored per-property, which in practice adds a word to the size of objects for each property (for most property types).
A hypothetical type wrapper based version might look something like:
@RealmObject
class Document {
@PrimaryKey var _id: ObjectId
var title: String = ""
var content: String?
}
@typeWrapper
struct RealmObject<S> {
enum Storage<S> {
case unmanaged(S)
case managed(RLMObject)
}
var storage: Storage<s>
init(memberwise storage: S) {
self.storage = .unmanaged(storage)
}
init(managed object: RLMObject) {
self.storage = .managed(object)
}
subscript<V>(storageKeyPath path: WritableKeyPath<S, V>) -> V
where V: RealmPersistable {
get {
switch storage {
case let .unmanaged(v):
return v[keyPath: path]
case let .managed(obj):
return V.get(obj, path)
}
}
set {
switch storage {
case let .unmanaged(v):
v[keyPath: path] = newValue
self = .unmanaged(v)
case let .managed(obj):
return V.set(obj, path, newValue)
}
}
}
func promoteToManaged(object: RLMObject) {
self.storage = .managed(object)
}
}
// Each type which can be stored in Realm conforms to RealmPersistable and
// provides functions for reading and writing values of that type
extension String: RealmPersistable {
static func get<S>(_ object: RLMObject, _ path: WritableKeyPath<S, String>) -> String {
return RLMGetString(object, keyPathToColumnId(path))
}
static func set<S>(_ object: RLMObject, _ path: WritableKeyPath<S, String>, _ value: String) {
RLMSetString(object, keyPathToColumnId(path), value)
}
}
func keyPathToColumnId<V: RealmObject, S>(_ path: WritableKeyPath<S, String>) -> Int {
// ?
}
// @PrimaryKey is just a no-op tag used during schema discovery and to
// constrain the property to valid types
@propertyWrapper
struct PrimaryKey<Value: RealmPrimaryKey> {
var value: Value
init(wrappedValue: Value) {
self.value = wrappedValue
}
var projectedValue: Self { return self }
var wrappedValue: Value {
get { value }
set { value = newValue }
}
}
I see two missing pieces for this to work. The first is the where
clause on the subscript. We don't support properties of arbitrary types and instead need them to conform to a specific protocol, which the proposal currently doesn't appear to allow.
The other is something not directly related to this proposal: I don't think keyPathToColumnId
is currently implementable. _enclosingInstance
has a similar problem where we need to turn a keypath into something that can identify which database column to access. There we take advantage of that the target of the keypath is a property wrapper we define, and store the column id on each of the property wrappers (initializing these property wrappers involves ivar_getOffset()
and pointer math, and is the part that I'm not sure is actually valid). With type wrappers we could not use the same approach.
There's probably some other problems which would pop up; this is just what occurred to me with an hour or two of thinking about it.