Sequelize: Querying a through table - javascript

I'm trying to query a through table (Application) and paginate/order the results, but can't seem to get the logic quite right.
The Many-To-Many association:
// Applicants M:N Jobs (through Application)
Applicant.belongsToMany(Job, { through: Application });
Job.belongsToMany(Applicant, { through: Application });
I've queried Application, and then nested the queries for each side of the relation:
exports.getApplications = async (req, res, next) => {
const index = req.query.index || 0;
const limit = req.query.limit || 10;
const applications = await Application.findAll({ limit: parseInt(limit, 10), index: parseInt(index)});
let results = [];
try {
await Promise.all(applications.map(async (application) => {
const job = await Job.findOne({ where: { id: application.jobId } });
const applicant = await Applicant.findOne({ where: { id: application.applicantId } });
results.push({application, job, applicant});
}));
res.status(200).json({msg: 'success', applications: results});
} catch(err) {
console.log(err);
}
}
It seems to work, but feels a bit hacky. Is there a way of querying the through table and getting the associated data from both the Jobs and Applicant tables at the same time?
Thanks!
*EDIT: So I'm trying to return an array of application objects that look something like this:
[
{
applicationId: application.id,
companyId: job.companyId,
company: job.company.name,
position: job.title,
applicantId: applicant.id,
firstName: applicant.firstName,
lastName: applicant.lastName,
},
{...},
{...}
]
...but I'd like to paginate the application results. So:
Application.findAll({ limit, index });
Ideally I'd also then like to be able to order by Job/Applicant properties too
More info:
So thanks to the help so far it looks like I need to also create a belongsTo association for the Application and Job/Applicant so that I can query the Association table and get Job/Applicant data:
// Applicants M:N Jobs (through Application)
Applicant.belongsToMany(Job, { through: Application });
Job.belongsToMany(Applicant, { through: Application });
// Set associations so the Application table can be queried directly
Application.belongsTo(Job, { foreignKey: { name: 'jobId' }});
Application.belongsTo(Applicant, { foreignKey: { name: 'applicantId' }});
I currently create an application in one of my routes using applicant.addJob(currentJob);
// Applicant Model:
const Applicant = sequelize.define('applicant', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
cvUrl: {
type: Sequelize.STRING,
allowNull: true
}
});
// Job Model:
const Job = sequelize.define('job', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true
},
title: {
type: Sequelize.STRING,
allowNull: false
},
// **snip**
createdAt: {
type: Sequelize.DATE(3),
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE(3),
allowNull: false,
}
});
// Application Model:
const Application = sequelize.define('application', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
notNull: true
}
});

With the combination of previous answers
Applicant.belongsToMany(Job, { through: Application ,as:'jobs'});
Job.belongsToMany(Applicant, { through: Application,as:'applicants' })
with these aliases, you can include both into the application
const applications = await Application.findAll({
include: [
{model: Job, as:'jobs', where: {id: application.jobId}},
{model: Applicant, as:'applicants' where:{id: application.jobId}}
],
limit,
raw: true
});
also, you can set include as if applicant and job has an association
{model: Job, as:'jobs', where: {id: application.jobId}
include:[{model:Applicant as:'applicants'}]
}

If I understand your requirement properly, I think you want to do this way.
This is an example to order by Applicant.
For order by Job, change the order option with Job's attribute.
When you use the offset/limit, you need the order and the order option needs to ensure the ordering is always same and unique.
const applications = await Application.findAll({
include: [
{
model: Job
},
{
model: Applicant
}
],
subQuery: false,
order: [
['Applicant', 'lastName'],
['Applicant', 'firstName']
// lastName & firstName is not enough to get a unique order.
// In order to make sure the order is always same and pagination works properly,
// you should add either order by id or createdAt/updatedAt.
['Applicant', 'createdAt', 'desc']
],
offset: index,
limit,
raw: true // To flatten the response.
});

You can give aliases tou your associations and just get
Applicant.belongsToMany(Job, { through: Application ,as:'jobs'});
Job.belongsToMany(Applicant, { through: Application,as:'applicants' });
Then you'll be able to use a simple include
for example Get the job and its applicants with one single query
const job = await Job.findOne({
where: { id: application.jobId },
include: ['applicants'],
});
in your job object you'll get an array of applicants .
More reference for that in here

Related

Sequelize: Using "where" inside a child include overrides parent's "where" criteria. Composite primary key involved

So,
I have two tables, with a 1:M relationship. They have in common two primary keys: tenant and user_id.
I have defined the model relationship and btw, I am not sure if I did it correctly because I am still not sure how to handle composite primary keys on Sequelize. This works well with my many other queries, and I think it influences the problem.
// Sequelize model set-up:
const user = serviceLayerDB.define('user',
{ // Database columns:
tenant: {
type: Sequelize.STRING(45),
primaryKey: true
},
user_id: {
type: Sequelize.STRING(24),
primaryKey: true
},
status: {
type: Sequelize.STRING(11)
}
});
const user_component = serviceLayerDB.define('user_component',
{ // Database columns:
tenant: {
type: Sequelize.STRING(45),
primaryKey: true
},
user_id: {
type: Sequelize.STRING(24),
primaryKey: true
},
component_id: {
type: Sequelize.STRING(24),
primaryKey: true
},
active: {
type: Sequelize.BOOLEAN
}
});
// Sequelize relationship set-up:
user.hasMany(user_component, { foreignKey: 'user_id' });
user.hasMany(user_component, { foreignKey: 'tenant' });
BUT the problem comes when I have the following query:
// Retrieving user and related components.
function getSubscriptions() {
let options = {
where: {
tenant: 'company_A',
user_id: '1001'
},
include: [{ // Adding components, filtered by "active" value:
model: user_component,
where: {
active: 1
},
required: false
}]
};
user.findAll(options)
.then(function(data) {
if (data.length === 0) { // If no data found:
console.log('No data found');
return;
}
// Curate Sequelize result:
let curatedData = data.map(function(userInstance) { return userInstance.get({ plain: true}) }); // Workaround to be able to perform .get().
console.log(JSON.stringify(curatedData, null, 2));
})
.catch(function(error) {
console.log('critical', 'Failed to find data in database. Error: ' + error);
})
}
// Execute:
getSubscriptions();
What I want is to find the user and its components, but only the ones with the active value set to 1. It is not working: the result is every component with the value active set to 1 under the same "tenant", the child include is ignoring the "user_id" that we indicated in the parent.
Am I right to think this is related to my composite primary key? How to fix this in the most elegant manner?
You have to use aliases when you associate a model to another model more then once.
For instance:
user.hasMany(user_component, { foreignKey: 'user_id', as: 'UserComponents' });
user.hasMany(user_component, { foreignKey: 'tenant', as: 'TenantComponents' });
And afterwards you should decide for what exact association you wish to do an include operation:
the association by user_id field
let options = {
where: {
tenant: 'company_A',
user_id: '1001'
},
include: [{ // Adding components, filtered by "active" value:
model: user_component,
as: 'UserComponents'
where: {
active: 1
},
required: false
}]
};
the association by tenant field
let options = {
where: {
tenant: 'company_A',
user_id: '1001'
},
include: [{ // Adding components, filtered by "active" value:
model: user_component,
as: 'TenantComponents'
where: {
active: 1
},
required: false
}]
};
If you would like both child collections with the active: 1 condition you can do this:
let options = {
where: {
tenant: 'company_A',
user_id: '1001'
},
include: [{ // Adding components, filtered by "active" value:
model: user_component,
as: 'UserComponents'
where: {
active: 1
},
required: false,
separate: true
}, { // Adding components, filtered by "active" value:
model: user_component,
as: 'TenantComponents'
where: {
active: 1
},
required: false,
separate: true
}]
};
Please pay attention to separate: true option: this option tells sequielize to do separate queries for childs.
If you wish to get not all users but those ones with active only components (which ones: through user_id or tenant field?) you should set required: true in include. But in this case don't include both associations with required: true. This leads to miltiplication of amount of records in the result SQL query and consumes much more memory.

SequelizeDatabaseError: operator does not exist uuid = integer

I'm trying to make a join with two tables based on UUID (I have id too), those tables have some difficult relations...
When I run the query I get the error.
The first table is called users, and his UUID is called registry_uuid.
The second table beneficiaries but this table has two UUID, uuid_beneficiary and uuid_benefactor.
Why? because the first table has a column user_type_id and with this we can know if it's a user beneficiary or benefactor.
And the second table is to know which users are related.
Model Users:
const User = sequelize.define('users', {
registry_uuid: {
type: Sequelize.UUIDV4,
defaultValue: Sequelize.UUIDV4,
allowNull: false
},
user_type_id: {
type: Sequelize.INTEGER,
defaultValue: 1,
allowNull: false
}
}
Model Beneficiaries:
const Beneficiary = sequelize.define('beneficiaries', {
uuid_benefactor: {
type: Sequelize.UUIDV4,
defaultValue: Sequelize.UUIDV4,
allowNull: false
},
uuid_beneficiary: {
type: Sequelize.STRING,
defaultValue: Sequelize.UUIDV4,
allowNull: false
},
created_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW
},
disassociation_date: {
type: Sequelize.DATE,
defaultValue: null
}
}
Query:
async function getBenefactorOfBeneficiary (benefactorUuid, arrayAttributes) {
arrayAttributes = arrayAttributes || ['registry_uuid', 'name', 'last_name', 'picture']
return UserModel.findOne({
where: {
registry_uuid: {
[Op.eq]: benefactorUuid
}
},
include: [{
model: BeneficiaryModel,
}],
attributes: arrayAttributes,
raw: true
})
}
Relation:
UserModel.hasMany(beneficiaryModel, {foreignKey: 'uuid_benefactor'})
beneficiaryModel.belongsTo(UserModel, {foreignKey: 'registry_uuid'})
I expect the output:
Example:
{
"name": "string",
"lastName": "string",
"picture": "string",
"created_at" "string"
}
obviously I modify the response in controller
You should first check the models that you are including and their ID types. They must have same type. Beside that, let's say we have User and Role models. Every User can have only one role and a Role can be used by several Users. In this case, you will get this error if you write the associations wrong.
Wrong version:
// Under the user model associations
user.hasOne(models.role, { foreignKey: "roleId" });
// this will try to compare your userId with roleId of Role table
// Under the Role model associations
role.hasMany(models.user, { foreignKey: "roleId" });
Right version should be like:
// Under the user model associations
user.hasOne(models.role, { foreignKey: "roleId" });
// this will try to compare your roleId from User model with roleId of Role table
// Under the Role model associations
role.hasMany(models.user, { foreignKey: "roleId" });
If you need more details, read https://sequelize.org/master/manual/assocs.html

Problem adding allowNull in the foreignKey association 1:1 in the models with Sequelize

i am doing a simple project with node.js using Sequelize with MySQL database. I was modeling my models associations. One got the hasMany and the other the belongsTo, I set the informations in both(like, as:...,foreignKey:...,onDelete:...), and set in foreignKey the propertie allowNull. When I save de model with the belongsTo method passing the in the request body the foreignKey it saves properly. But when I do this with model that uses in association the method hasOne it doesn't work. It passes that I need to specify the model ID that uses the belongsTo method.
const connection = require('../../config/database');
const DataTypes = require('sequelize').DataTypes;
const Course = connection.define('course', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
courseName: {
type: DataTypes.STRING(50),
field: 'course_name',
allowNull: false
}
},
{ timestamps: false });
Course.associate = models => {
Course.belongsTo(models.Lecturer, {
foreignKey: {
field: 'lecturer_id',
allowNull: false
},
as: 'lecturer'
});
Course.belongsToMany(models.Student, {
through: 'student_course',
as: 'students',
foreignKey: 'curse_id',
otherKey: 'student_id'
})
}
module.exports = Course;
const connection = require('../../config/database');
const DataTypes = require('sequelize').DataTypes;
const Lecturer = connection.define('lecturer', {
lecturerName: {
type: DataTypes.STRING(50),
field: 'lecturer_name',
allowNull: false
}
},
{ timestamps: false });
Lecturer.associate = models => {
Lecturer.hasOne(models.Course, {
foreignKey: {
field: 'lecturer_id',
allowNull: false
},
as: 'course'
})
}
module.exports = Lecturer;
Result:
[ ValidationErrorItem {
message: 'course.courseId cannot be null',
type: 'notNull Violation',
path: 'courseId',
value: null,
origin: 'CORE',
instance: [Object],
validatorKey: 'is_null',
validatorName: null,
validatorArgs: [] } ]
I can't comment yet, but it looks like the error shows a camel-case column name (courseId) as opposed to a snake-case column name (course_id).
Also, you have misspelled "course_id" in your Course.belongsToMany association.

Model methods are not working in sails js

I am using sails v0.12 , I have different models in my MySql relational database but the area of concern lies in these 2 models which are
1. User
2. Appointment
I want to add apptCustomer and apptProvider data into appointment model
but the value is NULL in the database
My User model is :
module.exports = {
attributes: {
name: {
type: "string",
},
email: {
type: "email",
required: true,
unique: true
},
contact_no:{
type: "int",
maxLength: 15,
//required: true,
},
address: {
type:"longtext",
//required: true,
},
userId:{
type: "string",
primaryKey: true,
unique:true
},
gtoken:{
type: "string"
},
provider:{
type:"string"
},
cancel:{
type: "boolean",
// required: true
},
business_name:{
type:"string",
unique:true
},
business_category:{
type:"string"
},
roles:{ // Many to Many = User <-> User-role <-> Role
collection:'Role',
through:'userrole',
},
services:{
collection:'Service',
through:'serviceprovider', // Many to Many = User (provider) <-> Service-provider <-> Service
},
schedules:{ // One to One = User (provider) - Schedule
collection:'Schedule',
via:'owner',
},
providerAppointments:{ // One to Many = User(customer) - multiple Appointments
collection:'Appointment',
via:'appProvider',
},
customerAppointments:{
collection:'Appointment', // One to Many = User(provider) - multiple Appointments
via:'appCustomer'
}
}
};
And my Appointment Model is
module.exports = {
attributes: {
appointment_id:{
type:'int',
primaryKey: true,
autoIncrement:true
},
appointmentDate: {
type: 'datetime',
required: true,
},
start_time: {
type: 'string',
required: true,
},
end_time: {
type: 'string',
},
status: {
type: 'string',
enum: ['booked', 'canceled']
},
appProvider: {
model: 'user',
},
appCustomer:{
model: 'user',
},
serviceAppointment: {
model: 'service',
}
}
};
And my Model Methods are as follows
Appointment.findOrCreate({appointmentDate:req.body.values.appointmentDate, start_time:req.body.values.start_time, end_time:req.body.end_time, status: status},{appointmentDate:req.body.values.appointmentDate, start_time:req.body.values.start_time, end_time:req.body.end_time, status: status})
.exec(function apptCreated(err,appt){
if(err) { sails.log('err',err)}
Service.findOne({ service_id : req.body.values.selected_service})
.exec(function(err,service){
service.serviceAppointments.add(appt);
service.save(function(err,result){
if(err) { sails.log(err)}
})
}),
User.find({userId: req.body.businessId})
.populate('roles')
.exec(function(err,provider){
_.map( provider.roles, role => { // Problem lies here .. this method is not working
if(role.role_id==1){
provider.providerAppointments.add(appt);
provider.save(function(err, result){
if(err) { sails.log(err)}
sails.log('appointment added to provider')
})
}
})
}),
//Appointment adding to Customer
User.find({userId: req.body.customerId})
.populate('roles')
.exec(function(err,customer){
_.map( customer.roles, role => { // Problem lies here... this method is not working
if(role.role_id==2){
customer.customerAppointments.add(appt)
customer.save(function(err, result){
if(err) { sails.log(err)}
sails.log('appointment added to customer')
})
}
})
}),
// Adding contact to customer
User.update({userId: req.body.customerId},{ contact_no: req.body.values.contact_no}) // this method works fine
.exec(function userUpdated(err, user){
if(err) { return sails.log(err)}
sails.log('contact number updated',user);
})
})
As far as I can see, at the moment that you call provider.providerAppointments.add, provider.providerAppointments is still just a regular property - possibly an array of ids.
I think you need to add .populate('providerAppointments') to your User.find... if you do that, then provider.providerAppointments should have a .add method that works the way you expect.
Of course, if this is the source of error, I would have expected a pretty clear error message, like provider.providerAppointments.add is not a function or some such. But try adding the populate, see if it fixes your problem.

Sequelize.js insert a model with one-to-many relationship

I have two sequelize models with one-to-many relationship. Let's call them Owner and Property.
Assume they are defined using the sails-hook-sequelize as such (simplified).
//Owner.js
module.exports = {
options: {
tableName: 'owner'
},
attributes: {
id: {
type: Sequelize.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING(255)
},
associations: function () {
Owner.hasMany(Property, {
foreignKey: {
name: 'owner_id'
}
});
}
}
//Property.js
module.exports = {
options: {
tableName: 'property'
},
attributes: {
id: {
type: Sequelize.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING(255)
}
}
Now assume I want to insert an Owner record in my database and insert a few property records to associate with the owner. How do I do this?
I'm looking for something like
Owner.create({name:'nice owner',
property: [{name:'nice property'},
{name:'ugly property'}]});
Surprisingly I can't find this in the Sequelize documentation.
You can't associate property existing records when you create the owner, you have to do that right after, with promise chain.
Owner.create({name:'nice owner'}).then(function(owner){
owner.setProperties([{name:'nice property'}, {name:'ugly property'}]).then(/*...*/);
});
To avoid any problems with those associations (owner created but some associations failed), it's better to use transactions.
sequelize.transaction(function(t) {
return Owner.create({name:'nice owner'}, {transaction: t}).then(function(owner){
return owner.setProperties([{name:'nice property'}, {name:'ugly property'}], {transaction : t});
});
});
However, if you want to create new Owner associated to new Properties you can do something like
Owner.create({
name: 'nice owner',
property: [
{ name: 'nice property'},
{ name: 'ugly property'}
]
},{
include: [ Property]
});
See http://docs.sequelizejs.com/en/latest/docs/associations/#creating-with-associations

Categories

Resources