PJH's Community Site - Entity

박정호·2022년 11월 21일
0

Community Project

목록 보기
2/14
post-thumbnail

🚀 Start

데이터베이스에 쓰일 필드와 여러 엔티티간 연관관계를 정의할 Entity를 생성해보자!

✔️ Entity란

Entity 클래스는 실제 DB 테이블과 매핑되는 핵심 클래스로, 데이터베이스의 테이블에 존재하는 칼럼들을 필드로 가지는 객체

Entity는 DB의 테이블과 1대 1로 대응되며, 때문에 테이블이 가지지 않는 컬럼을 필드로 가져서는 안된다. 또한 Entity 클래스는 다른 클래스를 상속받거나 인터페이스의 구현체여서는 안된다.


만약 ORM 없이 데이터베이를 생성할 때는 어떻게 할까?

CREATE TABLE board (
id INTEGER AUTO_INCREMENT PRIMARY KEY. title VARCHAR(255) NOT NULL,
decsription VARCHAR(255) NOT NULL
)

반면, TypeORM을 사용할 때는 데이터베이스 테이블로 변환되는 Class이기 때문에 위와 같이 사용하지 않고 클래스를 생성한 후 그 안에 칼럼들을 정의해준다.



✔️ Entity 생성

1. 폴더 및 파일 생성

2. Entity 생성을 위한 필요 모듈 설치

  • bcryptjs

    • 비밀번호를 암호화해서 데이테베이스에 저장할 수 있게 해준다.
  • class-validator

    • 데코레이터를 이용해서 요청에서오는 오브젝트의 프로퍼티를 검증하는 라이브러리
      입니다.
  • class-transformer

    • class-transformer를 사용하면 일반 개체를 클래스의 일부 인스턴스로 또는 그 반대 로 변환할 수 있다.
npm install bcryptjs class-validator class-transformer —save 

npm install @types/bcryptjs —save-dev

3. Base Entity 생성

모든 Entity에 id, CreateAt, updateAt이 필요. 따라서, BaseEntity를 따로 생성해서 다른 Entity에서 상속 받아서 사용

  • @PrimaryGeneratedColumn()
    • PrimaryGeneratedColumn () 데코레이터 클래스는 id 열이 Board 엔터 티의 기본 키 열임을 나타내는 데 사용.
/src/Entity.ts

export default abstract class Entity extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn()
  createAt: Date;

  @UpdateDateColumn()
  updateAt: Date;
}


✔️ User Entity 생성

사용자에 대한 필수적인 정보인 email, password, username 그리고 필수적인 권한인 createdAtuodateAt에 대한 데이터를 생성해보자.

👉 User Table Column



👉 User Table 작성

  • @Entity()

    • Entity () 데코레이터 클래스는 User 클래스가 엔티티임을 나타내는 데 사용된다.
      ( = CREATE TABLE user 부분)
  • @Column()

    • Column () 데코레이터 클래스는 User 엔터티의 emailusername과 같은 다른 열을 나타내는 데 사용된다.
  • @Index()
    • 데이터베이스 인덱스를 생성한다. 엔터티 속성 또는 엔터티에 사용 할 수 있고, 엔티티에 사용될 때 복합 열로 인덱스를 생성할 수 있다.
/src/entities/User.ts

@Entity('users')
export default class User extends BaseEntity {
  @Index()
  @IsEmail(undefined, { message: '이메일 주소가 잘못되었습니다.' })
  @Length(1, 255, { message: '이메일 주소는 비워둘 수 없습니다' })
  @Column({ unique: true })
  email: string;

  @Index()
  @Length(3, 32, { message: '사용자 이름은 3자 이상이어야 합니다.' })
  @Column({ unique: true })
  username: string;

  @Exclude()
  @Column()
  @Length(6, 255, { message: '비밀번호는 6자리 이상이어야 합니다.' })
  password: string;

  @OneToMany(() => Post, post => post.user)
  posts: Post[];

  @OneToMany(() => Vote, vote => vote.user)
  posts: Vote[];

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 6);
  }
}


👉 1:N 관계 생성

유저와 게시물 데이터의 관계는 1 대 N 관계로 형성된다.

✏️ Example

  • 여러가지 Board을 작성할 수 있는 User (One to Many)

  • 여러가지 Board는 하나의 User로 인해 작성 (Many to One)



✔️ Sub Entity 생성

게시글에 대한 필수적인 정보인 name, title 그리고 선택적인 정보인 description, imageURN, bannerURN 그리고 게시글의 기능인 createedAt, updatedAt에 대한 데이터를 생성해보자.

더하여, 사용자의 권한이 필요하므로 User의 username외래키(Foreign Key)로 가져오자.

💡 잠깐) 외래키란 ?
외래 키는 참조하는 테이블에서 1개의 키(속성 또는 속성의 집합)에 해당하고, 참조하는 측의 관계 변수는 참조되는 측의 테이블의 키를 가리킨다. 참조하는 테이블의 속성의 행 1개의 값은, 참조되는 측 테이블의 행 값에 대응 된다.



👉 Sub Table Column



👉 Sub Table 작성

  • @JoinColumn()

    • @JoinColumn을 통해서 어떤 관계쪽이 외래 키를 가지고 있는지 나타낸다.
    • @JoinColumn을 설정하면 데이터베이스에
      propertyName + referencedColumnName이라는 열이 자동으로 생성.
    • 이 데코레이터는 @ManyToOne의 경우 선택 사항이지만 @OneToOne의 경우 필수
  • name

    • 외래 키 속성명.
    • 이 name이 없다면 propertyName + referencedColumnNamedefault
  • referencedColumnName

    • 참조 엔티티의 참조 속성명
    • id가 default

👉 만약 name, referencedColumnName 둘 다 없다면 FK + id 이다. (ex. user_id)

/src/entities/Sub.ts

@Entity('subs')
export default class Sub extends BaseEntity {
  @Index()
  @Column({ unique: true })
  name: string;

  @Column()
  title: string;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ nullable: true })
  imageUrn: string;

  @Column({ nullable: true })
  bannerUrn: string;

  @Column()
  username: string;

  @ManyToOne(() => User)
  @JoinColumn({ name: 'username', referencedColumnName: 'username' })
  user: User;

  @OneToMany(() => Post, post => post.sub)
  posts: Post[];

  @Expose()
  get imageUrl(): string {
    return this.imageUrn
      ? `${process.env.APP_URL}/images/${this.imageUrn}`
      : 'https://www.gravatar.com/avatar?d=mp&f=y';
  }

  @Expose()
  get bannerUrl(): string | undefined {
    return this.bannerUrn
      ? `${process.env.APP_URL}/images/${this.bannerUrn}`
      : undefined;
  }
}

💡 잠깐) URI, URL, URN?

URL과 URN의 차이점

  • URL은 어떻게 리소스를 얻을 것이고 어디에서 가져와야 하는지 명시하는 URI이다.
  • URN은 리소스를 어떻게 접근할 것인지 명시하지 않고 경로와 리소스 자체를 특정하는 것을 목표로 하는 URI이다.

참고하자 👉 [네트워크/기본] URI, URL 및 URN의 차이점



✔️ class-tranformer Module

앞서 Entity 작성시 @Expose라는 데코레이터를 사용하였고, 이는 class-tranformer을 사용하기 위함인데 이는 무엇일까?

Class-transformer를 사용하면 plain object(일반 객체)를 클래스 인스턴스로 변환 할 수 있다.



👉 class-tranformer 사용 이유

🧐 만약 다음과 같은 JavaScript Object가 존재한다면, firstNameLastName을 어떻게 합쳐줄까?

[{
	{"id": 1,
     "firstName": "Johny",
     "lastName": "Cage",
     "age": 27
    },
    {"id": 2,
     "firstName": "Imsmoil",
     "lastName": "Andrew",
     "age": 12
    },
    {"id": 3,
     "firstName": "Peter",
     "lastName": "Parker",
     "age": 23
    },
}]

class-transform 없이 구현하기

fetch('users.json').then((users: Ueser[]) => {
	return users.map(u => toFullName(u));
});
export function toFullName(user){
	return `${user.firstName} ${user.lastName}`
}

class-transform 사용하여 구현하기

  • class-transformer에 있는 plainToInstance 메서드를 사용해서 JS Object
    대신 클래스의 인스턴스를 사용할 수 있다. 그래서 클래스에서 정의해둔 로직을 이용해서 원하는 Full Name을 만들 수 있다.
export class User{
	id: number;
	firstName: string;
	lastName: string;
	age: number;

	getName(){
    	return this.firstName + ' ' + this.lastName;
    }

	isAdult(){
    	return this.age > 36 && this.age < 60;
    }
}

...
fetch('users.json').then((users: Object[]) => {
  const realUsers = plainToInstance(User, users);
  
  return realUsers.map(u => u.getName());  
});

👍 이렇게 User 클래스에 정의해 놓은 로직을 바로 가져다가 쓸 수 있기 때문에 상태
와 행위가 함께 있는 응집력이는 코드가 된다.

이런 식으로 리터럴 객체 대신 클래스의 인스턴스를 넘겨주려면 class-transform
있는 plainToInstance를 사용하면 된다.

이와 반대로 instanceToPlain을 사용해서 클래스로 있는걸 리터럴 객체로 만들어 줄 수도 있다.



👉 Class Transformer 사용 방법

현재 작성하는 엔티티를 생성할 때 상태뿐 아니라 행위까지 엔티티안에 정의를 해서 그걸 프론트엔드에서 사용할 수 있게 할 수 있다.

getter 및 Method return value 노출
@Expose() 데코레이터를 해당 getter 또는 메서드로 설정하여 getter 또는 메서드가 반환하는 것을 드러낼 수 있다.

import { Expose } from 'class-transformer';
export class User {
  id: number;
  firstName: string;
  lastName: string;
  password: string;
  @Expose()
  get name() {
    return this.firstName + ' ' + this.lastName;
  }
  @Expose()
  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
}

특정 속성 건너뛰기

  • 변환 중에 일부 속성을 건너뛰고 싶을 때가 있을 때 @Exclude 데코레이터를 사용하여 수행할 수 있다.
import { Exclude } from 'class-transformer';
export class User {
  id: number;
  email: string;
  
  @Exclude()
  password: string;
}


✔️ Post Entity 생성

글 작성 및 수정시에 필수적인 정보는 identifier, title, slug 가 있고, 선택적인 정보로는 body 그리고 핵심 기능인 createdAt, updatedAt에 대한 데이터를 생성해보자.

더하여, 사용자의 권한이 필요하므로 User의 username외래키로, 수정시에 게시글을 판별하기 위한 Sub에서 게시글이름인 subName을 외래키로 가져온다.

💡 잠깐) slug

슬러그는 페이지나 포스트를 설명하는 핵심 단어의 집합이다.

원래 신문이나 잡지 등 에서 제목을 쓸 때, 중요한 의미를 포함하는 단어만 이용해 제목을 작성하는 법을 말 한다.

보통 슬러그는 페이지나 포스트의 제목에서 조사, 전치사, 쉼표, 마침표 등을 빼고 띄어쓰기는 하이픈(-)으로 대체해서 만들며 URL 에 사용된다.

슬러그를 URL에 사용함으로써, 검색 엔진에서 더 빨리 페이지를 찾아주고 검색엔진의 정확도를 높여준다.



👉 Post Table Column



👉 Post Table 작성

여기서 중요하게 봐야할 것은 identifier가 독립적인 식별자가 되기 위하여 7길이의 idmakeId를 통해 받는다. ( 👉 makeId function )

또한, URL에 사용되기 위해 핵심적인 단어를 가져야하는 slug를 만들기 위하여 slugifytitle을 전달하여 필터링 해준다. ( 👉 slugify function )

따라서, 해당 기능들을 하는 makeId, slugify 메서드를 생성한다.

src/utils/helpers.ts

export const makeId = length => {
  let result = '';
  const characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
};

export const slugify = str => {
  str = str.replace(/^\s+|\s+$/g, ''); // trim
  str = str.toLowerCase();

  // remove accents, swap ñ for n, etc
  const from = 'ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;';
  const to = 'aaaaaeeeeeiiiiooooouuuunc------';
  for (let i = 0, l = from.length; i < l; i++) {
    str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
  }

  str = str
    .replace(/[^a-z0-9 -]/g, '') // remove invalid chars
    .replace(/\s+/g, '-') // collapse whitespace and replace by -
    .replace(/-+/g, '-'); // collapse dashes

  return str;
};

전체코드

/src/entities/Post.ts

@Entity('posts')
export default class Post extends BaseEntity {
  @Index()
  @Column()
  identifier: string; // 7 Character id

  @Column()
  title: string;

  @Index()
  @Column()
  slug: string;

  @Column({ nullable: true, type: 'text' })
  body: string;

  @Column()
  username: string;

  @ManyToOne(() => User, user => user.posts)
  @JoinColumn({ name: 'username', referencedColumnName: 'username' })
  user: User;

  @ManyToOne(() => Sub, sub => sub.posts)
  @JoinColumn({ name: 'subName', referencedColumnName: 'name' })
  sub: Sub;

  @Exclude()
  @OneToMany(() => Comment, comment => comment.post)
  comments: Commnet[];

  @Exclude()
  @OneToMany(() => ValidationTypes, vote => vote.post)
  votes: Vote[];

  @Expose() get url(): string {
    return `/r/${this.subName}/${this.identifier}/${this.slug}`;
  }

  @Expose() get commentCount(): number {
    return this.comments?.length;
  }

  @Expose() get voteScore(): number {
    return this.votes?.reduce((memo, curt) => memo + (curt.value || 0), 0);
  }

  protected userVote: number;

  setUserVote(user: User) {
    const index = this.votes?.findIndex(v => v.username === user.username);
    this.userVote = index > -1 ? this.votes[index].value : 0;
  }

  @BeforeInsert()
  makeIdAndSlug() {
    this.identifier = makeId(7);
    this.slug = slugify(this.title);
  }
}


✔️ Vote Entity 생성

게시글 좋아요, 싫어요 기능을 위해 필수적인 값은 value 그리고 핵심 기능인 createdAt, updatedAt에 대한 데이터를 생성해보자.

더하여, 좋아요,싫어요를 받는 게시글의 판별을 위해서 선택적인 값으로 postId, commentId를 외래키로 받아오고, 투표 권한을 위해서 username을 외래키로 받는다.



👉 Vote Table Column



👉 Vote Table 작성

/src/entities/Vote.ts

@Entity('votes')
export default class Vote extends BaseEntity {
  @Column()
  value: number;

  @ManyToOne(() => User)
  @JoinColumn({ name: 'username', referencedColumnName: 'username' })
  user: User;

  @Column()
  username: string;

  @Column({ nullable: true })
  postId: number;

  @ManyToOne(() => Post)
  post: Post;

  @Column({ nullable: true })
  commentId: number;

  @ManyToOne(() => Comment)
  comment: Comment;
}


✔️ Comment Entity 생성

게시글 댓글 기능을 위해 필수적인 값은 identifier, body 그리고 핵심 기능인 createdAt, updatedAt에 대한 데이터를 생성해보자.

더하여, 좋아요,싫어요를 받는 게시글의 판별을 위해서 선택적인 값으로 postId를 외래키로 받아오고, 투표 권한을 위해서 username을 외래키로 받는다.



👉 Comment Table Column



👉 Comment Table 작성

/src/entities/Comment.ts

@Entity('comments')
export default class Comment extends BaseEntity {
  @Index()
  @Column()
  identifier: string;

  @Column()
  body: string;

  @Column()
  username: string;

  @ManyToOne(() => User)
  @JoinColumn({ name: 'username', referencedColumnName: 'username' })
  user: User;

  @Column()
  postId: number;

  @ManyToOne(() => Post, post => post.comments, { nullable: false })
  post: Post;

  @Exclude()
  @OneToMany(() => Vote, Vote => Vote.comment)
  votes: Vote[];

  protected userVote: number;

  setUserVote(user: User) {
    const index = this.votes?.findIndex(v => v.username === user.username);
    this.userVote = index > -1 ? this.votes[index].value : 0;
  }

  @Expose() get voteScore(): number {
    const initialValue = 0;
    return this.votes?.reduce(
      (previousValue, currentObject) =>
        previousValue + (currentObject.value || 0),
      initialValue
    );
  }

  @BeforeInsert()
  makeId() {
    this.identifier = makeId(8);
  }
}
profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글

관련 채용 정보