Understanding Associations in Sequelize - mysql

I'm trying to understand associations in Sequelize. I'm starting from existing database tables so some of the fields may not match up to the defaults in Sequelize. I've used Sequelizer to generate my models directly from the database.
I'm accustomed to writing queries but now I'm trying to learn how an ORM like Sequelize works.
Here's my models.
models/user.js
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
field: "id"
},
username: {
type: DataTypes.STRING(20),
allowNull: false,
field: "username"
},
fullname: {
type: DataTypes.STRING(60),
allowNull: false,
field: "fullname"
},
createdat: {
type: DataTypes.DATE,
allowNull: false,
field: "createdat"
},
updateat: {
type: DataTypes.DATE,
allowNull: true,
field: "updateat"
},
deletedat: {
type: DataTypes.DATE,
allowNull: true,
field: "deletedat"
}
},
{
tableName: "users",
timestamps: false
}
);
User.associate = function(models) {
models.User.hasMany(models.Ticket),
{ as: "createdbyname", foreignKey: "createdby" };
};
return User;
};
models/ticket.js
module.exports = (sequelize, DataTypes) => {
const Ticket = sequelize.define(
"Ticket",
{
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
field: "id"
},
details: {
type: DataTypes.STRING(45),
allowNull: true,
field: "details"
},
assignedto: {
type: DataTypes.INTEGER(11),
allowNull: true,
field: "assignedto"
},
createdby: {
type: DataTypes.INTEGER(11),
allowNull: true,
field: "createdby"
},
createdat: {
type: DataTypes.DATE,
allowNull: false,
field: "createdat"
},
updatedat: {
type: DataTypes.DATE,
allowNull: true,
field: "updatedat"
},
deletedat: {
type: DataTypes.DATE,
allowNull: true,
field: "deletedat"
}
},
{
tableName: "tickets",
timestamps: false
}
);
Ticket.associate = function(models) {
models.Ticket.belongsTo(models.User,
{ foreignKey: "createdby" });
};
return Ticket;
};
In my route handler, I'm calling User.findAll as follows:
models.User.findAll({
include: [models.Ticket]
})
The result I expect to see is a query that looks like this:
SELECT
`User`.`id`,
`User`.`username`,
`User`.`fullname`,
`User`.`createdat`,
`User`.`updateat`,
`User`.`deletedat`,
`Tickets`.`id` AS `Tickets.id`,
`Tickets`.`details` AS `Tickets.details`,
`Tickets`.`assignedto` AS `Tickets.assignedto`,
`Tickets`.`createdby` AS `Tickets.createdby`,
`Tickets`.`createdat` AS `Tickets.createdat`,
`Tickets`.`updatedat` AS `Tickets.updatedat`,
`Tickets`.`deletedat` AS `Tickets.deletedat`
FROM
`users` AS `User`
LEFT OUTER JOIN
`tickets` AS `Tickets` ON `User`.`id` = `Tickets`.`createdby`
The query I see running in the console is:
SELECT
`User`.`id`,
`User`.`username`,
`User`.`fullname`,
`User`.`createdat`,
`User`.`updateat`,
`User`.`deletedat`,
`Tickets`.`id` AS `Tickets.id`,
`Tickets`.`details` AS `Tickets.details`,
`Tickets`.`assignedto` AS `Tickets.assignedto`,
`Tickets`.`createdby` AS `Tickets.createdby`,
`Tickets`.`createdat` AS `Tickets.createdat`,
`Tickets`.`updatedat` AS `Tickets.updatedat`,
`Tickets`.`deletedat` AS `Tickets.deletedat`,
`Tickets`.`UserId` AS `Tickets.UserId`
FROM
`users` AS `User`
LEFT OUTER JOIN
`tickets` AS `Tickets` ON `User`.`id` = `Tickets`.`UserId`;
Note difference in LEFT OUTER JOIN clause. This is throwing an error as follows:
Unhandled rejection SequelizeDatabaseError: Unknown column 'Tickets.UserId' in 'field list'
I need some help figuring out where I've gone wrong here.

When defining associations, like belongsTo, you can specify a foreignKey and a targetKey. The foreignKey corresponds to the field in the source table (remember, the syntax is sourceModel.belongsTo(targetModel, options)). The targetKey corresponds to the field in the target table.
In your case, you made a mistake in the association in the models/ticket.js file, you used:
models.Ticket.belongsTo(models.User, { foreignKey: "createdby" });
Here, foreignKey references the source table, Ticket. Therefore, your are telling Sequelize to use the field createdBy for the Ticket table, and the default (the primary key) for the User table. As createdBy does not exists within Ticket, Sequelize falls back to the default case, where it uses Ticket.UserID.
To fix your association (and query), you need to update your belongsTo to the following:
models.Ticket.belongsTo(models.User, { targetKey: "createdby" });

Plant.findAll({
include: Farm,
order: [['name', 'ASC']]
})
.then(data=> {
res.send(data)
})
.catch(err => {
res.send(err)
console.log(err);
})
let arrQuery = [
queryInterface.addColumn('farms', 'dayPlayed' , Sequelize.STRING),
queryInterface.removeColumn('plants', 'dayPlayed' , Sequelize.STRING),
]
return Promise.all(arrQuery)

Related

I created oneToMany relation but how I can get the single record belongs to the many record in Sequalize

Tagcategories Model
export const TagCategories = sequelize.define(
"tag_categories",
{
categoryId: {
type: DataTypes.INTEGER,
field: "category_id",
autoIncrement: true,
primaryKey: true,
},
title: {
type: DataTypes.STRING(50),
field: "title",
allowNull: false,
unique: true,
},
},
);
TagCategories.hasMany(TagGroups, {
foreignKey: "categoryId",
sourceKey: "categoryId",
});
export default TagCategories;
TagGroups Model
export const TagGroups = sequelize.define(
"tag_groups",
{
tagGroupId: {
type: DataTypes.INTEGER,
field: "tag_group_id",
autoIncrement: true,
primaryKey: true,
},
categoryId: {
type: DataTypes.INTEGER,
field: "category_id",
allowNull: false,
},
title: {
type: DataTypes.STRING(50),
field: "title",
allowNull: false,
unique: true,
},
},
);
In the above models I establish oneToMany relationship between the TagCategories and TagGroups
But I want to fetch the record from the TagGroup table with the TagCategories details.
Thanks in advance
Did you look at examples in official documentation?
Also, you need to add an association from TagGroups to TagCategories:
// there is no need to indicate `sourceKey` if this field is a primary key
TagGroups.belongsTo(TagCategories, {
foreignKey: "categoryId",
});
It's better to define all associations in static functions and call all of them separately after all models will be registered.
See the question and my answer here how to do it
In your case, the final request would be something like this
const tagGroup = await TagGroups.findOne({
where: {
tagGroupId: groupId
},
include: [{
model: TagCategories
}]
})

Sequelize Mysql - Unable to Insert model with one to many relationship

Design Overview: I've an application with Invoice creation and Inventory management features in it. Let's first understand the database design with 2 entities that we have as below:
Invoices
Items
Now, here I've a M:N relationship between these 2 entities because one invoice can contain multiple items and one item can be included in many such invoices.
So, I've created a 3rd table which we call joining table to associate these entities as shown in the image below,
Problem Statemet: I'm unable to insert model in the child table(invoice_items) using include attribute. Look at the code below to understand what's wrong happening here?
3 Model Classes as below:
1. Invoice:
Note: Providing with fewer attributes to keep it short.
module.exports = (sequelize, DataTypes) => {
const Invoice = sequelize.define('Invoice', {
invoiceId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
invoiceNumber: {
type: DataTypes.INTEGER(6).UNSIGNED.ZEROFILL,
allowNull: false,
unique: true
},
invoiceTotal: {
type: DataTypes.DECIMAL(9,2),
allowNull: false,
defaultValue: 0.00
},
paymentTotal: {
type: DataTypes.DECIMAL(9,2),
allowNull: false,
defaultValue: 0.00
},
invoiceDate: {
type: DataTypes.DATEONLY,
defaultValue: DataTypes.NOW,
allowNull: false
}
}, {
underscored: true
});
Invoice.associate = function (model) {
Invoice.belongsTo(model.Customer, {
as: 'customer',
foreignKey: {
name: "cust_id",
allowNull: false
}
});
// association with 3rd table
Invoice.hasMany(model.InvoiceItem, {
as: 'invoice_item',
constraints: true,
onDelete: 'NO ACTION',
foreignKey: {
name: "invoice_id",
allowNull: false
}
});
};
return Invoice;
}
2. Item:
Note: Providing with fewer attributes to keep it short.
module.exports = (sequelize, DataTypes) => {
const Item = sequelize.define('Item', {
itemId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
itemName: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: ''
},
// this is a opening stock
quantityInStock: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
},
unitPrice: {
type: DataTypes.DECIMAL(9,2),
allowNull: false,
defaultValue: 0.00
}
}, {
underscored: true
});
Item.associate = function (model) {
// association with 3rd table
Item.hasMany(model.InvoiceItem, {
as: 'invoice_item', // alias name of a model
constraints: true,
onDelete: 'NO ACTION',
foreignKey: {
name: "item_id",
allowNull: false
}
});
};
return Item;
}
3. Invoice_Item:
Note: Providing with fewer attributes to keep it short.
module.exports = (sequelize, DataTypes) => {
const InvoiceItem = sequelize.define('InvoiceItem', {
invoiceItemId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
quantity: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
},
rate: {
type: DataTypes.DECIMAL(9,2),
allowNull: false,
defaultValue: 0.00
}
}, {
underscored: true
});
InvoiceItem.associate = function(model) {
InvoiceItem.belongsTo(model.Invoice, {
as: 'invoice',
foreignKey: {
name: "invoice_id",
allowNull: false
}
});
InvoiceItem.belongsTo(model.Item, {
as: 'item',
foreignKey: {
name: "item_id",
allowNull: false
}
});
}
return InvoiceItem;
}
Now, I'm using below code to create an invoice with the list of items in it. But, this is not inserting the child records in the joining table(invoice_items). What's wrong here in the code below?
invoice = await Invoice.create({
"invoiceNumber": req.body.invoiceNumber,
"invoiceDate": req.body.invoiceDate,
"invoiceTotal": req.body.invoiceTotal,
"paymentTotal": req.body.paymentTotal,
"cust_id": req.body.customer.custId,
invoice_items: [{
item_id: 1,
quantity: 2,
rate: 300
}]
}, {
include: [{
association: InvoiceItem,
as: 'invoice_item'
}]
});
After trying so many variations, I understand that there was a problem in the association of my model classes. And, below is the way of associating both Invoice and Item model classes for M:N(Many to Many) relationships. I can now update the join table(invoice_items) by inserting the record in it for each invoice we create in the system with the items in it.
Invoice.associate = function (model) {
// association in Invoice model class
Invoice.belongsToMany(model.Item, {
through: 'InvoiceItem',
constraints: true,
onDelete: 'NO ACTION',
foreignKey: {
name: "invoice_id", // foreign key column name in a table invoice_items table
allowNull: false
}
});
};
Item.associate = function (model) {
// association in Item model class
Item.belongsToMany(model.Invoice, {
through: 'InvoiceItem',
constraints: true,
onDelete: 'NO ACTION',
foreignKey: {
name: "item_id", // foreign key column name in a table invoice_items
allowNull: false
}
});
};
Create Invoice with Items in it:
Note: Passing itemId (1) as a parameter in the addItems() method. If you have multiple items in an invoice then you can add forEach loop here to iterate over each item and individually pass the itemId, quantity and rate for an item sold to the customer.
// first create the invoice
invoice = await Invoice.create(invoice);
// Next, add record in the join table
await invoice.addItems([1], {
through: {
quantity: item.quantity,
rate: item.rate
}
});
Database Tables with one Test Result:
1. Invoice Table:
2. Invoice_items Table(Join Table):

Problem using both include and attributes in findAll() query in Sequelize

I searched on the internet a common issue but I did not find anything so I post the question here.
I am using Sequelize for Node and I am trying to query my PostgreSQL database to find all the objects in a specific table. However, I would like to be able to replace de foreign keys by data from their linked table (In other words, join tables and remove the ids). I managed to do this by using both the "include" option to join the table and the "attributes" option to unselect the Id and only keep the joined value, but since I have a hasMany relationship, I get an error when executing the query.
Here is the options I use :
options = {
include: [
{
model: models.ProviderType,
attributes: ['id', 'name'],
as: 'providerType',
},
{
model: models.SavingProduct,
attributes: ['id', 'name'],
as: 'savingProducts',
},
],
attributes: [
'id',
'name',
'siteUrl',
'logoPath',
'createdAt',
'updatedAt',
'deletedAt',
],
}
And it executed the following query :
SELECT "Provider".*, "providerType"."id" AS "providerType.id", "providerType"."name" AS "providerType.name", "savingProducts"."id" AS "savingProducts.id", "savingProducts"."name" AS "savingProducts.name"
FROM (
SELECT "Provider"."id", "Provider"."name", "Provider"."siteUrl", "Provider"."logoPath", "Provider"."createdAt", "Provider"."updatedAt", "Provider"."deletedAt"
FROM "Provider" AS "Provider"
WHERE "Provider"."deletedAt" IS NULL
LIMIT 20 OFFSET 0
) AS "Provider"
LEFT OUTER JOIN "ProviderType" AS "providerType" ON "Provider"."providerTypeId" = "providerType"."id"
LEFT OUTER JOIN "SavingProduct" AS "savingProducts" ON "Provider"."id" = "savingProducts"."providerId" AND ("savingProducts"."deletedAt" IS NULL);
The error thrown is : "column Provider.providerTypeId does not exist". It is logical because, when selecting the embedded SELECT, I do not select Provider.providerTypeId as I don't want this column to be returned, and because of that, when trying to JOIN the tables, SQL do not find this column and return an error.
In other words, Sequelize first select the wanted values and then join while I would like it to do the opposite, ie join the tables and then select only the asked columns.
So I understand the error but I don't find any way to replace the foreign keys by data from the linked tables. It is possible I did not take the right direction to do that so if you see something easier and cleaner, I would really appreciate!
Followed are the models definition :
For Provider
module.exports = (sequelize, DataTypes) => {
const Provider = sequelize.define('Provider', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
siteUrl: {
type: DataTypes.STRING,
allowNull: true,
},
logoPath: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
freezeTableName: true,
paranoid: true,
});
Provider.associate = (models) => {
Provider.belongsTo(models.ProviderType, { as: 'providerType' });
Provider.hasMany(models.SavingProduct, { as: 'savingProducts', foreignKey: 'providerId' });
};
return Provider;
};
For SavingProduct
module.exports = (sequelize, DataTypes) => {
const SavingProduct = sequelize.define('SavingProduct', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
freezeTableName: true,
paranoid: true,
});
SavingProduct.associate = (models) => {
SavingProduct.belongsTo(models.Provider, { as: 'provider', foreignKey: 'providerId' });
};
return SavingProduct;
};
Please do not hesitate to tell me if I am not clear, I would be happy to provide you with more informations.
Thank you very much!

Dynamic define database in Sequelize for multi-tenant support return wrong query syntax

I'm Working on Multi-tenant Application (SAAS) with Shared Database Isolated Schema principle.
I've tried solution from https://github.com/renatoargh/data-isolation-example
from this article https://renatoargh.wordpress.com/2018/01/10/logical-data-isolation-for-multi-tenant-architecture-using-node-express-and-sequelize/
This is My Sequelize Model using schema Option
module.exports = (sequelize, DataTypes) => {
const Task = sequelize.define('Task', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
field: 'Id'
},
description: {
type: DataTypes.STRING(100),
allowNull: false,
field: 'Description'
},
done: {
type: DataTypes.BOOLEAN,
allowNull: false,
default: false,
field: 'Done'
},
taskTypeId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'TaskTypeId'
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'UserId'
}
}, {
freezeTableName: true,
tableName: 'Tasks',
createdAt: false,
updatedAt: false
})
Task.changeSchema = schema => Task.schema(schema)
Task.associate = models => {
Task.belongsTo(models.TaskType, {
as: 'taskType',
foreignKey: 'taskTypeId'
})
}
return Task
}
And Stop at this problem
SELECT
`Task`.`Id` AS `id`,
`Task`.`Description` AS `description`,
`Task`.`Done` AS `done`,
`Task`.`TaskTypeId` AS `taskTypeId`,
`Task`.`UserId` AS `userId`,
`taskType`.`Id` AS `taskType.id`,
`taskType`.`Description` AS `taskType.description`
FROM `tenant_1.Tasks` AS `Task` LEFT OUTER JOIN `shared.TaskTypes` AS `taskType`
ON `Task`.`TaskTypeId` = `taskType`.`Id`
WHERE `Task`.`UserId` = 1;
as you see, FROM `tenant_1.Tasks` in mysql is a wrong syntax. it must be FROM `tenant_1`.`Tasks`
how to change `tenant_1.Tasks` to `tenant_1`.`Tasks`
Are you using MySQL? If so, that is the expected behavior.
From the documentation of Model.schema:
Apply a schema to this model. For postgres, this will actually place the schema in front of the table name - "schema"."tableName", while the schema will be prepended to the table name for mysql and sqlite - 'schema.tablename'.

Unable to run a join query using sequelize

I need to run a join query using sequelize and I have been reading the documentation at sequelize doc. But as I run the following snippet, I get an error.
let channelUsersM = UserModel.get(); // Table name: channel_users
let channelM = ChannelModel.get(); // Table name: channel
channelUsersM.belongsTo(channelM, {as: 'channel',foreign_key: 'channel_id',targetKey:'id'});
channelM.hasMany(channelUsersM,{foreign_key: 'channel_id'});
channelUsersM.findAll({
attributes: ['username'],
where: {
usertype: this.userType,
channel: {
name: channelName
}
},
include: [channelM]
}).then((r) => {
resolve(r);
}).catch((err) => {
reject(err);
});
Error says: channel is not associated to channel_users!
What could be the reason for this? I know how to directly run a SQL query using sequelize, but I do not want to go with it.
For easier understanding here, is the equivalent sql query that I am trying with sequelize:
select cu.username from channel as ch left join
channel_users as cu on ch.id = cu.channel_id
ch.name = 'some-name' and cu.usertype = 'some-type';
Here is the definition of models if required:
For channel_users:
channel_id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
field: 'channel_id'
},
userid: {
type: Sequelize.INTEGER,
field: 'userid'
},
username: {
type: Sequelize.CHAR(255),
field: 'username'
},
password: {
type: Sequelize.TEXT,
field: 'password'
},
usertype: {
type: Sequelize.ENUM('user', 'moderator','speaker','owner'),
field: 'usertype'
}
For channel:
id: {
type: Sequelize.INTEGER,
field: 'id',
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.CHAR(255),
field: 'name'
},
display_name: {
type: Sequelize.TEXT,
field: 'display_name'
},
creatorid: {
type: Sequelize.INTEGER,
field: 'creatorid'
},
password: {
type: Sequelize.TEXT,
field: 'password'
},
createdAt: {
type: Sequelize.DATE,
field: 'createdAt'
},
modifiedAt: {
type: Sequelize.DATE,
field: 'modifiedAt'
}
You have defined an alias in the belongsTo association, so you also need to include the alias in include attribute when querying. Moreover, the channel.name column value should also be included in the include object of the query.
channelUsersM.findAll({
attributes: ['username'],
where: {
usertype: this.userType
},
include: [
{
model: channelM,
as: 'channel',
attributes: [],
where: { name: channelName }
}
]
}).then((r) => {
resolve(r);
}).catch((err) => {
reject(err);
});
The attributes: [] in include is added in order to prevent returning any fields from the channel table (according to you query you want only the username field from channel_users table).