API with NestJS #7. Creating relationships with Postgres and TypeORM

388 阅读4分钟

One-to-one

nest g res modules/address --no-spec
// src/modules/address/entities/address.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'addresses' })
export class AddressEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public street: string;

  @Column()
  public city: string;

  @Column()
  public country: string;
}

// src/modules/users/entities/user.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Exclude, Expose } from 'class-transformer';
import { AddressEntity } from 'src/modules/address/entities/address.entity';

@Entity({ name: 'users' })
export class UserEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column({ unique: true })
  public email: string;

  @Column()
  public name: string;

  @Column()
  @Exclude()
  public password: string;

  @OneToOne(() => AddressEntity)
  @JoinColumn()
  public address: AddressEntity;
}

Above, we use the  @OneToOne() decorator. Its argument is a function that returns the class of the entity that we want to make a relationship with.

The second decorator, the  @JoinColumn(), indicates that the User entity owns the relationship. It means that the rows of the User table contain the addressId column that can keep the id of an address. We use it only on one side of the relationship

image.png

image.png

The above can be simplified without the CONSTRAINT  keyword.

CREATE TABLE user (
   // ...  
   addressId integer REFERENCES address (id)
) 

Both  ON UPDATE NO ACTION and  ON DELETE NO ACTION are a default behavior. They indicate that Postgres will raise an error if we attempt to delete or change the id of an address that is currently in use.

The MATCH SIMPLE refers to a situation when we use more than one column as the foreign key. It means that we allow some of them to be null

Inverse relationship

// src/modules/address/entities/address.entity.ts
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { UserEntity } from '../../users/entities/user.entity';

@Entity({ name: 'addresses' })
export class AddressEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public street: string;

  @Column()
  public city: string;

  @Column()
  public country: string;

  @OneToOne(() => UserEntity, (user: UserEntity) => user.address)
  public user: UserEntity;
}

The crucial thing is that the inverse relationship is a bit of an abstract concept, and it does not create any additional columns in the database.

 

Storing the information about both sides of the relationship can come in handy. We can easily relate to both sides, for example, to fetch the addresses with users.

    getAllAddressesWithUsers() {
        return this.addressRepository.find({ relations: ['user'] });
    } 

If we want our related entities always to be included, we can make our relationship eager.

  @OneToOne(() => Address, {  eager: true})
  @JoinColumn()
  public address: Address;

Now, every time we fetch users, we also get their addresses. Only one side of the relationship can be eager.

Saving the related entities

@OneToOne(() => Address, {
  eager: true,
  cascade: true
})
@JoinColumn()
public address: Address;

image.png

One-to-many and many-to-one

// src/modules/users/entities/user.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Exclude } from 'class-transformer';
import { AddressEntity } from 'src/modules/address/entities/address.entity';
import { PostEntity } from 'src/modules/posts/entities/post.entity';

@Entity({ name: 'users' })
export class UserEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column({ unique: true })
  public email: string;

  @Column()
  public name: string;

  @Column()
  @Exclude()
  public password: string;

  @OneToOne(() => AddressEntity, { eager: true, cascade: true })
  @JoinColumn()
  public address: AddressEntity;

  @OneToMany(() => PostEntity, (post: PostEntity) => post.author)
  public posts?: PostEntity[];
}

// src/modules/posts/entities/post.entity.ts
import { Transform } from 'class-transformer';
import { UserEntity } from 'src/modules/users/entities/user.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'posts' })
export class PostEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public title: string;

  @Column()
  public content: string;

  @Column({ nullable: true })
  @Transform((value) => {
    if (value !== null) {
      return value;
    }
  })
  public category?: string;

  @ManyToOne(() => UserEntity, (author: UserEntity) => author.posts)
  public author: UserEntity;
}

// src/modules/posts/posts.controller.ts
//...
import RequestWithUser from '../authentication/interface/requestWithUser.interface';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  @UseGuards(JwtAuthenticationGuard)
  create(@Body() createPostDto: CreatePostDto, @Req() req: RequestWithUser) {
    return this.postsService.create(createPostDto, req.user);
  }
//...
}
// src/modules/posts/posts.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PostEntity } from './entities/post.entity';
import { UserEntity } from '../users/entities/user.entity';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostEntity)
    private postsRepository: Repository<PostEntity>,
  ) {}

  async create(createPostDto: CreatePostDto, user: UserEntity) {
    const newPost = await this.postsRepository.create({
      ...createPostDto,
      author: user,
    });
    await this.postsRepository.save(newPost);
    return newPost;
  }

  findAll() {
    return this.postsRepository.find({ relations: ['author'] });
  }

  async findOne(id: number) {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: ['author'],
    });
    if (post) {
      return post;
    }
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }

  async update(id: number, updatePostDto: UpdatePostDto) {
    await this.postsRepository.update(id, updatePostDto);
    const updatedPost = await this.postsRepository.findOne({
      where: { id },
      relations: ['author'],
    });
    if (updatedPost) {
      return updatedPost;
    }
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }

  async remove(id: number) {
    const deleteResponse = await this.postsRepository.delete(id);
    if (!deleteResponse.affected) {
      throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
    }
  }
}

image.png

image.png

image.png

image.png

If we look into the database, we can see that the side of the relationship that uses  ManyToOne() decorator stores the foreign key.

This means that the post stores the id of the author and not the other way around.

Many-to-many

nest g res modules/category --no-spec
//src/modules/category/entities/category.entity.ts
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { PostEntity } from '../../posts/entities/post.entity';

@Entity({ name: 'categories' })
export class CategoryEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public name: string;

  @Column({
    nullable: true,
    default: null,
  })
  public pid: number;

  @ManyToMany(() => PostEntity, (post: PostEntity) => post.categories)
  public posts: PostEntity[];
}
// src/modules/posts/entities/post.entity.ts
import { Transform } from 'class-transformer';
import { UserEntity } from 'src/modules/users/entities/user.entity';
import {
  Column,
  Entity,
  JoinTable,
  ManyToMany,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { CategoryEntity } from '../../category/entities/category.entity';

@Entity({ name: 'posts' })
export class PostEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public title: string;

  @Column()
  public content: string;

  @Column({ nullable: true })
  @Transform((value) => {
    if (value !== null) {
      return value;
    }
  })
  public category?: string;

  @ManyToOne(() => UserEntity, (author: UserEntity) => author.posts)
  public author: UserEntity;

  @ManyToMany(
    () => CategoryEntity,
    (category: CategoryEntity) => category.posts,
  )
  @JoinTable()
  public categories: CategoryEntity[];
}
CREATE TABLE public.post_categories_category
(
    "postId" integer NOT NULL,
    "categoryId" integer NOT NULL,
    CONSTRAINT "PK_91306c0021c4901c1825ef097ce" PRIMARY KEY ("postId", "categoryId"),
    CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd" FOREIGN KEY ("postId")
        REFERENCES public.post (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE CASCADE,
    CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3" FOREIGN KEY ("categoryId")
        REFERENCES public.category (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE CASCADE
)

Thanks to doing the above, we can now easily fetch categories along with their posts.

//
getAllCategories() {
  return this.categoriesRepository.find({ relations: ['posts'] });
}
 
async getCategoryById(id: number) {
  const category = await this.categoriesRepository.findOne(id, { relations: ['posts'] });
  if (category) {
    return category;
  }
  throw new CategoryNotFoundException(id);
}
 
async updateCategory(id: number, category: UpdateCategoryDto) {
  await this.categoriesRepository.update(id, category);
  const updatedCategory = await this.categoriesRepository.findOne(id, { relations: ['posts'] });
  if (updatedCategory) {
    return updatedCategory
  }
  throw new CategoryNotFoundException(id);
}

image.png

source