SQL 삽입(Insert)
Drizzle ORM은 데이터베이스 테이블에 행을 삽입하는 가장 SQL스러운 방법을 제공합니다.
행 하나 삽입하기
Drizzle을 사용해 데이터를 삽입하는 것은 매우 직관적이며 SQL과 유사합니다. 직접 확인해 보세요:
await db.insert(users).values({ name: 'Andrew' });
insert into "users" ("name") values ("Andrew");
특정 테이블에 대한 삽입 타입이 필요하다면 typeof usersTable.$inferInsert
구문을 사용할 수 있습니다.
type NewUser = typeof users.$inferInsert;
const insertUser = async (user: NewUser) => {
return db.insert(users).values(user);
}
const newUser: NewUser = { name: "Alef" };
await insertUser(newUser);
Insert returning
PostgreSQL과 SQLite에서는 행을 삽입하고 해당 행을 반환받을 수 있습니다. 다음과 같이 사용할 수 있습니다:
await db.insert(users).values({ name: "Dan" }).returning();
// 부분 반환
await db.insert(users).values({ name: "Partial Dan" }).returning({ insertedId: users.id });
$returningId 삽입
MySQL은 INSERT
이후에 RETURNING
을 기본적으로 지원하지 않습니다. autoincrement
(또는 serial
) 타입의 primary key
에 대해서만 insertId
와 affectedRows
필드를 통해 접근할 수 있습니다. Drizzle에서는 이러한 경우를 자동으로 처리하고, 삽입된 모든 ID를 별도의 객체로 반환하는 방법을 제공합니다.
import { boolean, int, text, mysqlTable } from 'drizzle-orm/mysql-core';
const usersTable = mysqlTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
});
const result = await db.insert(usersTable).values([{ name: 'John' }, { name: 'John1' }]).$returningId();
// ^? { id: number }[]
또한 Drizzle에서는 $default
함수를 사용해 런타임에 커스텀 primary key
를 생성할 수 있습니다. 이렇게 생성된 키도 $returningId()
호출 시 반환됩니다.
import { varchar, text, mysqlTable } from 'drizzle-orm/mysql-core';
import { createId } from '@paralleldrive/cuid2';
const usersTableDefFn = mysqlTable('users_default_fn', {
customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(createId),
name: text('name').notNull(),
});
const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]).$returningId();
// ^? { customId: string }[]
만약
primary key
가 없다면, 해당 쿼리의 타입은{}[]
가 됩니다.
여러 행 삽입하기
await db.insert(users).values([
{ name: 'Andrew' },
{ name: 'Dan' }
]);
위 코드는 데이터베이스에 여러 행을 한 번에 삽입하는 예제입니다. users
테이블에 name
이 ‘Andrew’와 ‘Dan’인 두 개의 행을 추가합니다. values
메서드에 배열을 전달하여 여러 행을 동시에 삽입할 수 있습니다.
Upserts와 충돌 처리
Drizzle ORM은 Upserts와 충돌 처리를 위한 간단한 인터페이스를 제공합니다.
충돌 시 아무 작업도 하지 않음
onConflictDoNothing
은 충돌이 발생할 경우 삽입 작업을 취소합니다:
await db.insert(users)
.values({ id: 1, name: 'John' })
.onConflictDoNothing();
// 명시적으로 충돌 대상을 지정
await db.insert(users)
.values({ id: 1, name: 'John' })
.onConflictDoNothing({ target: users.id });
충돌 시 업데이트하기
onConflictDoUpdate
는 충돌이 발생할 경우 해당 행을 업데이트합니다:
await db.insert(users)
.values({ id: 1, name: 'Dan' })
.onConflictDoUpdate({ target: users.id, set: { name: 'John' } });
where
절
on conflict do update
구문은 두 가지 위치에 where
절을 사용할 수 있습니다. 하나는 충돌 대상(예: 부분 인덱스)에 포함되는 경우이고, 다른 하나는 update
절에 포함되는 경우입니다.
-- 충돌 대상에 where 절 사용
insert into employees (employee_id, name)
values (123, 'John Doe')
on conflict (employee_id) where name <> 'John Doe'
do update set name = excluded.name;
-- update 절에 where 절 사용
insert into employees (employee_id, name)
values (123, 'John Doe')
on conflict (employee_id) do update set name = excluded.name
where name <> 'John Doe';
Drizzle에서 이러한 조건을 지정하려면 setWhere
와 targetWhere
절을 사용할 수 있습니다.
// 충돌 대상에 where 절 사용
await db.insert(employees)
.values({ employeeId: 123, name: 'John Doe' })
.onConflictDoUpdate({
target: employees.employeeId,
targetWhere: sql`name <> 'John Doe'`,
set: { name: sql`excluded.name` }
});
// update 절에 where 절 사용
await db.insert(employees)
.values({ employeeId: 123, name: 'John Doe' })
.onConflictDoUpdate({
target: employees.employeeId,
set: { name: 'John Doe' },
setWhere: sql`name <> 'John Doe'`
});
복합 인덱스 또는 복합 기본 키를 사용한 onConflictDoUpdate
업서트 예제입니다.
await db.insert(users)
.values({ firstName: 'John', lastName: 'Doe' })
.onConflictDoUpdate({
target: [users.firstName, users.lastName],
set: { firstName: 'John1' }
});
중복 키 업데이트
MySQL은 ON CONFLICT
절 대신 ON DUPLICATE KEY UPDATE
를 지원합니다. MySQL은 기본 키와 고유 인덱스를 기반으로 충돌 대상을 자동으로 결정하며, 어떤 고유 인덱스가 충돌하더라도 해당 행을 업데이트합니다.
Drizzle는 onDuplicateKeyUpdate
메서드를 통해 이를 지원합니다:
// MySQL은 기본 키와 고유 인덱스를 기반으로 대상을 자동으로 결정합니다.
await db.insert(users)
.values({ id: 1, name: 'John' })
.onDuplicateKeyUpdate({ set: { name: 'John' } });
MySQL은 충돌 시 아무 작업도 수행하지 않는 기능을 직접 지원하지 않지만, 특정 컬럼의 값을 자기 자신으로 설정하여 동일한 효과를 얻을 수 있습니다:
import { sql } from 'drizzle-orm';
await db.insert(users)
.values({ id: 1, name: 'John' })
.onDuplicateKeyUpdate({ set: { id: sql`id` } });
with insert
절 사용하기
with
절을 사용하면 복잡한 쿼리를 더 작은 하위 쿼리로 나눌 수 있습니다. 이를 공통 테이블 표현식(CTE)이라고 합니다:
// user_count라는 CTE를 정의
const userCount = db.$with('user_count').as(
db.select({ value: sql`count(*)`.as('value') }).from(users)
);
// user_count CTE를 사용하여 데이터 삽입
const result = await db.with(userCount)
.insert(users)
.values([
{ username: 'user1', admin: sql`((select * from ${userCount}) = 0)` }
])
.returning({
admin: users.admin
});
-- user_count CTE 정의
with "user_count" as (select count(*) as "value" from "users")
-- user_count를 참조하여 데이터 삽입
insert into "users" ("username", "admin")
values ($1, ((select * from "user_count") = 0))
-- 삽입된 데이터의 admin 컬럼 반환
returning "admin"
이 예제에서는 user_count
라는 CTE를 정의하고, 이를 사용하여 users
테이블에 데이터를 삽입합니다. 삽입 시 admin
컬럼의 값은 user_count
의 결과에 따라 결정됩니다. 마지막으로 삽입된 데이터의 admin
컬럼 값을 반환합니다.
INSERT INTO … SELECT
SQLite 문서에 따르면:
INSERT 문의 두 번째 형태는 VALUES 절 대신 SELECT 문을 포함합니다. SELECT 문을 실행하여 반환된 각 행에 대해 테이블에 새로운 항목이 삽입됩니다. 열 목록이 지정된 경우, SELECT 결과의 열 수는 열 목록의 항목 수와 동일해야 합니다. 그렇지 않으면, 열 목록이 지정되지 않은 경우 SELECT 결과의 열 수는 테이블의 열 수와 동일해야 합니다. ORDER BY 및/또는 LIMIT 절을 포함한 복합 SELECT 문을 포함한 모든 SELECT 문은 이 형태의 INSERT 문에서 사용할 수 있습니다.
파싱 모호성을 피하기 위해, SELECT 문은 WHERE 절을 항상 포함해야 합니다. upsert-clause가 있는 경우, WHERE 절이 단순히 “WHERE true”라도 포함되어야 합니다. WHERE 절이 없으면 파서가 “ON” 토큰이 SELECT의 조인 제약 조건인지, 아니면 upsert-clause의 시작인지 알 수 없습니다.
PostgreSQL 문서에 따르면:
삽입할 행을 제공하는 쿼리(SELECT 문)
MySQL 문서에 따르면:
INSERT … SELECT를 사용하면 SELECT 문의 결과로부터 테이블에 많은 행을 빠르게 삽입할 수 있습니다. SELECT 문은 하나 이상의 테이블에서 선택할 수 있습니다.
Drizzle는 모든 SQL 방언에 대해 현재 구문을 지원하며, 모든 방언이 동일한 구문을 공유합니다. 일반적인 시나리오와 API 사용법을 살펴보겠습니다. INSERT 문 내부에서 SELECT를 사용하는 여러 방법이 있으며, 여러분이 선호하는 방식을 선택할 수 있습니다:
select
함수 내부에 쿼리 빌더를 전달할 수 있습니다.- 콜백 내부에 쿼리 빌더를 사용할 수 있습니다.
- 원하는 커스텀 SELECT 쿼리를 포함한 SQL 템플릿 태그를 전달할 수 있습니다.
const insertedEmployees = await db
.insert(employees)
.select(
db.select({ name: users.name }).from(users).where(eq(users.role, 'employee'))
)
.returning({
id: employees.id,
name: employees.name
});
const qb = new QueryBuilder();
await db.insert(employees).select(
qb.select({ name: users.name }).from(users).where(eq(users.role, 'employee'))
);