I'm attempting to eager load a belongs-to-many association where I am loading three nested associations. Here are the models, which result in three database tables programs, programDates and peopleProgramDates
program.js:
module.exports = function(sequelize, DataTypes) {
const Program = sequelize.define('program', {
name: DataTypes.STRING
});
Program.associate = ({programDate}) => {
Program.hasMany(programDate);
};
return Program;
};
program_date.js:
module.exports = function(sequelize, DataTypes) {
const ProgramDate = sequelize.define('programDate', {
date: DataTypes.DATEONLY,
volunteerLimit: DataTypes.INTEGER
}, {
indexes: [
{
unique: true,
fields: ['programId', 'date']
}
]
});
ProgramDate.associate = ({program, person}) => {
ProgramDate.belongsTo(program);
ProgramDate.belongsToMany(person, {through: 'peopleProgramDates'});
};
return ProgramDate;
};
In my controller, I want to return an object with all of the programs, programDates and peopleProgramDates:
const {bus, family, person, volunteerType, program, programDate} = require('../models');
exports.get = (request, response) => {
return Promise.all([
bus.findAll({ include: [{model: family, include: [person]}] })
.then(buses => buses.map(addBusCount)),
volunteerType.findAll({include: [person]})
.then(volunteerTypes => volunteerTypes.map(addVolunteerCount)),
// this query hangs the application
program.findAll( { include: [{ model: programDate, include: [{association: 'peopleProgramDates'}] }]} )
.then(programs => programs.map(processPrograms))
])
.then(([buses, volunteerTypes, programs]) =>
response.render('pages/register', {
buses,
volunteerTypes,
programs
})
);
};
At the moment, processPrograms() is a function that simply returns the same array of objects, and so should not be relevant here. addBusCount and addVolunteerCount should similarly not be relevant.
I think the issue may be that peopleProgram dates is not a real sequelize model, but the result of the the belongsToMany through: association on ProgramDate.
This post seems to suggest I can use the association: property in order to load the data from the through association, however the query hangs the application.
If I remove the join table from the query, then the data loads fine:
program.findAll( { include: [programDate] } )
Bonus points: Ultimately what I really need is simply a count of peopleProgramDates returned with the programDate objects. Perhaps I can simply define such on the programDates model, however perhaps we can address that in a separate question. Nevertheless, if there is a compelling reason to use this approach, such as performance, then maybe we should go that way after all.
The solution was to add an alias to the belongsToMany through association:
// program_date.js
ProgramDate.belongsToMany(person, {through: 'peopleProgramDates', as: 'peopleProgDates'});
And then reference the alias in the include property:
program.findAll( { include: [{ model: programDate, include: [{association: 'peopleProgDates'}] }]} )
Related
Here's my query for fetching shows from a database, plus its associated venue and bands.
I really only want to get the names of the bands and venue. (name is the field in both of those tables.) The code below is fetching the whole record, though, and not just the field that I want.
const getAllShows = async (req, res) => {
try {
const shows = await Show.findAll({
include: [
{ model: User, as: 'bands', through: { attributes: ['name'] } },
{ model: Venue, through: { attributes: ['name'] }}
],
});
res.status(200).send(shows);
}
catch(err) {
res.send(400);
}
}
The attributes is misplaced - it doesn't belong under the through (btw, depending on your associations, you may not even need through).
Try changing like this:
{ model: User, as: 'bands', attributes: ['name']},
You might also consider field aliases, like this:
{ model: User, as: 'bands', attributes: [['name', 'band_name']]},
hth
I'm using node v9.5, sequelize v4.33 (postgres dialect).
I have two first-class models: Driver (specific people) and Car (generic make+model combinations). Thus far, they've been connected by a many-to-many join table. Now I want to start tracking additional properties on that join table, but am having trouble declaring these relationships so they actually work.
const Driver = sqlz.define('Driver', {
id: { primaryKey: true, type: DataTypes.UUID },
name: DataTypes.string
})
const Car = sqlz.define('Car', {
id: { primaryKey: true, type: DataTypes.UUID },
make: DataTypes.string,
model: DataTypes.string
})
// old associations; worked great when requirements were simpler
Driver.belongsToMany(Car, {
through: 'DriverCar',
as: 'carList',
foreignKey: 'driverId'
})
Car.belongsToMany(Driver, {
through: 'DriverCar',
as: 'driverList',
foreignKey: 'carId'
})
Now I want to begin tracking more information about the relationship between a car and its driver, like the color of that specific car.
Step 1: I update the migration script, adding a new column to the join table like so:
queryInterface.createTable( 'DriverCar', {
driverId: {
type: sqlz.UUID,
allowNull: false,
primaryKey: true,
references: {
model: 'Driver',
key: 'id'
}
},
carId: {
type: sqlz.UUID,
allowNull: false,
primaryKey: true,
references: {
model: 'Car',
key: 'id'
}
},
createdAt: {
type: sqlz.DATE,
allowNull: false
},
updatedAt: {
type: sqlz.DATE,
allowNull: false
},
// new column for join table
color: {
type: Sequelize.STRING
}
})
Step 2: I define a new sqlz model for DriverCar:
const DriverCar = sqlz.define('DriverCar', {
color: DataTypes.string
})
(I assume I only need to define the interesting properties, and that driverId and carId will still be inferred from the associations that will be defined.)
Step 3: I need to update the associations that exist among Driver, Car, and DriverCar.
This is where I'm stuck. I have attempted updating the existing associations, like so:
Driver.belongsToMany(Car, {
through: DriverCar, // NOTE: no longer a string, but a reference to new DriverCar model
as: 'carList',
foreignKey: 'driverId'
})
Car.belongsToMany(Driver, {
through: DriverCar, // NOTE: no longer a string, but a reference to new DriverCar model
as: 'driverList',
foreignKey: 'carId'
})
This executes without error, but the new color property is not fetched from the join table when I try driver.getCarList(). (Sqlz is configured to log every SQL statement, and I have verified that no properties from the join table are being requested.)
So, instead, I tried spelling out this relationship more explicitly, by associating Driver to DriverCar, and then Car to DriverCar:
// Driver -> Car
Driver.hasMany(DriverCar, {
as: 'carList',
foreignKey: 'driverId'
})
// Car -> Driver
Car.hasMany(DriverCar, {
foreignKey: 'carId'
})
I also tell sqlz that DriverCar won't have a standard row id:
DriverCar.removeAttribute('id')
At this point, requesting a Driver's carList (driver.getCarList()) seems to work, because I can see join table props being fetched in SQL. But saving fails:
driverModel.setCarList([ carModel1 ])
UPDATE DriverCar
SET "driverId"='a-uuid',"updatedAt"='2018-02-23 22:01:02.126 +00:00'
WHERE "undefined" in (NULL)
The error:
SequelizeDatabaseError: column "undefined" does not exist
I assume this error is occurring because sqzl doesn't understand the proper way to identify rows in the join table, because I've failed to establish the necessary associations. And frankly, I'm not confident I've done this correctly; I'm new to ORMs, but I was expecting I'd need to specify 4 assocations:
Driver -> DriverCar
DriverCar -> Car
Car -> DriverCar
DriverCar -> Driver
To recap: I have 2 first-class entities, joined in a many-to-many relationship. I'm trying to add data to the relationship, have discovered that the ORM requires defining those associations differently, and am having trouble articulating the new associations.
A note about your aliases
Before going to the answer, I would like to point out that your choice of aliases (carList and driverList) could be better, because although the auto-generated sequelize methods .setCarList() and .setDriverList() do make sense, the methods .addCarList(), .addDriverList(), .removeCarList() and .removeDriverList() are nonsense, since they take only a single instance as a parameter, not a list.
For my answer, I won't use any aliases, and let Sequelize default to .setCars(), .setDrivers(), .addCar(), .removeCar(), etc, which make much more sense to me.
Example of working code
I've made a 100% self-contained code to test this. Just copy-paste it and run it (after running npm install sequelize sqlite3):
const Sequelize = require("sequelize");
const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'db.sqlite' });
const Driver = sequelize.define("Driver", {
name: Sequelize.STRING
});
const Car = sequelize.define("Car", {
make: Sequelize.STRING,
model: Sequelize.STRING
});
const DriverCar = sequelize.define("DriverCar", {
color: Sequelize.STRING
});
Driver.belongsToMany(Car, { through: DriverCar, foreignKey: "driverId" });
Car.belongsToMany(Driver, { through: DriverCar, foreignKey: "carId" });
var car, driver;
sequelize.sync({ force: true })
.then(() => {
// Create a driver
return Driver.create({ name: "name test" });
})
.then(created => {
// Store the driver created above in the 'driver' variable
driver = created;
// Create a car
return Car.create({ make: "make test", model: "model test" });
})
.then(created => {
// Store the car created above in the 'car' variable
car = created;
// Now we want to define that car is related to driver.
// Option 1:
return car.addDriver(driver, { through: { color: "black" }});
// Option 2:
// return driver.setCars([car], { through: { color: "black" }});
// Option 3:
// return DriverCar.create({
// driverId: driver.id,
// carId: car.id,
// color: "black"
// });
})
.then(() => {
// Now we get the things back from the DB.
// This works:
return Driver.findAll({ include: [Car] });
// This also works:
// return car.getDrivers();
// This also works:
// return driver.getCars();
})
.then(result => {
// Log the query result in a readable way
console.log(JSON.stringify(result.map(x => x.toJSON()), null, 4));
});
The code above logs as expected (as I would expect, at least):
[
{
"id": 1,
"name": "name test",
"createdAt": "2018-03-11T03:04:28.657Z",
"updatedAt": "2018-03-11T03:04:28.657Z",
"Cars": [
{
"id": 1,
"make": "make test",
"model": "model test",
"createdAt": "2018-03-11T03:04:28.802Z",
"updatedAt": "2018-03-11T03:04:28.802Z",
"DriverCar": {
"color": "black",
"createdAt": "2018-03-11T03:04:28.961Z",
"updatedAt": "2018-03-11T03:04:28.961Z",
"driverId": 1,
"carId": 1
}
}
]
}
]
Note that there is no secret. Observe that the extra attribute that you're looking for, color, comes nested in the query result, not in the same nesting level of the Car or Driver. This is the correct behavior of Sequelize.
Make sure you can run this code and get the same result I do. My version of Node is different but I doubt that could be related to anything. Then, compare my code to your code and see if you can figure out what is causing you problems. If you need further help, feel free to ask in a comment :)
A note about many-to-many relationships with extra fields
Since I stumbled myself upon problems with this, and this is related to your situation, I thought I should add a section in my answer alerting you to the "trap" of setting up an overcomplicated many-to-many relationship (it's a lesson that I learned myself after struggling for a while).
Instead of repeating myself, I will just add a brief quote of what I said in Sequelize Issue 9158, and add links for further reading:
Junction tables, the tables that exist in relational databases to represent many-to-many relationships, initially have only two fields (the foreign keys of each table defining the many-to-many relationship). While it's true that it's possible to define extra fields/properties on that table, i.e. extra properties for the association itself (as you put in the issue title), care should be taken here: if it's getting overcomplicated, it's a sign that you should "promote" your junction table to a full-fledged entity.
Further reading:
My own self-answered question involving an overcomplicated setup of many-to-many relationships in sequelize: FindAll with includes involving a complicated many-to-(many-to-many) relationship (sequelizejs)
And its sibling question: Is it OK to have a many-to-many relationship where one of the tables involved is already a junction table?
I have three models ā Book, User and Institution ā which are associated to one another as follows:
Books are associated to Institutions via a Book_Institution join table (many to many relationship)
Book.belongsToMany(models.Institution, { through: 'Book_Institution' })
and
Institution.belongsToMany(models.Book, { through: 'Book_Institution' })
Users can be associated to Institutions in two ways: as reader or author. This is done via two join tables: Author_Institution and Reader_Institution:
Institution.belongsToMany(models.User, { through: 'Author_Institution' })
Institution.belongsToMany(models.User, { through: 'Reader_Institution' })
and
User.belongsToMany(models.Institution, { through: 'Author_Institution' })
User.belongsToMany(models.Institution, { through: 'Reader_Institution' })
(Each time leaving out foreignKey for brevity.)
I want to query the Book model to find all books that belong to an author. Sequelize provides the include option to easily join two associated tables. The problem Iām stuggling with is that using include as shown below defaults to the Reader_Institution association. How can I specify which association should be used?
getBooks: (obj, args, context) => {
const { user } = context
return Book.findAll({
attributes: ['id', 'path'],
include: [{
include: [{
attributes: ['id'],
model: User,
where: { id: user }
}],
model: Institution,
required: true // inner join
}]
})
}
Thanks in advance for your help.
I use as which allows you to reference the relationship through that alias.
Institution.belongsToMany(models.User, {
through: 'Author_Institution', // many-to-many relationship table name
as: 'AuthorInstitution' // alias
})
With your models set up this way, you can use as to to specify which relationship you want to include when querying.
getBooks: (obj, args, context) => {
const { user } = context
return Book.findAll({
attributes: ['id', 'path'],
include: [{
include: [{
attributes: ['id'],
model: User,
where: { id: user },
as: 'AuthorInstitution'
}],
model: Institution,
required: true // inner join
}]
})
}
Also, with this methodology, it allows you you to reference the relationship data via the as, so you can do book.AuthorInstitution and it will be the value of that object.
Suppose we have a such structure in NodeJs Sequelize.
var User = sequelize.define('user', {/* ... */})
var Project = sequelize.define('project', {/* ... */})
Project.hasMany(User)
In this part of video presenter offers to save embedded objects with two steps using promises. In our case it would be something like:
Project.create({
...
}).then(function(project){
User.create({
...
projectId:project.id
})
})
But this approach will result two db calls.
So, is it possible to save embedded objects (Project which contains User e.g. User must have Project's id as a foreign key) into the db with one db call or within a transaction using Sequelize?
You should be able to insert parent-children by passing an array of objects into a key with the same name as the "as" value used on the "include". Although the documentation is light on the usage, you can see it handled in the source code here.
No promises (pun semi-intended) that this is actually run in single SQL query, not sure of the exact implementation in Sequelize. You should be able to enable logging (logging: console.log in the Sequelize(options)) to see what it's running.
// specify an "as" value and require a User.project_id value
Project.hasMany(User, { as: 'Users', foreignKey: { allowNull: false } });
// define your Project -> Users as json with the "as" value as the key
const project = {
name: 'project name',
Users: [
{
name: 'user 1',
},
{
name: 'user 2',
},
],
};
// create a transaction and "include" the Model in the create, txn falls back in .catch()
sequelize.transaction(t =>
Project.create(project, {
include: [{
model: User,
as: 'Users',
}],
transaction: t,
})
)
.catch(e => console.log('the txn failed because', e));
I have an Assignment schema which has references to Groups and Projects.
Assignment == Group [One-One Relationship]
Assignment == Projects [One-Many Relationship]
Below is my Asssignment Schema
var AssignmentSchema = new Schema({
name: String,
group: {
type: Schema.Types.ObjectId,
ref: 'Group'
},
projects: [{type: mongoose.Schema.Types.ObjectId, ref: 'Project'}],
});
If a Group/Project is removed, how can i update my Assignment Schema.
var ProjectSchema = new Schema({
name: String
});
var GroupSchema = new Schema({
name: String
});
From couple of answers in stackoverflow, i came to know about the remove middleware, but i am not sure how to implement it for one-one and one-many relationship. Can anyone show me an example of doing it.
ProjectSchema.pre('remove', function(next){
this.model('Assignment').update(
);
});
Relationships:
A one-to-one is a relationship such that a state has only one
capital city and a capital city is the capital of only one state
A one-to-many is a relationship such that a mother has many
children, and the children have only one mother
A many-to-many is a relationship such that a book can be written by
several authors or co-authors, while an author can write several
books.
one-one relationship - If a Project/Group is removed, how can i update my Assignment Schema.
Typically you will have one project mapped to one assignment and similarly one assignment mapped to one project. what you can do here is removing a project and then find the associated project in assignment model and remove their references.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
Assignment.update({_id: project.assignment}},
{$pull: {projects: project._id}},
function (err, numberAffected) {
console.log(numberAffected);
} else {
console.log(err);
}
});
});
});
}
one-many relationship - If a Project/Group is removed, how can i update my Assignment Schema.
In this scenario we are removing a project and then finding all the assignments which belongs to this project and removing its reference from them. Here the situation is, there can be many assignments for a single project.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
Assignment.update({_id: {$in: project.assingments}},
{$pull: {project: project._id}},
function (err, numberAffected) {
console.log(numberAffected);
} else {
console.log(err);
}
});
});
});
}
Remove middleware
You could achieve the same thing via middleware as pointed out by Johnny, just a correction on that..
ProjectSchema.pre('remove', function (next) {
var project = this;
project.model('Assignment').update(
{ projects: {$in: project.assignments}},
{ $pull: { project: project._id } },
{ multi: true },
next
);
});
Typically there can be many projects belonging to an assignment and many assignments belonging to the same project. You will have an assignment column in your Project Schema where one project will relate to multiple assignments.
Note: remove middleware won't work on models and it would only work on your documents. If you are going with remove middleware ensure in your delete function, you find project by id first and then on the returned document apply the remove method, so for the above to work... your delete function would look like this.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
console.log(numberAffected);
}
});
});
}
In the remove middleware, you're defining the actions to take when a document of the model for that schema is removed via Model#remove. So:
When a group is removed, you want to remove the group reference to that group's _id from all assignment docs.
When a project is removed, you want to remove the projects array element references to that project's _id from all assignment docs.
Which you can implement as:
GroupSchema.pre('remove', function(next) {
var group = this;
group.model('Assignment').update(
{ group: group._id },
{ $unset: { group: 1 } },
{ multi: true },
next);
});
ProjectSchema.pre('remove', function (next) {
var project = this;
project.model('Assignment').update(
{ projects: project._id },
{ $pull: { projects: project._id } },
{ multi: true },
next);
});