Проблема упорядочения с включением модели в sequelize v6 [Решена] — это не была ошибка sequelize

#mysql #node.js #sequelize.js

#mysql #node.js #sequelize.js

Вопрос:

Я пытаюсь добавить предложение order в функцию метода, но получил эту неизвестную ошибку столбца.

Error: Unknown column 'Product.createdAt' in 'order clause'

Итак, я просмотрел SQL и обнаружил, что проблема заключается в имени столбца в предложении order, сгенерированном sequelize.

Я буду очень благодарен за любую идею по решению этой проблемы.

Система

  • windows 10
  • nodejs v13.11.0
  • sequelize [CLI: 6.2.0, ORM: 6.3.4]

Маршрутизатор

 
...

const models = require('./models')
const { Product, ViewItem, Category, ... } = models

...

//Middleware apply the req.filter, generated filter object will be like this

const req = {
   ...req,
   filter : {
      order : [
         [{ model : Product, as : 'Product'}, 'rating', 'desc'],
         ['createdAt', 'desc']
      ],
      ...
   }
}

const products = await ViewItem.listAndApply({
                subquery : false,
                include: [
                    { model : Brand, as : 'Brand', required: true },
                    { 
                        model : Category,
                        as : 'Category', 
                        required: true,
                        attributes : [ 'id', 'level', 'name' ],
                        where : req.filters.toJson({ namespace : 'Category' }) 
                    },
                    { model : ProductImage, as : 'ProductImage', required: false },
                    { model : ProductStatus, as : 'ProductStatus', required: false },
                    { model : ProductType, as : 'ProductType', required: false },
                    { model : ProductRating, as : 'ProductRating', required : true }
                ],
                where : req.filters.toJson(),
                limit : limit,
                offset : offset,
                order : req.filters.order
...

 

models/ViewItem.js

 ViewItem.listAndApply = async function (options) {
    const {
      ProductView,
      Product,
    } = require('.')

    const payload = lodash.cloneDeep(options)
    let {
      include,
    } = payload

    if (!include) include = []
    .
    .
    .
    const items = await ViewItem.findAll({
      subquery: false,
      include: [
        .
        .
        .
        {
          model: Product,
          as: 'Product',
          required: true,
          where: product.where,
          include: include
        }
      ],
      where: where,
      limit: limit,
      order: order
    })

    //Please don't mind the applyView(), this function do nothing with sequelize level.
    
    return items.map(item => applyView(item))
  }
 

Этот SQL сломан (то, что я получил от sequelize)

 SELECT 
  `ViewItem`.*, 
.
.
.
FROM 
  (
    SELECT 
      .
      .
      .
      `Product`.`rating` AS `Product.rating`, 
      .
      .
      .
    FROM 
      `ViewItems` AS `ViewItem` 
.
.
.
ORDER BY 
  `Product`.`rating` ASC,
  `ViewItem`.`createdAt` DESC;
 

This SQL works fine (What I want to generate)

 SELECT 
  `ViewItem`.*, 
.
.
.
FROM 
  (
    SELECT 
      .
      .
      .
      `Product`.`rating` AS `Product.rating`, 
      .
      .
      .
    FROM 
      `ViewItems` AS `ViewItem` 
.
.
.
ORDER BY 
  `Product.rating` ASC,
  `ViewItem`.`createdAt` DESC;
 

What I tried to solve this problem.

 // SQL broken with syntax error, col name return is " ``. "
req.filter.order.push([sequelize.literal('Product.rating'), 'desc'])

// also broken with same error above
req.filter.order.push([sequelize.col('Product.rating'), 'desc'])

// same

 

I’m
Any idea for resolving this error?


Model Code added

models/Product.js

 'use strict'

module.exports = (sequelize, DataTypes) => {
  const Product = sequelize.define('Product', {
    name: DataTypes.STRING,
    sku: DataTypes.STRING,
    supplier: DataTypes.STRING,
    manufacturer: DataTypes.STRING,
    thumbnail: DataTypes.STRING,
    wholeSalePrice: DataTypes.INTEGER,
    retailPrice: DataTypes.INTEGER,
    originalPrice: DataTypes.INTEGER,
    discount: DataTypes.FLOAT,
    description: DataTypes.STRING,
    domestic: {
      type: DataTypes.BOOLEAN,
      defaultValue: true
    },
    underAge: {
      type: DataTypes.BOOLEAN,
      defaultValue: true
    },
    display: {
      type: DataTypes.BOOLEAN,
      defaultValue: true
    },
    views: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    reviews : {
      type : DataTypes.INTEGER,
      defaultValue : 0
    },
    jjim : {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    sold: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    rating: {
      type: DataTypes.FLOAT,
      defaultValue: 0
    }
  }, { timestamps: true, paranoid: true })

  Product.associate = function(models) {
    // associations can be defined here
    Product.belongsToMany(models.Category, { as: 'Category', through: 'ProductCategory', onDelete: 'CASCADE' })
    Product.belongsToMany(models.ProductView, { as: 'ProductView', through: 'ProductViewMap', onDelete: 'CASCADE' })
    Product.belongsTo(models.Brand, { as: 'Brand' })
    Product.hasMany(models.ProductImage, { as: 'ProductImage', foreignKey: 'ProductId' , onDelete: 'CASCADE' })
    Product.hasMany(models.ProductItem, { as: 'ProductItem', foreignKey: 'ProductId', onDelete: 'CASCADE' })
    Product.belongsTo(models.ProductStatus, { as: 'ProductStatus' })
    Product.belongsTo(models.ProductType, { as: 'ProductType' })
    Product.hasOne(models.ProductRating, { foreignKey : 'ProductId', as : 'ProductRating' })
    Product.hasMany(models.UserJjim, { as : 'Jjims'})
  }

  Product.afterCreate(async (product, option) =>{
    const {
      sequelize,
      Product,
      ProductViewMap,
      ViewItem,
      ProductRating
    } = require('.')

    const { id, name, thumbnail } = product
    const DEFAULT_VIEW = 1

    // await ProductViewMap.create({ ProductId : id, ProductViewId : DEFAULT_VIEW })
    const item = await ViewItem.create({ ProductId : id, ProductViewId : DEFAULT_VIEW })
    console.log(item)
    await ProductRating.create({ ProductId : id })
  })

  Product.prototype.findActiveItem = async function(){
    const { ViewItem, View } = require('.')

    return ViewItem.findOne({
      subquery : false,
      include : [
        { model : View, as : 'View', required : true }
      ],
      where : {
        display : true,
        ProductId : this.id
      }
    })
  }

  Product.prototype.findActiveView = async function(){
    const { ViewItem, View } = require('.')

    return View.findOne({
      subquery: false,
      include : [
        { 
          model : View, 
          as : 'View', 
          required : true,
          include : [
            {
              model : ViewItem,
              as : 'ViewItem',
              required : true,
              where : {
                display : true
              }
            }
          ]
        }
      ]
    })
  }

  return Product
}

 

models/ViewItem.js

 'use strict'

const lodash = require('lodash')

module.exports = (sequelize, DataTypes) => {
  const ViewItem = sequelize.define('ViewItem', {
    name: DataTypes.STRING,
    thumbnail: DataTypes.STRING,
    retailPrice: DataTypes.INTEGER,
    discount: DataTypes.FLOAT,
    staticDiscount: DataTypes.INTEGER,
    description: DataTypes.STRING,
    promotion: {
      type: DataTypes.JSON,
      defaultValue: null
    },
    position: DataTypes.INTEGER,
    display: DataTypes.BOOLEAN
  }, {
    timestamps: true,
    paranoid: true
  })

  ViewItem.associate = function (models) {
    // associations can be defined here
    ViewItem.belongsTo(models.ProductView, {
      as: 'ProductView',
      foreignKey: 'ProductViewId',
      onDelete: 'CASCADE'
    })
    ViewItem.belongsTo(models.Product, {
      as: 'Product',
      onDelete: 'CASCADE'
    })
  }

  /* Hook */
  ViewItem.afterCreate(async function (item) {
    const {
      Product,
      ProductViewMap
    } = require('.')

    const {
      id,
      ProductId,
      ProductViewId
    } = item

    /* Join table에 row를 추가합니다 */
    await ProductViewMap.create({
      ProductId,
      ProductViewId
    })

    /* View 아이템중에 같은 Product를 가르키는 상품의 노출 설정을 false로 합니다. */
    const product = await Product.findOne({
      where: {
        id: ProductId
      }
    })

    await ViewItem.update({
      display: false,
    }, {
      where: {
        ProductId: ProductId
      }
    })

    /* 생성되는 ViewItem의 초기 display 속성은 Product의 display를 따릅니다. */
    await this.update({
      display: product.display
    }, {
      where: {
        id: id
      }
    })
  })

  ViewItem.listAndApply = async function (options) {
    const {
      ProductView,
      Product,
    } = require('.')

    const payload = lodash.cloneDeep(options)
    let {
      include,
      where,
      order,
      limit,
      offset,
      product
    } = payload

    if (!include) include = []
    if (!product) product = {}

    where = where ? where : null
    order = order ? order : null
    limit = limit ? limit : null
    offset = offset ? offset : null
    product.where = product.where ? product.where : null

    const items = await ViewItem.findAll({
      subquery: false,
      include: [{
          model: ProductView,
          as: 'ProductView',
          required: true
        },
        {
          model: Product,
          as: 'Product',
          required: true,
          where: product.where,
          include: include
        }
      ],
      where: where,
      limit: limit,
      order: order
    })

    return items.map(item => applyView(item))
  }

  return ViewItem
}


/* private function */
const applyView = (item) => {
  if (!item) return null
  if (!item.Product) return null
  if (!item.ProductView) return null

  const product = lodash.cloneDeep(item.Product.toJSON())
  const view = lodash.cloneDeep(item.ProductView.toJSON())
  /* 
    View를 적용합니다.
  
    #1. 가격정보를 수정합니다.
    #2. 스티커와 태그를 추가합니다.
    #3. 상세페이지 url을 추가합니다.
  */

  if (view.additionalDiscount) {
    const discount = product.discount   view.additionalDiscount
    const retailPrice = Math.floor(product.originalPrice * (1 - discount / 100))

    product.discount = discount
    product.retailPrice = retailPrice
  } else if (view.discount) {
    const retailPrice = Math.floor(product.retailPrice * (1 - view.discount / 100))
    const discount = Math.floor((1 - retailPrice / product.originalPrice) * 100)

    product.retailPrice = retailPrice
    product.discount = discount
  }

  if (view.staticDiscount) {
    const retailPrice = product.retailPrice - view.staticDiscount
    const discount = Math.floor((1 - retailPrice / product.originalPrice) * 100)

    product.retailPrice = retailPrice
    product.discount = discount
  }

  if (view.tag) product.tag = view.tag

  if (view.sticker) product.sticker = sticker

  product.detailUrl = view.detailUrl ? `${view.detailUrl}/${item.id}` : `/products/${item.id}`

  /*
    ViewItem 

    #1. 상품정보를 수정합니다.
    #2. 가격정보를 수정합니다.
    #3. retailPrice가 있다면 기존 계산값을 무시하고 이 값을 사용합니다.
  */

  if (item.name) product.name = item.name
  if (item.thumbnail) product.thumbnail = item.thumbnail
  if (item.description) product.description = item.description

  if (item.discount) {}
  if (item.staticDiscount) {}

  if (item.retailPrice) product.retailPrice = item.retailPrice

  return ({
    ...product,
    ProductView: view,
    id: item.id,
    ProductId: item.ProductId,
    ProductViewId: item.ProductViewId
  })
}
 

Полный код составляет около 500 строк, я сократил некоторые несвязанные части для удобства чтения.
Другие модели не связаны с предложением order.

Комментарии:

1. Пожалуйста, поделитесь кодом миграции, если вы используете, и, пожалуйста, поделитесь кодом модели.

Ответ №1:

Не могли бы вы попробовать ниже, чтобы упорядочить литерал sequelize:

 req.filter.order.push([sequelize.literal(`"Product.rating"`), 'desc'])
// OR
req.filter.order.push([sequelize.literal("`Product.rating`"), 'desc'])
 

Комментарии:

1. Оба » Procut.rating » и "Product.rating" не работает для меня. оба производят » `. DESC » для моего предложения order. Любое другое решение для добавления буквального запроса в предложение order? если да, должен ли я запрашивать через интерфейс запросов, это мой единственный вариант для решения этой проблемы?

2. Извини, брат. Моя проблема была не в самом sequelize, а в моем методе экземпляра. Я скопировал объект option перед запросом, чтобы предотвратить мутацию объекта option, используя Lodash.deepClone . Но оказывается, что даже deepClone не может скопировать цепочку прототипов. Я проверил это с sequelize.literal('Product.rating') instanceof sequelize.Utils.SequelizeMethod помощью и copied instaceof sequelize.Utils.SequelizeMethod . Это причина, по которой я получил Error: Order must be type of array or instance of a valid sequelize method. . Спасибо за ваши усилия @Abishek Shah

3. для меня это сработало для упорядочения по включенному полю модели.

Ответ №2:

Моя проблема была не в самом sequelize, а в моем методе экземпляра. Я скопировал объект option перед запросом, чтобы предотвратить мутацию объекта option, используя lodash.cloneDeep() . Но оказывается, что даже глубокий клон не может скопировать цепочку прототипов.

 const payload = lodash.cloneDeep(options)//I massed up in here
 

Я могу продемонстрировать это, выполнив приведенный ниже код.

 const sequelize = require('sequelize')
const lodash = require('lodash')

const orginal = sequelize.literal('Product.rating')
const copied = lodash.cloneDeep(original)

console.log(original instanceof sequelize.Utils.SequelizeMethod )//true
console.log(copied instanceof sequelize.Utils.SequelizeMethod)//false
 

Именно по этой причине я получил

 Error: Order must be type of array or instance of a valid sequelize method.
 

Поскольку copied объект не имеет указания цепочки прототипов SequelizeMethod внутри генератора запросов sequlieze, процесс не смог соответствовать моему буквальному запросу, и это привело к тому, что я получил неработающий запрос.

В то же время я все еще не могу разрешить ошибку неизвестного имени столбца в предложении order при передаче массива для option.order аргумента. Я abstract/query-generator.js кратко взглянул и получил некоторое представление о решении этой проблемы. Генерация предложения order в getQueryOrders() , было бы даже лучше, чем сейчас, если генератор запросов order ссылается на выбранное имя столбца.

 SELECT 
  `ViewItem`.*, 
.
.
.
FROM 
  (
    SELECT 
      .
      .
      .
      `Product`.`rating` AS `Product.rating`,
      #this column name should be equal to order clause's column name
      .
      .
      .
    FROM 
      `ViewItems` AS `ViewItem` 
.
.
.
ORDER BY 
  `Product`.`rating` ASC,#otherwise, it invokes unknow column name error.
  `ViewItem`.`createdAt` DESC;

# Then how about referring the selected column name in the where and order clause
# instead of managing each clause loosely?
 

Комментарии:

1. Если вы все еще хотите клонировать объект, вы можете попробовать использовать npm rfdc — npmjs.com/package/rfdc . В документации говорится, что он также клонирует prototype amp; properties. Я сам не пробовал, но это может стоить попробовать.

2. Я думаю, что объект клонирования все еще будет полезен, поскольку он может предотвратить изменение исходного объекта. Звучит так, что я мог бы это использовать. Спасибо, что дали мне знать @AbhishekShah