Search code examples
javascriptmysqlnode.jssequelize.jses6-promise

Sequelize save/create method only works at first time on CRUD app


General issue:
I've been having a hard time trying to build a simple CRUD app using JavaScript + node JS + express + sequelize (MySQL).

Contextualization about the project
My CRUD app is being developed to manage students from a particular English teacher.
There are some table models created: Alunos - English: Students, Boletos - English: Pay Order, Aulas - English: Classes, and others that are not mandatory to explain for this issue for now.

Specific issue:
There is a post route that takes some of my body contents and inserts a new student to table "Alunos". After the row containing student data is created, I need to create registries for "Boletos", it would be 12 month of Pay Orders registered into this table. There are two problems with this part: the first I register a student, it works fine, but I could not get the auto-increment id generated by the model to insert in the foreign key "AlunoId", so the foreign key for "Boletos" table is set to null. The other problem is that the both entries (1 into "Alunos" and 12 into "Boletos") works fine at first, to registry the first student, but after the page is refreshed and I try to registry another student, node JS throws an error:

(node:5720) UnhandledPromiseRejectionWarning: SequelizeUniqueConstraintError: Validation error
    at Query.formatError (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\mysql\query.js:242:16)
    at Query.run (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\mysql\query.js:77:18)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\sequelize.js:619:16
    at async MySQLQueryInterface.insert (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\abstract\query-interface.js:749:21)
    at async model.save (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\model.js:3954:35)
    at async D:\ProgramacaoEstudos\ProjetoCRUD2\routes\admin.js:101:30
(node:5720) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)

Code:

  1. Models - Alunos:
// Create Aluno table model and export to be called to other file
module.exports = (sequelize, DataTypes) => {
  const Aluno = sequelize.define('Aluno', {
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    surname: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    birth: {
      type: DataTypes.DATEONLY,
      allowNull: false,
    },
    phone_number: {
      type: DataTypes.STRING,
      allowNull: true,
      unique: true,
    },
    mobile_number: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    residential_address: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    profession: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    company: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    num_classes_week: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    classes_day: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    classes_time: {
      type: DataTypes.TIME,
      allowNull: false,
    },
    starts_at: {
      type: DataTypes.DATEONLY,
      allowNull: false,
    },
    value: {
      type: DataTypes.FLOAT,
      allowNull: false,
    },
    discount: {
      type: DataTypes.FLOAT,
      defaultValue: 0,
    },
    due_date: {
      type: DataTypes.DATEONLY,
      allowNull: false,
    },
  });

  Aluno.associate = (models) => {
    Aluno.hasMany(models.Aula, {
      onDelete: 'CASCADE',
    });
  };

  Aluno.associate = (models) => {
    Aluno.hasMany(models.Boleto, {
      onDelete: 'NO ACTION',
    });
  };

  Aluno.associate = (models) => {
    Aluno.hasMany(models.Assiste_Aula, {
      onDelete: 'NO ACTION',
    });
  };

  return Aluno;
};

  1. Models - Boletos:
module.exports = (sequelize, DataTypes) => {
    const Boleto = sequelize.define('Boleto', {
        value: {
            type: DataTypes.FLOAT,
            allowNull: false
        },
        due_date: {
            type: DataTypes.DATEONLY,
            allowNull: false
        },
        status: {
            type: DataTypes.INTEGER,
            defaultValue: 0,
            allowNull: false
        }
    })

    Boleto.associate = (models) => {
        Boleto.belongsTo(models.Aluno, {
            foreignKey: 'AlunoId'  
        })
    }

    return Boleto
}

Routes - Post - registry:

// Post Routes
router.post('/realizado', async (req, res) => {
  // Create Aluno
  // Compute values of some attributes
  var cls_day = req.body.classes_day;
  var num_cls = cls_day.length;

  var dias;
  for (let i = 0; i < num_cls; i++) {
    if (i + 1 < num_cls) {
      dias += cls_day[i] + '-';
    } else {
      dias += cls_day[i];
    }
  }

  // Instantiate Aluno model
  const aluno = db.Aluno.build({
    name: req.body.name,
    surname: req.body.surname,
    birth: req.body.birth,
    phone_number: req.body.phone_number,
    mobile_number: req.body.mobile_number,
    email: req.body.email,
    residential_address: req.body.residential_address,
    profession: req.body.profession,
    company: req.body.company,
    num_classes_week: num_cls,
    classes_day: dias,
    classes_time: req.body.classes_time,
    starts_at: req.body.starts_at,
    value: req.body.value,
    discount: req.body.discount,
    due_date: req.body.due_date,
  });

  // Insert into database
  const newAluno = await aluno.save();

  // Create boleto
  var objList = [];
  for (var i = 0; i < 12; i++) {
    var firstDt = new Date(req.body.due_date);

    // Compute current date
    var dt = firstDt.setMonth(firstDt.getMonth() + i);
    //dt.setDate(dt.getMonth() + i)
    // Build boleto object
    var boleto;
    boleto = db.Boleto.build({
      value: req.body.value,
      due_date: dt,
      alunoId: newAluno.id,
    });

    objList.push(boleto);
  }

  for (var i = 0; i < objList.length; i++) {
    try {
      await objList[i].save();
    } catch (err) {
      console.log(err);
    }
  }
  res.render('realizado');
});

Final Considerations: Since I'm new with node JS and JavaScript I didn't know about the Promises and the syntactic sugar await/async. I've already studied with a bunch of videos and I think I got the basics concepts about it, but I'm not being able to apply it to the project.


Solution

  • You need to either use db.Aluno.create() or set db.Aluno.build({...}, { isNewRecord: true }) to let Sequelize know it's an insert and not a record with a primary key value of 0. Your DB likely sees the ID of 0 and either inserts it or sets it to 1, either way you will get a conflict on the second insert.

    It's also a good idea to wrap your router/controller code in a try/catch to handle any errors. Use a transaction that is passed to all your queries/inserts so that you can roll them all back if there is an error at any stage - const transaction = await sequelize.transaction(); const aluno = await db.Aluno.create({...}, { transaction });. Commit at the end with await transaction.commit() or in the catch block roll back with await transaction.rollback().

    Don't use await in a for loop - its the same as blocking for each call which is inefficient and slow. Instead you can pass an array of promises to Promise.all() and resolve them concurrently.... objList.push(boleto.save()); (don't await here) and then await Promise.all(objList);.

    Some last notes - it's good to use const when a variable won't change and let when it might to make your results more consistent. You should also try to have an explicit return from arrow functions.

    Here is your code with the changes applied.

    // Post Routes
    router.post('/realizado', async (req, res) => {
      // we may or may not be able to create a transaction, so use let
      let transaction;
      try {
        // start a new transaction an pass to all the create() calls
        transaction = await sequelize.transaction();
    
        // Compute values of some attributes
        var cls_day = req.body.classes_day;
        var num_cls = cls_day.length;
    
        var dias;
        for (let i = 0; i < num_cls; i++) {
          if (i + 1 < num_cls) {
            dias += cls_day[i] + '-';
          } else {
            dias += cls_day[i];
          }
        }
    
        // Create Aluno model, use const since it won't change
        const aluno = await db.Aluno.create(
          {
            name: req.body.name,
            surname: req.body.surname,
            birth: req.body.birth,
            phone_number: req.body.phone_number,
            mobile_number: req.body.mobile_number,
            email: req.body.email,
            residential_address: req.body.residential_address,
            profession: req.body.profession,
            company: req.body.company,
            num_classes_week: num_cls,
            classes_day: dias,
            classes_time: req.body.classes_time,
            starts_at: req.body.starts_at,
            value: req.body.value,
            discount: req.body.discount,
            due_date: req.body.due_date,
          },
          {
            // use the transaction
            transaction,
          }
        );
    
        // Create boleto insert promise array
        // Use const because we will be adding items, but into the same const array
        const promises = [];
        for (let i = 0; i < 12; i++) {
          const firstDt = new Date(req.body.due_date);
          // put the promise into the promises array
          promises.push(
            // the create call here will start the insert but not wait for it to complete
            db.Boleto.create(
              {
                value: req.body.value,
                due_date: firstDt.setMonth(firstDt.getMonth() + i),
                alunoId: aluno.id,
              },
              {
                // use the transaction so we can rollback if there are errors
                transaction,
              }
            )
          );
        }
    
        // await the result of all the boleto inserts
        await Promise.all(promises);
    
        // no errors, we can commit the transaction
        await transaction.commit();
    
        return res.render('realizado');
      } catch (err) {
        console.log(err);
        if (transaction) {
          await transaction.rollback;
        }
        return res.status(500).send(err.message);
      }
    });