postsql, mysql 등을 위해 설계된 SQL 쿼리 빌더.
발음은 크넥스, 크넥스는 레고같은 장난감 브랜드 이름이라고 한다.
ORM은 DB 엔티티를 프로그래밍 언어의 객체와 매핑해서 사용할 수 있게 해준다. 러닝 커브가 있어서 진입 장벽이 좀 있다.
쿼리 빌더는 사용이 단순하다.
raw sql vs query builder vs orm
raw query의 단점
orm의 단점
query builder 장점
단점
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