2

I am trying to figure out how to correctly set this block of code up so that the commit function will wait until all of the rows are inserted. Currently I am reading a csv and need to insert a new row per column into a table. I also need to add a row into a parent table for that. I need all of that to finish before I call commit. I have yet to master callbacks so please be gentle.

db.beginTransaction(function (err) {
    if (err) 
    {
        //could not begin a transaction for some reason.
        logger.error("beginTransaction error: " +err);
    }
    //need to wrap this *************
    db.query("INSERT INTO TABLE VALUES ('" + unique_id + "','real_time','" + msg + "',1,1,LOCALTIMESTAMP)", function(err){
        if(err)
        {
            logger.error("error insert into parent table: "+err);
        }
    });

    for(var i = 0; i < headers.length; i++)
    {
        //replaces single quote (') with two single quotes to escape it ('')
        values[i] = values[i].replace("'","''");
        db.query("INSERT INTO TABLE VALUES ('" + unique_id + "','" + headers[i] + "',0,'" + values[i] + "')", function(err){
            if(err)
            {
                logger.error("error insert into child table: "+err);
            }
        });
    }
    //To here ************
    db.commitTransaction(function (err) {
        if (err) 
        {
            //error during commit
            logger.error("Commit error: "+err);
        }
    }); //end of commitTransaction
    callback();
});//End of beginTransaction
Crash667
  • 343
  • 2
  • 16
  • **WARNING**: Don't manually escape things, *especially* not with a completely inadequate hack like that `replace` call. Use prepared statements with placeholder values. Most drivers support these, but you can also use [Sequelize](http://docs.sequelizejs.com) as a more abstract adapter. With that you can use Promise-driven code or async/await to avoid a lot of the callback mess if you prefer. – tadman Jan 12 '18 at 18:44
  • @tadman I will look that up, I am still fairly new to node – Crash667 Jan 12 '18 at 18:46
  • If you want to wait until all the previous callbacks have been fired then you need to nest them. Right now this queues up a bunch of queries, then commits the transaction before they've even started. Don't worry if this is confusing. Asynchronous code can be tricky, that's why `async`/`await` can help. If you want block of code A to wait until after B is done then you *must* call A inside B's callback. – tadman Jan 12 '18 at 18:46
  • @tadman well another hack I had done was use the sync versions of the functions which not what I want. I need all those queries to be able to fire at the same time for performance reasons. Won't nesting them make it like using the sync commands? – Crash667 Jan 12 '18 at 18:48
  • You really do not want to use synchronous commands in Node. That freezes the whole JavaScript runtime until that call completes. For certain things, like reading config files on boot, or within a small, self-contained script that's not so damaging but in a production application that can absolutely kill performance. – tadman Jan 12 '18 at 18:50
  • @tadman so you are saying that nesting them would not freeze the whole javascript runtime but rather it would just wait until the previous finishes? Allowing other commands to run? – Crash667 Jan 12 '18 at 18:52
  • 1
    A sync call blocks your process, while an async one relinquishes control until the result it's looking for comes back or it generates a timeout error. – tadman Jan 12 '18 at 19:03

3 Answers3

2

There's three basic ways of tackling this synchronization problem which I'll demonstrate here using new style arrow functions. The traditional Node way is with callbacks:

a((err, resultA) => {
  // Fires when A is done or errored out

  if (err) {
    // Log, panic, etc.
    return;
  }

  b((err, resultB) => {
    // Fires when A and B are done or A is done and B errored out

    if (err) {
      // Log, panic, etc.
      return;
    }

    c((err, resultC) => {
     // Fires when A, B and C are done or A and B are done and C errored out

      if (err) {
        // Log, panic, etc.
        return;
      }
    });
  });
});

This is what people refer to as "callback hell" because the nesting and error propagation code gets more and more ridiculous as your dependencies grow in complexity. I find this style unsustainable for any non-trivial application.

The next style is Promise-driven:

a().then(resultA => {
  // Fires when A is done
  return b();
}).then(resultB => {
  // Fires when B is done
  return c();
}).then(resultC => {
  // Fires when C is done
}).catch(err => {
  // Fires if any of the previous calls produce an error
});

This tends to be a lot "flatter" and easier to follow, but it's still a lot of heavy syntax for what should be simple. The newer async/await style builds on promises by adding support for them to the JavaScript syntax:

try {
  let resultA = await a();
  let resultB = await a();
  let resultC = await a();
} catch(err) {
  // Fires when any error occurs
}

This works inside any function tagged async like:

async function runQueries() {
  // async code
}

Which can make your life a lot easier. You can also use conventional try/catch notation for errors, they're propagated accordingly.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • 1
    That does look a lot cleaner. I will research these libraries along with promises in order to better understand node. This is my first node program that is not just an express server for REST endpoints. – Crash667 Jan 12 '18 at 19:20
1

As tadman states, it's very important that you don't manually escape values and use parameterized queries instead. Make sure you fix this first.

Unfortunately it doesn't look like node-odbc supports promises. You may be able to get it to work with something like Bluebird.promisify. Currently what you want is to keep track of how many successful inserts were completed and then commit the transaction if they all complete successfully.

let successfulInsertions = 0;
let insertionAttempts = 0;
for(var i = 0; i < headers.length; i++) {
    db.query("INSERT INTO TABLE VALUES (?, ?, ?, ?)", params, err => {
        if (err) {
            logger.error("error insert into child table: "+err);
        }
        else {
          successfulInsertions++;
        }
        insertionAttempts++;

        if (insertionAttempts === headers.length) {
            if (successfulInsertions === insertionAttempts) {
                db.commitTransaction();
            }
            else {
                db.rollbackTransaction();
            }
            callback();
        }
    });
}

There are libraries out there that will help with this but they would require rewriting your code a bit. If you can use Bluebird's promisifyAll on the node-odbc library I would rewrite it using async/await (this is still asynchronous and functions the same):

await db.beginTransactionAsync();
try {
  await db.queryAsync("INSERT INTO TABLE VALUES (?, ?, ?, ?)", params);

  await Promise.all(headers.map((header, i) =>
    db.queryAsync("INSERT INTO TABLE VALUES (?, ?, ?, ?)", [unique_id, header, 0, values[i])
  );
  await db.commitTransactionAsync();
} catch (err) {
  logger.error(err);
  db.rollbackTransaction();
}

Note that if beginTransaction throws an error for some reason this code will throw an error.

Explosion Pills
  • 188,624
  • 52
  • 326
  • 405
  • I appreciate this answer. This is just going to be a hack fix for now until we rewrite the process using a message queuing and microservices. Also I do need to learn promises. – Crash667 Jan 12 '18 at 19:20
0

I don't really understand your code, why don't you return or stop your code if there is an error?

Here for example, you should have your code like this

db.beginTransaction(function (err) {
    if (err) 
        return logger.error("beginTransaction error: " +err), db.rollback(/*...*/)
    //....
}

Second, you should change your whole code with the async/await syntax.

async function foo () {
    await new Promise((next, err)=> {
        db.beginTransaction(e => e ? err(e) : next())
    })

    await new Promise((next, err)=> {
        db.query(`INSERT INTO TABLE VALUES ('${unique_id}','real_time','${msg}',1,1,LOCALTIMESTAMP)`, e => e ? err(e) : next())
    })

    for (var i = 0; i < headers.length; i++) {
        await new Promise((next, err)=> {
            db.query(`INSERT INTO TABLE VALUES ('${unique_id}','${headers[i]}',0,'${values[i]}')`, e => e ? err(e) : next())
        })
    }

    await new Promise((next, err)=> {
        db.commitTransaction(e => e ? err(e) : next())
    })
}

foo()
.then(()=> callback())
.catch(e=> logger.error(`Error: ${e}`))
Fernando Carvajal
  • 1,869
  • 20
  • 19
  • I actually do have the rollback function in my code now but I don't do this on a beginTransaction error. If beginTransaction has an error, what am I rolling back? I wrote this before ever learning about promises and the in's and out's of node js. I will be looking into learning resources for node. I do agree that async/await is a good way to go I just need the basics first. – Crash667 Jan 24 '18 at 18:03
  • The basic of **async/await** is the Promise syntax, you should learn how to use them first, then you'll realize that when a **async function** return, it is just a **Promise.resolve()**, and any error being throw inside it or any Promise.reject() will be catched in the **async function** .catch() – Fernando Carvajal Jan 24 '18 at 20:01
  • Thanks for the direction. I will take some time to learn those as they are more useful/powerful for readable callbacks. – Crash667 Jan 24 '18 at 20:16