Sequelize belongsToMany get source model by querying on target model - javascript

I have two models Brand and Campaign.
A Brand can have many Campaigns
export default(sequelize, DataTypes)=> {
const Brand = sequelize.define('Brand', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
})
Brand.associate = models=> {
Brand.belongsToMany(models.Campaign, {
through: models.CampaignBrand,
foreignKey: 'brand',
})
}
return Brand
}
A Campaign can also have many Brand
export default(sequelize, DataTypes)=> {
const Campaign = sequelize.define('Campaign', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
})
Campaign.associate = models=> {
Campaign.belongsToMany(models.Brand, {
through: models.CampaignBrand,
foreignKey: 'campaign',
})
}
return Campaign
}
And here is through model:
export default(sequelize, DataTypes)=> {
const CampaignBrand = sequelize.define('CampaignBrand', {
// see enums
status: {
type: DataTypes.INTEGER,
allowNull: false,
},
roleText: {
type: DataTypes.STRING,
},
})
CampaignBrand.associate = models=> {
CampaignBrand.belongsTo(models.Campaign, {
foreignKey: 'campaign',
targetKey: 'id',
onDelete: 'CASCADE',
})
}
return CampaignBrand
}
In case I want to get Campaigns by brand. What should I do?
I have tried query likes document mentioned but it does not work for me
With Belongs-To-Many you can query based on through relation and select specific attributes. For example using findAll with through
User.findAll({
include: [{
model: Project,
through: {
attributes: ['createdAt', 'startedAt', 'finishedAt'],
where: {completed: true}
}
}]
});
I have found some ways to work around, but it is not what I am looking for:
SOLUTION 1:
Update belongsToMany Brand to hasMany CampaignBrand and the query by CampaignBrand.brand
SOLUTION 2:
Get Campaign by querying Brand
Any other advices?
Dialect: postgres
Database version: 9.4
Sequelize version: 4.2.1

I think you don't need this association in the the through model:
CampaignBrand.associate = models=> {
CampaignBrand.belongsTo(models.Campaign, {
foreignKey: 'campaign',
targetKey: 'id',
onDelete: 'CASCADE',
})
}
You already have the belongsToMany association in the definitions of Brand and Campaign, so I think you just need to create the CampaignBrand model with your status and roleText attributes.
As I understand it, then you can query brands through campaigns and it should return each brand element and its associated campaigns,
Brand.findAll({
include: [{
model: Campaign
}]
});

This answer is kinda old, but with belongstomany associations, you can use mixins.
https://sequelize.org/api/v6/class/src/associations/belongs-to-many.js~belongstomany
const campaigns = await CampaignModel.findAll();
const campaignsBrandsModels = await campaigns.getBrandsModels({
limit:1,
through: {where:{pivot_field:'pivot_value'} } // << You want this, i think!
});
Now, remember that, the mixins (get, set, count...) set their name with inflection library, so, a way to set the names without error (or without spending a lot of time searching the name in in the console) is setting an 'as' alias in the Association
CampaignBrand.belongsToMany(models.Campaign, {
through: models.CampaignBrand,
foreignKey: 'brand',
as: 'BrandsModels' //<<<< This is Super important! at least for easy usage
})
You can achieve this with an include in the findAll method too, BUT!
the 'limit: #' part will not work! (Will give you the error of "This only works with hasMany because separate: true")
I really hope this can help anyone, seya!

Related

Sequelize Self Relationship with junction table

I want to build a sequelize relationship that represents : An item is composed by a specific amount of others items.
Database tables
Item (itemId, name)
Ingredient (ingredientId, itemParentId, itemChildrenId, amount)
Sequelize models
// Item.js
class Item extends Sequelize.Model { }
Item.init({
itemId: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: Sequelize.STRING,
}, {
sequelize: db,
})
// Ingredient.js
class Ingredient extends Sequelize.Model { }
Ingredient.init({
ingredientId: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
amount: Sequelize.INTEGER,
}, {
sequelize: db,
})
And I am just trying to write the correct sequelize association to match my database logic, so I tried :
// Association.js
Item.belongsToMany(Item, { through: Ingredient, as: 'ingredients', foreignKey: 'itemParentId' })
But I'm having this error Unknown column 'ingredients->ingredient.ingredientItemId' in 'field list', which is true but I do not know how to specify the right keys/columns.
Any help, please!
I see few problems: First, you're performing Item.init within the Ingredient model. Probably a mistake on your part. Change it to Ingredient.init.(personally i never used this "init" api, i define models differently, so i'm not sure how it works)
Second, change the primary keys of both Ingredient and Item to be just "id", like:
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
}
Also, your association isn't correct, it needs to be through a junction table:
Item.belongsToMany(Ingredient, { through: "ItemIngredient"})
Note that "through" referes here to a table name, that Sequelize will create automatically if you're using model.sync(), and if not- you will need to create it yourself(or with a migration, which i recommend), with columns: itemId, ingredientId.
You also need to add the "reverse" association, like that:
Ingredient.belongsToMany(Item, { through: "ItemIngredient"})

How can a model (user) has only one other model (eg workplace), but the workplace belongs to many users in Sequelize.js?

I would like to have one model (that's the user) that belongs to one other model (that should be the workplace), but the workplace (e.g. Apple) should have many users that belong to them. How can I do that using Sequelize?
I already tried to use one User.hasOne(models.Workplace), but this only allows each Workplace to have one User, because it creates the column UserId in the Workplace table.
This is the code of my user model:
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
username: {
type: DataTypes.STRING
},
firstname: {
type: DataTypes.STRING
}
}, {})
User.associate = (models) => {
models.User.hasOne(models.User)
}
return User
}
And this is the code of my workplace model:
module.exports = (sequelize, DataTypes) => {
const Workplace = sequelize.define('Workplace', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
}
}, {})
return Workplace
}
Thank you!
Let's think about this problem in lower level.
I assume you use some kind of relational database since you're using Sequelize on top of it.
Your requirement is that user has only one other model (eg workplace), but the workplace belongs to many users.
It can be translated into user model has foreign key workplaceId in their table and workspace has no foreign key in their table.
For example, { userId: 1, workspaceId: 1 }, { userId: 2, workspaceId: 1 }, { workspaceId: 1 } would be what you expected.
How can we achieve this?
Just set belongsTo relationship in user model.
(It generates workspaceId in your table)
It's always good to think in database layer first because after all Sequelize is just an abstract layer for that.

sequelize find data that is associated

I have 2 models Prophet and Task and they have a m:n relationship:
Prophets
const prophet = sequelize.define('prophets', {
name: {
type: Sequelize.STRING,
primaryKey: true,
unique: true
}
});
prophet.relationship = function(task) {
prophet.belongsToMany(task, {
through: 'prophetTasks',
as: 'tasks',
foreignKey: 'prophetName'
});
};
Tasks
const task = sequelize.define('tasks', {
name: {
type: Sequelize.STRING,
primaryKey: true,
unique: true
}
});
task.relationship = function(prophet) {
task.belongsToMany(prophet, {
through: 'prophetTasks',
as: 'prophets',
foreignKey: 'taskName'
});
};
EDITED:
my problem is sometimes I have to update a prophet which might remove some relationships with tasks, but I cant figure out how to delete the tasks that have no more relationship with any prophets.
I believe I should find all tasks that doesnt belong in prophetTasks table anymore, but I dont know how to query that with sequelize
You can use ON DELETE CASCADE
So whenever the row is deleted, data containing it as a foreign key will be deleted.

Sequelize Populate Relation Query Either Table

I have the following two tables in Sequelize
const Tokens = sequelize.define("Tokens", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
active: {
type: DataTypes.BOOLEAN
}
});
and
const User = sequelize.define("Users", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
first_name: {
type: DataTypes.STRING
}
});
User.associate = models => {
models["Users"].hasMany(models["Tokens"], {foreignKey: 'userID', as: 'tokens_userid'});
};
I'm trying to run the following query in Sequelize.
const token = await db.Tokens.findOne({
where: {
id,
active: true
},
include: ["tokens_userid"]
});
But I'm getting the following error.
Error: Association with alias "tokens_userid" does not exists
My main goal is to get the user based on a Token ID. Now I would just move that association to the User table, but the problem with that later on I will want to get all the tokens for a given User ID. So I will run into this problem either way.
I tried adding the following line, but it was complaining about circular relations or something like that.
models["Tokens"].hasOne(models["User"], {foreignKey: 'userID', as: 'tokens_userid'});
How can I query either the Users or Tokens table and have it populate correctly with the relation?
I was able to solve this by adding the following line to my table.
models["Tokens"].belongsTo(models["User"], {foreignKey: 'userID', as: 'tokens_userid_from_token'});
Basically what I tried before but changed hasOne to belongsTo.
Hopefully this helps someone else.

How do I ORM additional columns on a join table in sequelize?

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?

Categories