Backend/NestJS

TypeORM - Custom repository(사용자 정의 레포지토리)

hou27 2022. 3. 26. 00:14

여기로!

현재 더 나은 방법을 포스팅해두었습니다.

TypeORM-Custom-Repository-개선안 - hou27

 

TypeORM - Custom Repository 개선안

이전 내용 사실 이전에 TypeORM이 0.3.x 버전 이상으로 올라가면서 @EntityRepository(User) export class UserRepository extends Repository { async customMethod(userId: number): Promise { ... } } 위와 같이 @EntityRepository() 데코레

hou27.tistory.com

ㄴ 이 글을 추천드립니다!!

 

들어가며

데이터 베이스 작업 로직을 작성하던 중 내가 작성한 함수를 typeORM에 정의된 메서드처럼 사용할 수 있는 방법이 있다는 걸 알게 되었다.

 

https://orkhan.gitbook.io/typeorm/docs/custom-repository

 

Custom repositories - typeorm

You can create a custom repository which should contain methods to work with your database. Usually custom repositories are created for a single entity and contains its specific queries. For example, let's say we want to have a method called findByName(fir

orkhan.gitbook.io

 

위 링크에서 확인할 수 있듯이, 우리는 사용자 정의 레포지토리를 만들어 사용할 수 있다.

일반적으로 단일 Entity에 대해 생성되며, 특정 query를 포함하게 된다.

(현재 TypeORM 버전이 올라감에 따라 아래 내용은 이전 버전으로, 문서 내용과 상이하다.

현재 버전(0.3)에서 적용가능한 공식 문서와는 다른 방법의 custom repository 적용방법은 더 아래에 추가해두었다.)

 

이전 버전 ver

import {
  DeleteResult,
  EntityRepository,
  getConnection,
  Repository,
} from 'typeorm';
import { User } from '../entities/user.entity';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async deleteAccountById(userId: number): Promise<DeleteResult> {
    const deleteResult = await getConnection()
      .createQueryBuilder()
      .delete()
      .from(User)
      .where('id = :id', { id: userId })
      .execute();

    return deleteResult;
  }
}

내가 정의한 Custom Repository는 위와 같다.

 

user service에서 사용할 id를 통해 유저 하나를 삭제하는 기능을 위처럼 구현했다.

 

지금 구현한 방식은 추상 레포지토리와 달리 레포지토리의 모든 메서드가 노출된다.
즉 Abstract Repository에는 공개 메서드가 없는 것이다.
AbstractRepository의 확장은 표준 레포지토리의 모든 메서드를 공개하지 않는 경우에 유용하다고 한다.

 

일반 Repository를 통해 확장하여 만든 사용자 정의 레포지토리로는 내부에 있는 모든 메서드와 표준 Entity 레포지토리의 모든 메서드에 접근이 가능하다.

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly users: Repository<User>,
    ...
  ) {}

기존 User Service에선 위처럼 Repository를 주입하고, 생성자를 통해 사용했었다.

 

그러나 사용자 정의로 구현한 지금은 Repository를 통해 확장한 것이다.

@Injectable()
export class UserService {
  constructor(
    private readonly users: /*Repository<User>*/ UserRepository,
    ...
  ) {}

위와 같이 해당 Repository를 바로 사용하며, 주입도 필요치 않은 것을 볼 수 있다.

 

 

그랬더니 위 사진처럼 정말 메서드처럼 간편하게 사용할 수 있었다!

또한 표준 Repository를 확장한 것이므로 기존 메서드도 모두 접근할 수 있었다.

 


2022.07.25. 추가

TypeORM 0.3에서의 Custom Repository

TypeORM이 0.3 버전 이후로 이전 Custom Repository 만들기 방법이 삭제되었다

 

내가 사용하는 Custom Repository 방식을 아래 Repository에 작성해두었다.

 

https://github.com/hou27/nestjs-examples

 

GitHub - hou27/nestjs-examples: NestJS Example codes Repository

NestJS Example codes Repository. Contribute to hou27/nestjs-examples development by creating an account on GitHub.

github.com

 

+ 이슈도 달리고 이분 덕에 오류도 잡게 되어 지금이라도 설명을 추가하고자 한다.

 

 

TypeORM 0.3 ver Custom Repository

Folder Structure

    src
    └── users
    │   ├── entities
    │   │   └── user.entity.ts
    │   ├── repositories
    │   │   └── user.repository.ts
    │   ├── users.controller.ts
    │   ├── users.module.ts
    │   └── users.service.ts
    ├── app.module.ts
    └── main.ts

 

핵심 코드들을 살피며 설명을 진행하겠다.

user.repository.ts

import { User } from '../entities/user.entity';
import { Repository } from 'typeorm';

export interface UserRepository extends Repository<User> {
  this: Repository<User>;

  getUserWithPassword(userId: number): Promise<any>;
}

type CustomUserRepository = Pick<UserRepository, 'getUserWithPassword'>;

export const customUserRepositoryMethods: CustomUserRepository = {
  async getUserWithPassword(userId: number): Promise<any> {
    try {
      const user = await this.users.findOne({
        where: { id: userId },
        select: { id: true, email: true, name: true, password: true },
      });

      return { ok: true, user };
    } catch (e) {
      return { ok: false, error: e.message };
    }
  },
};

우선

@EntityRepository(User)

위 데코레이더가 deprecated되었기 때문에 다른 방식으로 custom repository를 작성해야한다.

아직까진 NestJS와 관계없는 Typescript의 기능을 통해 작성된 단계이며,

 

Typescript picktype-keys를 통해 

type CustomUserRepository = Pick<UserRepository, 'getUserWithPassword'>;

위와 같이 새로운 type을 정의해준 후,

export const customUserRepositoryMethods: CustomUserRepository = {
  async getUserWithPassword(userId: number): Promise<any> {
   ...
  },
};

이렇게 정의해준 새로운 커스텀 메소드까지 정의해줬다.

 

 

users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import {
  getDataSourceToken,
  getRepositoryToken,
  TypeOrmModule,
} from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { DataSource } from 'typeorm';
import { customUserRepositoryMethods } from './repositories/user.repository';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [
    {
      provide: getRepositoryToken(User),
      inject: [getDataSourceToken()],
      useFactory(dataSource: DataSource) {
        // 기존 repository를 custom 버전으로 overriding
        return dataSource
          .getRepository(User)
          .extend(customUserRepositoryMethods);
      },
    },
    UsersService,
  ],
})
export class UsersModule {}

이제 users module의 providers 부분에서

{
  provide: getRepositoryToken(User),
  inject: [getDataSourceToken()],
  useFactory(dataSource: DataSource) {
    // Override default repository for User with a custom one
    return dataSource
      .getRepository(User)
      .extend(customUserRepositoryMethods);
  },
},

위와 같이 기존 RepositoryToken을 가져오고,

useFactory 구문을 사용하여 공급자를 동적으로 생성해주는데,

이때 기존 Repository를 조금 전 user.repository.ts에서 작성한

customUserRepositoryMethods를 확장한 친구를 반환해줌으로써
결국 Overriding된 Custom Repository를 공급자로 사용할 수 있게 되는 것이다.

 

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRepository } from './repositories/user.repository';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly users: UserRepository,
  ) {}

  async getMyInfo(userId: number): Promise<User> {
    try {
      const user = await this.users.getUserWithPassword(userId);

      return user;
    } catch (e) {
      console.log(e);
      return null;
    }
  }
}

service에서 custom repository를 사용할 땐,

이전 typeorm 버전과 달리 repository를 커스텀 버전으로 Overriding한 것이므로

service에서 주입받아 사용하는 과정에서 NestJS가 인식하여

forFeature에 등록된 repository를 주입해줄 수 있도록 @InjectRepository()를 추가해야한다.

 


참고자료

 

Custom repositories