Wednesday, 5 April 2017

Sequelize generates bad SQL on queries with nested includes

Given the following models :

sequelize.define('Tag', {
        id : {
            type : INTEGER,
            allowNull : false,
            primaryKey : true,
            autoIncrement : true
        },
        slug : {
            type : STRING(40),
            allowNull : false
        },
        moderated : {
            type : BOOLEAN,
            allowNull : false,
            defaultValue : false
        },
        moderatedBy : {
            type : INTEGER,
            defaultValue : null,
            field : 'moderated_by'
        },
        moderatedDate : {
            type : DATE,
            defaultValue : null,
            field : 'moderated_date'
        }
    }, {
        tableName : 'tag',
        classMethods : {
            associate : (models) => {
                model.belongsToMany(models.TagType, {
                    as : 'types',
                    through : models.TagHasTagType,
                    foreignKey : 'tag_id',
                    otherKey : 'tag_type_id'
                })
                /**
                 * To get the TagTranslation objects for this tag :
                 * 
                 * tagInstance.getTranslations()
                 */
                model.hasMany(models.TagTranslation, {
                    as : 'translations',
                    foreignKey : 'tag_id'
                })
            }
        }
    })
sequelize.define('Language', {
        id : {
            type : INTEGER,
            allowNull : false,
            primaryKey : true,
            autoIncrement : true
        },
        fullname : {
            type : STRING(20),
            allowNull : false
        },
        // isoCode for ISO 639-1 language code
        isoCode : {
            type : STRING(2),
            allowNull : false,
            field : 'iso_639_1'
        },
        daletLocale : {
            type : STRING(2),
            allowNull : false,
            field : 'dalet_locale'
        },
        isRTL : {
            type : BOOLEAN,
            defaultValue : false,
            field : 'is_rtl'
        },
        live : {
            type : BOOLEAN,
            defaultValue : false
        },
        websiteSubdomain : {
            type : STRING(20),
            defaultValue : null,
            field : 'website_subdomain'
        },
        collateLocale : {
            type : STRING(10),
            allowNull : false,
            field : 'collate_locale'
        },
        priority : {
            type : INTEGER,
            defaultValue : 1
        }
    }, {
        tableName : 'lang'
    })

sequelize.define('TagTranslation', {
        tagId : {
            type : INTEGER,
            allowNull : false,
            primaryKey : true,
            references : {
                model : 'tag',
                key : 'id'
            },
            field : 'tag_id'
        },
        langId : {
            type : INTEGER,
            allowNull : false,
            primaryKey : true,
            references : {
                model : 'lang',
                key : 'id'
            },
            field : 'lang_id'
        },
        slug : {
            type : STRING(40),
            allowNull : false
        },
        title : {
            type : STRING(80),
            allowNull : false
        },
        hotTopic : {
            type : BOOLEAN,
            allowNull : false,
            defaultValue : false,
            field : 'hot_topic'
        },
        hotTopicStartDate : {
            type : DATE,
            defaultValue : null,
            field : 'hot_topic_start_date'
        },
        hotTopicEndDate : {
            type : DATE,
            defaultValue : null,
            field : 'hot_topic_end_date'
        }
    }, {
        tableName : 'tag_translation',
        classMethods : {
            associate : (models) => {
                model.belongsTo(models.Language, {
                    as : 'language',
                    foreignKey : 'lang_id'
                })
            }
        }
    })

I want to be able to retrieve all the tags that have a translation with a given slug in a given language. Example : tags with slug like 'cam%' and language = 'english'

For that, I use the following code :

function getTagsBySlugAndLanguage({ slug, language }) {
    const options = {
        limit : 100,
        offset : 0
    }

    const include = [{
                as : 'translations',
                model : TagTranslation,
                where : {
                    slug : {
                        like : slug
                    }
                },
                include : [{
                    as : 'language',
                    model : Language,
                    where : {
                        fullname : {
                            like : language
                        }
                    }
                }]
            }]

            return Tag.findAndCountAll(
                Object.assign({}, options, { include })
            )
}

It generates me two different queries (one for the count, and one for the actual fetch).

Count query :

SELECT count(`Tag`.`id`) AS `count`
FROM `tag` AS `Tag`
INNER JOIN `tag_translation` AS `translations`
    ON `Tag`.`id` = `translations`.`tag_id`
        AND `translations`.`slug` LIKE 'cam%'
INNER JOIN `lang` AS `translations.language`
    ON `translations`.`lang_id` = `translations.language`.`id`
        AND `translations.language`.`fullname` LIKE '%en%';

Fetch query :

SELECT `Tag`.*,
         `translations`.`tag_id` AS `translations.tagId`,
         `translations`.`lang_id` AS `translations.langId`,
         `translations`.`slug` AS `translations.slug`,
         `translations`.`title` AS `translations.title`,
         `translations`.`hot_topic` AS `translations.hotTopic`,
         `translations`.`hot_topic_start_date` AS `translations.hotTopicStartDate`,
         `translations`.`hot_topic_end_date` AS `translations.hotTopicEndDate`,
         `translations`.`lang_id` AS `translations.lang_id`,
         `translations`.`tag_id` AS `translations.tag_id`
FROM 
    (SELECT `Tag`.`id`,
         `Tag`.`slug`,
         `Tag`.`moderated`,
         `Tag`.`moderated_by` AS `moderatedBy`,
         `Tag`.`moderated_date` AS `moderatedDate`,
         `translations.language`.`id` AS `translations.language.id`,
         `translations.language`.`fullname` AS `translations.language.fullname`,
         `translations.language`.`iso_639_1` AS `translations.language.isoCode`,
         `translations.language`.`dalet_locale` AS `translations.language.daletLocale`,
         `translations.language`.`is_rtl` AS `translations.language.isRTL`,
         `translations.language`.`live` AS `translations.language.live`,
         `translations.language`.`website_subdomain` AS `translations.language.websiteSubdomain`,
         `translations.language`.`collate_locale` AS `translations.language.collateLocale`,
         `translations.language`.`priority` AS `translations.language.priority`
    FROM `tag` AS `Tag`
    INNER JOIN `lang` AS `translations.language`
        ON `translations`.`lang_id` = `translations.language`.`id`
            AND `translations.language`.`fullname` LIKE '%en%'
    WHERE 
        (SELECT `tag_id`
        FROM `tag_translation` AS `TagTranslation`
        WHERE (`TagTranslation`.`tag_id` = `Tag`.`id`
                AND `TagTranslation`.`slug` LIKE 'cam%') LIMIT 1 ) IS NOT NULL LIMIT 0, 100) AS `Tag`
    INNER JOIN `tag_translation` AS `translations`
    ON `Tag`.`id` = `translations`.`tag_id`
        AND `translations`.`slug` LIKE 'cam%';

The fetch query results in the following:

SequelizeBaseError: ER_BAD_FIELD_ERROR: Unknown column 'translations.lang_id' in 'on clause'

It seems to be a bug with Sequelize, and I have no idea how I can get around this :(

Is there a better way to perform this?

Versions:

Dialect: mysql Database version: 5.6.35 Sequelize version: 3.30.2



via Shahor

No comments:

Post a Comment