36

I'm using Sequelize in my Express app. I need to generate a query that has a subquery in the WHERE clause.

SELECT *
  FROM MyTable
 WHERE id NOT IN (
       SELECT fkey
         FROM MyOtherTable
        WHERE field1 = 1
          AND field2 = 2
          AND field3 = 3
       )

I first tried relations/associations through my models but couldn't get it to work. Something like:

MyTable.find( {
    where: {
        id: {
            $notIn: // <= what goes here? Can I somehow reference my include model?
        }
    },
    include: [ {
        model: MyOtherTable,
        where: {
          field1: 1,
          field2: 2,
          field3: 3
    } ]
} );

Then I tried using Sequelize.where(), no luck there.

Then I tried Sequelize.literal() and that works but not sure if it's a "proper" way of doing a subquery in a where clause in Sequelize as I'm new to it.

MyTable.find( {
    where: {
        id: {
            $notIn: sequelize.literal( 
                '( SELECT fkey ' +
                    'FROM MyOtherTable ' +
                   'WHERE field1 = ' + field1 +
                    ' AND field2 = ' + field2 +
                    ' AND field3 = ' + field3 + 
                ')'
        }
    } 
} );

I also know that I could use Sequelize.query() but don't really know if I should reach for it or if literal()is the right away as I feel like there's something I'm overlooking.

I would really like to know how to perform a subquery in a WHERE clause with Sequelize the "proper" way.

Thanks for the feedback!

hungerstar
  • 21,206
  • 6
  • 50
  • 59
  • Looking at SO I came to this github issue https://github.com/sequelize/sequelize/issues/3961, through this question http://stackoverflow.com/questions/38882185/sequelize-statementwhere-in-statementwhere/39040218#39040218, and apparently using sequelize.literal is the only way for the time being. – galileopy Sep 12 '16 at 17:53
  • Seems like using `sequelize.literal(...)` is still the way to go – Aaron C Oct 28 '16 at 22:06
  • I found this src that may help you (UNTESTED). let me know of your test after you follow this http://srlm.io/2015/02/04/sequelize-subqueries/ – Pristine Kallio Nov 18 '16 at 04:00
  • 2
    wouldn't this solution allow sql injection? – Shahar Hadas Feb 07 '17 at 21:50
  • @Sash depends on the source of `field1`, `field2`, `field3` and/or if they've already been sanitized/escaped. For this example the values are defined on the backend. – hungerstar Feb 07 '17 at 22:44
  • I've added an example snippet of how I use it my code. It's a different approch. – Shahar Hadas Feb 07 '17 at 23:50
  • If you are not using data from the client(s) consuming the API (no need to worry about SQL injection), your solution works perfectly already. And it's easier to implement than the accepted answer. – Lucio Mollinedo Jun 10 '21 at 12:55

2 Answers2

54

I have encountered a similar issue in my project. The way I choose to implement it is a bit different for two reasons:

  1. If at one point in time Sequelize decides to implement sub queries - the syntax is ready.
  2. Use Sequelize protection against SQL injection.

Here is my code snippet, hope it helps.

Sequelize v5

const tempSQL = sequelize.dialect.QueryGenerator.selectQuery('MyOtherTable',{
    attributes: ['fkey'],
    where: {
          field1: 1,
          field2: 2,
          field3: 3
    }})
    .slice(0,-1); // to remove the ';' from the end of the SQL

MyTable.find( {
    where: {
        id: {
              [Sequelize.Op.notIn]: sequelize.literal(`(${tempSQL})`)
        }
    } 
} );

Sequelize v6

const tempSQL = sequelize.dialect.queryGenerator.selectQuery('MyOtherTable',{
    attributes: ['fkey'],
    where: {
          field1: 1,
          field2: 2,
          field3: 3
    }})
    .slice(0,-1); // to remove the ';' from the end of the SQL

MyTable.find( {
    where: {
        id: {
              [Sequelize.Op.notIn]: sequelize.literal(`(${tempSQL})`)
        }
    } 
} );

Some people might choose to not use the tempSQL variable and simply build the SQL inside the find structure (maybe using a helper method?)

I also think this might be the basis for a sub queries extension for sequelize as it uses the same syntax almost.

Shahar Hadas
  • 2,641
  • 1
  • 27
  • 29
  • Are `where` objects in SQL generator and in find query safely interchangeable? Can I use the same syntax here and there? – 1valdis Oct 18 '18 at 08:38
  • 2
    ASFAIK yes. The `find` method uses the `QueryGenerator` internally. That's how I came with this approach in the 1st place. – Shahar Hadas Oct 20 '18 at 05:37
  • Thank you. This saved me when I needed to use limit/offset with `where` for included model. I generated a subquery like you show here and it works like charm. – 1valdis Oct 22 '18 at 13:08
  • This looks very nice solution. But unfortunately it did not work for me. dialect property does not exist in sequelize variable. Is this because of different latest version I am using? – Ayyaz Zafar Jul 08 '20 at 17:59
  • Are you using v6 or v5? On v6 try to use getDialect() – Shahar Hadas Jul 08 '20 at 17:59
  • 1
    I am using v6 but getDialect() method is not available to use – Ayyaz Zafar Jul 08 '20 at 18:14
  • Whenever I use this Sequelize.getDialect() it shows this error: Property 'getDialect' does not exist on type 'typeof import("node_modules/sequelize/types/index")'.ts(2339) – Ayyaz Zafar Jul 08 '20 at 18:23
  • It exists in the code - https://github.com/sequelize/sequelize/blob/e05351492ac535f7eff977e29041c4a2eb4ee0d6/src/sequelize.js#L378 but I'm not using v6 as of yet myself... – Shahar Hadas Jul 08 '20 at 18:26
  • @AyyazZafar `sequelize.getDialect()` is an instance method - **NOT** static method – Shahar Hadas Jul 08 '20 at 18:27
  • Ok i tried .getDialect() from instance method but then getDialect().QueryGenerator was not available – Ayyaz Zafar Jul 08 '20 at 18:37
  • @AyyazZafar if you'll look at the v5 to v6 migration guide (https://sequelize.org/master/manual/upgrade-to-v6.html), you'll see they changed `QueryGenerator ` to `queryGenerator` >> All instances of QueryInterface and QueryGenerator have been renamed to their lowerCamelCase variants eg. queryInterface and queryGenerator when used as property names on Model and Dialect, the class names remain the same. – Shahar Hadas Jul 08 '20 at 18:42
  • Okay thanks but actually .getDialect() returns only a string == 'mysql' That's why we cannot use .getDialect().queryGenerator() :( – Ayyaz Zafar Jul 10 '20 at 05:36
  • 1
    I tried both `Sequelize.mysql.queryGenerator.selectQuery(...)` and `Sequelize.mysql.QueryGenerator.selectQuery(...)` - both result in an error `cannot read property selectQuery of undefined`. – Eggon Oct 06 '20 at 11:30
  • @Eggon Which version of Sequelize are you using? – Shahar Hadas Oct 06 '20 at 17:46
  • @ShaharHadas I'm using v. 5.21.3. – Eggon Oct 07 '20 at 11:33
  • @Eggon just realized you are calling `Sequelize.mysql...` and not `sequelize.dialect...` which is using the sequelize connection instance. Look at the examples above – Shahar Hadas Oct 07 '20 at 15:14
  • 1
    @ShaharHadas I see, I read more thoroughly other comments here, I was able to do it using the Sequelize instance. Thanks! Upvoted the answer now! :) – Eggon Oct 07 '20 at 15:59
  • Note that `selectQuery` doesn't support some options that `findAll` and the like support; for instance, `paranoid`. You'll have to invoke some more internal methods in order for them the work; for instance, `selectQuery(..., Model._paranoidClause(options))`. – Eyal Roth Feb 11 '21 at 14:47
  • 2
    This works for me `sequelize.getQueryInterface().queryGenerator.selectQuery` – leopragi Mar 10 '22 at 10:07
4

In addition to @Shahar Hadas answer, because i fall into some errors using the code he showed.

Here is a more complexe example. In this example we have a main table named "Artist" in a Many-to-Many relationship with "Tag". "Tag" are associated to a predefined list of tags i named "TagType". We want to fetch all Artists linked to all the searched tags (TagType Id).

const tagsSearched = [1, 2];

const subQueryOptions = {
    attributes: ['id'], // You have to list at least one attribute
    include: [
        {
            model: models.Tag,
            required: true,
            attributes: [], // Avoid the only_full_group_by error
            through: {
                attributes: [], // Avoid the only_full_group_by error
            },
            include: {
                model: models.TagType,
                required: true,
                attributes: [], // Avoid the only_full_group_by error
                where: {
                    id: {
                        [Op.in]: tagsSearched, // Array of tags searched
                    }
                },
            },
        }
            ],
    group: sequelize.col('artist.id'), // Group by the main parent ID of this query
    having: sequelize.where(sequelize.fn('count', 
            sequelize.col('tags.tagType.id')), {
                [Op.gte]: tagsSearched.length,
    }), // Get only the artists that have at least the "tagsSearched" associated
}

// Parse the subQueryOptions, this operation would serialize the queryOptions
Model._validateIncludedElements.bind(models.Artist)(subQueryOptions);

// First argument has to be a string (table name, by default in plural)
// Second argument is our serialized query options
// Third argument is the model used
const artistsSubQuery = sequelize.dialect.queryGenerator.selectQuery("artists", subQueryOptions, models.Artist)
.slice(0,-1); // to remove the ';' from the end of the SQL query

models.Artist.findAll({
    where: {
        id: {
            [Op.in]: sequelize.literal(`(${artistsSubQuery})`),
        }
    }
});

I will update this in case of questions.

Alfonso Tienda
  • 3,442
  • 1
  • 19
  • 34
rob-art
  • 517
  • 6
  • 18