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

Data Loss in Nested Objects When Using Spread Operator for Update in Realm JS #6758

Closed
tharwi opened this issue Jun 25, 2024 · 6 comments
Closed

Comments

@tharwi
Copy link

tharwi commented Jun 25, 2024

How frequently does the bug occur?

Always

Description

I am experiencing data loss in nested objects when updating an object in Realm JS using the spread operator. Specifically, after the update, one of my nested lists (trackingDataList) gets reset to an empty list.

Realm models

class TrackingData extends Realm.Object {
	id!: number;
	progress!: number;

	static schema: Realm.ObjectSchema = {
		name: 'TrackingData',
		primaryKey: 'id',
		properties: {
			id: 'int',
			progress: 'int',
		},
	};
}

export default class Profile extends Realm.Object {
	id!: string;
	attemptID!: number;
	lastAccessDate!: Date | null;
	trackingDataList!: TrackingData[];
	sortOrder!: number;
	from!: Date;
	userId!: string;

	static schema: Realm.ObjectSchema = {
		name: 'Profile',
		primaryKey: 'id',

		properties: {
			id: 'string',
			attemptID: 'int',
			lastAccessDate: {type: 'date', optional: true},
			trackingDataList: 'TrackingData[]',
			sortOrder: 'int',
			from: 'date',
			userId: 'string',
		},
	};
}

Code Sample

const updatedProfile = { ...currentProfile, lastAccessDate: Moment.utc().format() };

realm().write(() => {
  realm().create('Profile', updatedProfile, 'modified');
});

Before Update:

{
  "id": "12345_2_2",
  "attemptID": 2,
  "lastAccessDate": null,
  "trackingDataList": [
    {
      "id": 1,
      "progress": 50
    }
  ],
  "sortOrder": 1,
  "from": "2024-06-10T10:27:02.909Z",
  "userId": "12345"
}

After Update

{
  "id": "12345_2_2",
  "attemptID": 2,
  "lastAccessDate": "2024-06-25T16:26:43Z",
  "trackingDataList": [],
  "sortOrder": 1,
  "from": "2024-06-10T10:27:02.909Z",
  "userId": "12345"
}

Stacktrace & log output

N/A

Can you reproduce the bug?

Always

Reproduction Steps

  1. Create an object in Realm with nested objects/lists.
  2. Update the object using the spread operator and change a single key.
  3. Observe that some nested lists are reset or missing after the update.

Version

12.10.0

What services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

iOS 17.2

Build environment

react-native: 0.74.2
node: v18.17.1

Cocoapods version

1.15.2

Copy link

sync-by-unito bot commented Jun 25, 2024

➤ PM Bot commented:

Jira ticket: RJS-2847

@kneth
Copy link
Contributor

kneth commented Jul 1, 2024

@tharwi I am not able to reproduce. How does your code differs from #6774?

@sync-by-unito sync-by-unito bot added the Waiting-For-Reporter Waiting for more information from the reporter before we can proceed label Jul 1, 2024
@tharwi
Copy link
Author

tharwi commented Jul 1, 2024

Hi @kneth, thanks for the reply.

Can you change the below line

      const updatedProfile = { ...currentProfile, lastAccessDate: new Date() };

to this

const profile = realmInstance.objects('Profile')[0];
const updatedProfile = {...profile, lastAccessDate: new Date()};

@github-actions github-actions bot added Needs-Attention Reporter has responded. Review comment. and removed Waiting-For-Reporter Waiting for more information from the reporter before we can proceed labels Jul 1, 2024
@elle-j
Copy link
Contributor

elle-j commented Jul 3, 2024

@tharwi, the issue you're seeing is more related to what you're assigning rather than the spread operation itself. When you're using the spread operator it performs a shallow copy, so the trackingDataList on your object will still be the Realm.List from your currentProfile.

This means that when you pass that Realm.List to realm.create(.., .., UpdateMode.Modified), it will basically perform a self-assignment (e.g. object.list = object.list). We do not cache our collections, so even though you're using UpdateMode.Modified, we currently do not know if it's actually the same list being assigned in this case, and we need to clear the underlying collection being assigned to before adding the items of the RHS list (which is why the list becomes empty here).

This is of course something we'd like to fix as soon as possible and the initial work can be tracked here.

As an example, let's say you want to update the valueToUpdate property below:

class ListItem extends Realm.Object {
  value!: number;

  static schema: ObjectSchema = {
    name: "ListItem",
    properties: {
      value: "int",
    },
  };
}

class ObjectWithList extends Realm.Object {
  _id!: BSON.ObjectId;
  list!: Realm.List<ListItem>;
  valueToUpdate!: string;

  static schema: ObjectSchema = {
    name: "ObjectWithList",
    primaryKey: "_id",
    properties: {
      _id: "objectId",
      list: "ListItem[]",
      valueToUpdate: "string",
    },
  };
}

const realm = new Realm({ schema: [ObjectWithList, ListItem] });

const _id = new BSON.ObjectId();
const object = realm.write(() => {
  return realm.create(ObjectWithList, { _id, list: [{ value: 1 }], valueToUpdate: "original" });
});

expect(object.list.length).equals(1);

const objectShallowCopy = { ...object };

// Since it's a shallow copy, the list is still the Realm List.
expect(objectShallowCopy.list).to.be.instanceOf(Realm.List);

realm.write(() => {
  // Unfortunately, passing in the same Realm List again basically
  // performs a self-assignment, clearing the list.
  return realm.create(ObjectWithList, objectShallowCopy, UpdateMode.Modified);
});

// 💥 This will fail.
expect(object.list.length).equals(1);

Workaround:

From the code example you provided, it looks like you only want to update your lastAccessDate field. You can instead pass only the fields to be updated to realm.create() when using UpdateMode.Modified, rather than passing the spread.

realm.write(() => {
-  return realm.create(ObjectWithList, objectShallowCopy, UpdateMode.Modified);
+  return realm.create(ObjectWithList, { _id, valueToUpdate: "updated" }, UpdateMode.Modified);
});

Or skip realm.create() and update the field on the object directly:

 realm.write(() => {
-  return realm.create(ObjectWithList, objectShallowCopy, UpdateMode.Modified);
+  object.valueToUpdate = "updated";
});

@tharwi
Copy link
Author

tharwi commented Jul 9, 2024

Hi @elle-j , Thanks for the update. Will use one of the workaround for now.

@nirinchev
Copy link
Member

Closing since this is tracked in realm/realm-core#7422.

@nirinchev nirinchev closed this as not planned Won't fix, can't repro, duplicate, stale Jul 22, 2024
@sync-by-unito sync-by-unito bot removed the Needs-Attention Reporter has responded. Review comment. label Jul 22, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants