ID photo of Ciro Santilli taken in 2013 right eyeCiro Santilli OurBigBook logoOurBigBook.com  Sponsor 中国独裁统治 China Dictatorship 新疆改造中心、六四事件、法轮功、郝海东、709大抓捕、2015巴拿马文件 邓家贵、低端人口、西藏骚乱
nodejs/sequelize/many_to_many.js
#!/usr/bin/env node
// https://cirosantilli.com/sequelize-example
const assert = require('assert');
const path = require('path');
const { DataTypes, Op } = require('sequelize');
const common = require('./common')
const sequelize = common.sequelize(__filename, process.argv[2])
;(async () => {
// Create the tables.
const Animal = sequelize.define('Animal', {
  species: { type: DataTypes.STRING },
});
const Tag = sequelize.define('Tag', {
  name: { type: DataTypes.STRING },
});
// AnimalTag is the species of the relation table.
// Sequelize creates it automatically for us.
// On SQLite that table looks like this:
// CREATE TABLE `AnimalTag` (
//   `createdAt` DATETIME NOT NULL,
//   `updatedAt` DATETIME NOT NULL,
//   `AnimalId` INTEGER NOT NULL REFERENCES `Animals` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
//   `TagId` INTEGER NOT NULL REFERENCES `Tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
//   PRIMARY KEY (`AnimalId`, `TagId`)
// );
Animal.belongsToMany(Tag, { through: 'AnimalTag' });
Tag.belongsToMany(Animal, { through: 'AnimalTag' });
await sequelize.sync({force: true});

let bat, frog, dog, flying, mammal, reptile
async function reset() {
  // The cascade: true is needed here otherwise `await Animal.truncate()` fails with:
  // ``
  // cannot truncate a table referenced in a foreign key constraint
  // ``
  // on TaggreSQL 13.4.
  // https://github.com/sequelize/sequelize/issues/11289
  // https://stackoverflow.com/questions/56700466/taggresql-truncate-table-with-foreign-key-constraint
  await sequelize.truncate({ cascade: true })

  // Create some animals and tags.

  bat = await Animal.create({ species: 'bat' })
  frog = await Animal.create({ species: 'frog' })
  dog = await Animal.create({ species: 'dog' })
  flying = await Tag.create({ name: 'flying' })
  mammal = await Tag.create({ name: 'mammal' })
  reptile = await Tag.create({ name: 'reptile' })

  // Autogenerated add* methods

  // Make bat have flying tag
  await bat.addTag(flying)

  // Also works.
  //await bat.addTag(flying.id)
  // Make bat and dog be mammal
  await mammal.addAnimals([bat, dog])
}
await reset()
let rows;

// Autogenerated get* methods

// Get tags that a animal has.

const batTags = await bat.getTags({ order: [['name', 'ASC']] })
assert.strictEqual(batTags[0].name, 'flying');
assert.strictEqual(batTags[1].name, 'mammal');
assert.strictEqual(batTags.length, 2);
assert.strictEqual(await bat.countTags(), 2)

const frogTags = await frog.getTags({ order: [['name', 'ASC']] })
assert.strictEqual(frogTags.length, 0);

const dogTags = await dog.getTags({ order: [['name', 'ASC']] })
assert.strictEqual(dogTags[0].name, 'mammal');
assert.strictEqual(dogTags.length, 1);

// Don't SELECT any attributes of the JOIN table with joinTableAttributes
// to be a bit more efficient. when we don't need that data.
//
// TODO I don't even know to how access that data when joinTableAttributes
// is not given. At many_to_many_custom_table.js, we are able to do that,
// but it doesn't seem to be populated when the implicit table is used.
const batTagsNoJoin = await bat.getTags({ order: [['name', 'ASC']], joinTableAttributes: [] })
assert.strictEqual(batTagsNoJoin[0].name, 'flying');
assert.strictEqual(batTagsNoJoin[1].name, 'mammal');
assert.strictEqual(batTagsNoJoin.length, 2);
assert.strictEqual(await bat.countTags(), 2)

// Same as get but with the animal name instead of the model object.
// This is almost always what you want to use in a website query,
// where the input is usually some unique name.
{
  async function getTags(species, opts={}) {
    return Tag.findAll(
      Object.assign({
        include: [{
          model: Animal,
          where: { species },
        }],
        order: [['name', 'ASC']],
      }, opts)
    )
  }
  rows = await getTags('bat')
  common.assertEqual(rows, [
    { name: 'flying' },
    { name: 'mammal' },
  ])
  // We can aslo access bat data here.
  // But note that dog, who also has tag mammal, is missing
  // because the where selects it out. So we generally never want to do that.
  // like this.
  assert.strictEqual(rows[0].Animals[0].species, 'bat');
  assert.strictEqual(rows[0].Animals.length, 1);
  assert.strictEqual(rows[1].Animals[0].species, 'bat');
  assert.strictEqual(rows[1].Animals.length, 1);

  // TODO is subQuery: false ever needed in the following cases? It was in some other related cases witih limit.
  // Would need to understand the subQuery break a bit better to deciede.
  rows = await getTags('bat', { limit: 1 })
  common.assertEqual(rows, [
    { name: 'flying' },
  ])
  rows = await getTags('bat', { order: [['name', 'DESC']] })
  common.assertEqual(rows, [
    { name: 'mammal' },
    { name: 'flying' },
  ])
  rows = await getTags('dog', { limit: 1 })
  common.assertEqual(rows, [
    { name: 'mammal' },
  ])
}

// Yet another way that can be useful in nested includes.
{
  const batTags = (await Animal.findOne({
    where: { id: bat.id },
    include: [{
      model: Tag,
    }],
    order: [[Tag, 'name', 'ASC']],
  })).Tags
  assert.strictEqual(batTags[0].name, 'flying');
  assert.strictEqual(batTags[1].name, 'mammal');
  assert.strictEqual(batTags.length, 2);
}

// TODO Find all tags that bat has, and for each tag include
// data about all animals that has that tag. Doesn't seem possible.
// in Sequelize, as we would need to include the same model twice
// under different aliases, which does not seem supported:
// * https://github.com/sequelize/sequelize/issues/8013
// * https://github.com/sequelize/sequelize/issues/7754
if (false) {
rows = await Tag.findAll({
  include: [
    {
      model: Animal,
      where: { id: bat.id },
    },
    {
      model: Animal,
      where: { id: frog.id },
    }
  ],
  order: [['name', 'ASC']],
})
console.error(rows);
assert.strictEqual(rows[0].name, 'flying');
assert.strictEqual(rows[0].Animals[0].species, 'bat');
assert.strictEqual(rows[0].Animals.length, 1);
assert.strictEqual(rows[1].name, 'mammal');
assert.strictEqual(rows[1].Animals[0].species, 'bat');
assert.strictEqual(rows[1].Animals[1].species, 'dog');
assert.strictEqual(rows[1].Animals.length, 2);
assert.strictEqual(batTags.length, 2);
}

// TODO Get all tags, and also determine if dog has each of of them or not.
// by doing LEFT OUTER JOIN. Managed only with super many to many.
// https://stackoverflow.com/questions/31091420/nodejs-sequelize-include-where-required-false
// https://stackoverflow.com/questions/39658204/sequelize-how-to-do-a-where-condition-on-joined-table-with-left-outer-join
//
// TaggreSQL used to work with the desired, stopped at 6.14.0:
//
// SELECT
//   "Tag"."id",
//   "Tag"."name",
//   "Tag"."createdAt",
//   "Tag"."updatedAt",
//   "Animals"."id" AS "Animals.id"
// FROM
//   "Tags" AS "Tag"
//   LEFT OUTER JOIN (
//     "AnimalTag" AS "Animals->AnimalTag"
//     INNER JOIN "Animals" AS "Animals" ON "Animals"."id" = "Animals->AnimalTag"."AnimalId"
//   ) ON "Tag"."id" = "Animals->AnimalTag"."TagId"
//   AND "Animals"."id" = 3
// ORDER BY
//   "Tag"."name" ASC;
//
// but SQLite fails with:
//
// SELECT
//   `Tag`.`id`,
//   `Tag`.`name`,
//   `Tag`.`createdAt`,
//   `Tag`.`updatedAt`,
//   `Animals`.`id` AS `Animals.id`
// FROM
//   `Tags` AS `Tag`
// LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
//   ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
// LEFT OUTER JOIN `Animals` AS `Animals`
//   ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
//   AND `Animals`.`id` = 3
// ORDER BY `Tag`.`name` ASC;
//
// Running that manually gives:
//
// 1|flying|
// 2|mammal|
// 2|mammal|3
// 3|reptile|
//
// The correct query would have been:
//
// SELECT
//   `Tag`.`id`,
//   `Tag`.`name`,
//   `Tag`.`createdAt`,
//   `Tag`.`updatedAt`,
//   `Animals`.`id` AS `Animals.id`
// FROM
//   `Tags` AS `Tag`
// LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
//   ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
//   AND `Animals->AnimalTag`.`AnimalId` = 3
// LEFT OUTER JOIN `Animals` AS `Animals`
//   ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
// ORDER BY `Tag`.`name` ASC;
//
// which gives the desired:
//
// 1|flying|
// 2|mammal|3
// 3|reptile|
if (false) {
  rows = await Tag.findAll({
    include: [{
      model: Animal,
      where: { id: bat.id },
      // No effect.
      //on: { id: dog.id },
      through: {
        //attributes: [],
        // Same as putting the where outside of through.
        //where: { AnimalId: dog.id },
      },
      required: false,
      //attributes: ['id'],
    }],
    order: [['name', 'ASC']],
  })
  console.error(rows.map(row => row.Animals.map(animal => animal.id)));
  assert.strictEqual(rows[0].name, 'flying');
  assert.strictEqual(rows[0].Animals.length, 0);
  assert.strictEqual(rows[1].name, 'mammal');
  assert.strictEqual(rows[1].Animals.length, 1);
  assert.strictEqual(rows[2].name, 'reptile');
  assert.strictEqual(rows[2].Animals.length, 0);
  assert.strictEqual(rows.length, 3);
}

// Get animals that have a given tag.

const flyingTagged = await flying.getAnimals({order: [['species', 'ASC']]})
assert.strictEqual(flyingTagged[0].species, 'bat');
assert.strictEqual(flyingTagged.length, 1);

const mammalTagged = await mammal.getAnimals({order: [['species', 'ASC']]})
assert.strictEqual(mammalTagged[0].species, 'bat');
assert.strictEqual(mammalTagged[1].species, 'dog');
assert.strictEqual(mammalTagged.length, 2);

const reptileTagged = await reptile.getAnimals({order: [['species', 'ASC']]})
assert.strictEqual(reptileTagged.length, 0);

// Autogenerated has* methods

// Check if animal has tag.
assert( await bat.hasTag(flying))
assert( await bat.hasTag(flying.id)) // same
assert( await bat.hasTag(mammal))
assert(!await bat.hasTag(reptile))

// Check if tag has animal.
assert( await flying.hasAnimal(bat))
assert(!await flying.hasAnimal(frog))
assert(!await flying.hasAnimal(dog))

// AND of multiple has checks at once.
assert( await bat.hasTags([flying, mammal]))
assert(!await bat.hasTags([flying, mammal, reptile]))

// Autogenerated count* methods
assert.strictEqual(await bat.countTags(), 2)
assert.strictEqual(await flying.countAnimals(), 1)

// Count how many tags each animal has: GROUP BY + COUNT aggregate.
// Order return by animals with most tags first.
// INNER JOIN due to `required: true`, so 0 counts are not present.
// https://stackoverflow.com/questions/29869077/how-to-count-a-group-by-query-in-nodejs-sequelize
rows = await Animal.findAll({
  attributes: [
    'species',
    [sequelize.fn('COUNT', '*'), 'count'],
  ],

  // Workaround for: https://github.com/sequelize/sequelize/issues/5481#issuecomment-964387232
  raw: true,
  includeIgnoreAttributes: false,

  include: [
    {
      model: Tag,
      required: true,
    },
  ],
  group: ['Animal.species'],
  order: [[sequelize.col('count'), 'DESC']],
})
assert.strictEqual(rows[0].species, 'bat')
assert.strictEqual(parseInt(rows[0].count, 10), 2)
assert.strictEqual(rows[1].species, 'dog')
assert.strictEqual(parseInt(rows[1].count, 10), 1)
assert.strictEqual(rows.length, 2)

// Same but grouping by PK id as a generally better workaround.
rows = await Animal.findAll({
  attributes: [
    'species',
    [sequelize.fn('COUNT', '*'), 'count'],
  ],
  include: [
    {
      model: Tag,
      required: true,
      attributes: [],
      through: {attributes: []},
    },
  ],
  group: ['Animal.id'],
  order: [[sequelize.col('count'), 'DESC']],
})
assert.strictEqual(rows[0].species, 'bat')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 2)
assert.strictEqual(rows[1].species, 'dog')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 1)
assert.strictEqual(rows.length, 2)

// Count how many tags each animal has but use
// LEFT OUTER JOIN + COUNT(column), so 0 counts will be present.
rows = await Animal.findAll({
  attributes: [
    'species',
    [sequelize.fn('COUNT', sequelize.col('Tags.id')), 'count'],
  ],
  include: [
    {
      model: Tag,
      required: false,
      attributes: [],
      through: {attributes: []},
    },
  ],
  group: ['Animal.id'],
  order: [[sequelize.col('count'), 'DESC']],
})
assert.strictEqual(rows[0].species, 'bat')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 2)
assert.strictEqual(rows[1].species, 'dog')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 1)
assert.strictEqual(rows[2].species, 'frog')
assert.strictEqual(parseInt(rows[2].get('count'), 10), 0)
assert.strictEqual(rows.length, 3)

// Count how many tags each animal has but
// only consider animals with less than 2 tags.
rows = await Animal.findAll({
  attributes: [
    'species',
    [sequelize.fn('COUNT', sequelize.col('Tags.id')), 'count'],
  ],
  include: [
    {
      model: Tag,
      required: false,
      attributes: [],
      through: {attributes: []},
    },
  ],
  group: ['Animal.id'],
  order: [[sequelize.col('count'), 'DESC']],
  having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Tags.id')), Op.lt, 2)
})
assert.strictEqual(rows[0].species, 'dog')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].species, 'frog')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)

// Count how many tags each animal that has a given tag has.
async function getTagCountForAnimalsThatAreTagged(tagId) {
  return Tag.findAll({
    attributes: [
      sequelize.col('Animals.id'),
      [sequelize.fn('COUNT', 'Tag.id'), 'count'],
    ],
    raw: true,
    includeIgnoreAttributes: false,
    include: [
      {
        model: Animal,
        where: { '$Tag.id$': tagId },
        include: [
          {
            model: Tag,
            required: false
          }
        ]
      },
    ],
    group: ['Animals.id'],
    order: [[sequelize.col('count'), 'DESC']],
  })
}
rows = await getTagCountForAnimalsThatAreTagged(flying.id)
assert.strictEqual(rows[0].id, bat.id)
assert.strictEqual(parseInt(rows[0].count, 10), 2)
assert.strictEqual(rows.length, 1)
rows = await getTagCountForAnimalsThatAreTagged(mammal.id)
assert.strictEqual(rows[0].id, bat.id)
assert.strictEqual(parseInt(rows[0].count, 10), 2)
assert.strictEqual(rows[1].id, dog.id)
assert.strictEqual(parseInt(rows[1].count, 10), 1)
assert.strictEqual(rows.length, 2)
rows = await getTagCountForAnimalsThatAreTagged(reptile.id)
assert.strictEqual(rows.length, 0)

// Autogenerated add* methods
// Trying to add multiple times does not raise due to unique constraint validation
// it always does SELECT check before the INSERT.
await bat.addTag(flying)
await bat.addTag(flying)
await reset()

// Autogenerated remove* methods

// bat doesn't fly anymore.
await bat.removeTag(flying)
// bat and dog aren't mammals anymore.
await mammal.removeAnimals([bat, dog])
// Check that no-one is anything anymore.
assert.strictEqual(await bat.countTags(), 0)
assert.strictEqual(await flying.countAnimals(), 0)
await reset()

// Autogenerated create* method
// Create a new tag and automatically make bat have it.
const tag3 = await bat.createTag({ name: 'tag3' })
assert(await bat.hasTag(tag3))
assert(await tag3.hasAnimal(bat))
await reset()

// Autogenerated set* method
// Make bat have exactly these tags. Remove all others.
await bat.setTags([mammal, reptile])
assert(!await bat.hasTag(flying))
assert( await bat.hasTag(mammal))
assert( await bat.hasTag(reptile))
assert(!await bat.hasTag(tag3))
await reset()

// DELETE all animals with tag mammal.
// https://stackoverflow.com/questions/40890131/sequelize-destroy-record-with-join
await Animal.findAll({
  attributes: ['id'],
  raw: true,
  include: [
    {
      model: Tag,
      where: { name: 'mammal' },
      attributes: [],
      through: { attributes: [] },
    }
  ],
}).then(ids => Animal.destroy({ where: { id: ids.map(id => id.id) } }))
rows = await sequelize.models.Animal.findAll({ order: [['species', 'ASC']] })
common.assertEqual(rows, [
  { species: 'frog' },
])
await reset()

// Access join table.
rows = await sequelize.models.AnimalTag.findAll({order: [
  ['AnimalId', 'ASC'], ['TagId', 'ASC'],
]})
assert.strictEqual(rows[0].AnimalId, bat.id)
assert.strictEqual(rows[0].TagId, flying.id)
assert.strictEqual(rows[1].AnimalId, bat.id)
assert.strictEqual(rows[1].TagId, mammal.id)
assert.strictEqual(rows[2].AnimalId, dog.id)
assert.strictEqual(rows[2].TagId, mammal.id)
assert.strictEqual(rows.length, 3)

// Modify join table.
await sequelize.models.AnimalTag.truncate({})
rows = await sequelize.models.AnimalTag.findAll()
assert.strictEqual(rows.length, 0)
await reset()

})().finally(() => { return sequelize.close() });