knex.js query builder

x·2022년 7월 7일
0

knex란?

https://knexjs.org/

postsql, mysql 등을 위해 설계된 SQL 쿼리 빌더.

발음은 크넥스, 크넥스는 레고같은 장난감 브랜드 이름이라고 한다.

  • 노드 스타일의 콜백, 프로미스 인터페이스 제공
  • 쿼리, 스키마 빌더 기능 제공, 트랜잭션 지원, 커넥션 풀링과 표준화된 응답 지원
💡 커넥션 풀링? DB Connection 객체를 미리 만들어서 효율적으로 사용하게 해줌

ORM vs Query Builder

ORM은 DB 엔티티를 프로그래밍 언어의 객체와 매핑해서 사용할 수 있게 해준다. 러닝 커브가 있어서 진입 장벽이 좀 있다.

쿼리 빌더는 사용이 단순하다.

왜 knex를 쓸까?

raw sql vs query builder vs orm

raw query의 단점

  • query injection 위험이 있다. sql에 추가적인 구문을 붙여 조작하는 것.
  • 오타를 방지할 수 없다.
  • 특정 DB만의 쿼리를 쓰면 다른 DBMS로 옮겨가기 어렵다

orm의 단점

  • 러닝 커브
  • 올바른 쿼리를 만들기 쉽지 않음. 잘못된 쿼리를 만들기 쉬움
  • 아주 복잡한 쿼리는 사용할 수 없을 수 있다.

query builder 장점

  • 프로그래밍 언어의 클래스, 함수, 변수 등을 사용하면서 쿼리를 작성할 수 있고 raw query의 단점을 보완할 수 있다.
  • 단점을 보완하되 쿼리 빌딩은 복잡하지 않기 때문에 쿼리 수행 시간이 크게 다르지 않다.
  • 쿼리 재사용성

단점

  • 복잡한 쿼리를 만들 땐 좀 어려울 수 있다.

orm과 query builder로 커버가 안되는 쿼리가 있으므로 raw query를 지원하기도 한다.

결론 : raw query의 단점을 보완하고 ORM보다 사용이 단순하고 가벼워서 사용한다.

설치

yarn add @types/knex knex sqlite3 mysql

설정

knex.ts : 환경에 따라 knex 인스턴스를 생성한다.

knexfile.ts : knex 설정을 위한 값을 지정한다.

knex 모듈은 설정 객체(config)를 가져와서 Knex를 만드는 함수다. 생성된 knex 인스턴스로 쿼리, 테이블 등을 만드는 빌더를 사용할 수 있다.

const knex = require('knex');
const k = knex(config[env]);

knex.ts

/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const knex = require('knex');
const knexServerlessMysql = require('knex-serverless-mysql');
import { Knex } from 'knex';
const env = process.env.NODE_ENV || 'test';
import config from '../../knexfile';
console.log(`env ${env}`);

const serverlessMysql = require('serverless-mysql');

let k: Knex;
if (
  env === 'test' ||
  env === 'local' ||
  env === 'local_to_dev' ||
  env === 'local_to_production'
) {
  k = knex(config[env]);
} else if (env === 'production' || env === 'dev') {
  const mysql = serverlessMysql({
    config: config[env]['connection'],
  });
  k = knex({
    client: knexServerlessMysql,
    mysql,
  });
}

export { k };

knexfile.ts

import type { Knex } from 'knex';
import dotenv from 'dotenv';
import { resolve } from 'path';
import path from 'path';
dotenv.config({
  path: resolve(__dirname, `.env.${String(process.env.NODE_ENV)}`),
});

console.log(__dirname);
// Update with your config settings.

const config: { [key: string]: Knex.Config } = {
  test: {
    client: 'sqlite3',  // 단일 파일, 메모리에 저장되는 DB
    connection: {
      filename: path.join(__dirname, '/core/db/sqlite3_test'),
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
    seeds: {
      directory: './core/db/seeds',
    },
    pool: {
      min: 1,
      max: 1,
    },
    useNullAsDefault: true,
  },
  local: {
    client: 'sqlite3',
    connection: {
      filename: path.join(__dirname, '/core/db/sqlite3'),
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
    seeds: {
      directory: './core/db/seeds',
    },
    pool: {
      min: 1,
      max: 1,
    },
    useNullAsDefault: true,
  },
  dev: {
    client: 'mysql',
    connection: {
      host: process.env.DB_END_POINT,
      database: process.env.DB_DATABASE,
      user: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
    },
    seeds: {
      directory: path.join(__dirname + '/core/db/seeds'),
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
  },
  local_to_dev: {
    // seeding 할 때 knex.ts에서 knex(config[env])을 실행하기 위한 설정
    client: 'mysql',
    connection: {
      host: process.env.DB_END_POINT,
      database: process.env.DB_DATABASE,
      user: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
    },
    seeds: {
      directory: './core/db/seeds',
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
  },
  production: {
    client: 'mysql',
    connection: {
      host: process.env.DB_END_POINT,
      database: process.env.DB_DATABASE,
      user: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
  },
  local_to_production: {
    // seeding 할 때 knex.ts에서 knex(config[env])을 실행하기 위한 설정
    client: 'mysql',
    connection: {
      host: process.env.DB_END_POINT,
      database: process.env.DB_DATABASE,
      user: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
    },
    seeds: {
      // relative path, not absolute path
      directory: './core/db/seeds',
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: path.join(__dirname + '/core/db/migrations'),
    },
  },
};

export default config;

사용 예시

schema

migration 파일 만들기

npx knex migrate:make migration_name -x ts

import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
  const result = await knex.schema.createTable('users', (table) => {
    table.bigIncrements('id');
    table.string('discord_user_id', 20).notNullable().unique();
    table.string('email', 50).nullable();
    table.string('public_key', 50).notNullable().unique();
    table.timestamp('created_at').defaultTo(knex.fn.now());
    table.timestamp('updated_at').defaultTo(knex.fn.now());
  });
  console.log(result);
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTable('users');
}

migrate 적용하기

NODE_ENV=local npx knex migrate:up

migrate 되돌리기

NODE_ENV=local npx knex migrate:down

query

async getUser(userId?: number, discordUserId?: string) {
  try {
    console.log(
      `getUser userId ${String(userId)} discordUserId ${String(
        discordUserId
      )}`
    );
    const result: IUser[] = await this.knex('users')
      .select('*')
      .where((builder) => {
        if (userId) {
          console.log('get user by user id');
          builder.where('users.id', userId);
        } else if (discordUserId) {
          console.log('get user by discord user id');
          builder.where('users.discord_user_id', discordUserId);
        }
      })
      .leftJoin('users_roles', 'users.id', '=', 'users_roles.user_id')
      .leftJoin('roles', 'users_roles.role_id', '=', 'roles.id');
    console.log(`getUser result ${JSON.stringify(result)}`);
    return result;
  } catch (error) {
    console.error(error);
    return false;
  }
}

seeding

seeding은 DB 테이블에 필요한 값을 넣어주는 것

import { k } from '../../db/knex';

export async function seed(): Promise<void> {
  await k('users').insert(...);
} 

NODE_ENV=local_to_dev npx knex seed:run --specific=seedDiscordRolesDev.ts

prepared statement?

https://github.com/knex/knex/issues/802#issuecomment-223808427

https://github.com/knex/knex/issues/718

0개의 댓글