Sequelize find based on association

How would I use Sequelize to find all people where a column in the relation satisfies a condition?

An example would be to find all Books whose author's last name is 'Hitchcock'. The book schema contains a hasOne relation with the Author's table.

Edit: I understand how this could be done with a raw SQL query, but looking for another approach


Here's a working sample of how to user Sequelize to get all Books by an Author with a certain last name. It looks quite a bit more complicated than it is, because I am defining the Models, associating them, syncing with the database (to create their tables), and then creating dummy data in those new tables. Look for the findAll in the middle of the code to see specifically what you're after.

    module.exports = function(sequelize, DataTypes) {

    var Author = sequelize.define('Author', {

        id: {
            type: DataTypes.INTEGER,
            allowNull: false,
            autoIncrement: true,
            primaryKey: true
        },
        firstName: {
            type: DataTypes.STRING
        },
        lastName: {
            type: DataTypes.STRING
        }

    })

    var Book = sequelize.define('Book', {

        id: {
            type: DataTypes.INTEGER,
            allowNull: false,
            autoIncrement: true,
            primaryKey: true
        },
        title: {
            type: DataTypes.STRING
        }

    })

    var firstAuthor;
    var secondAuthor;

    Author.hasMany(Book)
    Book.belongsTo(Author)

    Author.sync({ force: true })
        .then(function() {
            return Book.sync({ force: true });
        })
        .then(function() {
            return Author.create({firstName: 'Test', lastName: 'Testerson'});
        })
        .then(function(author1) {
            firstAuthor=author1;
            return Author.create({firstName: 'The Invisible', lastName: 'Hand'});
        })
        .then(function(author2) {
            secondAuthor=author2
            return Book.create({AuthorId: firstAuthor.id, title: 'A simple book'});
        })
        .then(function() {
            return Book.create({AuthorId: firstAuthor.id, title: 'Another book'});
        })
        .then(function() {
            return Book.create({AuthorId: secondAuthor.id, title: 'Some other book'});
        })
        .then(function() {
            // This is the part you're after.
            return Book.findAll({
                where: {
                   'Authors.lastName': 'Testerson'
                },
                include: [
                    {model: Author, as: Author.tableName}
                ]
            });
        })
        .then(function(books) { 
            console.log('There are ' + books.length + ' books by Test Testerson')
        });
  }

In the newest version of Sequilize (5.9.0) the method proposed by @c.hill does not work.

Now you need to do the following:

return Book.findAll({
    where: {
        '$Authors.lastName$': 'Testerson'
    },
    include: [
        {model: Author, as: Author.tableName}
    ]
});

For documentation!

Check the eager loading section

https://sequelize.org/master/manual/eager-loading.html

For the above answers! You can find it in the doc at the following title

Complex where clauses at the top-level

From the doc:

To obtain top-level WHERE clauses that involve nested columns, Sequelize provides a way to reference nested columns: the '$nested.column$' syntax.

It can be used, for example, to move the where conditions from an included model from the ON condition to a top-level WHERE clause.

User.findAll({
  where: {
    '$Instruments.size$': { [Op.ne]: 'small' }
  },
  include: [{
    model: Tool,
    as: 'Instruments'
  }]
});

Generated SQL:

SELECT
  `user`.`id`,
  `user`.`name`,
  `Instruments`.`id` AS `Instruments.id`,
  `Instruments`.`name` AS `Instruments.name`,
  `Instruments`.`size` AS `Instruments.size`,
  `Instruments`.`userId` AS `Instruments.userId`
FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
  `user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';

For a better understanding of all differences between the inner where option (used inside an include), with and without the required option, and a top-level where using the $nested.column$ syntax, below we have four examples for you:

// Inner where, with default `required: true`
await User.findAll({
  include: {
    model: Tool,
    as: 'Instruments',
    where: {
      size: { [Op.ne]: 'small' }
    }
  }
});

// Inner where, `required: false`
await User.findAll({
  include: {
    model: Tool,
    as: 'Instruments',
    where: {
      size: { [Op.ne]: 'small' }
    },
    required: false
  }
});

// Top-level where, with default `required: false`
await User.findAll({
  where: {
    '$Instruments.size$': { [Op.ne]: 'small' }
  },
  include: {
    model: Tool,
    as: 'Instruments'
  }
});

// Top-level where, `required: true`
await User.findAll({
  where: {
    '$Instruments.size$': { [Op.ne]: 'small' }
  },
  include: {
    model: Tool,
    as: 'Instruments',
    required: true
  }
});

Generated SQLs, in order:

-- Inner where, with default `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
  `user`.`id` = `Instruments`.`userId`
  AND `Instruments`.`size` != 'small';

-- Inner where, `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
  `user`.`id` = `Instruments`.`userId`
  AND `Instruments`.`size` != 'small';

-- Top-level where, with default `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
  `user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';

-- Top-level where, `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
  `user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';

And that give us a good look how the join's are done!