question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Support updating/deleting with associations in a single step

See original GitHub issue

Issue Description

Note: I am creating this issue to centralize a topic that often appears in different issues.

Is your feature request related to a problem? Please describe.

Similarly to the possibility of running a single create call to create associated object together with the main object, users often want to update / delete an object with associated objects as well, such as in #7703, #9678 and https://github.com/RobinBuschmann/sequelize-typescript/issues/309. I am opening this issue to track all this in a single place.

Currently, our docs clarify that this is not possible.

In contrast, performing updates and deletions involving nested objects is currently not possible. For that, you will have to perform each separate action explicitly.

Describe the solution you’d like

I don’t have a clear concrete idea in mind. In fact, I think this is the biggest challenge for implementing this - to decide how exactly this will work. See this comment on #6808 and this comment on #5471.

Why should this be in Sequelize

This is a complex task that would be great to have covered by the ORM itself.

Describe alternatives/workarounds you’ve considered

The alternative/workaround is to do every create/update/delete by hand on a case-by-case basis…

Additional context

Very related issues: #6808 and #5471 Other related issues: #9199 #9233 #7703 #11699

Issue Template Checklist

Is this issue dialect-specific?

  • No. This issue is relevant to Sequelize as a whole.
  • Yes. This issue only applies to the following dialect(s): XXX, YYY, ZZZ

Would you be willing to resolve this issue by submitting a Pull Request?

  • Yes, I have the time and I know how to start.
  • Yes, I have the time but I don’t know how to start, I would need guidance.
  • No, I don’t have the time, although I believe I could do it if I had the time…
  • No, I don’t have the time and I wouldn’t even know how to start.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:95
  • Comments:16 (2 by maintainers)

github_iconTop GitHub Comments

33reactions
Nitemaericcommented, Jan 29, 2020

Hi @papb , I’m a long time Ruby on Rails developer and therefore I have some input regarding this comment: https://github.com/sequelize/sequelize/issues/6808#issuecomment-525883974

ActiveRecord (RoR’s ORM) goes by this rule of thumb:

  1. If the nested association parameters does not contain the primaryKey of that record, it will create a new record.
  2. If the nested association parameters contain the primaryKey of that record, it will update the record.
  3. If the nested association parameters contain both the primaryKey and the _destroy key has a truthy value, ActiveRecord will destroy the record.

Again with JSON examples:

Assuming all primary keys in this example are id.

  1. Create
{
  id: 1,
  subcollection: [
    {
      attributeA: 'new value for attributeA' // There is no primary key in this object
    }
  ]
}
  1. Update
{
  id: 1,
  subcollection: [
    {
      id: 2,
      attributeA: 'new value for attributeA on subcollection id = 2' // The primary key is present in this case
    }
  ]
}
  1. Destroy
{
  id: 1,
  subcollection: [
    {
      id: 2,
      attributeA: 'new value for attributeA on subcollection id = 2' // This is ignored
      _destroy: true // Primary key is present and the _destroy key is both present and truthy
    }
  ]
}

Hope it gives some direction towards bringing this functionality to Sequelize.

7reactions
filipesarturicommented, Dec 6, 2020

I implemented the fillAndSave method in my base class of my models to help me in these cases. So I can do things like:

await model.fillAndSave([
  id: 1,
  foo: 'bar',
  someAssociation: [
    {
      id: 2,
      name: 'existing object'
    },

    {
      name: 'new object'
    },
  ]
])

Model:

import Sequelize from 'sequelize'

function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase().concat(string.slice(1))
}

export default class Model extends Sequelize.Model {
  async fill(values) {
    const attributes = this.constructor.attributes
    const associations = this.constructor.associations
    const getFilledModelInstance = async ({ model, data }) => {
      let modelInstance = data

      if (!(modelInstance instanceof model)) modelInstance = new model()

      return modelInstance.fill(data)
    }

    if (this.associationsData === undefined) this.associationsData = {}

    for (const key in values) {
      const association = associations[key]
      const value = values[key]

      if (!association) {
        if (attributes[key] !== undefined) this.setDataValue(key, value)
        else this[key] = value
        continue
      }

      const model = association?.target

      this.associationsData[key] =
        Array.isArray(value) === true
          ? await Promise.all(
              value.map(data => getFilledModelInstance({ model, data }))
            )
          : await getFilledModelInstance({ model, data: value })
    }

    this.isNewRecord = this.getIsNewRecord()

    return this
  }

  getThroughModelsName() {
    const associations = this.constructor.associations
    const throughModelsName = []

    for (const associationName in associations) {
      const association = associations[associationName]

      if (association.throughModel !== undefined)
        throughModelsName.push(association.throughModel.name)
    }

    return throughModelsName
  }

  async deepSave({ parent, associationName } = {}) {
    let modelInstance = this

    const associationsData = modelInstance.associationsData ?? {}

    if (modelInstance.isNewRecord === true && parent) {
      const createMethodName = parent.constructor.getMethodName(
        'create',
        associationName,
        'singular'
      )
      const createValue = modelInstance?.toObject?.() ?? modelInstance
      const throughModelsName = parent.getThroughModelsName()

      let createOptions = {}

      throughModelsName.some(throughModelName => {
        const throughValue = modelInstance[throughModelName]

        if (throughValue !== undefined) {
          createOptions = {
            through: throughValue
          }

          return true
        }

        return false
      })

      modelInstance = await parent[createMethodName](createValue, createOptions)
    } else {
      await modelInstance.save()
    }

    for (const associationName in associationsData) {
      let value = associationsData[associationName]

      value =
        Array.isArray(value) === true
          ? await Promise.all(
              value.map(item =>
                item.deepSave({
                  parent: modelInstance,
                  associationName
                })
              )
            )
          : await value.deepSave({
              parent: modelInstance,
              associationName
            })

      const setMethodName = modelInstance.constructor.getMethodName(
        'set',
        associationName
      )

      await modelInstance?.[setMethodName]?.(value)
    }

    return modelInstance
  }

  async fillAndSave(values) {
    await this.fill(values)

    return this.deepSave()
  }

  getIsNewRecord() {
    const primaryKeyAttributes = this.constructor.primaryKeyAttributes ?? []

    if (primaryKeyAttributes.length === 0) return

    return primaryKeyAttributes.some(primaryKeyAttribute =>
      [undefined, null].includes(this[primaryKeyAttribute])
    )
  }

  static getMethodName(prefix = '', value = '', mode) {
    let methodName = value

    switch (mode) {
      case 'plural':
      case 'pluralize':
        const { plural: pluralize } = require('pluralize')

        methodName = pluralize(methodName)
        break

      case 'singular':
      case 'singularize':
        const { singular: singularize } = require('pluralize')

        methodName = singularize(methodName)
        break
    }

    methodName = capitalizeFirstLetter(methodName)
    methodName = `${prefix}${methodName}`

    return methodName
  }
}

I hope this helps in something … 😉🙏

Read more comments on GitHub >

github_iconTop Results From Across the Web

Updating a HABTM model associations only deletes the ...
Creating new HABTM associations work just fine for me, but each time I try to update them, all the associations get deleted. This...
Read more >
Delete an association - HubSpot API
Delete an association between 2 CRM objects. If you need to remove multiple associations, you can use the batch delete endpoint. Required ...
Read more >
How to implement insert, update, delete with a many-to-many ...
1st step - try to insert tags, one-by-one, ignoring duplication errors. After this all tags in array are present in a table. 2nd...
Read more >
Updating or deleting a custom patch baseline (console)
In the navigation pane, choose Patch Manager. -or- · Choose the patch baseline that you want to update or delete, and then do...
Read more >
Deleting a user account - GitLab Docs
Delete user and contributions to delete the user and their associated records. This option also removes all groups (and projects within these groups)...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found