简体   繁体   中英

How do I make this complex self-joined query with sequelize?

I'm trying to build a query in sequelize which basically makes a join between two tables: Resource and ContentTag, which are in a manyToMany relationship: In sql the query I'd like to obtain whould look like this:

`SELECT * FROM Resources-Ctag
NATURAL JOIN (SELECT * FROM Resource-Ctag WHERE ContentTag.keyword="A")as ResourceWithTagA 
NATURAL JOIN (SELECT * FROM Resource-Ctag WHERE ContentTag.keyword="B")as ResourceWithTagB
WHERE  ContentTag.keyword="C"`

Where Resources-Ctag is hypotetically the bridge table which associates resources to contentTags. The objective is to retrieve all resources that contains at least all the three tag together: A, B, C.

Here is the Resource model in sequelize:

import { ContentTag } from "./content_tag";

export class Resource extends Model {
  // Default incremental ID
  public id!: number;

  // Model attributes
  public name!: string;

  public description!: string;

  public format!: string;

  public path!: string;

  public metadata!: any;
  public typologyTagId!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  // Associations

  public addContentTag!: HasManyAddAssociationMixin<ContentTag, number>;
  public getContentTags!: HasManyGetAssociationsMixin<ContentTag>;
  public setContentTags!: HasManySetAssociationsMixin<ContentTag, number>;

  public static initialize(sequelize) {
    this.init(
      {
        id: {
          primaryKey: true,
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV4,
        },
        name: {
          type: DataTypes.STRING,
          allowNull: false,
          unique: false,
        },
        description: {
          type: DataTypes.STRING,
          allowNull: true,
          unique: false,
        },
        format: {
          type: DataTypes.STRING,
          allowNull: false,
          unique: false,
        },
        path: {
          type: DataTypes.STRING,
          allowNull: false,
          unique: false,
        },
        metadata: {
          type: DataTypes.JSON,
          allowNull: true,
          unique: false,
        },
      },
      {
        sequelize,
        timestamps: true,
        paranoid: true,
        tableName: "resource",
      }
    );
  }
}

And here is the ContentTag Model:

export class ContentTag extends Model {
  // Default incremental ID
  public id!: string;

  // Model attributes
  public keyword!: string;

  public static initialize(sequelize) {
    this.init(
      {
        id: {
          primaryKey: true,
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV4,
        },
        keyword: {
          type: DataTypes.STRING,
          allowNull: false,
          unique: false,
        },
      },
      {
        sequelize,
        timestamps: true,
        createdAt: false,
        updatedAt: false,
        tableName: "content_tag",
      }
    );
  }
}

I've tried this but it doesnt work:

router.get("/", async (req: any, res: Response) => {
  try {

    const { typologyTag, timeFrom, timeTo, author } = req.query;
    const cTags = JSON.parse(req.query.contentTags)
    const where = {};
    const include = [
      {
        model: TypologyTag,
        as: "typologyTag",
      },
      {
        model: User,
        as: "author",
      },
      {
        model: ContentTag,
        as: "contentTags",
      },
    ];

    if (typologyTag) {
      include[0]["where"] = { keyword: { [Op.eq]: typologyTag } };
    };
    if (author) {
      include[1]["where"] = { displayName: { [Op.eq]: author } };
    };
    if (timeFrom && timeTo) {
      where["createdAt"] = {
        [Op.lt]: timeTo,
        [Op.gt]: new Date(timeFrom)
      };
    } else if (timeFrom) {
      where["createdAt"] = {
        [Op.gt]: new Date(timeFrom)
      };
    } else if (timeTo) {
      where["createdAt"] = {
        [Op.lt]: new Date(timeTo)
      };
    };

   


    if (cTags && cTags.length != 0 && cTags.reduce(
      (accumulator, currentValue) => accumulator && currentValue,
      true)) {
      include[2]["where"] = { keyword: { [Op.eq]: cTags[0] } };
      if (cTags[1]) {
        include[2]["include"] = [{
          model: Resource,
          as: "resources",
          include: [{
            model: ContentTag,
            as: "contentTags",
            where: { keyword: { [Op.eq]: cTags[1] } },
            include: [{
              model: Resource,
              as: "resources",
              include: [{
                model: ContentTag,
                as: "contentTags",
                where: { keyword: { [Op.eq]: cTags[2] } },
              }]
            }]
          }]
        }]
      }
    };


    const result = await Resource.findAll({
      where: where,
      include: include,
    });
....

I made such a huge effort thinking about that solution but now I realise it obviously can't work. Now I have no more clue on how to solve that query.

You could fix the self-join but also can consider the alternative SQL like this.

Join ContentTag where keyword is in "A", "B", "C" to Resource and count the keyword per Resource.id where the count should be greater than or equal to 3.

SELECT r.* 
FROM Resources r 
JOIN (SELECT * 
      FROM ContentTags 
      WHERE keyword IN ('A', 'B', 'C')) c ON r.id = c.resource_id
GROUP BY r.id
HAVING COUNT(DISTINCT c.keyword) >= 3;

In Sequelize JS (sorry, I don't write TypeScript).

Resource.findAll({
  include: {
    attributes: [], // If you need attributes from ContentTag, you need to add aggregation or change to window function.
    model: ContentTag,
    where: {
      keyword: {
        [Op.in]: ['A', 'B', 'C']
      }
    }
  },
  group: ['Resources.id'],
  having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.literal('DISTINCT keyword')), Op.gte, 3)
})

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM