Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract Class / Polymorphism Support #1109

Open
Kirow opened this issue Nov 9, 2014 · 30 comments
Open

Abstract Class / Polymorphism Support #1109

Kirow opened this issue Nov 9, 2014 · 30 comments

Comments

@Kirow
Copy link

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
Copy link
Contributor

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
Copy link

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.

@puttin
Copy link

puttin commented May 21, 2015

realm/realm-java#761 is related issue

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

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

@jpsim jpsim added T-Feature Blocked This issue is blocked by another issue labels Sep 28, 2015
@jpsim
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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

@toph-allen
Copy link

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.

@eanagel
Copy link

eanagel commented Jul 10, 2020

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

@bdkjones
Copy link

bdkjones commented Apr 2, 2021

7 years and counting.

@bdkjones
Copy link

bdkjones commented Feb 9, 2022

@JadenGeller since it doesn't seem like Realm is going to improve anything here, it would be nice if you updated your type-erased wrapper example. It's 7 years out of date and throws all kinds of compiler errors now. This GitHub issue seems to be THE canonical example of how to make Realm do this, even though it's not part of the official docs.

Additionally, it doesn't look like it'll work with the new @Persisted(primaryKey: true) property wrapper. I'm doing this:

@Persisted(primaryKey: true) var uuid: UUID = UUID()

And that's crashing in AnyPaymentMethod's init(), in the guard statement that ensures we have a primaryKeyName.

To get around that, I did this:

final class AWSRemote: Remote
{
    @Persisted(primaryKey: true) var uuid: UUID = UUID()
    
    // This is required to make the type-erased wrapper `AnyRemote` work. It can't access the @Persisted(primaryKey: true) property wrapper.
    class override func primaryKey() -> String? {
        return "uuid"
    }
}

That runs just fine, but I don't know if using @Persisted(primaryKey:) and primaryKey() simultaneously is safe. The rest of the compiler errors are just outdated Swift syntax.

@dianaafanador3
Copy link
Contributor

dianaafanador3 commented Feb 14, 2022

Hi @bdkjones we still have Polymorphic inheritance on our roadmap, have in mind this is a complex and long project which takes development both on Core and SDK side.
As a workaround, I'm updating the type-erasure example posted on this issue for modern schema declaration.

class PaymentMethod: Object {
    @Persisted var owner: String
}

class CreditCardPaymentMethod: PaymentMethod {
    @Persisted(primaryKey: true) var id: String = ObjectId.generate().stringValue
    @Persisted var cardNumber: String = ""
    @Persisted var csv: String = ""
}

class PaypalPaymentMethod: PaymentMethod {
    @Persisted(primaryKey: true) var id: String = ObjectId.generate().stringValue
    @Persisted var username: String = ""
    @Persisted var password: String = ""
}

class Purchase: Object {
    @Persisted(primaryKey: true) var id: ObjectId = ObjectId.generate()
    @Persisted var product: String = ""
    @Persisted var price: Int = 0
    @Persisted var paymentMethod: AnyPaymentMethod?
}

class AnyPaymentMethod: Object  {
    @Persisted(primaryKey: true) var primaryKey: String = ""
    @Persisted var typeName: 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(describing: type(of: paymentMethod))
        guard let primaryKeyName = type(of: paymentMethod).sharedSchema()?.primaryKeyProperty?.name else {
            fatalError("`\(typeName)` does not define a primary key")
        }
        guard let primaryKeyValue = paymentMethod.value(forKey: 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(describing: 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().object(ofType: type, forPrimaryKey: primaryKey) else {
            fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
        }
        return value
    }
}

and can be used the same as proposed.

We will post on this issue any update regarding Polymorphism feature.
Lastly, you can find updated documentation on this issue here https://docs.mongodb.com/realm/sdk/swift/fundamentals/object-models-and-schemas/.

@sync-by-unito sync-by-unito bot added the Waiting-For-Reporter Waiting for more information from the reporter before we can proceed label Feb 14, 2022
@sync-by-unito sync-by-unito bot removed Waiting-For-Reporter Waiting for more information from the reporter before we can proceed Blocked This issue is blocked by another issue labels Feb 22, 2022
@bdkjones
Copy link

@dianaafanador3 Thanks for updating the workaround example. I DO understand that this is a large feature, but 8 years is a very long time and I don't think realm realizes just how long issues like this have been open.

Plus, polymorphism isn't some newfangled, fringe idea. It has existed for the better part of a century precisely because it's incredibly useful. And sure, the magic vomit in the example above is an ugly workaround, but when folks see issues like this go unaddressed for a decade, the thing they should be asking is: "Do I really want to tie myself to Realm?"

@ianpward
Copy link

ianpward commented Jul 7, 2022

Hi Product for Realm here - we realize how long this issue has been open, although perhaps that is a testament to just how long Realm has been a product and how many evolutions we've needed to go through over the years to get to this age. I will say that we do plan to do this because it would give us an incredibly unique position in the market, which, I would observe, there are no other cross-platform mobile databases that have Polymorphism/ Inheritance along with a backend database syncing solution, I believe this is evidence to how technically complex this feature is. We need to get this feature right across all of our SDKs + Sync + MongoDB.

@bdkjones
Copy link

bdkjones commented Jul 8, 2022

@ianpward Thanks for the comment. I think it would be very helpful/reassuring if Realm had an official roadmap and timetable for major features like this. Eight years of "it's on the to-do list" comments really make me nervous about tying apps to Realm.

@marchy
Copy link

marchy commented May 28, 2023

@bdkjones CC: @ianpward
Agreed I think Realm could really benefit from a public roadmap.

EF Core's roadmap is of particular influence. They've done a fantastic job of setting out the goals for the year ahead, putting out releases across the whole year, and calling out the things that won't make the cut for that year and re-prioritizing for the next major cycle.

I think Realm could benefit a lot from this strategy, and the entire feedback loop and trust-building that it creates.

@rursache
Copy link

rursache commented Jun 21, 2023

9 years and counting...

@dianaafanador3 any updates on this?

@davidgodzsak
Copy link

davidgodzsak commented Jan 16, 2024

I think either I use Realm wrong or the suggested workaround here is pretty terrible. So please point me to the right direction if I use this tool wrong.

I use SwiftUI and a FlexibleSync config with @AutoOpen. With this solution I need to first of all subscribe all of the child types (in the example all the payment methods), otherwise I cannot add new entities to the database. This does not sound so bad at first, but it means that:

  • I either need to mess up my data structures
  • Accept sub-optimal security and loads of unnecessary sync

Why?
I subscribe to the children classes in the realm, but cannot even specify query conditions to what to sync, (e.g. only get payment methods that correspond to a specific purchase), because no data is stored in the child class regarding this.

So I can either mess up my data modelling by adding redundant data to the children that I use for filtering, or sync on all data in each child's table (which is unnecessary, bad idea in scenarios when data transmission is expensive, and could even have security concerns - loading stuff into the local db and memory that do not belong to the user. e.g. I load every CreaditCardPaymentMethod stored in the DB for every user that opens the app... does not feel too safe).

Another flaw of this is that in the example we just create a new Realm() instance every time we want to resolve a value. With AutoOpen (and probable all other sane implementations) we need to pass in the realm instance to the value getter (of AnyPaymentMethod in this case).

If this is what is expected me to have polymorphism, then that's pretty bad and I cannot understand how this project could survive now almost 10 years with this ticket open.

EDIT:
I mean the data modeling is already messed up because I had to create separate mongodb collections for every class that is a child of a polymorphic parent type and do some weird linking through references... If I want to query this later with another service I need to write this obscure mapping and pollute the logic (even if that service is capable of polymorphism) this will also lead to an n+1 query problem...

Sooo messed up!

@kpolleck
Copy link

kpolleck commented Sep 1, 2024

Being just a little bit silly, should we plan a 10-year-old party for this request?

OK, OK...we are all in software development, and we know that there is a prioritization process for requirements--and it considers ROI, so difficult problems may not make the cut for a release.

I'm relatively new to using Realm and, in adding features to my application, I decided to refactor it to use subclasses that I want to store in Realm...thinking certainly that would be supported, no? After all, it's supported in CoreData. But, after a bit of refactoring and testing, I realized I should have researched that more in advance.

Thus, it seems to me that we need to make application architecture decisions based on what is available now (or maybe on a published roadmap), so I'm now rethinking my decision to use Realm at all!

@davidgodzsak
Copy link

welp, now they are deprecating Realm altogether :D

@jadar
Copy link

jadar commented Sep 9, 2024

@davidgodzsak Thanks for the heads up. To be fair, it sounds like the on-device SDKs will continue to exist. Just Realm Sync is deprecated and will be removed (which I never used, so that is a bit of complexity that can be deleted!) Maybe as open-source, we can get some better sync APIs that work with more technologies now!

@rursache
Copy link

rursache commented Sep 9, 2024

@jadar @davidgodzsak any link on Realm being deprecated?

@jadar
Copy link

jadar commented Sep 9, 2024

@rursache https://www.mongodb.com/docs/atlas/device-sdks/deprecation/#std-label-device-sdks-deprecation

@bdkjones
Copy link

bdkjones commented Sep 9, 2024

Realm Sync was the ENTIRE reason we chose Realm. Without the sync component, everyone is better served using Swift Data, the first-party solution that doesn't get suddenly terminated because some idiot MBA ran a spreadsheet.

@rursache
Copy link

rursache commented Sep 9, 2024

SwiftData is a joke compared to Realm and even CoreData which actually wraps

@bdkjones
Copy link

bdkjones commented Sep 9, 2024

@rursache You know what Core Data does NOT do? Randomly get nuked from orbit and cancelled. Call me crazy, but "continues to exist" is a pretty good feature for a product to have.

Realm is dead and irrelevant as of today. Zero reason to use it.

@marchy
Copy link

marchy commented Sep 10, 2024

@davidgodzsak Thanks for the heads up. To be fair, it sounds like the on-device SDKs will continue to exist. Just Realm Sync is deprecated and will be removed (which I never used, so that is a bit of complexity that can be deleted!) Maybe as open-source, we can get some better sync APIs that work with more technologies now!

Wow that is a pretty epic fail.

We chose to omit Realm Sync and built our entire sync layer by hand (both client-side and server-side) as to be able to have complete control over its many intricacies.

Bullet dodged on that one!

NOTE: We’ve since moved to GRDB over Realm as well due to the insane immutable-per-thread architecture Realm was built on, so wouldn’t have been an issue there either. But in both cases, the right answer ended up to go lower-level and not trust the black-box abstractions. At this point our ORM does the least possible thing - fetch records, not even relations between them, and we do all the object stitching ourselves. I wish it wasn’t this case, but reality just forced us otherwise.

@andrefmsilva
Copy link

@marchy

NOTE: We’ve since moved to GRDB over Realm as well due to the insane immutable-per-thread architecture Realm was built on, so wouldn’t have been an issue there either. But in both cases, the right answer ended up to go lower-level and not trust the black-box abstractions. At this point our ORM does the least possible thing - fetch records, not even relations between them, and we do all the object stitching ourselves. I wish it wasn’t this case, but reality just forced us otherwise.

Did you notice any performance or memory usage impact with GRDB?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests