Abstract Class / Polymorphism Support #1109

Open
Kirow opened this Issue Nov 9, 2014 · 9 comments

Projects

None yet

9 participants

@Kirow
Kirow commented Nov 9, 2014

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)
@jpsim
Member
jpsim commented Nov 11, 2014

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.

@raspu
raspu commented Nov 20, 2014

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.

@alazier alazier added the backlog label Dec 5, 2014
@puttin
puttin commented May 21, 2015

realm/realm-java#761 is related issue

@segiddins segiddins changed the title from Abstract Classes Support to Abstract Class / Polymorphism Support Jun 23, 2015
@nekonari

@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.

@astigsen astigsen referenced this issue in realm/realm-core Jul 13, 2015
Open

Typed Links #946

@jpsim
Member
jpsim commented Sep 28, 2015

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)
@mrackwitz
Member

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
@wanbok
wanbok commented Mar 4, 2016

@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?

@mrackwitz
Member

@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.

@JadenGeller
Contributor
JadenGeller commented Jun 23, 2016 edited

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—even 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment