Apollo Server에 TypeORM으로 MySQL DB 연결하고 GraphQL API 만들기

Written on September 8, 2020

기본적인 GraphQL API 서버 구축이 되었으니 이제 db을 연결해 볼 차례이다. 여기서는 db는 MySQL을, ORM은 TypeORM을 사용할 예정이다. Sequelize가 더 익숙하지만 TypeGraphQL을 사용하게 되면서 TypeORM을 사용했을 떄 훨씬 생산성이 높아지기 때문에 TypeORM을 사용해 보기로 했다.

MySQL과 TypeORM 연결하기

라이브러리 설치

먼저 필요 모듈을 설치한다.

$ npm i mysql2 typeorm class-validator


DB 커넥션 설정

DB 커넥션 설정을 위해 .env 파일에 아래 내용을 추가해 준다. TypeORM의 DB 커넥션 설정은 .env 뿐만 아니라, ormconfig.json, ormconfig.js, ormconfig.xml 등 다양한 방법으로 할 수 있다. 공식문서 참고

TYPEORM_CONNECTION=mysql
TYPEORM_HOST= // host
TYPEORM_USERNAME= // username
TYPEORM_PASSWORD= // password
TYPEORM_DATABASE= // db name
TYPEORM_PORT= // db port
TYPEORM_SYNCHRONIZE=true
TYPEORM_LOGGING=true
TYPEORM_ENTITIES=src/entity/**/*.ts

서버와 연결하기

server.ts 파일의 const app = express(); 아래 코드를 추가하고 실행시키면, DB가 연결되었음을 확인할 수 있다.

...
import { createConnection } from 'typeorm';
...

async function initalize() {
  try {
    await createConnection();
    console.log('DB Connected!');
  } catch (e) {
    console.log(e);
  }
}
initalize();
...

Entity 추가하기

이제 Entity를 생성 할 차례인데, 아래와 같이 1:N 관계의 DB를 만들어 볼 생각이다.

graphql-db-structure

Project Entity

먼저, src 폴더 내 entity 폴더를 생성하고, Project.ts 파일을 생성해 준다.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BaseEntity,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';
import User from './User'

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

  @Column()
  title: string;

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

  @Column('timestampz')
  @CreateDateColumn()
  createdAt: string;

  @Column('timestampz')
  @UpdateDateColumn()
  updatedAt: string;

  @ManyToOne(() => User, (user) => user.projects, {
    onDelete: 'CASCADE',
  })
  @JoinColumn({ name: 'ownerId' })
  user: User;
}

이 때 프로퍼티가 이니셜라이즈 되지 않았다는 오류가 뜨는데 이를 해결해 주기 위해 tsconfig.jsoncompilerOptions"strictPropertyInitialization": false를 추가해 주거나 프로퍼티에 !를 추가해 해결할 수 있다.

여기에 바로 TypeGraphQL의 타입을 함께 정의해 줄 수 있다.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BaseEntity,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  JoinColumn,
} from 'typeorm';
import { Field, Int, ObjectType } from 'type-graphql';
import User from './User';

@ObjectType()
@Entity()
export default class Project extends BaseEntity {
  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  title: string;

  @Field({ nullable: true })
  @Column({ nullable: true })
  content: string;

  @Column('timestampz')
  @CreateDateColumn()
  createdAt: string;

  @Column('timestampz')
  @UpdateDateColumn()
  updatedAt: string;

  @ManyToOne(() => User, (user) => user.projects, {
    onDelete: 'CASCADE',
  })
  @Field(() => User)
  @JoinColumn({ name: 'ownerId' })
  user: User;
}

User Entity

동일한 방식으로 User entity를 정의한다.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BaseEntity,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { Field, Int, ObjectType } from 'type-graphql';
import Project from './Project';

@ObjectType()
@Entity()
export default class User extends BaseEntity {
  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id: number;

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

  @Field()
  @Column()
  password: string;

  @Column('timestampz')
  @CreateDateColumn()
  createdAt: string;

  @Column('timestampz')
  @UpdateDateColumn()
  updatedAt: string;

  @OneToMany(() => Project, (project) => project.user)
  @Field(() => [Project])
  projects: Project[];
}

CRUD 작성하기

먼저 Resolver의 Query와 Mutation에서 Argument는 아래와 같이 작성해 줄 수 있다.

  @Query(() => User, { nullable: true })
  async getUser(@Arg('id') id: number) {
    try {
      return await User.findOne({ where: { id } });
    } catch (err) {
      return err;
    }
  }

User Resolver

먼저, src/types/userInput.ts 파일을 생성하고, 아래와 같이 input type을 정의한다. class-validator를 이용해 타입을 검증해 줄 수 있다.

import { InputType, Field } from 'type-graphql';
import { IsEmail, Length } from 'class-validator';
import User from '../entity/User';

@InputType()
export class UserInput implements Partial<User> {
  @Field()
  @IsEmail()
  email: string;

  @Field()
  @Length(6, 12)
  password: string;
}

@InputType()
export class UpdateUserInput implements Partial<User> {
  @Field()
  @Length(6, 12)
  password: string;
}

다음으로, src/resolvers/userResolver.ts 파일을 생성하고, 아래와 같이 CRUD를 작성한다. 실제 서비스를 구축할때는 비밀번호는 암호화하여 저장해야 한다.

import { Query, Resolver, Mutation, Arg } from 'type-graphql';
import User from '../entity/User';
import { UserInput, UpdateUserInput } from '../types/userInput';

@Resolver()
export class UserResolver {
  @Query(() => [User])
  async getAllUsers() {
    return await User.find();
  }

  @Query(() => User, { nullable: true })
  async getUser(@Arg('id') id: number) {
    try {
      return await User.findOne({ where: { id } });
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => Boolean)
  async createUser(@Arg('data') data: UserInput) {
    try {
      const { email, password } = data;
      await User.insert({ email, password });
      return true
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => User, { nullable: true })
  async updateUser(@Arg('id') id: number, @Arg('data') data: UpdateUserInput) {
    try {
      const user = await User.findOne({ where: { id } });
      if (!user) {
        throw new Error(`The user with id: ${id} does not exist!`);
      }
      Object.assign(user, data);
      await user.save();
      return user;
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => Boolean)
  async deleteUser(@Arg('id') id: number) {
    try {
      const user = await User.findOne({ where: { id } });
      if (!user) {
        throw new Error(`The user with id: ${id} does not exist!`);
      }
      await user.remove();
      return true;
    } catch (err) {
      return err;
    }
  }
}

Project Resolver

먼저, src/types/projectInput.ts 파일을 생성하고, type을 정의해 준다.

import { InputType, Field } from 'type-graphql';
import Project from '../entity/Project';

@InputType()
export class ProjectInput implements Partial<Project> {
  @Field()
  title: string;

  @Field({ nullable: true })
  content?: string;
}

@InputType()
export class UpdateProjectInput implements Partial<Project> {
  @Field({ nullable: true })
  title?: string;

  @Field({ nullable: true })
  content?: string;
}

먼저, src/resolvers/projectResolver.ts 파일을 생성하고, CRUD를 작성해 준다.

import { Query, Resolver, Mutation, Arg } from 'type-graphql';
import User from '../entity/User';
import Project from '../entity/Project';
import { ProjectInput, UpdateProjectInput } from '../types/projectInput';

@Resolver()
export class ProjectResolver {
  @Query(() => [Project])
  async getProjects() {
    return await Project.find({ relations: ['user', 'user.projects'] });
  }

  @Query(() => [Project])
  async getProjectsByUser(@Arg('id') id: number) {
    return await Project.find({ where: { ownerId: id } });
  }

  @Query(() => Project, { nullable: true })
  async getSingleProject(@Arg('id') id: number) {
    try {
      return await Project.findOne({ where: { id } });
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => Boolean)
  async createProject(@Arg('id') id: number, @Arg('data') data: ProjectInput) {
    try {
      const user = await User.findOne({ where: { id } });
      if (!user) {
        throw new Error(`The user with id: ${id} does not exist!`);
      }
      const { content, title } = data;
      await Project.insert({ content, title, user });
      return true;
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => Boolean)
  async updateProject(
    @Arg('id') id: number,
    @Arg('data') data: UpdateProjectInput,
  ) {
    try {
      const project = await Project.findOne({ where: { id } });
      if (!project) {
        throw new Error(`The project with id: ${id} does not exist!`);
      }
      Object.assign(project, data);
      await project.save();
      return true;
    } catch (err) {
      return err;
    }
  }

  @Mutation(() => Boolean)
  async deleteProject(@Arg('id') id: number) {
    try {
      const project = await Project.findOne({ where: { id } });
      if (!project) {
        throw new Error(`The project with id: ${id} does not exist!`);
      }
      await project.remove();
      return true;
    } catch (err) {
      return err;
    }
  }
}

마지막으로 server.ts 파일의 schema 정의 부분에 작성한 Resolver를 넘겨준다.

...
import { UserResolver } from './resolvers/UserResolver';
import { ProjectResolver } from './resolvers/ProjectResolver';
...

const schema = buildSchemaSync({
  resolvers: [UserResolver, ProjectResolver],
});
...


관련 post

👩🏻‍💻 배우는 것을 즐기는 프론트엔드 개발자 입니다
부족한 블로그에 방문해 주셔서 감사합니다 🙇🏻‍♀️

in the process of becoming the best version of myself