realm-js icon indicating copy to clipboard operation
realm-js copied to clipboard

Spread operation doesn't work

Open amereii opened this issue 4 years ago • 36 comments

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

  • Realm JS SDK Version: 5.0.3
  • Node or React Native: 10.15.3 - 0.62.2
  • Client OS & Version: ? Windows 10 10.0.18362
  • Which debugger for React Native: None

amereii avatar Apr 27 '20 12:04 amereii

+1

tomershohet avatar May 04 '20 13:05 tomershohet

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.

josephmbeveridge avatar May 05 '20 18:05 josephmbeveridge

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

plouh avatar May 05 '20 19:05 plouh

+1

vbhv19 avatar May 05 '20 23:05 vbhv19

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 avatar May 06 '20 09:05 blagoev

@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

blagoev avatar May 06 '20 09:05 blagoev

@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 :)

josephmbeveridge avatar May 06 '20 14:05 josephmbeveridge

@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

blagoev avatar May 06 '20 18:05 blagoev

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

josephmbeveridge avatar May 07 '20 15:05 josephmbeveridge

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.

plouh avatar May 08 '20 06:05 plouh

Is the problem still there?

forkeer avatar May 29 '20 11:05 forkeer

@forkeer Yes

josephmbeveridge avatar Jun 01 '20 15:06 josephmbeveridge

The same for me(

TuTejsy avatar Jun 02 '20 09:06 TuTejsy

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)

TPXP avatar Jun 21 '20 17:06 TPXP

Same thing on realm 6

filipef101 avatar Jul 09 '20 11:07 filipef101

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

amereii avatar Jul 13 '20 05:07 amereii

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

MrGVSV avatar Sep 22 '20 19:09 MrGVSV

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

olczake avatar Sep 29 '20 08:09 olczake

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 <mySchema>

Rob117 avatar Jun 13 '21 06:06 Rob117

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

kneth avatar Jun 18 '21 10:06 kneth

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

hellforever avatar Feb 08 '22 22:02 hellforever

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.

ital0 avatar Feb 09 '22 14:02 ital0

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

tomduncalf avatar Feb 09 '22 16:02 tomduncalf

@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

hellforever avatar Feb 10 '22 16:02 hellforever

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

ital0 avatar Feb 10 '22 17:02 ital0

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

tomduncalf avatar Feb 10 '22 17:02 tomduncalf

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 :(

ital0 avatar Feb 11 '22 18:02 ital0

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

tomduncalf avatar Feb 14 '22 10:02 tomduncalf

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

ital0 avatar Mar 30 '22 17:03 ital0

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

tomduncalf avatar Mar 31 '22 08:03 tomduncalf