• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

관계 설정 질문 드립니다.

22.06.20 14:38 작성 조회수 322

0

 조현영님 안녕하세요. 얼마 전에 nestjs에 관심이 생겨서 nestjs로 프로젝트 진행중에 mongodb를 쓰다가 typeorm 한 번 써볼까 해서 강의 결제하게 되었습니다. 제가 현재 4가지 entity들을 만들었습니다.

// product.entity.ts

import { float } from "aws-sdk/clients/lightsail";
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
import { CommonEntity } from "src/common/entities/common.entity";
import { Column, Entity } from "typeorm";

@Entity("products")
export class ProductEntity extends CommonEntity {
  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 20, unique: true, nullable: false })
  name: string;

  @IsNumber()
  @IsNotEmpty()
  @Column({ type: "int", unsigned: true, nullable: false })
  price: number;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 20, nullable: false })
  origin: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 20, nullable: false })
  type: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "text", nullable: true })
  description: string;

  @Column({ type: "varchar", default: "no image yet" })
  imgUrl?: string;

  @Column({ type: "int", default: 50 })
  quantity: number;

  @Column({ type: "float", default: 0.0 })
  rating: float;
}
// user.entity.ts

import { Exclude } from "class-transformer";
import { IsNotEmpty, IsString, IsEmail } from "class-validator";
import { CommonEntity } from "src/common/entities/common.entity";
import { Column, Entity } from "typeorm";

@Entity("users")
export class UserEntity extends CommonEntity {
  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 20, nullable: false })
  realName: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 20, unique: true, nullable: false })
  nickName: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "date", nullable: false })
  birth: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "enum", enum: ["male", "female"] })
  gender: string;

  @IsEmail()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 60, unique: true, nullable: false })
  email: string;

  @Exclude()
  @Column({ type: "varchar", nullable: false })
  password: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", length: 15, unique: true, nullable: false })
  phoneNumber: string;

  @Column({ type: "smallint", default: 0 })
  point: number;

  @Column({
    type: "enum",
    enum: ["general", "special", "admin"],
    default: "general",
  })
  userType: string;
}
// upload.entity.ts

import { IsNotEmpty, IsString } from "class-validator";
import { CommonEntity } from "src/common/entities/common.entity";
import { Column, Entity } from "typeorm";

@Entity("images")
export class ImagesEntity extends CommonEntity {
  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", nullable: false, unique: true })
  imageFileName: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", nullable: false })
  uploader: string;
}

@Entity("videos")
export class VideosEntity extends CommonEntity {
  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", nullable: false, unique: true })
  videoFileName: string;

  @IsString()
  @IsNotEmpty()
  @Column({ type: "varchar", nullable: false })
  uploader: string;
}
// common.entity.ts

import { IsUUID } from "class-validator";
import {
  CreateDateColumn,
  DeleteDateColumn,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from "typeorm";

export abstract class CommonEntity {
  @IsUUID()
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @CreateDateColumn({
    type: "timestamp",
  })
  createdAt: Date;

  @UpdateDateColumn({
    type: "timestamp",
  })
  updatedAt: Date;

  @DeleteDateColumn({
    type: "timestamp",
  })
  deletedAt: Date;
}

 product user upload common 4개의 entity가 있습니다. id, createdAt, updatedAt, deletedAt등을 common entity에서 각각 다른 entity들에게 상속 시켜서 사용합니다.

아직 관계 설정은 적용하지 않았습니다. 질문이 무엇이냐면

1. product entity에서 imgUrl이라는 컬럼이 있는데 이 컬럼을 upload entity에 있는 images entity의 imageFileName라는 컬럼과 매치 시키려 하는데 햇갈리는 부분이 상품한개에 이미지한개 이니까 OneToOne이 맞는지 아니면 상품 여러개가 이미지 여러개를 가질 수 있으니 ManyToMany가 맞는지 궁금합니다.

2. user entity에서 nickName이라는 컬럼이 있는데 이 컬럼을 images entity의 uploader컬럼과 매치 시키려 하는데 사용자 한명이 이미지를 한개만 업로드 하는것이 아니니까 user entity의 nickName컬럼에 OneToMany를 적용시키고 image entity의 uploader컬럼에 ManyToOne을 적용시키는것이 맞는지 궁금합니다. 

3. 위의 2가지 방법이 아니라면 image entity를 product entity와 user entity가 서로 ManyToMany 관계를 거쳐서 나온 중간 테이블(엔티티)로 만들어서 사용해야 할까요?

 

답변 1

답변을 작성해보세요.

0

1. 이 부분은 스스로 답변을 하셔야 합니다. 제가 어떤 서비스를 만드시는지 추측할 수가 없어서요. 이 부분은 사실 헷갈릴 게 없는데요. 상품 하나가 이미지 하나를 가지는 것 아닌가요? 아니면 상품 하나가 이미지 여러개를 가지나요? 상품 여러개가 여러개의 이미지를 가지나요?(이건 확실히 아닌듯 ㅎㅎ)

2. 네 맞습니다. user와 images는 oneToMany입니다. images와 user는 manyToOne이고요.

3. image와 product는 1번 답변에 따라서 달라집니다.

이승훈님의 프로필

이승훈

질문자

2022.06.20

답변 감사드립니다. 생각해보니 제가 원하는 방식은 상품1개 사진1개 였는데 이쪽 부분이 적응이 안되서 햇갈렸네요. 상품 이미지에 접근할 일이 있을 때 product entity의 imgUrl을 통해서 하길 원하는데 그럴 때는 imgUrl에 @JoinColumn()을 설정해주면 될까요?

imgUrl은 그냥 @Column이고

product entity에

img라는 가상의 컬럼을 만들어서 거기에 @JoinColumn을 해야 나중에

product.img.imageFileName할 수 있습니다.

이승훈님의 프로필

이승훈

질문자

2022.06.20

혹시 imagesEntity에 있는 imageFileName 컬럼은 어떻게 해야 할까요? 여기에는 OneToOne 데코레이터를 붙여야 할까요?

네 그 값이 product테이블에 들어가는 외래키라면 거기 위에 붙이시면 됩니다.

이승훈님의 프로필

이승훈

질문자

2022.06.20

그리고 product entity에 img라는 가상의 컬럼을 만들어라 라고 하셨는데 이것은 images entity의 id를 통해서 접근하기 위함인가요? 가상의 컬럼은 그냥 entity 클래스 필드에서 프로퍼티 만들듯이 만든 후 @JoinColmun()을 적용시키면 될까요?

네네 그 가상 속성을 통해 불러오기 위함입니다. JoinColumn과 OneToMany를 적용시키면 됩니다.

이승훈님의 프로필

이승훈

질문자

2022.06.20

질문이 많아져서 죄송하지만 만약 product entity에 img 가상 컬럼에 @JoinColumn을 적용시키고 

images entity에 imageFileName에 @OneToOne을 적용시킨다면 이미지를 create할 때 (업로드)

upload라는 앤드 포인트가 있는 컨트롤러에서 서비스, 리포지토리를 거쳐서 image entity를 거쳐서 이미지 파일(imageFIleName: url형식, uploader: User(아직 관계는 적용시키지 않음))을 만들게 됩니다. 이 때 코드의 끼치는 영향이 있을까요? 혹시 모르니 코드 남겨드립니다.

import { ImageUploadDto } from "./dto/image-upload.dto";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { ImagesEntity, VideosEntity } from "./entities/upload.entity";
import { Repository } from "typeorm";
import { ImageReturnDto } from "./dto/image-return.dto";

@Injectable()
export class UploadRepository {
  constructor(
    @InjectRepository(ImagesEntity)
    private readonly imagesRepository: Repository<ImagesEntity>,
    @InjectRepository(VideosEntity)
    private readonly videosRepository: Repository<VideosEntity>,
  ) {}

  async uploadImg(imageUploadDto: ImageUploadDto): Promise<ImageReturnDto> {
    const { uploader, imageFileName } = imageUploadDto;
    const fileNameOnUrl = `http://localhost:${process.env.PORT}/media/${imageFileName}`;

    const image = await this.imagesRepository.save({
      imageFileName: fileNameOnUrl,
      uploader,
    });

    console.log(image);

    const originalName = fileNameOnUrl.replace(
      `http://localhost:${process.env.PORT}/media/`,
      "",
    );

    return { name: originalName, url: fileNameOnUrl };
  }
}

코드는 상관없는데 uploader도 uploader의 아이디가 들어가게 연결해야하지 않을까요.

이승훈님의 프로필

이승훈

질문자

2022.06.20

제가 볼일이 좀 있었어서 조현영님께서 말씀하신 "네네 그 가상 속성을 통해 불러오기 위함입니다. JoinColumn과 OneToMany를 적용시키면 됩니다." 이 문장을 지금봤는데 1:1 관계인데 OneToMany를 사용하는 이유를 여쭤봐도될까요?

아 그럼 OneToOne입니다.

이승훈님의 프로필

이승훈

질문자

2022.06.21

어찌저찌 해서 product와 image의 관계를 적용시켰습니다. 그런데 한 상품 정보를 가져올 때 image 컬럼 전부를 가져오게 되어서 이 중에서 imageFileName 만을 가져오게 하고 싶습니다. 다 가져오고 형식을 수정해서 응답하는 것이 아닌 디비에서 가져올 때 부터 imageFileName만 가져오게 할 수 있을까요? 아래는 가져 왔을 때 형식입니다.

{
    "success": true,
    "statusCode": 200,
    "message": "nodejs 교과서에 해당하는 상품 정보를 가져옵니다.",
    "result": {
        "id": "c45bcca1-3a64-4161-ae74-a3503470461f",
        "name": "nodejs 교과서",
        "price": 36000,
        "origin": "한국",
        "type": "book",
        "description": "관리자가 제일 먼저 구매했었던 nodejs 책",
        "image": {
            "id": "9d007b77-2783-478d-8eb1-7010df75470f",
            "createdAt": "2022-06-20T14:23:43.193Z",
            "updatedAt": "2022-06-20T14:23:43.193Z",
            "deletedAt": null,
            "imageFileName": "http://localhost:8000/media/XL-1655767422194.jpeg",
            "uploader": "shere"
        },
        "rating": 0,
        "createdAt": "2022-06-20T14:43:09.679Z",
        "updatedAt": "2022-06-20T14:43:09.679Z"
    }
}

아래는 가져오는 로직입니다.

 async findProductOneByName(name: string): Promise<ProductEntity> {
    try {
      return await this.productRepository.findOneOrFail({
        where: { name },
        relations: ["image"],
      });
    } catch (err) {
      throw new NotFoundException("해당 상품이름은 존재하지 않습니다.");
    }
  }

 

select: ['image.imageFileName'], 추가하면 되지 않을까 싶습니다.

이승훈님의 프로필

이승훈

질문자

2022.06.21

되게 간단했네요 감사합니다!

이승훈님의 프로필

이승훈

질문자

2022.06.21

조현영님 죄송하지만 아래 처럼 select: ['image.imageFileName']을 적용할 시 No overload matches this call 에러가 납니다.

await this.productRepository.findOneOrFail({
        where: { name },
        relations: ["image"],
        select: ["image.imageFileName"],
      });

오버로드 3개중에서 마지막 select로 넣은게 문제가 되는거 같습니다. 

Overload 3 of 3, '(conditions?: FindConditions<ProductEntity>, options?: FindOneOptions<ProductEntity>): Promise<ProductEntity>', gave the following error.

    Argument of type '{ where: { name: string; }; relations: string[]; select: string[]; }' is not assignable to parameter of type 'FindConditions<ProductEntity>'.

      Object literal may only specify known properties, and 'where' does not exist in type 'FindConditions<ProductEntity>'.

해석해보니 FindConditions<ProductEntity>유형에 'where'가 없다는 내용인거같습니다. findOneOrFail()에 인수를 위와 같이 주는것이 맞을까요?

제가 findOneOrFail 방식을 안 써서 모르겠네요. 저는 createQueryBuilder 방식만 써서요. 공식문서 찾아보세요. 이 부분부터는.

이승훈님의 프로필

이승훈

질문자

2022.06.21

알겠습니다!