How to efficiently replace values for id fields in an object that was cloned?

Viewed 208

Suppose I have two records in a table:

[
    {
        id: 1,
        name: 'Michael',
        associations: [
            {
                from: 1,
                to: 2
            }
        ]
    },
    {
        id: 2,
        name: 'John',
        associations: [
            {
                from: 2,
                to: 1
            }
        ]
    },
]

If I clone these two objects, I need to end up with the following:

[
    {
        id: 1,
        name: 'Michael',
        associations: [
            {
                from: 1,
                to: 2
            }
        ]
    },
    {
        id: 2,
        name: 'John',
        associations: [
            {
                from: 2,
                to: 1
            }
        ]
    },
    {
        id: 3,
        name: 'Michael',
        associations: [
            {
                from: 3,
                to: 4
            }
        ]
    },
    {
        id: 4,
        name: 'John',
        associations: [
            {
                from: 4,
                to: 3
            }
        ]
    },
]

How can I effectively achieve this?

What I am currently doing is cycling through all records and running an INSERT for each. For each element, I track its oldId and newId and afterwards, I query for all records that have one of these newIds. Then, I replace the values. Obviously, this is fine for two records, but if I have thousands of records that need to be cloned, each containing thousands of associations, this can hit performance very hard.

Here is the code. The function cloneElements is the initiator.

const updateElementAssociationsById = async ({ id, associations }) => {
    return dbQuery(id, associations); // UPDATE ... SET associations WHERE id
}

const handleUpdateAssociations = async ({ data }) => {
    const promises = [];
    const records = await dbQuery(data.map(({ newId }) => newId)) ; // SELECT * FROM ... WHERE id in ANY 

    records.forEach(({ id, ...record }) => {
        const associations = [];

        record.associations.forEach((association) => {
            const sourceElement = data.find(({ oldId }) => oldId === association.from);
            const targetElement = data.find(({ oldId }) => oldId === association.to)

            association.from = sourceElement.newId;
            association.to = targetElement.newId;

            associations.push(association);
        });

        promises.push(updateElementAssociationsById({ id, associations }))
    });

    await Promise.all(promises);
}

const createElement = async ({ element, data }) => {
    const newElement = await dbQuery(element); // INSERT INTO ... RETURNING *;

    data.push({
        oldId: element.id,
        newId: newElement.id,
    });   
}

const cloneElements = async (records) => {
    const promises = [];
    const data = [];

    records.forEach((element) => {
        promises.push(createElement({ element, records, data }));
    });

    await Promise.all(promises);

    await handleUpdateAssociations({ data });
}
3 Answers

Assuming you won't duplicate enough in a lifetime to hit the int constraint.

The maximum number of available ids given the int constraint is +2147483627 , which is approximately 2.1B records.

If we don't re-use ids, we can skip querying the database for that new id, because the new id is just an offset of the previous one.

async function cloneElements(records) {
  // get highest unique ID
  // assuming the records are ordered by id, you can just get the last element of the array.
  const highestID = records[records.length-1].id;

  // offset everything.
  const newRecords = records.map(record => ({
    id: record.id + highestID,
    ...record, // "name" key
    associations: record.associations.map(asso => ({
      from: asso.from + highestID,
      to: asso.to + highestID,
    }))
  }));
  
  await Promise.all(
    newRecords.map(newRecord => dbQuery(newRecord))
  );
}

There is a more optimized way to get the offset, especially if you intend to clone only a part of the saved records, see: https://www.postgresql.org/message-id/DF9C8135-DC67-11D7-B235-000A959C9730@starlett.lv


Edit: It feels strange that you have range on ids, on your database design. I think you can also solve that "cloning a list of entities" purely with SQL (Insert into ... select from with an increment). Also, you actually don't need to set the ID in JS, if you create them in the right order, the generated id should be correct.

For cloning, You just need to do like below,

 let arrayFormat = [
    {
        id: 1,
        name: 'Michael',
        associations: [
            {
                from: 1,
                to: 2
            }
        ]
    },
    {
        id: 2,
        name: 'John',
        associations: [
            {
                from: 2,
                to: 1
            }
        ]
    },
];

let arrayLen = arrayFormat.length
let tempArr = JSON.parse(JSON.stringify(arrayFormat))
let id = arrayFormat.length
tempArr.map((arr, index) => {
  if(index === arrayLen) {
   return false
  } 

  id += 1

  arr.id = id
    if(arr.id % 2 === 0) {
        arr.associations[0].from = arr.id
        arr.associations[0].to = arr.id - 1
    } else {
        arr.associations[0].from = arr.id
        arr.associations[0].to = arr.id + 1
    }
    

    arrayFormat.push(arr)
})

console.log(JSON.stringify(arrayFormat))

I would do this by creating an interim lookup table. In js it could look like this:

const lookup = new Map();

In Postgres it could look like this:

CREATE TEMPORARY TABLE IF NOT EXISTS lookup(old_id int NOT NULL, new_id int NOT NULL);

Then when you clone, you create this association:

lookup.set(old_id, new_id);

and / or:

INSERT INTO lookup(old_id,new_id) values($new_id,$old_id);

Then, I would use a recursive idempotent function to get, or clone-and-get records as needed, giving us O(1) on each non-virgin lookup:

const lookup = new Map();
const getOrClone = (oldId) => {
    let rowPromise = lookup.get(oldId);
    if (!rowPromise) {
        const getOrCloneImpl = async () => {
            let oldRow = await db.sql(`SELECT * FROM Elements WHERE id = ${oldId}`);
            const row = await db.sql(`INSERT INTO Elements ...stuff from OldRow`);
            // @note: row now contains a new unique ID generated by Postgres

            if ("associations" in row) {
                // call self recursively to update "association" IDs
                let newAssociations = row.associations.map(tuple => {
                    const fromRow = await getOrClone(tuple.from);
                    const toRow = await = getOrClone(tuple.to);
                    return {
                      from: fromRow.id,
                      to: toRow.id
                    };
                });
                row.associations = await Promise.all(newAssociations);
            }
            return row;
        };
        rowPromise = getOrCloneImpl();
        lookup.set(oldId, rowPromise);
    }
    return rowPromise;
};

So that the final solution might look something like this:

const cloneElements = async (recordIds) => {
    const promises = recordIds.map(getOrClone);
    await Promise.all(promises);
}
Related