Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 824
Matt Brandt Avatar asked May 02 '17 21:05

Matt Brandt


3 Answers

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).

like image 102
NoNameProvided Avatar answered Nov 13 '22 04:11

NoNameProvided


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)

like image 13
Javi Marzán Avatar answered Nov 13 '22 04:11

Javi Marzán


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

like image 3
Aleksandr Primak Avatar answered Nov 13 '22 04:11

Aleksandr Primak