Node.js용 SQL 쿼리 빌더 – 쿼리, 스키마, 마이그레이션 및 트랜잭션.
Knex.js를 설치하고 데이터베이스 연결을 구성합니다
# knex와 데이터베이스 드라이버를 설치합니다
npm install knex --save
# 데이터베이스 드라이버 (하나 선택)
npm install pg # PostgreSQL
npm install mysql2 # MySQL
npm install sqlite3 # SQLite
npm install tedious # MSSQLconst knex = require('knex')({
client: 'mysql2',
connection: {
host: '127.0.0.1',
port: 3306,
user: 'your_database_user',
password: 'your_database_password',
database: 'myapp_test'
},
pool: { min: 0, max: 7 }
});// 연결 문자열
const knex = require('knex')({
client: 'pg',
connection: process.env.DATABASE_URL,
searchPath: ['knex', 'public']
});
// SQLite 메모리
const knex = require('knex')({
client: 'sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true
});조회, 삽입, 업데이트 및 삭제 작업
// 전체 조회
knex('users').select('*')
// SELECT * FROM users
// 특정 컬럼 조회
knex('users').select('id', 'name', 'email')
// SELECT id, name, email FROM users
// 별칭을 사용한 조회
knex('users').select('id', 'name as full_name')
// SELECT id, name as full_name FROM users
// 첫 번째 결과
knex('users').first('*')
// SELECT * FROM users LIMIT 1// 단일 행 삽입
knex('users').insert({ name: 'John', email: 'john@example.com' })
// 다중 행 삽입
knex('users').insert([
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
])
// 반환값과 함께 삽입 (PostgreSQL)
knex('users')
.insert({ name: 'John', email: 'john@example.com' })
.returning('id')// 모든 행 업데이트
knex('users').update({ status: 'active' })
// 특정 행 업데이트
knex('users')
.where('id', 1)
.update({ name: 'John Updated' })
// 반환값과 함께 업데이트
knex('users')
.where('id', 1)
.update({ name: 'John' })
.returning('*')// 모든 행 삭제
knex('users').del()
// 특정 행 삭제
knex('users').where('id', 1).del()
// 반환값과 함께 삭제
knex('users')
.where('id', 1)
.del()
.returning('*')다양한 조건으로 데이터 필터링
// 간단한 where
knex('users').where('id', 1)
knex('users').where({ id: 1, status: 'active' })
// 연산자를 사용한 where
knex('users').where('age', '>', 18)
knex('users').where('created_at', '<=', '2024-01-01')
// 다중 조건 (AND)
knex('users')
.where('status', 'active')
.where('age', '>', 18)
// OR 조건
knex('users')
.where('status', 'active')
.orWhere('role', 'admin')// IN 조건
knex('users').whereIn('id', [1, 2, 3])
// NOT IN 조건
knex('users').whereNotIn('status', ['banned', 'deleted'])
// NULL 조건
knex('users').whereNull('deleted_at')
// NOT NULL 조건
knex('users').whereNotNull('email')
// BETWEEN 조건
knex('users').whereBetween('age', [18, 65])
// NOT BETWEEN 조건
knex('users').whereNotBetween('age', [0, 18])// 복잡한 중첩 조건
knex('users')
.where('status', 'active')
.where(function() {
this.where('role', 'admin')
.orWhere('role', 'moderator')
})
// WHERE status = 'active' AND (role = 'admin' OR role = 'moderator')
// EXISTS 조건
knex('users')
.whereExists(function() {
this.select('*')
.from('orders')
.whereRaw('orders.user_id = users.id')
})
// Raw WHERE
knex('users').whereRaw('age > ?', [18])여러 테이블의 데이터를 결합
// 내부 조인
knex('users')
.join('orders', 'users.id', '=', 'orders.user_id')
.select('users.name', 'orders.total')
// 왼쪽 조인
knex('users')
.leftJoin('orders', 'users.id', 'orders.user_id')
.select('users.name', 'orders.total')
// 오른쪽 조인
knex('users')
.rightJoin('orders', 'users.id', 'orders.user_id')
// 전체 외부 조인
knex('users')
.fullOuterJoin('orders', 'users.id', 'orders.user_id')// 다중 조건 조인
knex('users')
.join('orders', function() {
this.on('users.id', '=', 'orders.user_id')
.andOn('orders.status', '=', knex.raw('?', ['completed']))
})
// OR 조건 조인
knex('users')
.join('orders', function() {
this.on('users.id', '=', 'orders.user_id')
.orOn('users.email', '=', 'orders.email')
})
// 교차 조인
knex('users').crossJoin('roles')집계 함수와 GROUP BY 연산
// 카운트
knex('users').count('id as total')
// 고유 카운트
knex('users').countDistinct('email')
// 합계
knex('orders').sum('total as revenue')
// 평균
knex('orders').avg('total as average_order')
// 최소 및 최대
knex('orders').min('total as min_order')
knex('orders').max('total as max_order')// 그룹화
knex('orders')
.select('user_id')
.count('id as order_count')
.groupBy('user_id')
// 다중 컬럼 그룹화
knex('orders')
.select('user_id', 'status')
.sum('total as revenue')
.groupBy('user_id', 'status')
// HAVING 절
knex('orders')
.select('user_id')
.count('id as order_count')
.groupBy('user_id')
.having('order_count', '>', 5)
// Raw HAVING
knex('orders')
.select('user_id')
.sum('total as revenue')
.groupBy('user_id')
.havingRaw('SUM(total) > ?', [1000])쿼리 결과를 정렬하고 페이지네이션
// 오름차순 정렬
knex('users').orderBy('name')
knex('users').orderBy('name', 'asc')
// 내림차순 정렬
knex('users').orderBy('created_at', 'desc')
// 다중 정렬 컬럼
knex('users')
.orderBy('status', 'asc')
.orderBy('created_at', 'desc')
// 배열을 이용한 정렬
knex('users').orderBy([
{ column: 'status', order: 'asc' },
{ column: 'name', order: 'desc' }
])
// Raw 정렬
knex('users').orderByRaw('age DESC NULLS LAST')// 제한
knex('users').limit(10)
// 오프셋
knex('users').offset(20)
// 페이지네이션
knex('users')
.limit(10)
.offset(20)
// 고유값
knex('users').distinct('email')
// 다중 컬럼 고유값
knex('users').distinct('email', 'status')데이터베이스 테이블 정의 및 생성
// 테이블 생성
knex.schema.createTable('users', function(table) {
table.increments('id').primary();
table.string('name', 100).notNullable();
table.string('email', 255).unique().notNullable();
table.integer('age');
table.boolean('is_active').defaultTo(true);
table.timestamps(true, true);
});
// 존재하지 않을 경우 테이블 생성
knex.schema.createTableIfNotExists('users', function(table) {
table.increments('id');
table.string('email').unique();
});// 일반 컬럼 타입
table.increments('id') // 자동 증가 ID
table.integer('votes') // 정수
table.bigInteger('big_votes') // 큰 정수
table.text('description') // 텍스트
table.string('name', 255) // 길이 제한 문자열
table.float('amount') // 부동소수점
table.decimal('price', 8, 2) // Decimal(8,2)
table.boolean('is_admin') // 불리언
table.date('birth_date') // 날짜
table.datetime('created_at') // 날짜시간
table.time('alarm_time') // 시간
table.timestamp('updated_at') // 타임스탬프
table.json('metadata') // JSON
table.jsonb('data') // JSONB (PostgreSQL)
table.uuid('id') // UUID
table.enum('status', ['active', 'inactive']) // 열거형// 컬럼 수정자
table.string('email')
.notNullable() // NULL 허용 안 함
.unique() // 고유
.defaultTo('default') // 기본값
.unsigned() // 부호 없음 (숫자용)
.index() // 인덱스 추가
.comment('Email address') // 주석 추가
// 외래 키
table.integer('user_id')
.unsigned()
.notNullable()
.references('id')
.inTable('users')
.onDelete('CASCADE')
.onUpdate('CASCADE');
// 타임스탬프 헬퍼
table.timestamps(true, true) // created_at, updated_at
table.timestamp('created_at').defaultTo(knex.fn.now())기존 테이블 구조 수정
// 컬럼 추가
knex.schema.table('users', function(table) {
table.string('phone', 20);
});
// 여러 컬럼 추가
knex.schema.table('users', function(table) {
table.string('phone', 20);
table.string('address', 255);
table.integer('zip_code');
});
// 컬럼 수정
knex.schema.alterTable('users', function(table) {
table.string('email', 500).alter();
});
// 컬럼 이름 변경
knex.schema.table('users', function(table) {
table.renameColumn('name', 'full_name');
});// 컬럼 삭제
knex.schema.table('users', function(table) {
table.dropColumn('phone');
});
// 여러 컬럼 삭제
knex.schema.table('users', function(table) {
table.dropColumns('phone', 'address');
});
// 테이블 삭제
knex.schema.dropTable('users');
// 존재하면 테이블 삭제
knex.schema.dropTableIfExists('users');
// 테이블 이름 변경
knex.schema.renameTable('users', 'customers');// 테이블 존재 여부 확인
knex.schema.hasTable('users')
.then(exists => {
if (!exists) {
return knex.schema.createTable('users', table => {
table.increments('id');
});
}
});
// 컬럼 존재 여부 확인
knex.schema.hasColumn('users', 'email')
.then(exists => {
if (!exists) {
return knex.schema.table('users', table => {
table.string('email');
});
}
});데이터베이스 인덱스와 관계를 생성 및 관리
// 인덱스 생성
knex.schema.table('users', function(table) {
table.index('email');
});
// 이름이 있는 인덱스 생성
knex.schema.table('users', function(table) {
table.index('email', 'idx_users_email');
});
// 복합 인덱스
knex.schema.table('users', function(table) {
table.index(['status', 'created_at'], 'idx_status_created');
});
// 고유 인덱스
knex.schema.table('users', function(table) {
table.unique('email');
});
// 인덱스 삭제
knex.schema.table('users', function(table) {
table.dropIndex('email');
table.dropIndex([], 'idx_users_email'); // 이름으로
});// 외래 키 추가
knex.schema.table('orders', function(table) {
table.foreign('user_id')
.references('id')
.inTable('users');
});
// 동작이 있는 외래 키
knex.schema.table('orders', function(table) {
table.foreign('user_id')
.references('id')
.inTable('users')
.onDelete('CASCADE')
.onUpdate('RESTRICT');
});
// 이름이 있는 외래 키
knex.schema.table('orders', function(table) {
table.foreign('user_id', 'fk_orders_user')
.references('users.id');
});
// 외래 키 삭제
knex.schema.table('orders', function(table) {
table.dropForeign('user_id');
table.dropForeign([], 'fk_orders_user'); // 이름으로
});여러 쿼리를 원자적으로 실행
// 기본 트랜잭션
knex.transaction(async (trx) => {
await trx('users').insert({ name: 'John' });
await trx('orders').insert({ user_id: 1, total: 100 });
})
.then(() => console.log('트랜잭션 커밋됨'))
.catch((err) => console.error('트랜잭션 실패:', err));
// 반환값이 있는 트랜잭션
const userId = await knex.transaction(async (trx) => {
const [id] = await trx('users')
.insert({ name: 'John' })
.returning('id');
await trx('profiles')
.insert({ user_id: id, bio: '안녕하세요' });
return id;
});// 수동 트랜잭션 제어
const trx = await knex.transaction();
try {
await trx('users').insert({ name: 'John' });
await trx('orders').insert({ user_id: 1, total: 100 });
await trx.commit();
} catch (err) {
await trx.rollback();
throw err;
}
// 저장점이 있는 트랜잭션
knex.transaction(async (trx) => {
await trx('users').insert({ name: 'John' });
const savepoint = await trx.savepoint();
try {
await trx('orders').insert({ user_id: 1, total: 100 });
await savepoint.release();
} catch (err) {
await savepoint.rollback();
}
});필요할 때 Raw SQL 실행
// 간단한 Raw 쿼리
knex.raw('SELECT * FROM users WHERE id = ?', [1])
// 명명된 바인딩을 사용하는 Raw 쿼리
knex.raw('SELECT * FROM users WHERE id = :userId', {
userId: 1
})
// select에 Raw 사용
knex('users')
.select(knex.raw('COUNT(*) as total'))
.where('status', 'active')
// where에 Raw 사용
knex('users')
.whereRaw('age > ?', [18])
.orWhereRaw('status = ?', ['admin'])// Raw 업데이트
knex('users')
.update({
updated_at: knex.raw('NOW()'),
views: knex.raw('views + 1')
})
.where('id', 1)
// Raw 삽입
knex('users').insert({
name: 'John',
created_at: knex.raw('CURRENT_TIMESTAMP')
})
// 컬럼 참조에 knex.ref 사용
knex('users')
.where('created_at', '>', knex.ref('updated_at'))데이터베이스 스키마 버전 관리
// 마이그레이션 생성
npx knex migrate:make create_users_table
// 마이그레이션 실행
npx knex migrate:latest
// 마지막 배치 롤백
npx knex migrate:rollback
// 모든 마이그레이션 롤백
npx knex migrate:rollback --all
// 마이그레이션 상태 확인
npx knex migrate:status
// 특정 마이그레이션 실행
npx knex migrate:up 001_create_users.js// 마이그레이션 파일 구조
exports.up = function(knex) {
return knex.schema.createTable('users', function(table) {
table.increments('id').primary();
table.string('name', 100).notNullable();
table.string('email', 255).unique().notNullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
});
};
exports.down = function(knex) {
return knex.schema.dropTable('users');
};// 여러 작업이 포함된 마이그레이션
exports.up = async function(knex) {
// users 테이블 생성
await knex.schema.createTable('users', table => {
table.increments('id');
table.string('email').unique();
});
// orders 테이블 생성
await knex.schema.createTable('orders', table => {
table.increments('id');
table.integer('user_id').unsigned();
table.foreign('user_id').references('users.id');
});
// 초기 데이터 시드
await knex('users').insert([
{ email: 'admin@example.com' }
]);
};
exports.down = async function(knex) {
await knex.schema.dropTable('orders');
await knex.schema.dropTable('users');
};테스트 데이터를 사용해 데이터베이스 채우기
// 시드 파일 생성
npx knex seed:make 01_users
// 모든 시드 실행
npx knex seed:run
// 특정 시드 실행
npx knex seed:run --specific=01_users.js// 기본 시드 파일
exports.seed = async function(knex) {
// 기존 레코드 삭제
await knex('users').del();
// 시드 데이터 삽입
await knex('users').insert([
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Smith', email: 'jane@example.com' },
{ name: 'Bob Johnson', email: 'bob@example.com' }
]);
};// 관계가 있는 시드
exports.seed = async function(knex) {
// 올바른 순서로 테이블 정리
await knex('orders').del();
await knex('users').del();
// 사용자 삽입 및 ID 가져오기
const [userId1] = await knex('users')
.insert({ name: 'John', email: 'john@example.com' })
.returning('id');
const [userId2] = await knex('users')
.insert({ name: 'Jane', email: 'jane@example.com' })
.returning('id');
// 관련 주문 삽입
await knex('orders').insert([
{ user_id: userId1, total: 100.50 },
{ user_id: userId1, total: 75.25 },
{ user_id: userId2, total: 200.00 }
]);
};TypeScript를 이용한 타입 안전 쿼리
// 테이블 타입 정의
interface User {
id: number;
name: string;
email: string;
created_at: string;
updated_at: string;
}
interface UserInsert {
name: string;
email: string;
}
interface UserUpdate {
name?: string;
email?: string;
updated_at?: string;
}// Knex 타입 확장
import { Knex } from 'knex';
declare module 'knex/types/tables' {
interface Tables {
users: User;
users_composite: Knex.CompositeTableType<
User,
UserInsert,
UserUpdate
>;
}
}
// 타입 안전 쿼리
const users = await knex('users').select('*');
// users is typed as User[]
const newUser = await knex('users_composite')
.insert({ name: 'John', email: 'john@example.com' })
.returning('*');// 일반적인 쿼리 타입
const getUserById = async (id: number): Promise<User | undefined> => {
return knex<User>('users')
.where({ id })
.first();
};
const createUser = async (data: UserInsert): Promise<User> => {
const [user] = await knex<User>('users')
.insert(data)
.returning('*');
return user;
};
const updateUser = async (
id: number,
data: UserUpdate
): Promise<User | undefined> => {
const [user] = await knex<User>('users')
.where({ id })
.update(data)
.returning('*');
return user;
};고급 Knex.js 기능
// Upsert(삽입 또는 업데이트)
knex('users')
.insert({ id: 1, name: 'John', email: 'john@example.com' })
.onConflict('id')
.merge()
// 특정 컬럼을 사용한 Upsert
knex('users')
.insert({ id: 1, name: 'John', email: 'john@example.com' })
.onConflict('email')
.merge(['name'])
// Insert ignore (MySQL)
knex('users')
.insert({ email: 'john@example.com' })
.onConflict('email')
.ignore()// WITH (공통 테이블 식)
knex.with('active_users', (qb) => {
qb.select('*').from('users').where('status', 'active');
})
.select('*')
.from('active_users')
.where('age', '>', 18);
// 재귀 CTE
knex.withRecursive('tree', (qb) => {
qb.select('*').from('categories').where('parent_id', null)
.union((qb2) => {
qb2.select('c.*')
.from('categories as c')
.join('tree as t', 'c.parent_id', 't.id');
});
})
.select('*').from('tree');// 서브쿼리
knex('users')
.whereIn('id', function() {
this.select('user_id').from('orders').where('total', '>', 100);
})
// SELECT 내 서브쿼리
knex('users')
.select('name')
.select(
knex('orders')
.count('*')
.where('orders.user_id', knex.raw('??', ['users.id']))
.as('order_count')
)
// FROM 절의 서브쿼리
knex
.select('*')
.from(
knex('users')
.where('status', 'active')
.as('active_users')
)// 윈도우 함수
knex('orders')
.select('*')
.select(
knex.raw('ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_num')
)
// JSON 연산 (PostgreSQL)
knex('users')
.select('name')
.select(knex.raw("metadata->>'age' as age"))
.where(knex.raw("metadata->>'city'"), 'New York')
// 배열 연산 (PostgreSQL)
knex('users')
.whereRaw('? = ANY(tags)', ['javascript'])데이터베이스 연결 및 풀 관리
// 연결 풀 설정
const knex = require('knex')({
client: 'pg',
connection: {
host: '127.0.0.1',
user: 'user',
password: 'password',
database: 'myapp'
},
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 30000,
idleTimeoutMillis: 30000
}
});
// 연결 풀 파괴
await knex.destroy();
// 재초기화
knex.initialize();// 연결 afterCreate 훅
const knex = require('knex')({
client: 'pg',
connection: { /* ... */ },
pool: {
afterCreate: function(conn, done) {
conn.query('SET timezone="UTC";', function(err) {
done(err, conn);
});
}
}
});
// 동적 연결
const knex = require('knex')({
client: 'mysql',
connection: async () => {
const token = await getAuthToken();
return {
host: 'localhost',
user: 'user',
password: token,
database: 'myapp'
};
}
});// 쿼리 디버깅
const knex = require('knex')({
client: 'pg',
connection: { /* ... */ },
debug: true // 모든 쿼리 로그
});
// 특정 쿼리 디버깅
knex('users')
.select('*')
.debug()
.then(rows => console.log(rows));
// 실행 없이 SQL 얻기
const sql = knex('users')
.where('id', 1)
.toSQL();
console.log(sql.sql); // SQL 문자열
console.log(sql.bindings); // 파라미터