4

After reading https://stackoverflow.com/a/14797359/4158593 : about nodejs single thread and that it takes the first parameter of async function, processes it and then uses the callback to respond when everything is ready. What confused me is what if I had multiple queries that need to be excused all at once and tell nodeJS to block other requests by adding them in a queue.

To do that I realised that I need to wrap my queries in another callback. And promises do that pretty well.

    const psqlClient = psqlPool.connect(); 
    return psqlClient.query(`SELECT username FROM usernames WHERE username=$1`, ['me'])
    .then((data) => {
        if(!data.rows[0].username) {
           psqlClient.query(`INSERT INTO usernames (username) VALUES ('me')`);
        }
       else { ... } 
 });

This code is used during sign up, to check if username isn't taken before inserting. So it very important that nodejs puts other requests into a queue, and makes sure to select and insert at the same time. Because this code might allow people with the same username sent at the same time to select a username that has been already been taken, therefore two usernames will be inserted.

Questions

  1. Does the code above executes queries all at once?

  2. If 1 is correct, if I was to change the code like this

    const psqlClient = psqlPool.connect(); 
    return psqlClient.query(`SELECT username FROM usernames WHERE username=$1`, ['me'], function(err, reply) { 
    if(!reply.rows[0].username) {
    psqlClient.query(`INSERT INTO usernames (username) VALUES ('me')`);
    }
    });
    

    would that effect the behaviour?

  3. If 1 is wrong, how should this be solved? I am going to need this pattern (mainly using select and insert/update one after another) for things like making sure that my XML sitemaps don't contain more than 50000 urls by storing the count for each file in my db which happens dynamically.

Community
  • 1
  • 1
aloha_mate
  • 223
  • 6
  • 14

2 Answers2

2

The only thing that can guarantee data integrity in your case is a single SELECT->INSERT query, which was discussed here many times.

Some examples:

You should be able to find more of that here ;)


I also touched on this subject in a SELECT ⇒ INSERT example within pg-promise.


There is however an alternative, to make any repeated insert generate a conflict, in which case you can re-run your select to get the new record. But it is not always a suitable solution.

Community
  • 1
  • 1
vitaly-t
  • 20,421
  • 5
  • 85
  • 117
  • Thanks, so callbacks and promises don't help in my case, there might be still some collisions? – aloha_mate Oct 15 '16 at 20:59
  • 1
    @aloha_mate Yes, it can only be solved by the server-side. No matter what trick you pull within Node.js, it will never guarantee the data integrity. – vitaly-t Oct 15 '16 at 21:05
  • @aloha_mate apart from the extra solution I just added, relying on insert conflicts. – vitaly-t Oct 15 '16 at 22:55
  • cool thanks, yeah I considered it. Using `UNIQUE` key and `ON CONFLICT` is also a solution, but would require to re-run the query :( So I think select + insert in one query is beast! Thank you :D – aloha_mate Oct 16 '16 at 11:19
  • @aloha_mate A solution to what? Certainly not the question that you published. – vitaly-t Oct 19 '16 at 05:32
0

Here's a reference from the creator of node-postgres: https://github.com/brianc/node-postgres/issues/83#issuecomment-212657287. Basically queries are queued, but don't rely on them in production where you have many requests....

However you can use BEGIN and COMIT

var Client = require('pg').Client;

var client = new Client(/*your connection info goes here*/);
client.connect();

var rollback = function(client) {
  //terminating a client connection will
  //automatically rollback any uncommitted transactions
  //so while it's not technically mandatory to call
  //ROLLBACK it is cleaner and more correct
  client.query('ROLLBACK', function() {
    client.end();
  });
};

client.query('BEGIN', function(err, result) {
  if(err) return rollback(client);
  client.query('INSERT INTO account(money) VALUES(100) WHERE id = $1', [1], function(err, result) {
    if(err) return rollback(client);
    client.query('INSERT INTO account(money) VALUES(-100) WHERE id = $1', [2], function(err, result) {
      if(err) return rollback(client);
      //disconnect after successful commit
      client.query('COMMIT', client.end.bind(client));
    });
  });
});

Check out: https://github.com/brianc/node-postgres/wiki/Transactions

However this doesn't block the table. Here's a list of solutions: Update where race conditions Postgres (read committed)

Community
  • 1
  • 1
aloha_mate
  • 223
  • 6
  • 14
  • 1
    Transactions and repeated inserts got nothing to do with the integrity of `SELECT->INSERT` logic, which is what your question about. – vitaly-t Oct 19 '16 at 05:31
  • @vitaly-t yeah, but queries between `begin` and `commit` can be executed multiple times (select -> insert) using promises, and as I understand it will queue other incoming queries. Correct me if I'm wrong – aloha_mate Oct 19 '16 at 17:56
  • 1
    It will queue queries within a single `begin`/`commit` block, not across them, which is exactly the original issue - race condition. This fixes nothing. – vitaly-t Oct 20 '16 at 06:20