【问题标题】:Counting associated entries with Sequelize使用 Sequelize 计算关联条目
【发布时间】:2016-10-15 12:40:29
【问题描述】:

我有两张桌子,locationssensorssensors 中的每个条目都有一个指向 locations 的外键。使用 Sequelize,我如何获取来自 locations 的所有条目以及与 locations 中的每个条目相关联的 sensors 中的条目总数?

原始 SQL:

SELECT 
    `locations`.*,
    COUNT(`sensors`.`id`) AS `sensorCount` 
FROM `locations` 
JOIN `sensors` ON `sensors`.`location`=`locations`.`id`;
GROUP BY `locations`.`id`;

型号:

module.exports = function(sequelize, DataTypes) {
    var Location = sequelize.define("Location", {
        id: {
            type: DataTypes.INTEGER.UNSIGNED,
            primaryKey: true
        },
        name: DataTypes.STRING(255)
    }, {
        classMethods: {
            associate: function(models) {
                Location.hasMany(models.Sensor, {
                    foreignKey: "location"
                });
            }
        }
    });

    return Location;
};


module.exports = function(sequelize, DataTypes) {
    var Sensor = sequelize.define("Sensor", {
        id: {
            type: DataTypes.INTEGER.UNSIGNED,
            primaryKey: true
        },
        name: DataTypes.STRING(255),
        type: {
            type: DataTypes.INTEGER.UNSIGNED,
            references: {
                model: "sensor_types",
                key: "id"
            }
        },
        location: {
            type: DataTypes.INTEGER.UNSIGNED,
            references: {
                model: "locations",
                key: "id"
            }
        }
    }, {
        classMethods: {
            associate: function(models) {
                Sensor.belongsTo(models.Location, {
                    foreignKey: "location"
                });

                Sensor.belongsTo(models.SensorType, { 
                    foreignKey: "type"
                });
            }
        }
    });

    return Sensor;
};

【问题讨论】:

  • 这真的是你想要的SQL吗?我不认为这会像你想象的那样做。事实上,我不确定查询是否会运行而不会引发错误。
  • @dvlsg 我运行它,它正确返回了locations 表中的所有行和字段,并且为每一行返回了sensors 中正确数量的关联条目。
  • 其实@dvlsg,这是不对的。我做了更多测试(locations 表中有更多条目),结果我忘记了GROUP BY 声明。我已经编辑了问题。
  • 啊,好吧。这更有意义。我想也许 MySQL 正在拉一些我不知道的恶作剧(而且我知道他们使用隐含的 GROUP 语句来做到这一点,所以这并非完全不合理)。

标签: mysql node.js sequelize.js


【解决方案1】:

HAVINGORDER BYINNEROUTER JOIN 的示例 + 一些错误/不直观的行为

我在Sequelize query with count in inner join 进行了更详细的介绍,但这里是要点的快速摘要列表:

  • 你必须使用row.get('count')row.count不起作用
  • 你必须在 PostgreSQL 上parseInt
  • 此代码在 PostgreSQL 上失败,column X must appear in the GROUP BY clause or be used in an aggregate function 由于一个 sequelize 错误

OUTER JOIN 使用 required: false 包含 0 个计数的示例:

sqlite.js

const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
  dialect: 'sqlite',
  storage: 'tmp.sqlite'
}));
;(async () => {
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
  body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])

let rows = await User.findAll({
  attributes: [
    'name',
    [sequelize.fn('COUNT', sequelize.col('Posts.id')), 'count'],
  ],
  include: [
    {
      model: Post,
      attributes: [],
      required: false,
      through: {attributes: []},
      where: { id: { [Op.ne]: post2.id }},
    },
  ],
  group: ['User.name'],
  order: [[sequelize.col('count'), 'DESC']],
  having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Posts.id')), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].name, 'user2')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)
})().finally(() => { return sequelize.close() });

与:

package.json

{
  "name": "tmp",
  "private": true,
  "version": "1.0.0",
  "dependencies": {
    "pg": "8.5.1",
    "pg-hstore": "2.3.3",
    "sequelize": "6.5.1",
    "sqlite3": "5.0.2"
  }
}

和节点 v14.17.0。

INNER JOIN 版本不包括 0 计数:

let rows = await User.findAll({
  attributes: [
    'name',
    [sequelize.fn('COUNT', '*'), 'count'],
  ],
  include: [
    {
      model: Post,
      attributes: [],
      through: {attributes: []},
      where: { id: { [Op.ne]: post2.id }},
    },
  ],
  group: ['User.name'],
  order: [[sequelize.col('count'), 'DESC']],
  having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows.length, 1)

【讨论】:

    【解决方案2】:

    如何为它定义一个数据库视图,然后为该视图定义一个模型?只要您需要传感器的数量,就可以获取与查询中包含的视图的关系。这样代码可能看起来更干净,但我不知道是否会有性能成本。其他人可能会回答...

    CREATE OR REPLACE VIEW view_location_sensors_count AS
    select "locations".id as "locationId", count("sensors".id) as "locationSensorsCount"
    from locations
    left outer join sensors on sensors."locationId" = location.id
    group by location.id
    

    在为视图定义模型时,您删除了 id 属性并将 locationId 设置为主键。 您的模型可能如下所示:

    const { Model, DataTypes } = require('sequelize')
    
    const attributes = {
        locationID: {
            type: DataTypes.UUIDV4, // Or whatever data type is your location ID
            primaryKey: true,
            unique: true
        },
        locationSensorsCount: DataTypes.INTEGER
    }
    
    const options = {
        paranoid: false,
        modelName: 'ViewLocationSensorsCount',
        tableName: 'view_location_sensors_count',
        timestamps: false
    }
    
    
    /**
     * This is only a database view. It is not an actual table, so 
     * DO NOT ATTEMPT insert, update or delete statements on this model
     */
    class ViewLocationSensorsCount extends Model {
        static associate(models) {
            ViewLocationSensorsCount.removeAttribute('id')
            ViewLocationSensorsCount.belongsTo(models.Location, { as:'location', foreignKey: 'locationID' })
        }
    
    
        static init(sequelize) {
            this.sequelize = sequelize
            return super.init(attributes, {...options, sequelize})
        }
    }
    
    module.exports = ViewLocationSensorsCount
    

    最后,在您的 Location 模型中,您设置了与 Sensor 模型的 hasOne 关系。

    【讨论】:

      【解决方案3】:

      使用 Sequelize 计算关联条目

      Location.findAll({
          attributes: { 
              include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']] 
          }, // Sequelize.col() should contain a attribute which is referenced with parent table and whose rows needs to be counted
          include: [{
              model: Sensor, attributes: []
          }],
          group: ['sensors.location'] // groupBy is necessary else it will generate only 1 record with all rows count
      })
      

      注意:

      不知何故,这个查询会产生一个错误,例如 sensors.location is not exist in field list。 这是因为 subQuery 是由上述 sequelize 查询形成的。

      所以解决方案是提供 subQuery: false like example

      Location.findAll({
              subQuery: false,
              attributes: { 
                  include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']] 
              },
              include: [{
                  model: Sensor, attributes: []
              }],
              group: ['sensors.location']
          })
      

      注意: **有时这也可能会在 mysql 配置中生成错误 bcz,默认情况下在 sqlMode 中包含 only-full-group-by,需要将其删除才能正常工作。

      错误将如下所示..**

      错误:SELECT 列表的表达式 #1 不在 GROUP BY 子句中,并且包含在功能上不依赖于 GROUP BY 子句中的列的非聚合列“db.table.id”;这与 sql_mode=only_full_group_by 不兼容

      所以要解决这个错误,请遵循这个答案

      SELECT list is not in GROUP BY clause and contains nonaggregated column .... incompatible with sql_mode=only_full_group_by

      现在这将成功生成所有关联的计数

      希望这会对您或其他人有所帮助!

      【讨论】:

      • 很酷,但是如果您想向传感器表添加条件怎么办。例如,表中有一个 user_id 字段,我们如何过滤呢?这可能吗?
      • 是的,你可以,传递其中的 where 属性包括第 0 个索引对象以及模型和属性
      【解决方案4】:
      Location.findAll({
              attributes: { 
                  include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
              },
              include: [{
                  model: Sensor, attributes: []
              }]
          });
      

      它有效。但是当我添加“限制”时,我得到了错误:传感器未定义

      【讨论】:

      • 你可以试试 subQuery: false in query 选项。
      • @BhavyaSanchaniya 感谢添加 subQuery: false 我可以使用限制和偏移
      【解决方案5】:

      findAll()include()sequelize.fn() 一起用于COUNT

      Location.findAll({
          attributes: { 
              include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
          },
          include: [{
              model: Sensor, attributes: []
          }]
      });
      

      或者,您可能还需要添加group

      Location.findAll({
          attributes: { 
              include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
          },
          include: [{
              model: Sensor, attributes: []
          }],
          group: ['Location.id']
      })
      

      【讨论】:

      • 感谢您的回答。这正确计算了 sensourCount 字段,但它还包括来自结果中的 sensors 表的字段。此外,虽然它执行的 SQL 查询显示它包含 locations 表中的所有字段,但它们不包含在结果中(它在 then 子句中返回的对象)。
      • @MikkoP 好的,让我快速构建一个示例并调试,谢谢。
      • @MikkoP 您能否编辑问题并发布您的模型定义?谢谢。
      • 我添加了模型定义。这是您的查询输出的内容。 pastebin.com/PwnctW1Y
      • @MikkoP 谢谢,请查看更新。我认为您现在应该能够达到预期的结果。谢谢。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多