Self-Referencing ManyToMany Relationship TypeORM - mysql

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

Related

TypeORM find entities where entity.array contains id

I am using NestJS + typescript. This is my entity:
#Entity()
export class YearCourse {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#ManyToMany(() => User)
#JoinTable()
admins: User[];
}
And now given user I would like to find all yearCourse entities where given user is it's admin.
Prototype looks like this:
async getByUser(user: User): Promise<YearCourse[]> {
return this.yearCourseRepository.find({where: { admins.contains(user) }})
}
This obviously doesn't work. How to achieve this type of search?
To write more advanced SQL queries with TypeOrm, you can use the Query Builder.
Considering your admin id is stored as adminId (admin_id) in the User entity, it should look like this:
// query builder name ('year') is completely customisable
return this.yearCourseRepository.createQueryBuilder('year')
// load "admins" relation (user entity) and select results as "admins"
.leftJoinAndSelect('year.admins', 'admins')
// search where users have the "user.id" as "adminId"
.where('admins.adminId = :id', { id: user.id })
// get many results
.getMany();
I recommend you to write this directly in your repository file when using Query Builder for more cleanliness

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 🍻 !!!

return avg value in typeorm

I work at an project with typeorm.
In my project I have these entities:
Course.ts
export class Course extends DefaultBaseEntity {
#Column({name: "course_name"})
courseName: string;
#Column({name:"discipline"})
discipline: string;
#Column({default: 0}) rating: number;
}
and RatingsCourse.ts
export class CoursesRatings extends BaseEntity {
#PrimaryGeneratedColumn({type: "bigint"})
public id: number;
// other columns defined here
#Column({type: "bigint"})
userId: number;
#Column({type: 'bigint'}) courseId: number;
#Column() rating: number;
}
the idea it's every course has ratings. when I add a new rating I want to save in the rating field from Course the average ratings value from CoursesRatings
I code something like this :
await CoursesRatings.create({
courseId: ratingBody.courseId,
userId: user.id,
rating: ratingBody.rating
}).save();
const ratingsObjects = await CoursesRatings.find({
where: {
courseId: ratingBody.courseId,
userId: user.id
},
select: ["rating"]
});
const ratings: number[] = ratingsObjects.map(x => x.rating);
const avgRating = ratings.reduce((a, b) => a + b) / ratings.length;
const course = await Course.findOne(ratingBody.courseId);
course.rating = Math.fround(avgRating);
await course.save();
but it take some time.
I want to know how to optimize this code. I appreciate any help. Thanks!
One of the available options for implementing this is creating a database trigger as follows -
CREATE TRIGGER computeAvg
AFTER INSERT ON CoursesRatings
FOR EACH ROW
UPDATE Course
SET rating = (SELECT AVG(rating) FROM CoursesRatings
WHERE CoursesRatings.courseId = Course.id)
WHERE id = NEW.courseId;
If updating the course rating for every insert of user rating proves to be too costly then I would suggest scheduling a CRON job to update the course ratings of all the courses at a specific interval, let us say 1 day. That way it will become less costly in terms of DB operations as compared to the approach earlier suggested.
I know this question is old but for someone that may run into this kind of situation, I believe using a query will solve this.
const avgRating = await this.coursesRatings
.createQueryBuilder('x')
.where('x.courseId = :rating', { rating: ratingBody.courseId })
.select('AVG(rating)', 'avg')
.getRawOne();
With this, you can get access to the average rating, and you have to update Course
await this.course.save({
...course,
rate: String(Number(avgRating.avg).toFixed(1)),
});
I believe it helps... The reason I used String is that rating will be a floating type, not an integer.

TypeOrm LeftJoin with 3 tables

i want to create Left Join between 3 tables in TypeORM (NesjJS)
Table 1: User (can have multiple establishments with multiple roles)
Table 2: Establishment (can be associated to multiple users having multiple roles)
Table 3: Role (listing associations between User/Establishment and role Level)
Example:
==> "user" table
ID
name
1
jiji
2
jaja
3
jojo
4
jeje
==> "establishment" table
ID
name
10
est 1
11
est 2
12
est 3
==> "role" table
user_id
establishment_id
level
1
10
admin
1
11
editor
2
10
editor
3
11
reader
3
12
admin
i try with this relations but do not working form me :( when i try to insert data to role entity
export class EstablishmentEntity{
#PrimaryGeneratedColumn()
id: number;
...
#ManyToMany(() => RoleEntity, role => role.establishments)
roles: Role[];
}
export class UserEntity{
#PrimaryGeneratedColumn()
id: number;
...
#ManyToMany(() => UserEntity, role => role.users)
roles: Role[];
}
export class RoleEntity{
#PrimaryGeneratedColumn()
id: number;
...
#ManyToMany(() => UserEntity, user => user.roles)
users: UserEntity[];
#ManyToMany(() => EstablishmentEntity, establishment => establishment.roles)
establishments: EstablishmentEntity[];
}
and JoinTable create for me 4 tables :(
From what I've understood - you would like to fetch users with all their establishments and level inside. Consider you have custom column level there you need to use OneToMany reference in UserEntity to roles and EstablishmentEntity and ManyToOne in RoleEntity instead of ManyToMany. ManyToMany would be useful if you would only have User and Establishments ids in 3rd table and wanted to fetch all Establishments by user at once.
// RoleEntity:
...
#ManyToOne(() => UserEntity, user => user.roles)
public user: UserEntity;
#ManyToOne(() => EstablishmentEntity, establishment => establishment.roles)
public establishment: EstablishmentEntity;
// UserEntity:
...
#OneToMany(() => RoleEntity, roles => roles.user)
public roles: RoleEntity[];
// EstablishmentEntity
...
#OneToMany(() => RoleEntity, roles => roles.establishment)
public roles: RoleEntity[];
After you will be able to fetch data using left joins:
const usersWithRoles = await connection
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.roles", "roles")
.leftJoinAndSelect("roles.establishment", "establishment")
.getMany();
It will return for you all users with their roles and in certain establishments.
If you would like to save new role, you can do it like this:
await connection
.getRepository(RoleEntity)
.createQueryBuilder()
.insert()
.values({user, establishment, level })
.execute()
Please refer to this part of documentation

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

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.