Realm-cocoa: Abstract Class / Polymorphism Support

Created on 9 Nov 2014  路  11Comments  路  Source: realm/realm-cocoa

Does it possible to create smth like this:
Abstract Data Model

Blue - abstract classes. Such model will help me to incapsulate some logic and prevent code duplication.

In this example RLMItem can be associated with RLMCategory or/and RLMFolder. It would be very useful if i will be able to get all RLMFolders and RLMItems using [RLMCatItem allObjects].
Seems that it is not possible.

I've tried to make smth similar with subclasses, but as result - I have additional useless classes in schema.

This works well with CoreData, but can I expect smth like this in Realm?

_In other words:_
1) Abstract classes will not be displayed in Realm Browser
2) Ability to query abstract classes (this will include all subclass objects)

Blocked O-Community Pipeline-Idea-Backlog T-Feature

Most helpful comment

5. Using a type-erased wrapper for polymorphic relationships

Using a type-erased wrapper is a scalable workaround for the current lack of support for polymorphic relationships. It even allows you to maintain your inheritance hierarchy.

// Abstract class
class PaymentMethod: Object {
  dynamic var owner: String
}

class CreditCardPaymentMethod: PaymentMethod {
    dynamic var cardNumber: String = ""
    dynamic var csv: String = ""

    override static func primaryKey() -> String? {
        return "cardNumber"
    }
}

class PaypalPaymentMethod: PaymentMethod {
    dynamic var username: String = ""
    dynamic var password: String = ""

    override static func primaryKey() -> String? {
        return "username"
    }
}

// Define as many subclasses as you'd like鈥攅ven sub-sub-sub classes!

If you want to store an instance of _any_ subclass of PaymentMethod, define a type-erased wrapper that stores the type's name and the primary key.

class AnyPaymentMethod: Object {
    dynamic var typeName: String = ""
    dynamic var primaryKey: String = ""

    // A list of all subclasses that this wrapper can store
    static let supportedClasses: [PaymentMethod.Type] = [
        CreditCardPaymentMethod.self,
        PaypalPaymentMethod.self
    ]

    // Construct the type-erased payment method from any supported subclass
    convenience init(_ paymentMethod: PaymentMethod) {
        self.init()
        typeName = String(paymentMethod.dynamicType)
        guard let primaryKeyName = paymentMethod.dynamicType.primaryKey() else {
            fatalError("`\(typeName)` does not define a primary key")
        }
        guard let primaryKeyValue = paymentMethod.valueForKey(primaryKeyName) as? String else {
            fatalError("`\(typeName)`'s primary key `\(primaryKeyName)` is not a `String`")
        }
        primaryKey = primaryKeyValue
    }

    // Dictionary to lookup subclass type from its name
    static let methodLookup: [String : PaymentMethod.Type] = {
        var dict: [String : PaymentMethod.Type] = [:]
        for method in supportedClasses {
            dict[String(method)] = method
        }
        return dict
    }()

    // Use to access the *actual* PaymentMethod value, using `as` to upcast
    var value: PaymentMethod {
        guard let type = AnyPaymentMethod.methodLookup[typeName] else {
            fatalError("Unknown payment method `\(typeName)`")
        }
        guard let value = try! Realm().objectForPrimaryKey(type, key: primaryKey) else {
            fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
        }
        return value
    }
}

Now, we can create a type that stores an AnyPaymentMethod!

class Purchase: Object {
    dynamic var product: String = ""
    dynamic var price: Int = 0
    dynamic var paymentMethod: AnyPaymentMethod?
}

Let's check out how this is used in an example.

let realm = try! Realm()
try! realm.write {
    // Payment methods
    let creditCard = CreditCardPaymentMethod()
    creditCard.owner = "Jaden Geller"
    creditCard.cardNumber = "314159265358979"
    creditCard.csv = "001"
    realm.add(creditCard)

    let paypal = PaypalPaymentMethod()
    paypal.owner = "Jaden Geller"
    paypal.username = "ilovepersistance"
    paypal.password = "swifty"
    realm.add(paypal)

    // Purchases
    let carPurchase = Purchase()
    carPurchase.product = "car"
    carPurchase.price = 35000
    carPurchase.paymentMethod = AnyPaymentMethod(creditCard)
    realm.add(carPurchase)

    let gamePurchase = Purchase()
    gamePurchase.product = "game"
    gamePurchase.price = 20
    gamePurchase.paymentMethod = AnyPaymentMethod(paypal)
    realm.add(gamePurchase)
}

// Later, we want to check the payment method, use the `value` property on `AnyPaymentMethod`
for purchase in realm.objects(Purchase) {
    if let creditCard = purchase.paymentMethod?.value as? CreditCardPaymentMethod {
        print("We bought a \(purchase.product) from credit card #\(creditCard.cardNumber)")
    } else if let paypal = purchase.paymentMethod?.value as? PaypalPaymentMethod {
        print("We bought a \(purchase.product) from paypal username @\(paypal.username)")
    } else {
        fatalError("Unknown payment method")
    }
}

Awesome! This workaround is well-suited for cases where there are many subclasses since, unlike the option type approach, it doesn't require a single property for each supported subclass. This means that you can add new subclasses without performing migrations!

Caveats

  • Inverse relationships on PaymentMethods will not work properly. As a workaround, place the inverse relationship on AnyPaymentMethod.
  • PaymentMethods will not be recursively added to Realm when an object containing an AnyPaymentMethod is added to Realm. Make sure you add each PaymentMethod individually.
  • All subclasses supported by AnyPaymentMethod must use the _same_ type of primary key (though it does not need to be String).

All 11 comments

Hi @Kirow, there's definitely value in your modeling approach, but Realm's current inheritance implementation doesn't allow for the polymorphism you're looking for.

Your first goal would be fairly easy to support (avoiding creating unused tables in the db). However, empty tables in Realm are very small so even though this is annoying, it shouldn't have any significant performance or usability impact. If you're keen on filtering which tables are created in Realm, you could patch RLMSchema's +initialize method.

The second goal highlights a more general sore point in Realm's architecture, which is that containers for a class (RLMResults or RLMArray) can't contain instances of its subclass. So for this reason, we can't include subclasses in [RLMObject allObjects] for now.

So for the time being, you'll have to adapt your model to fit with Realm's inheritance approach. Meanwhile, we'll keep an eye on possible modeling improvements for Realm and we'll make sure to update this thread when we have anything to share.

I am having a similar issue to @Kirow's, I have an RLMArray defined to store an abstract class, and storing there subclasses of the abstract class, but of course is giving me back instances of the abstract class.

It will be really awesome if Realm supports this in the future.

@jpsim So what kind of inheritance/polymorphism _is_ available in Realm at this time? From what I can see, with no polymorphism in querying, nor in relationships, there's basically _no inheritance_ supported.

So what kind of inheritance/polymorphism is available in Realm at this time? From what I can see, with no polymorphism in querying, nor in relationships, there's basically no inheritance supported.

Sorry for the late reply. Inheritance in Realm at the moment gets you:

  • Class methods, instance methods and properties on parent classes are inherited in their child classes.
  • Methods and functions that take parent classes as arguments can operate on subclasses.

It does not get you:

  • Casting between polymorphic classes (subclass->subclass, subclass->parent, parent->subclass, etc.).
  • Querying on multiple classes simultaneously.
  • Multi-class containers (RLMArray/List and RLMResults/Results).

We're 100% behind adding this functionality in Realm, but as you can tell from the labels on this GH issue, it is neither a high priority for us at the moment, or easy to do. Some underlying architecture work is needed to move forward with these additional inheritance-related features.

In the meantime, you can work around these inheritance limitations in a number of ways:

1. Running queries on all related types and mapping back to arrays

class A: Object {
  dynamic var intProp = 0
}
class B: A {}
class C: A {}
extension Realm {
  func filter<ParentType: Object>(parentType parentType: ParentType.Type, subclasses: [ParentType.Type], predicate: NSPredicate) -> [ParentType] {
    return ([parentType] + subclasses).flatMap { classType in
      return Array(self.objects(classType).filter(predicate))
    }
}

// Usage

let realm = try! Realm()
let allAClassesGreaterThanZero = realm.filter(A.self, [B.self, C.self], NSPredicate(format: "intProp > 0")) // => [A]

2. Using an option type for polymorphic relationships

class A: Object {
  dynamic var intProp = 0
}
class B: A {}
class C: A {}
class AClasses: Object {
  dynamic var a: A? = nil
  dynamic var b: B? = nil
  dynamic var c: C? = nil
}
class D: Object {
  dynamic var polymorphicA: AClasses? = nil
}
// D's polymorphicA value can hold a wrapped A, B or C object

3. Initializing objects with their polymorphic counterparts

Instead of casting, you can copy the underlying values from one object to another if they share those properties:

class A: Object {
  dynamic var intProp = 0
}
class B: A {}

// Usage

let a = A(value: [42])
let b = B(value: a)

4. Alternative: Using Composition instead of Inheritance

Instead of using inheritance, you can avoid it and it's current limitations with Realm in some cases at all by composing your classes via linked objects.

class Animal: Object {
  dynamic var age = 0
}
class Duck : Object {
  dynamic var animal: Animal? = nil
  dynamic var name = ""
}
class Frog : Object {
  dynamic var animal: Animal? = nil
  dynamic var dateProp = NSDate()
}

// Usage
let duck = Duck(value: [ "animal": [ "age": 3 ], "name": "Gustav" ])

If you want to share behavior between multiple classes, you can e.g. facilitate Swift's default implementations of protocols:

protocol DuckType {
  dynamic var animal: Animal? { get }

  func quak() -> ()
}

extension DuckType {
  func quak() {
    for _ in 1...(animal?.age ?? 1) {
      print("quak")
    }
  }
}

extension Duck: DuckType {}
extension Frog: DuckType {}

// both can quak now

@mrackwitz Let me ask you about primaryKey. If Animal has id as primaryKey, what property is the best to Duck and Frog for primaryKey?
animal, animal's id or new id?

@wanbok: You would need to manually take over the value of the id property on Animal into a property you defined in each of your classes, here Duck and Frog. Only 'string' and 'int' properties can be designated the primary key.

5. Using a type-erased wrapper for polymorphic relationships

Using a type-erased wrapper is a scalable workaround for the current lack of support for polymorphic relationships. It even allows you to maintain your inheritance hierarchy.

// Abstract class
class PaymentMethod: Object {
  dynamic var owner: String
}

class CreditCardPaymentMethod: PaymentMethod {
    dynamic var cardNumber: String = ""
    dynamic var csv: String = ""

    override static func primaryKey() -> String? {
        return "cardNumber"
    }
}

class PaypalPaymentMethod: PaymentMethod {
    dynamic var username: String = ""
    dynamic var password: String = ""

    override static func primaryKey() -> String? {
        return "username"
    }
}

// Define as many subclasses as you'd like鈥攅ven sub-sub-sub classes!

If you want to store an instance of _any_ subclass of PaymentMethod, define a type-erased wrapper that stores the type's name and the primary key.

class AnyPaymentMethod: Object {
    dynamic var typeName: String = ""
    dynamic var primaryKey: String = ""

    // A list of all subclasses that this wrapper can store
    static let supportedClasses: [PaymentMethod.Type] = [
        CreditCardPaymentMethod.self,
        PaypalPaymentMethod.self
    ]

    // Construct the type-erased payment method from any supported subclass
    convenience init(_ paymentMethod: PaymentMethod) {
        self.init()
        typeName = String(paymentMethod.dynamicType)
        guard let primaryKeyName = paymentMethod.dynamicType.primaryKey() else {
            fatalError("`\(typeName)` does not define a primary key")
        }
        guard let primaryKeyValue = paymentMethod.valueForKey(primaryKeyName) as? String else {
            fatalError("`\(typeName)`'s primary key `\(primaryKeyName)` is not a `String`")
        }
        primaryKey = primaryKeyValue
    }

    // Dictionary to lookup subclass type from its name
    static let methodLookup: [String : PaymentMethod.Type] = {
        var dict: [String : PaymentMethod.Type] = [:]
        for method in supportedClasses {
            dict[String(method)] = method
        }
        return dict
    }()

    // Use to access the *actual* PaymentMethod value, using `as` to upcast
    var value: PaymentMethod {
        guard let type = AnyPaymentMethod.methodLookup[typeName] else {
            fatalError("Unknown payment method `\(typeName)`")
        }
        guard let value = try! Realm().objectForPrimaryKey(type, key: primaryKey) else {
            fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
        }
        return value
    }
}

Now, we can create a type that stores an AnyPaymentMethod!

class Purchase: Object {
    dynamic var product: String = ""
    dynamic var price: Int = 0
    dynamic var paymentMethod: AnyPaymentMethod?
}

Let's check out how this is used in an example.

let realm = try! Realm()
try! realm.write {
    // Payment methods
    let creditCard = CreditCardPaymentMethod()
    creditCard.owner = "Jaden Geller"
    creditCard.cardNumber = "314159265358979"
    creditCard.csv = "001"
    realm.add(creditCard)

    let paypal = PaypalPaymentMethod()
    paypal.owner = "Jaden Geller"
    paypal.username = "ilovepersistance"
    paypal.password = "swifty"
    realm.add(paypal)

    // Purchases
    let carPurchase = Purchase()
    carPurchase.product = "car"
    carPurchase.price = 35000
    carPurchase.paymentMethod = AnyPaymentMethod(creditCard)
    realm.add(carPurchase)

    let gamePurchase = Purchase()
    gamePurchase.product = "game"
    gamePurchase.price = 20
    gamePurchase.paymentMethod = AnyPaymentMethod(paypal)
    realm.add(gamePurchase)
}

// Later, we want to check the payment method, use the `value` property on `AnyPaymentMethod`
for purchase in realm.objects(Purchase) {
    if let creditCard = purchase.paymentMethod?.value as? CreditCardPaymentMethod {
        print("We bought a \(purchase.product) from credit card #\(creditCard.cardNumber)")
    } else if let paypal = purchase.paymentMethod?.value as? PaypalPaymentMethod {
        print("We bought a \(purchase.product) from paypal username @\(paypal.username)")
    } else {
        fatalError("Unknown payment method")
    }
}

Awesome! This workaround is well-suited for cases where there are many subclasses since, unlike the option type approach, it doesn't require a single property for each supported subclass. This means that you can add new subclasses without performing migrations!

Caveats

  • Inverse relationships on PaymentMethods will not work properly. As a workaround, place the inverse relationship on AnyPaymentMethod.
  • PaymentMethods will not be recursively added to Realm when an object containing an AnyPaymentMethod is added to Realm. Make sure you add each PaymentMethod individually.
  • All subclasses supported by AnyPaymentMethod must use the _same_ type of primary key (though it does not need to be String).

Is there any update on the status of this issue, or current recommendations for best practices? The Realm Swift documentation's section on model inheritance says that this feature is "on the roadmap" and links to this issue.

6 years is a long time to be "on the roadmap", maybe it's time to get this one done?

Was this page helpful?
0 / 5 - 0 ratings