How to include model in belongstToMany relation in Sequelize - mysql

I want to add in my sequelize query model, but i don't know how to do this correctly.
I have two tables: Regions and Countries. There is a relation between them called belongsToMany that is going through the table region_countries, there i have foreign keys region_id and country_id. For Region and Countries i have table with translation, for example: table translate_trgions has region and language_code name (en, fr, ru, etc.). And i am getting list of regions with countries with their translations.
My Region model
class Region extends Model {}
Region.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
},
{ sequelize, timestamps: false, tableName: 'regions', modelName: 'Region' }
);
class RegionTranslate extends Model {}
RegionTranslate.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: true,
},
region_id: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: Region,
key: 'id',
},
},
language_code: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: Language,
key: 'code',
},
},
},
{
sequelize,
timestamps: false,
tableName: 'regions_description',
modelName: 'RegionTranslate',
}
);
class RegionsCountries extends Model {}
RegionsCountries.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
region_id: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: Region,
key: 'id',
},
},
country_id: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: Country,
key: 'id',
},
},
},
{
sequelize,
timestamps: false,
tableName: 'region_countries',
modelName: 'RegionsCountries',
}
);
Region.hasMany(RegionTranslate, {
foreignKey: 'region_id',
as: 'translate_regions',
});
RegionTranslate.belongsTo(Region, { foreignKey: 'region_id' });
Region.belongsToMany(Country, {
through: RegionsCountries,
foreignKey: 'region_id',
});
Country.belongsToMany(Region, {
through: RegionsCountries,
foreignKey: 'country_id',
});
Region.belongsToMany(CountryTranslate, {
through: RegionsCountries,
as: 'translate_countries',
foreignKey: 'country_id',
otherKey: 'id',
});
My Country Model
class Country extends Model {}
Country.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
code: {
type: DataTypes.STRING,
allowNull: false,
},
unicode: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{ sequelize, timestamps: false, tableName: 'countries', modelName: 'Country' }
);
class CountryTranslate extends Model {}
CountryTranslate.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
language_code: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Language,
key: 'code',
},
},
country_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Country,
key: 'id',
},
},
name: {
type: DataTypes.BOOLEAN,
allowNull: true,
},
description: {
type: DataTypes.BOOLEAN,
allowNull: true,
},
},
{
sequelize,
timestamps: false,
tableName: 'countries_description',
modelName: 'CountryTranslate',
}
);
Country.hasMany(CountryTranslate, {
foreignKey: 'country_id',
as: 'translate_countries',
});
CountryTranslate.belongsTo(Country, { foreignKey: 'country_id' });
Country.belongsToMany(Region, {
through: RegionsCountries,
foreignKey: 'country_id',
});
My query in RegionController
const regions = await Region.findAll({
attributes: {
include: [[Sequelize.literal('translate_regions.name'), 'name']],
},
include: [
{
model: Country,
attributes: {
include: [[Sequelize.literal('translate_countries.name'), 'name']],
},
through: {
attributes: [],
},
},
{
model: RegionTranslate,
as: 'translate_regions',
where: { language_code: language },
attributes: [],
},
{
model: CountryTranslate,
as: 'translate_countries',
},
],
});
The output i'm getting
"regions": [
{
"id": 4,
"name": "Europe",
"Countries": [
{
"id": 3,
"code": "dza",
"unicode": "12",
"name": "Ангола"
}
],
"translate_countries": [
{
"id": 10,
"language_code": "ru",
"country_id": 5,
"name": "Ангола",
"description": null,
"RegionsCountries": {
"id": 10,
"region_id": 4,
"country_id": 4
}
}
]
}
]
The output that i want
"regions": [
{
"id": 4,
"name": "Europe",
"Countries": [
{
"id": 4,
"code": "and",
"unicode": "20",
"name": "Andorra"
}
]
}
]

When setting up your Model associations for many-to-many through a relational table, you need to specify the columns in the relational table that is the foreign key for your source table primary key, as well as the column that is the "other" foreign key for the related table primary key. It's helpful to specify an as property as well to identify the relationship.
This is an example, check other relationships as well.
// use otherKey to indicate the relationship
// country.id -> country_id, region_id -> region.id
Country.belongsToMany(Region, {
as: 'regions',
through: RegionsCountries,
foreignKey: 'country_id',
otherKey: 'region_id'
});
When you query for related records we can no specify the through and as property on the query.
Country.findByPk(countryId, {
include: [{
model: Region,
through: RegionsCountries,
as: 'regions',
}],
});

Related

Sequelize M:N association not creating through table record

When I create a recipe with associated tags using the through option, no record is created in the joining table in the mysql database I'm connected to.
Here are my models definitions:
export const Recipe = sequelize.define('Recipe', {
// Model attributes are defined here
title: {
type: DataTypes.STRING,
allowNull: false
},
image: {
type: DataTypes.STRING,
allowNull: true
},
prepTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
cookTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
totalTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
servings: {
type: DataTypes.INTEGER,
allowNull: false
},
rating: {
type: DataTypes.INTEGER,
allowNull: false
},
notes: {
type: DataTypes.STRING, allowNull: true
},
}, {
// Other model options go here
tableName: 'Recipes'
});
export const Tag = sequelize.define('Tag', {
// Model attributes are defined here
name: {
type: DataTypes.STRING,
allowNull: false
},
}, {
// Other model options go here
tableName: 'Tags'
});
export const RecipeTag = sequelize.define('RecipeTag', {
// Model attributes are defined here
}, {
// Other model options go here
timestamps: false,
tableName: 'RecipeTags'
});
Here are my associations:
Recipe.belongsToMany(Tag, {
through: RecipeTag,
foreignKey: 'recipeId',
as: 'tags'
})
Tag.belongsToMany(Recipe, {
through: RecipeTag,
foreignKey: 'tagId',
as: 'recipes'
})
Here is the create call:
Recipe.create(args, {
model: Tag,
through: RecipeTag,
as: 'tags'
});
And here is the data:
{
"title": "Test Recipe",
"image": "test",
"prepTime": 20,
"cookTime": 40,
"totalTime": 60,
"servings": 2,
"rating": 5,
"categoryId": 1,
"tags": [
{
"name": "New tag",
"id": 1
}
],
}
With this set up the create method only creates a new recipe. How can I use the create method to add a record to the joining RecipeTags table at the same time as creating a new recipe? I've managed to get it working by doing something like this:
args.tags.map(async (tag: { tagId: number }) => {
await RecipeTag.create({tagId: tag.tagId, recipeId: recipe.id})
});
But I'd rather have it done in the create if it's possible.
You need to wrap the association options with include.
Recipe.create(args, {
include: {
model: Tag,
through: RecipeTag,
as: 'tags'
}
});
UPDATE:
In order to prevent the duplicates, you can add ignoreDuplicates option and data must include the primary key value.
{
"title": "Test Recipe",
...
"tags": [
{
"name": "New tag",
"id": 1 # this is important
}
]
}
Then
Recipe.create(args, {
include: {
model: Tag,
through: RecipeTag,
as: 'tags',
ignoreDuplicates: true // Add this
}
});
There were some bugs for this option, I suggest you to use the newer version of Sequelize, if you haven't updated lately.

Sequelize returning one result when counting AVG in Eager Loading

I have been trying to fix this problem for a day now but no luck. I am using Sequelize With Nodejs and MySQL dialect.
I am querying for Influencers whilst also calculating their average ratings from a InfluencerRating record. They are associated through a oneToMany relation.
Here is my Influencer modal:
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('influencer', {
id: {
autoIncrement: true,
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
display_name: {
type: DataTypes.STRING(255),
allowNull: false
},
link_facebook: {
type: DataTypes.STRING(255),
allowNull: true
},
link_soundcloud: {
type: DataTypes.STRING(255),
allowNull: true
},
link_twitter: {
type: DataTypes.STRING(255),
allowNull: true
},
link_bandcamp: {
type: DataTypes.STRING(255),
allowNull: true
},
link_spotify: {
type: DataTypes.STRING(255),
allowNull: true
},
link_instagram: {
type: DataTypes.STRING(255),
allowNull: true
},
link_deezer: {
type: DataTypes.STRING(255),
allowNull: true
},
link_youtube: {
type: DataTypes.STRING(255),
allowNull: true
},
link_website: {
type: DataTypes.STRING(255),
allowNull: true
},
description_english: {
type: DataTypes.STRING(255),
allowNull: true
},
description_french: {
type: DataTypes.STRING(255),
allowNull: true
},
information_english: {
type: DataTypes.STRING(255),
allowNull: true
},
information_french: {
type: DataTypes.STRING(255),
allowNull: true
},
enabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 0
},
profile_image: {
type: DataTypes.STRING(255),
allowNull: true
},
banner_image: {
type: DataTypes.STRING(255),
allowNull: true
},
admin_public_status: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 1
}
}, {
sequelize,
tableName: 'influencer',
timestamps: false,
underscored: true,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "id" },
]
},
]
});
};
Here is my InfluencerRating modal:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('influencer_rating', {
id: {
autoIncrement: true,
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
description: {
type: DataTypes.STRING(255),
allowNull: false
},
influencer_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'influencer',
key: 'id'
}
},
rating: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
sequelize,
tableName: 'influencer_rating',
timestamps: false,
underscored: true,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "id" },
]
},
{
name: "type_id",
using: "BTREE",
fields: [
{ name: "influencer_id" },
]
},
]
});
};
And here is my associations between them both:
influencer_rating.belongsTo(influencer, { as: "influencer", foreignKey: "influencer_id"});
influencer.hasMany(influencer_rating, { as: "influencer_ratings", foreignKey: "influencer_id"});
Here is my query to find influencers and calculate their average ratings:
var influencers = await Influencer.findAll(
{
where: {
[db.Op.and]: [
{
enabled: true
},
{
display_name: {
[db.Op.like]: '%' + filter + '%'
}
},
{
admin_public_status: true
}
],
},
include: [
[
{
model: InfluencerRating,
as: "influencer_ratings",
required: false,
attributes: [[Sequelize.fn('AVG', Sequelize.col('rating')), 'rating']]
}
],
limit: 10,
offset: 0,
group: ['id']
}
);
The query should be returning 2 influencer Objects, but I am only getting one.
I get the wanted behaviour if I delete my :
attributes: [[Sequelize.fn('AVG', Sequelize.col('rating')), 'rating']]
I have tried calculating my average outside of my InfluencerRating include but not luck.
Made some changes to findAll. Can give it a try:
var influencers = await Influencer.findAll({
// whenever hasMany used in include, the main query gets converted to subquery
// if limit + offset is applied
subQuery: false, // <----- instructs not to make a sub query
attributes: {
include: [
[Sequelize.fn('AVG', Sequelize.col('influencer_ratings.rating')), 'total_rating']
],
},
where: {
[db.Op.and]: [
{ enabled: true },
{ display_name: { [db.Op.like]: '%' + filter + '%' } },
{ admin_public_status: true }
],
},
include: [ // removed extra nesting of array
{
model: InfluencerRating,
as: "influencer_ratings",
required: false,
attributes: [],
}
],
limit: 10,
offset: 0,
group: [
'id',
// 'influencer_ratings.influencer_id', // add if sql error occurs
],
order: [ // if needed
[Sequelize.literal('total_rating'), 'DESC'],
]
});
Few reference links:
Disable sub query generation(done by sequelize) when hasMany/belongsToMany relation included - https://github.com/sequelize/sequelize/issues/1756#issuecomment-54748245
Using SQL sub queries with ordering: https://sequelize.org/master/manual/sub-queries.html
Alternate approach:
var influencers = await Influencer.findAll({
attributes: {
include: [
[Sequelize.literal(`(
SELECT
AVG("influencer_ratings"."rating") AS "avg_rating"
FROM
"influencer" AS "influencer1"
LEFT OUTER JOIN (
"influencer_ratings" AS "influencer_ratings"
) ON "influencer1"."id" = "influencer_ratings"."influencer_id"
WHERE
"influencer1"."id" = "influencer"."id"
GROUP BY
"influencer1"."id"
)`), 'avg_rating'], // now this is just a subquery and will not interfere with other includes
],
},
where: {
[db.Op.and]: [
{ enabled: true },
{ display_name: { [db.Op.like]: '%' + filter + '%' } },
{ admin_public_status: true }
],
},
include: [ // removed extra nesting of array
// influencer_ratings include not needed now
// add other includes as needed
],
limit: 10,
offset: 0,
order: [ // if needed
[Sequelize.literal('avg_rating'), 'DESC'],
]
});

Obtaining key from Join Table in a Sequelize include

I'm working in Node JS and Sequelize/MySQL. I have two tables Agent and Neighborhood as well as a join table AgentNeighborhood. I'd like, when querying these tables to obtain the join table key.
AgentNeighborhood
const AgentNeighborhood = sequelize.define('agentneighborhood', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
agent_id: {
type: DataTypes.INTEGER,
required: true,
references: {
model: 'Agent',
key: 'id'
},
allowNull: false
},
neighborhood_id: {
type: DataTypes.INTEGER,
required: true,
references: {
model: 'Neighborhood',
key: 'id'
},
allowNull: false
},
},
{
freezeTableName: true,
underscore: true
})
Neighborhood
const Neighborhood = sequelize.define('neighborhood', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
...
...
},
{
freezeTableName: true,
})
Neighborhood.associate = (models) => {
Neighborhood.belongsToMany(models.agent, { through: 'agentneighborhood', foreignKey: 'neighborhood_id' })
}
Agent
const Agent = sequelize.define('agent', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
....
},
{
freezeTableName: true,
})
Agent.associate = (models) => {
Agent.belongsToMany(models.neighborhood, { through: 'agentneighborhood', foreignKey: 'agent_id' })
}
How can I query the keys so that my result will be as follows ?
{
id // the primary key for the AgentNeighborhood table
agent_id
agent {
id // should be same as agent_id
...
}
neighborhood_id
neighborhood {
id // should be same as neighborhood_id
...
}
}
I tried the following but get an error that neighborhood is not associated to agentneighborhood.
const agentneighborhoods = await models.agentneighborhood.findAll({where: {agent_id: 1}, include: [
{model: models.neighborhood},
{model: models.agent},
]})
return agentneighborhoods
You need to get list of neighbors associated with that agent_id.
const agentneighborhoods= await models.neighbors.findAll({
where: { "$agentneighborhood.agent_id$": agent_id },
include: [
{
as: "agentneighborhood",
model: models.agentneighborhood,
required: true
}
]
});
return agentneighborhoods

Sequelize: Using include for a joined table

I have 4 models: User, Skill, SkillToLearn, and SkillToTeach. The SkillToLearn and SkillToTeach contain two columns userId and skillId to keep track of a skill (given by skillId) that a user (given by userId) wants to learn or teach.
My goal is to use Sequelize's include statement such that I can return all users' data, including a list of skills they are learning and a list of skills they are teaching. I want a response similar to the following:
[
{
"id": 1,
"username": "janedoe",
"skillsLearning": [
{
"skillId": 1,
"name": "arts"
}
],
"skillsTeaching": [
{
"skillId": 2,
"name": "cooking"
}
]
}
]
However, instead, I'm getting the following response:
[
{
"id": 1,
"username": "janedoe",
"skillsLearning": [
{
"skillId": 1,
"Skill": {
"name": "arts"
}
}
],
"skillsTeaching": [
{
"skillId": 2,
"Skill": {
"name": "cooking"
}
}
]
}
]
I have tried to include Skill instead of SkillToLearn and SkillToTeach but I got an error saying that Skill is not associated to User. I am uncertain if my schemas and associations are incorrect.
User.js
const User = sequelize.define("User", {
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT
}
}, {
freezeTableName: true
});
User.associate = function (models) {
User.hasMany(models.SkillToLearn, {
as: "skillsLearning",
onDelete: "cascade",
foreignKey: "userId",
sourceKey: "id"
});
User.hasMany(models.SkillToTeach, {
as: "skillsTeaching",
onDelete: "cascade",
foreignKey: "userId",
sourceKey: "id"
});
};
Skill.js
const Skill = sequelize.define("Skill", {
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
}
}, {
freezeTableName: true
});
Skill.associate = function (models) {
Skill.hasMany(models.SkillToLearn, {
as: "usersLearning",
onDelete: "cascade",
foreignKey: "skillId",
sourceKey: "id"
});
Skill.hasMany(models.SkillToTeach, {
as: "usersTeaching",
onDelete: "cascade",
foreignKey: "skillId",
sourceKey: "id"
});
};
SkillToLearn.js
const SkillToLearn = sequelize.define("SkillToLearn", {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
skillId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
}
}, {
freezeTableName: true
});
SkillToLearn.associate = function (models) {
SkillToLearn.belongsTo(models.User, {
foreignKey: "userId",
targetKey: "id"
});
SkillToLearn.belongsTo(models.Skill, {
foreignKey: "skillId",
targetKey: "id"
});
};
SkillToTeach.js
const SkillToTeach = sequelize.define("SkillToTeach", {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
skillId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
}
}, {
freezeTableName: true
});
SkillToTeach.associate = function (models) {
SkillToTeach.belongsTo(models.User, {
foreignKey: "userId",
targetKey: "id"
});
SkillToTeach.belongsTo(models.Skill, {
foreignKey: "skillId",
targetKey: "id"
});
};
dataRoutes.js
db.User
.findAll({
attributes: ["id", "username"],
include: [
{
model: db.SkillToLearn,
as: "skillsLearning",
attributes: ["skillId"],
include: [
{
model: db.Skill,
attributes: ["name"]
}
]
},
{
model: db.SkillToTeach,
as: "skillsTeaching",
attributes: ["skillId"],
include: [
{
model: db.Skill,
attributes: ["name"]
}
]
}
]
})
.then(results => res.json(results));
});
Is there a way for me to get the skill's name without having it in an object? Should I just do multiple queries and construct the response using my own object literal? Thank you!

Sequelize says instanceMethod is not defined

I'm using sequelize to connect to a mysql db for development. I have a model called Dealer:
'use strict';
module.exports = function(sequelize, DataTypes) {
var Dealer = sequelize.define('Dealer', {
id: { allowNull: false, autoIncrement: true,
primaryKey: true, type: DataTypes.INTEGER.UNSIGNED },
...
created_at: { allowNull: false, type: DataTypes.DATE },
updated_at: { allowNull: false, type: DataTypes.DATE }
},
{underscored: true},
{
classMethods: {
associate: function(models) {
Dealer.hasMany(models.Job);
}
},
instanceMethods: {
getAllClientData: function(){
leads = [];
...
return leads;
},
}
});
return Dealer;
};
When I try to call the instance method on an object returned by a sequelize query in my dealerController.js file:
dealer.getAllClientData()
I get the error:
Unhandled rejection TypeError: dealer.getAllClientData is not a function
When i print the returned JSON to the console it reads as such:
{ dataValues:
{ id: 1,
....
}
...
'$modelOptions':
{ timestamps: true,
instanceMethods: {},
classMethods: {},
validate: {},
freezeTableName: false,
underscored: true,
underscoredAll: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: { id: '1' },
schema: null,
schemaDelimiter: '',
defaultScope: {},
scopes: [],
hooks: {},
indexes: [],
name: { plural: 'Dealers', singular: 'Dealer' },
omitNul: false,
...
}
...
}
Obviously my instanceMethod is not defined, and according to the sequelize docs I should have getters and setters available too.
I don't understand what step i'm missing here as I've read through much of the sequelize docs and even used their cli to generate the models and migrations.
Any thoughts?
Edit:
Here is what is output to log for dealer.prototype
{ _customGetters: {},
_customSetters: {},
validators: {},
_hasCustomGetters: 0,
_hasCustomSetters: 0,
rawAttributes:
{ id:
{ allowNull: false,
autoIncrement: true,
primaryKey: true,
type: [Object],
Model: Dealer,
fieldName: 'id',
_modelAttribute: true,
field: 'id' },
///Other Attributes
},
_isAttribute: { [Function] cache: MapCache { __data__: [Object] } },
Model: Dealer,
$Model': Dealer }
After reading the docs a little further and looking at some other model definitions I discovered that the issue was I had defined my model incorrectly.
In my definition above you'll notice I wrapped the underscored: true option in brackets, followed by my classMethods and instanceMethods wrapped in another set of brackets.
This is incorrect. The proper way to define a sequelize model is with two sets of brackets, the first containing your model attributes and the second containing all other options, including methods.
'use strict';
module.exports = function(sequelize, DataTypes) {
var Dealer = sequelize.define('Dealer', {
id: { allowNull: false, autoIncrement: true,
primaryKey: true, type: DataTypes.INTEGER.UNSIGNED },
...
created_at: { allowNull: false, type: DataTypes.DATE },
updated_at: { allowNull: false, type: DataTypes.DATE }
},
{
underscored: true,
classMethods: {
associate: function(models) {
Dealer.hasMany(models.Job);
}
},
instanceMethods: {
getAllClientData: function(){
leads = [];
...
return leads;
},
}
});
return Dealer;
};