realm / realm-js

Realm is a mobile database: an alternative to SQLite & key-value stores
https://realm.io
Apache License 2.0
5.62k stars 558 forks source link

Spread operation doesn't work #2844

Closed amereii closed 1 year ago

amereii commented 4 years ago

Goals

Spread operation works on older versions , After upgrading from 3.4.1 to 5.0.3 , Spread operation not works

Expected Results

Spread operation works on older versions , I passed ream object as props to react component by spreading it

<MyComponent {...realmObject}>

Actual Results

Speading creates empty object

Steps to Reproduce

const CarSchema = {
  name: 'Car',
  properties: {
    make:  'string',
    model: 'string',
    miles: {type: 'int', default: 0},
  }
};

Realm.open({schema: [CarSchema]})
  .then(realm => {
    // Create Realm objects and write to local storage
    realm.write(() => {
      const myCar = realm.create('Car', {
        make: 'Honda',
        model: 'Civic',
        miles: 1000,
      });
      myCar.miles += 20; // Update a property value
    });

    // Query Realm for all cars with a high mileage
    const car = realm.objects('Car')[0];

   const spread = {...car};
  console.log(spread)

    // Remember to close the realm when finished.
    realm.close();
  })
  .catch(error => {
    console.log(error);
  });

Code Sample

Version of Realm and Tooling

tomershohet commented 4 years ago

+1

josephmbeveridge commented 4 years ago

This bug is stopping us from being able to use v5 of Realm as spreading is extremely common. We can't guarantee someone down the line won't try to spread one of the results we got, and when they do it will inexplicably be an empty object.

The only workaround I see is to manually copy each property out of the result into a new object. However that requires reading every single result into memory, breaking the lazy loading of Realm that is so valuable. It also adds a ton of extra computations and specific code for each object type to grab the properties.

plouh commented 4 years ago

This works on 3.7.0-beta.2 but was already broken on first 5.0.0 -release

vbhv19 commented 4 years ago

+1

blagoev commented 4 years ago

Thanks for reporting this.

My investigation shows that this was not working in nodejs in realm 3. I assume you have it working in React Native with Realm 3. This was either a bug in our nodejs implementation for realm 3 or it was purposely done since all Realm Object properties in nodejs were defined as not enumerable.

In Realm 5 all properties of the Realm.Objects are defined as accessor properties on the prototype object and are not own properties of the instance . That's why spread operator is not working for these objects. This is a breaking change and is one of the requirements to be able to implement Realm with N-API

If you need to clone realm objects I would suggest doing something like this

const Realm = require('realm');
Object.defineProperty(Realm.Object.prototype, "clone", {
    value: function () {
        let result = {};
        for (const key in this) {
            result[key] = this[key];
        }

        return result;
    },
    writable: true,
    configurable: true,
    enumerable: false
});

and using it like so

<MyComponent {...realmObject.clone()}>

Since we already provide realmObject.keys() and realmObject.entries() for every Realm Object in Realm 5. We could provide this method as well so its available by default.

cheers

blagoev commented 4 years ago

@josephmbeveridge Could you elaborate more on the laziness part.

In particular this

However that requires reading every single result into memory, breaking the lazy loading of Realm that is so valuable.

I don't think you can use spread operator to retain the lazy properties of realm onto the cloned object. Or I am getting this wrong.

I would like to understand more why you can't migrate to Realm 5.

cheers

josephmbeveridge commented 4 years ago

@blagoev Since we don't want objects that can't be spread as they are dangerous in the rest of our app, we need to convert every single result from realm into a proper js object that follows the normally expected spreading rules. This requires reading each item from the result one at a time, forcing it all into memory, and converting it to an object with properties instead of accessors on the prototype.

If the results worked with spread already, we wouldn't have to perform any operations on them and could just pass them on. This lets us keep the lazy loading, and avoid reading objects until we actually need them.

To add on to this more, the rest of our app shouldn't have to be aware of results coming from Realm and having to perform different operations to get the data out of the object than we would for remote results. This breaks our data abstraction layer and makes development much more complicated and error-prone.

Thanks for taking the time to look into this btw :)

blagoev commented 4 years ago

@josephmbeveridge Thanks for the clarification.

Realm Objects are also proper js object that follows the normally expected spreading rules. The difference is they don't define own value properties but accessor properties in Realm 5.

What you describe is reasonable. If you control the spreading code as well you could make it work. The idea of passing realm objects around is still working, but you need to change the code responsible for spreading the object to {...realmObject.clone()} to achieve the desired behavior.

Previously Realm Objects were behaving as magic objects. both like JS proxies and defining own value properties. This is not possible with N-API in its current form for Realm.

Another option might be to proxify the objects just before passing them to the code responsible for spreading. You could do that also for collections and retrieve objects only when needed. This might as well not be feasible for your case.

let realmObject = realm.create(.....

const handler = {
  ownKeys(target) {
    return target.keys()
  }
};

const obj = new Proxy(realmObject, handler);
var spread = {...obj}

cheers

josephmbeveridge commented 4 years ago

@blagoev Thanks for the response.

Unfortunately having code above the database layer that knows we are using realm and does specific operations because we are using realm isn't going to work for us. We are intentionally abstracting the data layer to provide better separation of logic and hide implementation details. This not only protects us from changes in the database layer, but lets us use more than one data source for the same UI and business logic code, without them knowing where the data comes from. This is a fairly common pattern in large scale applications and one we use often, especially when we have to pull data from multiple sources (local db, remote server, etc) depending on the situation. This pattern of abstracting where the data comes from means that we cannot run an operation to extract data for only objects that came from realm (unless we do it very early and for all of the results, defeating the purpose of lazy loading as I was mentioning before) because we don't actually know where a given object came from (intentionally).

Of course our implementation isn't your problem, but I think this will be a fairly common issue for larger code bases that want to properly insulate their data layer.

plouh commented 4 years ago

The problem is also with external libraries like monocle.ts that previously managed to copy all the properties from realm objects but fail with Realm 5.x

Cloning before modifying would work but finding all the places where this happens is tedious work in a large RN application.

Maybe a lazy proxy over a Realm Collection that would do the clone once objects are accessed would be the best solution here.

forkeer commented 3 years ago

Is the problem still there?

josephmbeveridge commented 3 years ago

@forkeer Yes

TuTejsy commented 3 years ago

The same for me(

TPXP commented 3 years ago

Same issue here, code that worked on Realm v2.10.0 is now broken on Realm v6.0.2. Besides passing all properties at once, another use case is to use a local override while moving objects on the screen for example:

<Component position={{...realmData.position, ..this.state.positionOverride}} />

As a workaround, Object.assign can be used for this

<Component position={Object.assign(realmData.position, this.state.positionOverride)} />

Seems like the bug is located in the hasOwnProperty method for Realm objects:

for (let a in realmObject)
  console.log(a, realmObject.hasOwnProperty(a));

Will output:

prop1 false
prop2 false
....

In case it helps, the project I'm having this issue with is a React-Native app (RN v0.62.2) for Android (did not test on iOS yet)

filipef101 commented 3 years ago

Same thing on realm 6

amereii commented 3 years ago

@kneth @blagoev @fealebenpae take a look please!!

MrGVSV commented 3 years ago

I have the same issue on Realm 6.1.0. Are there plans to resolve this?

olczake commented 3 years ago

@blagoev The .clone() method works. Instead of spreading the realm object I spread the cloned realm object and it works like the older verion I used (4.0.0-beta.2).

Are there any significant drawbacks in using clone?

Rob117 commented 2 years ago

Typescript is misleading if you use clone. There is nothing to suggest anywhere that the Realm Object returned would not support the spread operator to clone like it would in a normal object. This means that, as others have mentioned, unless someone on the project tells you specifically that Realm does not support spread operators anymore, you have no way of knowing that it won't copy properties defined in the schema to a new object.

This is understandable when the implementation is explained, but it is unintuitive and strange behavior for an object to have. If I have an array of objects retrieved from the database that I have fed into a form, I need to

1) Retrieve the objects from Realm. No problem. 2a) Use the specific clone operator on each object before returning it from my database management layer (meaning if the method retrieving the obejcts doesn't edit them, this is a ton of wasted operations because I am cloning each object in a query) or 2b) need to have all the code written outside of the DB layer call clone() at the appropriate time, which breaks encapusulation of DB logic and requires that you ignore typescript support because nothing in TS will tell you that spread doesn't work on a DB realm object even though each property of that object is normally accessible.

For example, it is really unintuitive that intellisense and Typescript both say that

realmResultObj.propertyA // works as expected
{ ...realmResultObj }.propertyA // undefined, this is not intuitive at all

Not being able to support the spread operator is what it is, but is it at least possible to change typescript return values so it doesn't return types of <mySchema & Realm.Object> because it doesn't actually fulfill all of the requirements of an object instantiated of type

kneth commented 2 years ago

@Rob117 You might want to follow #3805 where we are investigating different approaches to enable the spread operator again.

hellforever commented 2 years ago

What is the progress of adding spread operator support for realm objects? Is there any workaround for now?

ital0 commented 2 years ago

What is the progress of adding spread operator support for realm objects? Is there any workaround for now?

@hellforever well I'm using realm 10.12.0 and when I use models.objects(modelName).toJSON() is solving my problem by now. This method is parsing the Realm Object to JS Object and I'm able to use spread operator. Also I couldn't found any reference at RealmJS API Docs.

Captura de Tela 2022-02-09 às 11 57 47

I don't know what could happen in a huge amount of data.

tomduncalf commented 2 years ago

Hi @ital0, I believe this is an oversight in our documentation, sorry about that. I will create a ticket for us to update the docs to add toJSON

hellforever commented 2 years ago

@ital0 Thanks for your answer. But toJSON() may have an impact on performance. In case we need to do some calculation, override some properties of a realm object, or add new properties to a realm object. Do you know any better ways to do it? If someone can give some advice, I will appreciate it so much

ital0 commented 2 years ago

@hellforever In my case I have all the Realm access abstracted in a separated layer. After getting the data I do all the rest (override or add some properties, do some calculations). In my tests doing toJSON in 100k data I didn't feel any performance issues.

tomduncalf commented 2 years ago

Just to add that we are planning to add spread operator support, but we need to ensure we do so in a way which doesn't significantly impact normal performance, so this is an going work in active progress.

It's good to hear that the toJSON workaround doesn't seem to impact real world performance too much for now

ital0 commented 2 years ago

Back here just to give feedback about toJSON method and performance. After doing more testing I can confirm that when you are managing a large amount of data the performance drops considerably. Probably I'll have to wait spread operator support :(

tomduncalf commented 2 years ago

@ital0 Sorry to hear that. We will update this issue with our progress on the spread operator.

ital0 commented 2 years ago

@tomduncalf any news about allowing spread operators on Realm Objects? I'm already following #3805 but I didn't receive any news anymore.

tomduncalf commented 2 years ago

No update yet I'm afraid. We'll update the ticket when we do get to look at it.

Waltari10 commented 2 years ago

If people are interested I published to npm an eslint-plugin that bans spread on Realm.Object type. https://www.npmjs.com/package/@waltari/eslint-plugin-ban-realm-spread

GitMurf commented 2 years ago

Does anyone know what the "opposite" of toJSON() is? In other words, I want to convert a Realm Object toJSON() so that I can stringify it and then I want to later JSON.parse() this stringified value and then take the parsed JSON object and turn it back into a Realm Object.

Or is this not possible to "convert back" to a Realm Object once you have converted it to JSON with toJSON() method?

kneth commented 2 years ago

@GitMurf realm.create("myClass", JSON.parse(someRealmObjectAsJson))) can do it for you.

GitMurf commented 2 years ago

@GitMurf realm.create("myClass", JSON.parse(someRealmObjectAsJson))) can do it for you.

@kneth Thank you for the quick reply! Sorry I wasn't very clear on my ask. I don't want to create a new realm object but instead want to "re-build" / "re-construct" the original Realm object (in JS memory) but not actually create a new item in Realm. Essentially I want to serialize in theory with toJSON() and then de-serialize back to its original Realm object state (outside of Realm, just in regular JS memory). Does that make sense? I am fairly new to Realm so please let me know if I don't quite understand something correctly with how Realm objects work. Thanks!

GitMurf commented 2 years ago

@kneth to help further clarify off my previous comment, this is a high level summary of what I want to do:

1) Query Realm for ObjectABC setting it to JS variable myRealmObject 2) Now I have ObjectABC Realm Object stored as myRealmObject 3) let myObjJson = myRealmObject.toJSON(); to create a JS JSON version of the object. 4) Now do some stuff with my other code utilizing this myObjJson variable (potentially have to pass it in a more serialized state hence using toJSON()) 5) Now I am done with my other work and want to get myObjJson back to myRealmObject. If this was just plain JS it would be comparable to after doing a stringify, I want to get back to my JS JSON object by using JSON.parse(myObjectString).

So I guess ultimately I am looking for how to take my myObjJson variable and doing a comparable operation as JSON.parse() to get my variable back to the original myRealmObject.

Does that make sense? Thanks!

kneth commented 2 years ago

@GitMurf

Does that make sense?

Yes, I think it does. If myRealmObject has a primary key, you can use realm.create("myClass", myObjJson, "modified"). The third parameter ("modified") will let create() to look up by primary key and update the object in the database.

thaynarbo commented 1 year ago

In our application, we're using .toJSON a lot because we work with immutability and so we copy the data by using js spreading operation and when the spreading takes place the data is not available anymore as people were discussing above, toJSON solves that problem. I wasn't seeing any problem with using toJSON for that matter, but after upgrading Realm to a newer version, I noticed that the type is returning something different -> toJSON(): Record<string, unknown>. Is there a way I could type it without having to force the type? The cast doesn't work here because they are a lot different.

image

from Realm types folder image

current realm version: 11.0.0 current react native version: 0.70.4 current typescript version: 4.8.3

takameyer commented 1 year ago

@thaynarbo That does look like a great improvement. Can you create a feature request for this? Then we can get it on our radar.

kneth commented 1 year ago

I am closing the issue as the spread operator works with v12.0.0-alpha.1. It will take some time to stabilise the current development before we release v12.0.0 but you are welcome to try our prereleases.