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
MySQL
SingleStore

PostgreSQL과 SQLite에서는 행을 삽입하고 해당 행을 반환받을 수 있습니다. 다음과 같이 사용할 수 있습니다:

await db.insert(users).values({ name: "Dan" }).returning();

// 부분 반환
await db.insert(users).values({ name: "Partial Dan" }).returning({ insertedId: users.id });

$returningId 삽입

PostgreSQL
SQLite
MySQL
SingleStore

MySQL은 INSERT 이후에 RETURNING을 기본적으로 지원하지 않습니다. autoincrement(또는 serial) 타입의 primary key에 대해서만 insertIdaffectedRows 필드를 통해 접근할 수 있습니다. 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와 충돌 처리를 위한 간단한 인터페이스를 제공합니다.

충돌 시 아무 작업도 하지 않음

PostgreSQL
SQLite
MySQL
SingleStore

onConflictDoNothing은 충돌이 발생할 경우 삽입 작업을 취소합니다:

await db.insert(users)
  .values({ id: 1, name: 'John' })
  .onConflictDoNothing();

// 명시적으로 충돌 대상을 지정
await db.insert(users)
  .values({ id: 1, name: 'John' })
  .onConflictDoNothing({ target: users.id });

충돌 시 업데이트하기

PostgreSQL
SQLite
MySQL

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에서 이러한 조건을 지정하려면 setWheretargetWhere 절을 사용할 수 있습니다.

// 충돌 대상에 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' }
  });

중복 키 업데이트

PostgreSQL
SQLite
MySQL
SingleStore

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 절 사용하기

select, update, delete와 함께 WITH 구문을 사용하는 방법을 확인해 보세요.

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 문에서 사용할 수 있습니다.

IMPORTANT

파싱 모호성을 피하기 위해, SELECT 문은 WHERE 절을 항상 포함해야 합니다. upsert-clause가 있는 경우, WHERE 절이 단순히 “WHERE true”라도 포함되어야 합니다. WHERE 절이 없으면 파서가 “ON” 토큰이 SELECT의 조인 제약 조건인지, 아니면 upsert-clause의 시작인지 알 수 없습니다.

PostgreSQL 문서에 따르면:

삽입할 행을 제공하는 쿼리(SELECT 문)

MySQL 문서에 따르면:

INSERT … SELECT를 사용하면 SELECT 문의 결과로부터 테이블에 많은 행을 빠르게 삽입할 수 있습니다. SELECT 문은 하나 이상의 테이블에서 선택할 수 있습니다.

Drizzle는 모든 SQL 방언에 대해 현재 구문을 지원하며, 모든 방언이 동일한 구문을 공유합니다. 일반적인 시나리오와 API 사용법을 살펴보겠습니다. INSERT 문 내부에서 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'))
);