Bulk Insertion in Sequelize hasMany() Association - node.js

First of all let me explain what I want to do. I have two model User and Product well define. I use method created by sequelize when defining association with hasMany() and belongsTo() to create my product.
user.hasMany(Product)
Product.belongsTo(User)
and I used this method to create the product from user.
User.findOne({where:{id:SomeID}}).then((userObj)=>{
userObj.createProduct(productobj).then((userObj)=>{
//product creation handling
}).catch((errorProduct)=>{
//product creation error handling
});
}).catch((error)=>{
//user creation error handling
});
My productobj is an array of json object that belongs to User.
It works fine when it's only one product but it failed when there is more than one product. So I wonder if there is any method that I can use to bulk create those products from the same user.
I may be doing it the wrong way. So if there is a better way then I will highly appreciate it. Thank you for your help.

The association user.hasMany(Product) will add below methods to User model.
/**
* The addAssociations mixin applied to models with hasMany.
* An example of usage is as follows:
*
* ```js
*
* User.hasMany(Role);
*
* interface UserInstance extends Sequelize.Instance<UserInstance, UserAttributes>, UserAttributes {
* // getRoles...
* // setRoles...
* addRoles: Sequelize.HasManyAddAssociationsMixin<RoleInstance, RoleId>;
* // addRole...
* // createRole...
* // removeRole...
* // removeRoles...
* // hasRole...
* // hasRoles...
* // countRoles...
* }
* ```
*
* #see https://sequelize.org/master/class/lib/associations/has-many.js~HasMany.html
* #see Instance
*/
For your case, you should use userModel.addProducts(productModels). E.g.
import { sequelize } from '../../db';
import { Model, DataTypes, HasManyAddAssociationsMixin } from 'sequelize';
class User extends Model {
public addProducts!: HasManyAddAssociationsMixin<Product, number>;
}
User.init(
{
email: {
type: DataTypes.STRING,
unique: true,
},
},
{ sequelize, modelName: 'users' },
);
class Product extends Model {}
Product.init(
{
name: DataTypes.STRING,
},
{ sequelize, modelName: 'products' },
);
User.hasMany(Product);
Product.belongsTo(User);
(async function test() {
try {
await sequelize.sync({ force: true });
// seed
await User.create(
{
email: 'example#gmail.com',
products: [{ name: 'apple' }, { name: 'amd' }],
},
{ include: [Product] },
);
// add another two products for this user
const SomeID = 1;
const productobj = [{ name: 'nvidia' }, { name: 'intel' }];
const productModels = await Product.bulkCreate(productobj);
const userObj = await User.findOne({ where: { id: SomeID } });
await userObj.addProducts(productModels);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
data row in the database:
=# select * from users;
id | email
----+-------------------
1 | example#gmail.com
(1 row)
node-sequelize-examples=# select * from products;
id | name | userId
----+--------+--------
1 | apple | 1
2 | amd | 1
3 | nvidia | 1
4 | intel | 1
(4 rows)
NOTE: These methods only accept target model(s) as their parameter, NOT a plain javascript object or array of objects. You need to bulkCreate the products. Then, use userModel.addProducts(productModels) to associate these products to this user

It seems this is not supported as yet (although requested since 2015). We have a few options to workaround:
If your model definitions allow it, you can do as with slideshowp2's answer: bulkCreate the products, and then assign them to the user. This won't work if you are allowing nulls on the customerId column (which is probably not a good idea).
If you know the userId, you can include this id in each product item to be created, then use Products.bulkCreate(). This will work.
Create every product individually against the user. There's various ways of doing this eg something like
Promise.all(
productArray.map((prod) => {
return user.createProduct(prod)
}))

Related

How do I cause n + 1 problems with NestJs TypeOrm?

I was studying TypeOrm and I'm trying to create an N+1 problem, but it's not happening properly. Company and employee have a 1:N relationship.
Could you tell me why N + 1 is not causing any problems? I've tried setting up Lazy and setting up Eager, but I've been doing left join continuously so that n + 1 doesn't cause problems.
entity
#Entity('COMPANY')
export class Company extends TimeStamped {
#PrimaryGeneratedColumn('increment')
companyId: number;
#Column({ type: 'varchar' })
companyName: string;
#OneToMany(() => Employee, (employee) => employee.company, {
onDelete: 'CASCADE'
})
employee: Employee[];
}
#Entity('EMPLOYEE')
export class Employee extends TimeStamped {
#PrimaryGeneratedColumn('increment')
employeeId: number;
#Column({ type: 'varchar' })
employeeName: string;
#ManyToOne(() => Company, (company) => company.employee)
#JoinColumn([{ name: 'companyId', referencedColumnName: 'companyId' }])
company: Company;
}
crud
#Injectable()
export class CompanyService {
constructor(
#InjectRepository(Company)
private readonly companyRepository: Repository<Company>
) {}
getAllCompany() {
return this.companyRepository.find({ relations: ['employee'] });
}
getCompany(companyId: number) {
return this.companyRepository.findOne(companyId, {
relations: ['employee']
});
}
setCompany(setComanyDto: SetCompanyDto) {
return this.companyRepository.save(setComanyDto);
}
}
#Injectable()
export class EmployeeService {
constructor(
#InjectRepository(Employee)
private readonly employeeRepository: Repository<Employee>,
#InjectRepository(Company)
private readonly companyRepository: Repository<Company>
) {}
getAllEmployee() {
return this.employeeRepository.find({
relations: ['company']
});
}
getEmployee(employeeId: number) {
return this.employeeRepository.findOne(employeeId, {
relations: ['company']
});
}
async setEmployee(setEmployeeDto: SetEmployeeDto) {
const employee: Employee = new Employee();
employee.employeeName = setEmployeeDto.employeeName;
employee.company = await this.companyRepository.findOne(
setEmployeeDto.companyId
);
return this.employeeRepository.save(employee);
}
}
I believe you have a good idea about what N+1 problem is. You can check this question if you need to understand it more clearly.
If you use eager loading, you will not see the N+1 problem anyway since it joins the related entity and return both entities in one query.
If you specify relations as you've done below, again you will not see the N+1 problem since it creates a join query and returns all in 1 single query.
this.companyRepository.find({ relations: ['employee'] });
To create the N+1 problem,
Update your Company entity like below:
#Entity('COMPANY')
export class Company extends TimeStamped {
#PrimaryGeneratedColumn('increment')
companyId: number;
#Column({ type: 'varchar' })
companyName: string;
#OneToMany(() => Employee, (employee) => employee.company, {
onDelete: 'CASCADE',
lazy: true
})
employee: Promise<Employee[]>
}
In your CompanyService, create a new function to simulate the N+1 problem like below:
#Injectable()
export class CompanyService {
async createNPlus1Problem() {
// Query all companies (let's say you have N number of companies)
// SELECT * FROM "COMPANY";
const companies = this.companyRepository.find();
// The following `for` loop, loops through all N number of
// companies to get the employee data of each
for(company of companies) {
// Query employees of each company
// SELECT * FROM "EMPLOYEE" WHERE "companyId"=?;
const employees = await company.employee;
}
}
}
So in the above example, you have 1 query to get the company data. And N queries to get the employee data. Hence the N+1 problem.
Hope this clarifies your problem. Cheers 🍻 !!!
You can try to use this library https://github.com/Adrinalin4ik/Nestjs-Graphql-Tools it allows to overcome n+1 with the simple decorator. And it has minimum deps.
You can use leftJoinAndSelect method with query builder.
https://orkhan.gitbook.io/typeorm/docs/select-query-builder#joining-relations
const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getOne()
SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE user.name = 'Timber'

TypeORM OneToMany filter in relations not effect to result

I have two tables:
#Entity('Reviews')
class Review {
...
#OneToMany((type) => MapCategory, map => map.review)
public categories: MapCategory[];
}
And:
#Entity('MapCategories')
export class MapCategory {
...
#ManyToOne(type => Review, (review) => review.categories)
public review: Review;
}
When I try the filter on 'categories' but the result doesn't filter 'categories' following the key that I already push.
const items = await this.reviewRepository.findAndCount({
relations: ['categories'],
where: {
categories: {
id: 1
}
}
});
We need to use queryBuilder for cases like this since find doesn't allow filtering relations:
const items = await reviewRepository.createQueryBuilder("review")
.leftJoinAndSelect("review.categories", "category")
.where("category.id = :id", { id })
.getManyAndCount()
I prefer to avoid query builder when possible.
There's a workaround for filtering based on relation fields for findOne()/find() methods that I've discovered recently. The problem with filtering related table fields only exists for ObjectLiteral-style where, while string conditions work perfectly.
Assume that we have two entities – User and Role, user belongs to one role, role has many users:
#Entity()
export class User {
name: string;
#ManyToOne(() => Role, role => role.users)
role: Role;
}
#Entity()
export class Role {
#OneToMany(() => User, user => user.role)
users: User[];
}
Now we can call findOne()/find() methods of EntityManager or repository:
roleRepository.find({
join: { alias: 'roles', innerJoin: { users: 'roles.users' } },
where: qb => {
qb.where({ // Filter Role fields
a: 1,
b: 2
}).andWhere('users.name = :userName', { userName: 'John Doe' }); // Filter related field
}
});
You can omit the join part if you've marked your relation as an eager one.

How to delete associated rows with sequelize

I am trying to delete rows associated with a row in a table, without deleting the main row (thus can't use CASCADE).
This is the raw PostgreSQL query that does what I want to achieve with SQL. Is sequelize able to generate such query:
DELETE FROM session USING user WHERE session.user_id = user.id AND user.username = 'bob'
The model is (not including irrelevant columns):
create table user (
id uuid primary key default uuid_generate_v4(),
username text UNIQUE NOT NULL
);
create table session (
id uuid primary key default uuid_generate_v4(),
user_id uuid NOT NULL references user(id) ON DELETE CASCADE
);
The association is defined in sequelize as:
Session.belongsTo(models.User, {
foreignKey: "user_id"
});
User.hasMany(models.Session, {
foreignKey: "user_id"
});
An alternative version of the query could be:
DELETE FROM session WHERE session.user_id = (SELECT user_id FROM user WHERE username = 'bob');
But I think sequelize doesn't handle subqueries yet?
I tried something along the lines:
return Session.destroy({
include: [
{ model: User, where: { username: 'bob' }}
],
truncate: false
});
However, sequelize complains:
Error: Missing where or truncate attribute in the options parameter of model.destroy.
If anyone gets here, this is how I "delete associated rows with sequelize": little help from the library:
Read the user from db (Users.findOne({ ... }))
call the method setSessions(array) provided by sequelize (the name depends on your model) which returns a promise.
/** #param {string} username */
const clearUserSessions = async (username) {
const userInstance = await Users.findOne({
where: { username },
include: ['sessions']
})
if (!userInstance) {
/* user not found */
return ...
}
await userInstance.setSessions([])
/* removed every session from user */
return ...
};
later:
try {
await clearUserSessions('bob')
} catch(err) {
...
}
return Session.destroy({
where: {
'$users.username$': 'bob'
},
include: [{
model: User,
as: 'users'
}],
});
Hope that helps. Try to reach me with comment.

How to add dynamic additional attributes into a junction table with ManyToMany association in Sequelize

I created a many-to-many association by sequelize in my koa app. But I had no idea on how to create additional attributes in the junction table. Thanks.
I referred to the official doc of sequelize but didn't find a solution. In brief:
"an order can have many items"
"an item can exist in many orders"
Then I created OrderItems as junction table.
But I have trouble in inserting value into the junction table
// definitions
const Item = sequelize.define('item', itemSchema);
const Order = sequelize.define('order', orderSchema);
// junction table
const OrderItems = sequelize.define('order_item', {
item_quantity: { type: Sequelize.INTEGER } // number of a certain item in a certain order.
});
// association
Item.belongsToMany(Order, { through: OrderItems, foreignKey: 'item_id' });
Order.belongsToMany(Item, { through: OrderItems, foreignKey: 'order_id' });
// insert value
const itemVals = [{ name: 'foo', price: 6 }, { name: 'bar', price: 7 }];
const orderVals = [
{
date: '2019-01-06',
items: [{ name: 'foo', item_quantity: 12 }]
},
{
date: '2019-01-07',
items: [{ name: 'foo', item_quantity: 14 }]
}
]
items = Item.bulkCreate(itemVals)
orders = Order.bulkCreate(orderVals)
//Questions here: create entries in junction table
for (let order of orders) {
const itemsInOrder = Item.findAll({
where: {
name: {
[Op.in]: order.items.map(item => item.name)
}
}
})
order.addItems(itemsInOrder, {
through: {
item_quantity: 'How to solve here?'
}
})
}
// my current manual solution:
// need to configure column names in junction table manually.
// Just like what we do in native SQL.
const junctionValList =[]
for (let orderVal of orderVals) {
orderVal.id = (await Order.findOne(/* get order id */)).dataValues.id
for (let itemVal of orderVal.items) {
itemVal.id = (await Item.findOne(/* get item id similarly */)).dataValues.id
const entyInJunctionTable = {
item_id: itemVal.id,
order_id: orderVal.id,
item_quantity: itemVal.item_quantity
}
junctionValList.push(entyInJunctionTable)
}
}
OrderItems.bulkCreate(junctionValList).then(/* */).catch(/* */)
In case that this script it's for seeding purpose you can do something like this:
/*
Create an array in which all promises will be stored.
We use it like this because async/await are not allowed inside of 'for', 'map' etc.
*/
const promises = orderVals.map((orderVal) => {
// 1. Create the order
return Order.create({ date: orderVal.date, /* + other properties */ }).then((order) => {
// 2. For each item mentioned in 'orderVal.items'...
return orderVal.items.map((orderedItem) => {
// ...get the DB instance
return Item.findOne({ where: { name: orderedItem.name } }).then((item) => {
// 3. Associate it with current order
return order.addItem(item.id, { through: { item_quantity: orderedItem.item_quantity } });
});
});
});
});
await Promise.all(promises);
But it's not an efficient way to do it in general. First of all, there are a lot of nested functions. But the biggest problem is that you associate items with the orders, based on their name and it's possible that in the future you will have multiple items with the same name.
You should try to use an item id, this way you will be sure about the outcome and also the script it will be much shorter.

Association sequelize 3 tables, many to many

I have 4 tables
- client
- project
- user
- userProject
One Project belongs to client and it needs to have client foreign key client_id.
UserProject has project_id and user_id foreign keys, belongs to project and user.
One user owns the clients of his projects.
How can I list the clients of one user?
I'm wondering you could use eager loading feature from sequalize:
Client.findAll({
include: [{
model: Project,
include: [{
model: User,
where: {
id: <UserId>
},
required: false
}]
}]
}).then(function(clients) {
/* ... */
});
This creates a many-to-many relation between user and project. Between user and client you therefor have a many-to-many-to-one relation. This is not supported by sequelize. I would have created an instance method on the User model like this:
User = sequelize.define('user', {
// ... definition of user ...
},{
instanceMethods: {
getClients: function() {
return this.getProjects().then(function (projects) {
var id_map = {};
for (var i = 0; i < projects.length; i++) {
id_map[projects[i].clientId] = 1;
}
return Client.findAll({
where: { id: [Object.keys(id_map)] }
});
});
}
}
});
and then call this function when you have a user instance like this, to get the clients:
user.getClients().then(function (clients) {
console.log(clients);
});

Resources