Search code examples
node.jsknex.js

how to check if any of given queries return any result in knex


I have two queries:

a) select id from ingredietns where name = my_param;

b) select word_id from synonyms where name = my_param;

Both return 0 or 1 row. I can also add limit 1 if needed (or in knex first()).

I can translate each into knex like this:

knex("ingredients").select('id').where('name', my_param) //do we need first()?

knex("synonyms").select('word_id').where('name', my_param) //do we need first()?

I need function called "ingredientGetOrCreate(my_param)". This function would

a) check if any of above queries return result b) if any of these return, then return ingredients.id or synonyms.word_id - only one could be returned c) if record doesn't eixst in any of tables, I need to do knex inesrt aand return newly added id from function

d) later I am not sure I also understand how to call this newly create function. Function ingredientGetOrCreate would be used later as seperate function or in the following scenario (like "loop") that doesn't work for me either:

knex("products") // for each product
    .select("id", "name")
    .map(function (row) {
        var descriptionSplitByCommas = row.desc.split(",");
        Promise.all(descriptionSplitByCommas
            .map(function (my_param) {
                // here it comes - call method for each param and do insert
                ingredientGetOrCreate(my_param)
                  .then(function (id_of_ingredient) {
                          knex('ingredients_products').insert({ id_of_ingredient });                      

                  });
         ...

I am stuck with knex and Promise queries because of asynchronouse part. Any clues, please? I though I can somehow use Promise.all or Promise.some to call both queries.

P.S. This is my first day with nodejs, Promise and knex.


Solution

  • As far as I decode your question, it consists of two parts:

    (1) You need to implement upsert logic (get-or-create logic).

    (2) Your get part requires to query not a single table, but a pair of tables in specific order. Table names imply that this is some sort of aliasing engine inside of your application.

    Let's start with (2). This could definitely be solved with two queries, just like you sense it.

    function pick_name (rows)
    {
        if (! rows.length) return null
        return rows[0].name
    }
    
    // you can sequence queries
    function ingredient_get (name)
    {
        return knex('ingredients')
        .select('id').where('name', name)
        .then(pick_name)
        .then(name =>
        {
            if (name) return name
    
            return knex('synonyms')
            .select('word_id').where('name', name)
            .then(pick_name)
        })
    }
    
    // or run em parallel
    function ingredient_get (name)
    {
        var q_ingredients = knex('ingredients')
        .select('id').where('name', name)
        .then(pick_name)
    
        var q_synonyms = knex('synonyms')
        .select('word_id').where('name', name)
        .then(pick_name)
    
        return Promise.all([ q_ingredients, q_synonyms ])
        .then(([name1, name2]) =>
        {
           return name1 || name2
        })
    }
    

    Important notions here:

    • Both forms works well and return first occurence or JS' null.
    • First form optimizes count of queries to DB.
    • Second form optimizes answer time.

    However, you can go deeper and use more SQL. There's a special tool for such task called COALESCE. You can consult your SQL documentation, here's COLASCE of PostgreSQL 9. The main idea of COALESCE is to return first non-NULL argument or NULL otherwise. So, you can leverage this to optimize both queries and answer time.

    function ingredient_get (name)
    {
        // preparing but not executing both queries
        var q_ingredients = knex('ingredients')
        .select('id').where('name', name)
    
        var q_synonyms = knex('synonyms')
        .select('word_id').where('name', name)
    
        // put them in COALESCE
        return knex.raw('SELECT COALESCE(?, ?) AS name', [ q_ingredients, q_synonyms ])
        .then(pick_name)
    

    This solution guarantees single query and furthermore DB engine can optimize execution in any way it sees appropriate.

    Now let's solve (1): We now got ingredient_get(name) which returns Promise<string | null>. We can use its output to activate create logic or return our value.

    function ingredient_get_or_create (name, data)
    {
        return ingredient_get(name)
        .then(name =>
        {
           if (name) return name
    
           // …do your insert logic here
           return knex('ingredients').insert({ name, ...data }) 
           // guarantee homohenic output across get/create calls:
           .then(() => name)
        })
    }
    

    Now ingredient_get_or_create do your desired upsert logic.

    UPD1: We already got ingredient_get_or_create which returns Promise<name> in any scenario (both get or create).

    a) If you need to do any specific logic after that you can just use then:

    ingredient_get_or_create(…)
    .then(() => knex('another_table').insert(…))
    .then(/* another logic after all */)
    

    In promise language that means «do that action (then) if previous was OK (ingredient_get_or_create)». In most of the cases that is what you need.

    b) To implement for-loop in promises you got multiple different idioms:

    // use some form of parallelism
    var qs = [ 'name1', 'name2', 'name3' ]
    .map(name =>
    {
       return ingredient_get_or_create(name, data)
    })
    
    var q = Promise.all(qs)
    

    Please, note, that this is an agressive parallelism and you'll get maximum of parallel queries as your input array provides.

    If it's not desired, you need to limit parallelism or even run tasks sequentially. Bluebird's Promise.map is a way to run map which analogous to example above but with concurrency option available. Consider the docs for details.

    There's also Bluebird's Promise.mapSeries which conceptually is an analogue to for-loop but with promises. It's like map which runs sequentially. Look the docs for details.

    Promise.mapSeries([ 'name1', 'name2', 'name3' ],
    (name) =>  ingredient_get_or_create(name, data))
    .then(/* logic after all mapSeries are OK */)
    

    I believe the last is what you need.