ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Nest.js] 13 - Relations - One To One, Many To One
    Nest.js 2021. 9. 15. 19:02

    Contents
    1. Relations?
    2. Relation Implementation on Board and User Entities
    3. Eager Relations
    4. Version-up CRUD(=> C)
    5. Version-up CRUD(=> R)
    6. Version-up CRUD(=> U)
    7. Version-up CRUD(=> D)


    1. Relations?

    - Relations helps us to work with related entities easily.

    - Relationships between entities are based on business logic or functional behaviors.

    1. one-to-one using @OneToOne

    // A client could register a profile picture.

    // A profile picture is owned by only one client.

     

    2. one-to-many using @OneToMany, 3. many-to-one using @ManyToOne

    // A client could have many cards.

    // Each card is owned by only one Client.

     

    4. many-to-many using @ManyToMany

    // Each Author wrote many books.

    // Each Book is written by many Authors.


    2. Relation Implementation on "Board" and "User" Entities

    - Based on User Entity, A user could write mutiple boards, but a board is created by a single user -> One-To-Many

    - src/auth/user.entity.ts

    ...
    
    @Entity()
    @Unique(['username'])
    export class User extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      ...
    
      @OneToMany((type) => Board, // 1
                 (board) => board.user, // 2
                 { eager: true } // 3
                ) 
      boards: Board[]; // 4
    }

    // 1. type => Board : Board Entity is related with User Entity

    // 2. board => board.user : the property how to access this user from Board Entity

    // 3. eager: true : setting it to true means when loads a user, it loads boards as well.

    // 4. Board[] : it should be an array because it would have mutiple Boards..

     

    - Based on Board Entity, A board is owned by a single user, but a user can create mutiple boards. -> Many-To-One

    - sre/boards/board.entity.ts

    ...
    
    @Entity()
    export class Board extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      ...
    
      @ManyToOne((type) => User, // 1
                 (user) => user.boards, // 2
                 { eager: false } // 3
                )
      user: User; // 4
    }

    // 1. type => User : User Entity is related with Board Entity

    // 2. user => user.boards : the property how to access a board from User Entity

    // 3. eager: false : setting it to false means when loads a user, it does not load boards.

    // 4. user : it should not be an array becuase a board is written by a single user.

     

    - Tables Created by the relation

    user table
    board table


    3. Eager Relations

    - Let's talk more about Eager and Lazy Relations.

    Eager Relations

    - Eager relations are loaded automatically each time we load entities from the database.

    - User Entity

    import { Board } from 'src/boards/board.entity';
    import { BaseEntity,Column,Entity,OneToMany,PrimaryGeneratedColumn,Unique,} from 'typeorm';
    
    @Entity()
    @Unique(['username'])
    export class User extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      username: string;
    
      @Column()
      password: string;
    
      @OneToMany((type) => Board, (board) => board.user, { eager: true })
      boards: Board[];
    }

    - Board Entity

    import { User } from 'src/auth/user.entity';
    import { BaseEntity,Column,Entity,ManyToOne,PrimaryGeneratedColumn,} from 'typeorm';
    import { BoardStatus } from './board-status.enum';
    
    @Entity()
    export class Board extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      title: string;
    
      @Column()
      description: string;
    
      @Column()
      status: BoardStatus;
    
      @ManyToOne((type) => User, (user) => user.boards, { eager: false })
      user: User;
    }

    // Look at eager option on these entities, when I load users I dont't need to join or specify relations I want to load.

    // They will be loaded automatically.

     

    - Load Users

    // The User, "jamescha", has written the two of boards, id is "48" and "47".

    // The Users, "jamescha2" and "jamescha3", has no boards so far, so it shows up as an empty array.

     

    - Load Boards

    //  Eager relations only work when I use find* methods. If I use "QueryBuilder", eager relations are disabled and have to use "leftJoinAndSelect" to load the relation.

    // Eager relations can only be used on one side of the relationship, using "eager : true" on both sides of relationship is disallowed

     

    ### Will update Lazy relation later :)


    4. Create a board with a user information

    - At this time, we use an AccessToken and Custom decorator(@GetUser()) to insert a board.

     

    - src/boards/boards.controller.ts

    ...
    
    @Controller('boards')
    @UseGuards(AuthGuard()) // 1
    export class BoardsController {
      constructor(private boardsService: BoardsService) {}
    
      ...
    
      @Post()
      createBoard(
        @Body() createBoardDto: CreateBoardDto,
        @GetUser() user: User, // 2
      ): Promise<Board> {
        return this.boardsService.createBoard(createBoardDto, user); // 3
      }
    
      ...
    }

    // 1. @UseGuards(AuthGuard()) - checks an access token for all request on 'boards'

    // 2. @GetUser() - if token is valid, get only user information from the Request Object via Custom decorator, @GetUser

    // 3. call its service's method with inputs of a board and user information.

     

    - src/boards/boards.service.ts

    ...
    
    @Injectable()
    export class BoardsService {
      constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
      ) {}
    
      ...
    
      async createBoard(
        createBoardDto: CreateBoardDto,
        user: User,
      ): Promise<Board> {
        return this.boardRepository.createBoard(createBoardDto, user); // 1
      }
    
      ...
    }

    // 1. call its repository's method with what received

     

    - src/boards/board.repository.ts

    ...
    
    @EntityRepository(Board)
    export class BoardRepository extends Repository<Board> {
      async createBoard(
        createBoardDto: CreateBoardDto,
        user: User,
      ): Promise<Board> {
        const { title, description, status } = createBoardDto;
    
        const board = this.create({
          title,
          description,
          status,
          user, // 1
        });
    
        await this.save(board);
        return board;
      }
    }

    // 1. add the whole user entity when the board entity created.

    // 2. save the board into DB.

     

    - Send a request to insert a board with access token

    insert an access token in Authorization in Header
    insert data for a board

     

    - board Table Result

    board table


    5. Read the boards that the user who logged in

    - here, update reading boards which are related to the access token.

     

    - src/boards/boards.controller.ts

    ...
    
    @Controller('boards')
    @UseGuards(AuthGuard())
    export class BoardsController {
      constructor(private boardsService: BoardsService) {}
    
      @Get('/:id')
      getBoardById(@Param('id') id: number, @GetUser() user: User): Promise<Board> { // 1
        return this.boardsService.getBoardById(id, user); // 2
      }
      
      @Get()
      getAllBoards(@GetUser() user: User): Promise<Board[]> { // 1
        return this.boardsService.getAllBoards(user); // 2
      }
    
      ...
    }

    // 1. add the custom dacorator to get user information.

    // 2. send the user to the service method.

     

    - src/boards/boards.service.ts

    ...
    
    @Injectable()
    export class BoardsService {
      constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
      ) {}
    
      async getBoardById(id: number, user: User): Promise<Board> {
        const found = await this.boardRepository.findOne({ // 1
          where: { id, userId: user.id },
        });
    
        if (!found) {
          throw new NotFoundException(`can't find Board with id ${id}`);
        }
    
        return found;
      }
    
      async getAllBoards(user: User): Promise<Board[]> {
        const query = this.boardRepository.createQueryBuilder('board'); // 2
    
        query.where('board.userId = :userId', { userId: user.id }); // 3
    
        const boards = await query.getMany(); // 4
    
        if (!boards) {
          throw new NotFoundException(`There is no boards in DB`);
        }
    
        return boards;
      }
    
      ...
    }

    // 1. findOne({ where : { id, userId: user.id}) - Repository API method with simple where clause.

    // 2. createQueryBuilder('board') - Using "QueryBuilder" that is one of the most powerful features of TypeORM,
                                                                it allows us to build SQL queries using elegant and convenient syntax,
                                                                execute tem and get automatically transformed entities.

                                                             - Shortly, "QueryBuilder" is used to make more complecated queries easily.

    // 3. query.where('board.userid = :userId', { userid: user.id })
           - add "where" clause to the query using the parameter "user" id.

    // 4. await query.getMany() - using getMany() method because the query's result might be many rows.

     

    ### ERROR & SOLUTION###

    when we use "board.userId" in the where clauses above, we did not make the column in the Board Entity.

    Even though the column is added in DB, we should add the column into Entity manually.

    - src/boards/board.entity.ts

    ...
    
    @Entity()
    export class Board extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      title: string;
    
      @Column()
      description: string;
    
      @Column()
      status: BoardStatus;
    
      @ManyToOne((type) => User, (user) => user.boards, { eager: false })
      user: User;
    
      @Column()
      userId: number;
    }

    // now we test it works properly below

     

    - Send a request to read all boards with access token and Check a response

    // signed in the user "jamescha" and use his token to send the request

    // get the result only 3 boards, id 47, 48, and 50, it is becasue the user wrote those boards.

    (check the table data below)

     

    - Send a request to read a specific board with board id and access token

    // signed in user "jamescha" and use 48 board id that is written by "jamescha", so it loads properly.

     

    - table in DB

     

    - send a request to read others' board

    // the accesstoken of "jamescha" is authentificated.

    // However, the board, id 49, is not written by the user, so it shows up 404 ERROR

     

    ### Why not 403 forbidden error? but 404 Not found error? ###

    - It looks weired because the result of request, which is not owned board id, is 404 Status code.

    - The reason is that 403 Status CODE is also an important information, thus it prevents exposing the information.


    6. Update a board with access token

    - A board can be updated by who is written by the user.

     

    - src/boards/boards.controller.ts

    ...
    
    @Controller('boards')
    @UseGuards(AuthGuard())
    export class BoardsController {
      constructor(private boardsService: BoardsService) {}
    
      ...
    
      @Put('/:id')
      updateBoard(
        @Param('id') id: number,
        @Body() updateBoardDto: UpdateBoardDto,
        @GetUser() user: User, // 1
      ): Promise<Board> {
        return this.boardsService.updateBoard(id, updateBoardDto, user); // 2
      }
    
      ...
    }

    // 1. add the custom dacorator to get user information.

    // 2. send the user to the service method.

     

    - src/boards/boards.service.ts

    ...
    
    @Injectable()
    export class BoardsService {
      constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
      ) {}
    
      ...
    
      async updateBoard(
        id: number,
        updateBoardDto: UpdateBoardDto,
        user: User,
      ): Promise<Board> {
        const board = await this.getBoardById(id, user); // 1
    
        board.title = updateBoardDto.title;
        board.description = updateBoardDto.description;
        board.status = updateBoardDto.status;
    
        await this.boardRepository.save(board);
        return board;
      }
    
      ...
    }

    // 1. use the getBoardById(id, user) method to get a board

     

    - Send a request to update a board and check a response

    access token
    data for update a board

     

    - send a request to update others' board

    // It is also 404 ERROR


    7. Delete a board with access token

    - A board can be deleted by who is written by the user.

     

    - src/boards/boards.controller.ts

    ...
    
    @Controller('boards')
    @UseGuards(AuthGuard())
    export class BoardsController {
      constructor(private boardsService: BoardsService) {}
    
      ...
    
      @Delete('/:id')
      deleteBoard(@Param('id') id: number, @GetUser() user: User): Promise<void> {
        return this.boardsService.deleteBoard(id, user);
      }
    }

     

    - src/boards/boards.service.ts

    import { Injectable, NotFoundException } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { User } from 'src/auth/user.entity';
    import { Board } from './board.entity';
    import { BoardRepository } from './board.repository';
    import { CreateBoardDto } from './dto/create-board.dto';
    import { UpdateBoardDto } from './dto/update-board.dto';
    
    @Injectable()
    export class BoardsService {
      constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
      ) {}
    
      ...
    
      async deleteBoard(id: number, user: User): Promise<void> {
        const board = await this.boardRepository.delete({ id, user }); // 1
    
        if (board.affected === 0) {
          throw new NotFoundException(`cannot find id Board with ${id}`);
        }
    
        console.log(board);
      }
    }

    // 1. await this.boardRepository.delete({ id, user }) - put user entity into the parameter, so it checks its id with DB
                                                                               - if the id in user Entity and found row's userId are different, it not affected

     

    - Send a request to delete a board and check a response

    // deleted the board with id 50, which is owned by the token's user.

     

    - send a request to delete others' board

    // it also is 404 ERROR CODE.

Designed by Tistory.