How to do cascading inserts with tables having auto increment id columns in TypeORM - mysql

I am trying to implement cascading inserts using typeorm. The child table entries include a foreign key reference to the ID field on the parent table.
It seems typeorm is not capturing the auto-increment id from the parent row and supplying it to the child inserts.
Here is the parent Entity:
import ...
#Entity("parent")
export class Parent {
#PrimaryGeneratedColumn()
id: number;
#OneToOne(type => Account, accountId => accountId.provider)
#JoinColumn({name: "account_id"})
accountId: Account;
#Column("varchar", {
name: "name",
nullable: false,
length: 255,
})
name: string;
#OneToMany(type => Child, Children => Children.parentId, {
cascadeInsert: true,
cascadeUpdate: true
})
Children: Child[];
}
and the child entity:
import ...
#Entity("child")
export class Child{
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(type => Parent, parentId => parentId.children)
#JoinColumn({name: "parent_id"})
parentId: Parent;
#Column("varchar", {
name: "number",
nullable: true,
length: 45,
})
number: string;
}
the console log shows that the sql being generated does not include the foreign key column, producing the obvious error ER_NO_DEFAULT_FOR_FIELD: Field 'parent_id' doesn't have a default value
info: executing query: START TRANSACTION
info: executing query: INSERT INTO parent(name, account_id) VALUES (?,?) -- PARAMETERS: ["Name","id"]
info: executing query: INSERT INTO child(number) VALUES (?) -- PARAMETERS: ["12345678"]
info: query failed: INSERT INTO child(number) VALUES (?) -- PARAMETERS: ["12345678"]
info: error: { Error: ER_NO_DEFAULT_FOR_FIELD: Field 'parent_id' doesn't have a default value
Is there some way that typeorm can be instructed to capture the LAST_INSERT_ID() or otherwise populate the foregin key id field on the child row?

If the id is not auto generated, but assigned before saving the entity, it works.
Use a uuid as primary key.
import ...
#Entity("parent")
export class Parent {
#PrimaryColumn()
id: string;
#OneToOne(type => Account, accountId => accountId.provider)
#JoinColumn({name: "account_id"})
accountId: Account;
#Column("varchar", {
name: "name",
nullable: false,
length: 255,
})
name: string;
#OneToMany(type => Child, Children => Children.parentId, {
cascadeInsert: true,
cascadeUpdate: true
})
Children: Child[];
}
and when you create the entity.
const parent: Parent = new Parent();
parent.id = "896b677f-fb14-11e0-b14d-d11ca798dbac"; // your uuid goes here
Unfortunately you can not use #PrimaryGeneratedColumn("uuid") either.
There are a lot of libraries to generate a uuid in JS.
Example with uuid-js - https://github.com/pnegri/uuid-js
const parent: Parent = new Parent();
parent.id = UUID.create(4).toString();
I think this is more like a workaround, rather than a solution to the problem. But I could not find a way to tell TypeORM to consider generated ids when cascading children.

Related

How to use innerJoinAndSelect to merge two tables into one?

everybody! I have two related tables, and one of them contains column with id of the second table's record. So I want to extract several properties from the second table and сoncatenate them with the result of the first one.
I faced the problem which is when I try to make a query via SQL string(that is SELECT * FROM `characters` `c` INNER JOIN `money` `m` ON `c`.`bank` = `m`.`id` WHERE uuid = ?) — everything works fine. But when I do the same things via TypeORM the queryBuilder returns only data from the first table (characters).
My chatacters Entity:
export default class Character extends BaseEntity {
#PrimaryGeneratedColumn()
public id: number;
#Column()
uuid: number;
...
#OneToOne(() => Money)
#JoinColumn()
bank: Money;
...
public static async findByUuid(uuid: number): Promise<Character> {
return await getManager()
.createQueryBuilder(Character, "c")
.innerJoinAndSelect(Money, "m", "c.bank = m.id")
.where("uuid = :uuid ", { uuid })
.getOne();
}
}
My money Entity:
export default class Money extends BaseEntity {
#PrimaryGeneratedColumn()
public id: number;
#Column()
type: number;
#Column()
holder: string;
#Column()
balance: number;
}
I tried to replace c.bank = m.id with c.bankId = m.id, but it also didn't work. I tried to remove #OneToOne relation, but I got the same effect. I turned "logging" option on and copied SQL string generated, then I executed it via PhpMyAdmin and ... It worked! It seems TypeORM modifies query's result and gives me the row of the first table only. I don't understand what goes wrong. Please, help.
You should be able to do this in two ways:
1. Using QueryBuilder (like you tried):
const character = await getManager()
.createQueryBuilder(Character, "c")
.innerJoinAndSelect("c.bank", "bank")
.where("c.uuid = :uuid ", { uuid })
.getOne();
return character;
2. Using findOne:
const character = await getManager()
.getRepository(Character)
.findOne({ relations: ["bank"], where: { uuid } });
return character;
Now you should be able to access bank using character.bank
Update: For removing the foreign key bankId from characters table, you can do something like below.
#OneToOne(() => Money, { createForeignKeyConstraints: false })
bank: Money;
See the reference from typeorm documentation.
As you said, if some other server is trying to save id into bank field, that would be problematic because your Character entity's bank property is of type Money. So better if you change the name of the relation to something else to avoid conflict.
Hope this helps. Cheers 🍻 !!!

Sequelize create with associations

I have been trying to define a relationship between 3 tables and then create them all in one create function. For some reason, while creating the 3 models, the linking IDs (foreign keys) are undefined and are not passing on. Here are the associations:
Person.js:
models.person.Lead = models.person.hasMany(models.lead, {
onDelete: "CASCADE",
foreignKey: "person_id"
});
Lead.js:
models.lead.Person = models.lead.belongsTo(models.person, {foreignKey: 'person_id'});
models.lead.Sealant_customer = models.lead.hasOne(models.sealant_customer, {
onDelete: "CASCADE",
foreignKey: 'lead_id'
})
sealantCustomer.js:
models.sealant_customer.Lead = models.sealant_customer.belongsTo(models.lead);
The build function:
let sealantCustomer = models.sealant_customer.build({
address: body.address,
city: body.city,
roof_size: body.roofSize,
last_sealed: body.lastSealed,
existingSealant: body.existingSealant,
leaks_freq: body.leaksFrequency,
floor: body.floor,
elevator: body.elevator,
panels: body.panels,
home_type: body.homeType,
urgency: body.urgency,
next_step: body.nextStep,
more_info: body.moreInfo,
lead: {
site,
url: body.url,
date,
ip: body.ip,
person: {
name: body.name,
email: body.email,
phone: body.phone,
date,
city: body.city ? body.city : undefined,
address: body.address ? body.address : undefined,
}
}
}, {
include: [{
model: models.lead,
association: models.sealant_customer.Lead,
include: [{
model: models.person,
association: models.lead.Person
}]
}]
})
The outputted object is good except for the fact that lead_id and person_id are nulls (Each model has its own ID, but not the associated model's id). I also should note there are no validation errors and the data is good.
The library has a bug in the build function as far as I can tell. Same syntax with create worked perfectly.
In Sequelize v6, the association identifier in the include section is not valid. Otherwise, this build function should properly work.

How to get create() return auto-increment primary key after creation in sails.js?

I have a model, Case.js:
...
attributes: {
id: {
type: 'integer',
unique: true,
primaryKey: true,
columnName: 'pid' //an auto-increment primary key, generated by MySQL
},
...
}
And I want to get this id after creation:
Case.create({...}).then(function(aCase){
console.log(aCase.id);
})
The creation succeeded, but the output I got is undefined.
I tried setting autoPK to false, and deleting "unique" and "primaryKey" entry, but the result didn't change.
Please tell me how to make create() return this id.
I've worked it out myself. The problem lies in my model Case.js.
In sails.js, if you want a primary key (usually id) created by MySQL with auto-increment to be returned after create(), your model should look like this:
module.exports = {
...
autoPK: false,
attributes: {
id: {
type: 'integer',
unique: true,
primaryKey: true,
autoIncrement: true,
},
...
}
}
Pay attention to "autoIncrement" attribute, it is necessary in my case, and probably in every auto-increment primary key.
I'm looking at the Sails.js documentation for creating a new entry in a database. The method is indicated as
Something.create(values).exec(function (err, records) {
});
In your case, you should have
Case.create({...}).exec(function(err, aCase){
console.log(aCase.id);
})

Self-Referencing ManyToMany Relationship TypeORM

I have just started using TypeORM and I'm struggling getting the following relationship to work:
User->Friends, whereas a Friend is also a User Object.
My getters, getFriends & getFriendsInverse are working, however; I do now want to distinguish between the two. In other words; when I perform a mysql join I do not want to do a left join on friends and another one on inverseFriends.
The getter getFriends() needs to return all friends, regardless of which "side" the object I'm on.
Does that make sense?
This is my model definition:
getFriends() {
// This method should also return inverseFriends;
// I do not want to return the concat version; this should
// happen on the database/orm level
// So I dont want: this.friends.concat(this.inverseFriends)
return this.friends;
}
#ManyToMany(type => User, user => user.friendsInverse, {
cascadeInsert: false,
cascadeUpdate: false,
})
#JoinTable()
friends = [];
#ManyToMany(type => User, user => user.friends, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: false,
})
friendsInverse = [];
I hope someone understands my question :D
Thanks
Matt
You can self-reference your relations. Here is an example of a simple directed graph (aka a node can have a parent and multiple children).
#Entity()
export class Service extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
#Index({ unique: true })
title: string;
#ManyToOne(type => Service, service => service.children)
parent: Service;
#OneToMany(type => Service, service => service.parent)
children: Service[];
}
An important note to keep in mind is that these relations are not auto loaded when reading an object from the DB with find* functions.
To actually load them, you have to use query builder at the moment and join them. (You can join multiple levels.) An example:
let allServices = await this.repository.createQueryBuilder('category')
.andWhere('category.price IS NULL')
.innerJoinAndSelect('category.children', 'product')
.leftJoinAndSelect('product.children', 'addon')
.getMany();
Please note how I used different names to reference them (category, product, and addon).
I believe I'm 3 years late, but better late than ever. The most upvoted answer does not answer the question, as it only works for tree-like and hierarchical structures, so if you follow that example, this would happen:
Fred
/ \
Albert Laura
/ \
John Foo
In this example, Foo can't be friends with Fred, because he can only have one parent. Friends is not a tree structure, it's like a net. The answer would be the following:
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
#Entity(UserModel.MODEL_NAME)
export class UserModel {
static MODEL_NAME = 'users';
#PrimaryGeneratedColumn()
id?: number;
#Column({ type: 'varchar', unique: true, length: 50 })
username: string;
#Column({ type: 'varchar', length: 50, unique: true })
email: string;
#ManyToMany(type => UserModel)
#JoinTable()
friends: UserModel[];
#Column({ type: 'varchar', length: 300 })
password: string;
}
This would create a table where relations between people would be saved. Now for the next important stuff. How do you query this and get a user's friends? It's not as easy as it seems, I've played hours with this and haven't been able to do it with TypeORM methods or even query builder. The answer is: Raw Query. This would return an array with the user's friends:
async findFriends(id: Id): Promise<UserModel[]> {
return await this.userORM.query(
` SELECT *
FROM users U
WHERE U.id <> $1
AND EXISTS(
SELECT 1
FROM users_friends_users F
WHERE (F."usersId_1" = $1 AND F."usersId_2" = U.id )
OR (F."usersId_2" = $1 AND F."usersId_1" = U.id )
); `,
[id],
);
}
(users_friends_users is the autogenerated name that typeORM gives to the table where the relations between users are saved)
2021 here, was searching for the same problem and find a way to solve it without custom raw SQL (providing same model as example for simplicity):
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, JoinTable } from 'typeorm';
#Entity(UserModel.MODEL_NAME)
export class UserModel {
static MODEL_NAME = 'users';
#PrimaryGeneratedColumn()
id?: number;
#Column({ type: 'varchar', unique: true, length: 50 })
username: string;
#Column({ type: 'varchar', length: 50, unique: true })
email: string;
#ManyToMany(type => UserModel)
#JoinTable({ joinColumn: { name: 'users_id_1' } })
friends: UserModel[];
#Column({ type: 'varchar', length: 300 })
password: string;
}
The key moment here is to set joinColumn for JoinTable.
When you are defining ManyToMany relationship, TypeORM automatically creates n-n table users_friends_users with one column named user_id_1 and another user_id_2 (they are automatically incremented if foreign key is the same)
So it is enough to choose any column from this table as "primary join column" and it works
I'm working on a similar feature and facing the same problem.
Defined the same #ManyToMany relation as #Aleksandr Primak , but the relation isn't bi-directionnal.
Example:
Case 1 :
Auto-generated table users_friends_users contains value
[userId_1, userId_2] = 70, 19
When I'm logged with userId=70 and request currentUser, it returns the friends[19]
Case 2 :
Auto-generated table users_friends_users contains previously's value reversed
[userId_1, userId_2] = 19, 70
Still logged with userId=70 and request currentUser, it returns an empty list of friends[]
So I guess the only way is to use Raw Query as #Javi Marzán said

Sequelize.js foreign key

When using Sequelize.js, the following code doesn't add any foreign key on tables.
var MainDashboard = sequelize.define('main_dashboard', {
title: Sequelize.STRING
}, {
freezeTableName: true
})
MainClient.hasOne(MainDashboard, { foreignKey: 'idClient' })
MainDashboard.hasOne(MainClient, { foreignKey: 'clientId' })
sequelize.sync({ force: true })
Is there any way to force Sequelize.js to add these foreign key constraints?
Before I had the same problem, and solved when I understood the functioning of settings Sequelize.
Straight to the point!
Suppose we have two objects: Person and Father
var Person = sequelize.define('Person', {
name: Sequelize.STRING
});
var Father = sequelize.define('Father', {
age: Sequelize.STRING,
//The magic start here
personId: {
type: Sequelize.INTEGER,
references: 'persons', // <<< Note, its table's name, not object name
referencesKey: 'id' // <<< Note, its a column name
}
});
Person.hasMany(Father); // Set one to many relationship
Maybe it helps you
Edit:
You can read this to understand better:
http://docs.sequelizejs.com/manual/tutorial/associations.html#foreign-keys
For Sequelize 4 this has been updated to the following:
const Father = sequelize.define('Father', {
name: Sequelize.STRING
});
const Child = sequelize.define('Child', {
age: Sequelize.STRING,
fatherId: {
type: Sequelize.INTEGER,
references: {
model: 'fathers', // 'fathers' refers to table name
key: 'id', // 'id' refers to column name in fathers table
}
}
});
Father.hasMany(Child); // Set one to many relationship
Edit:
You can read more on associations at https://sequelize.org/master/manual/assocs.html
You need to add foreignKeyConstraint: true
Try:
MainClient.hasOne(MainDashboard, { foreignKey: 'idClient', foreignKeyConstraint: true })
I just tried to run your code, and the rows seem to be created fine:
CREATE TABLE IF NOT EXISTS `main_dashboard` (`title` VARCHAR(255), `id` INTEGER NOT NULL auto_increment , `idClient` INTEGER, PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `main_client` (`id` INTEGER NOT NULL auto_increment, `clientId` INTEGER, PRIMARY KEY (`id`)) ENGINE=InnoDB;
clientId is added to main_client, and idClient is added to main_dashboard
It seems you have slightly confused what the hasOne method does. Each time you call hasOne an association is created, so your code effectively associates the two tables twice. The method you are looking for is belongsTo
If you want each client to have one dashboard, the code would be the following:
MainClient.hasOne(MainDashboard, { foreignKey: 'clientId' })
MainDashboard.belongsTo(MainClient, { foreignKey: 'clientId' })
This creates a clientId field on the main_dashboard table, which relates to the id field of the main_client table
In short belongsTo adds the relation to the table that you are calling the method on, hasOne adds it on the table that is given as argument.
It's amazingly simple.
const MainDashboard = this.sequelize.define('main_dashboard', {/* attributes */}),
MainClient = this.sequelize.define('main_client', {/* attributes */});
MainDashboard.belongsTo(MainClient, { foreignKey: 'clientId' }); // Adds clientId to MainDashboard
It will link this as a foreign key and you may use it as an association. Let me know if I'm missing anything.