DrizzleORM v0.29.0 릴리스
Nov 9, 2023

Drizzle ORM 버전 0.29.0은 최소 Drizzle Kit 버전 0.20.0을 필요로 하며, 그 반대도 마찬가지입니다. 따라서 Drizzle ORM을 새 버전으로 업그레이드할 때 Drizzle Kit도 함께 업그레이드해야 합니다. 이로 인해 버전 간에 일부 호환성이 깨지는 변경 사항이 발생할 수 있으며, 특히 Drizzle Kit을 업그레이드해야 하고 Drizzle ORM 버전이 <0.28.0보다 낮은 경우 더욱 그렇습니다.

새로운 기능

🎉 MySQL bigint 타입에 unsigned 옵션 추가

이제 bigint unsigned 타입을 지정할 수 있습니다.

const table = mysqlTable('table', {
  id: bigint('id', { mode: 'number', unsigned: true }),
});

자세한 내용은 문서에서 확인하세요.

🎉 개선된 쿼리 빌더 타입

0.29.0 버전부터, Drizzle의 모든 쿼리 빌더는 SQL에 최대한 부합하도록 설계되었습니다. 따라서 대부분의 메서드는 한 번만 호출할 수 있습니다. 예를 들어, SELECT 문에서는 WHERE 절이 하나만 존재할 수 있으므로 .where() 메서드는 한 번만 호출할 수 있습니다:

const query = db
  .select()
  .from(users)
  .where(eq(users.id, 1))
  .where(eq(users.name, 'John')); // ❌ 타입 에러 - where()은 한 번만 호출 가능

이러한 동작은 전체 쿼리를 한 번에 생성하는 전통적인 쿼리 빌딩에 유용합니다. 하지만 쿼리를 동적으로 생성하려는 경우, 예를 들어 쿼리 빌더를 받아서 추가 기능을 제공하는 공유 함수를 사용할 때 문제가 됩니다. 이 문제를 해결하기 위해 Drizzle은 쿼리 빌더에 ‘동적’ 모드를 제공하며, 이 모드에서는 메서드를 여러 번 호출할 수 있습니다. 이 모드를 활성화하려면 쿼리 빌더에서 .$dynamic()을 호출해야 합니다.

페이지 번호와 선택적인 페이지 크기를 기반으로 LIMIT 및 OFFSET 절을 추가하는 간단한 withPagination 함수를 구현해 보겠습니다:

function withPagination(
  qb: T,
  page: number,
  pageSize: number = 10,
) {
  return qb.limit(pageSize).offset(page * pageSize);
}

const query = db.select().from(users).where(eq(users.id, 1));
withPagination(query, 1); // ❌ 타입 에러 - 쿼리 빌더가 동적 모드가 아님

const dynamicQuery = query.$dynamic();
withPagination(dynamicQuery, 1); // ✅ 정상 동작

withPagination 함수는 제네릭 함수로, 쿼리 빌더의 결과 타입을 수정할 수 있습니다. 예를 들어, 조인을 추가할 수 있습니다:

function withFriends(qb: T) {
  return qb.leftJoin(friends, eq(friends.userId, users.id));
}

let query = db.select().from(users).where(eq(users.id, 1)).$dynamic();
query = withFriends(query);

더 자세한 내용은 문서에서 확인할 수 있습니다.

🎉 기본 키와 외래 키에 이름 지정 가능

데이터베이스의 제약 조건 이름이 64자 제한을 초과할 경우 문제가 발생할 수 있습니다. 이 경우 데이터베이스 엔진이 이름을 잘라내어 문제를 일으킬 가능성이 있습니다. 0.29.0 버전부터는 primaryKey()foreignKey()에 커스텀 이름을 지정할 수 있습니다. 또한 기존의 primaryKey() 구문은 더 이상 사용하지 않을 예정이며, 향후 릴리스에서 제거될 예정입니다.

const table = pgTable('table', {
  id: integer('id'),
  name: text('name'),
}, (table) => ({
  cpk: primaryKey({ name: 'composite_key', columns: [table.id, table.name] }),
  cfk: foreignKey({
    name: 'fkName',
    columns: [table.id],
    foreignColumns: [table.name],
  }),
}));

자세한 내용은 문서에서 확인할 수 있습니다.

🎉 읽기 전용 복제본(Read Replicas) 지원

이제 Drizzle의 withReplica 함수를 사용해 읽기 전용 복제본과 쓰기 작업을 위한 메인 데이터베이스 연결을 각각 지정할 수 있습니다. 기본적으로 withReplicas는 읽기 작업 시 랜덤으로 읽기 전용 복제본을 사용하고, 데이터 수정 작업에는 메인 인스턴스를 사용합니다. 또한, 어떤 읽기 전용 복제본 연결을 사용할지 선택하는 커스텀 로직을 지정할 수도 있습니다. 여러분은 가중치를 적용하거나 원하는 방식으로 선택 로직을 구현할 수 있습니다. 아래는 사용 예제입니다:

const primaryDb = drizzle({ client });
const read1 = drizzle({ client });
const read2 = drizzle({ client });

const db = withReplicas(primaryDb, [read1, read2]);

// 메인 데이터베이스에서 읽기
db.$primary.select().from(usersTable);

// read1 또는 read2 연결 중 하나에서 읽기
db.select().from(usersTable)

// 삭제 작업은 메인 데이터베이스에서 수행
db.delete(usersTable).where(eq(usersTable.id, 1))

아래는 읽기 전용 복제본 선택을 위한 커스텀 로직 구현 예제입니다. 첫 번째 복제본이 70% 확률로 선택되고, 두 번째 복제본은 30% 확률로 선택됩니다. 여러분은 원하는 방식으로 랜덤 선택 로직을 구현할 수 있습니다.

const db = withReplicas(primaryDb, [read1, read2], (replicas) => {
    const weight = [0.7, 0.3];
    let cumulativeProbability = 0;
    const rand = Math.random();

    for (const [i, replica] of replicas.entries()) {
      cumulativeProbability += weight[i]!;
      if (rand < cumulativeProbability) return replica;
    }
    return replicas[0]!
});

withReplicas 함수는 Drizzle ORM의 모든 다이얼렉트에서 사용 가능합니다.

더 자세한 내용은 문서에서 확인하세요.

🎉 집합 연산자 지원 (UNION, UNION ALL, INTERSECT, INTERSECT ALL, EXCEPT, EXCEPT ALL)

@Angelelz에게 큰 감사를 드립니다. API 논의부터 적절한 타입 검사와 런타임 로직, 그리고 다양한 테스트 케이스까지 많은 기여를 해주셨습니다. 덕분에 이번 릴리스에서 이 기능을 제공할 수 있었습니다.

사용 예제: 모든 집합 연산자는 두 가지 방식으로 사용할 수 있습니다: import 방식 또는 builder 방식

// Import 방식
import { union } from 'drizzle-orm/pg-core'

const allUsersQuery = db.select().from(users);
const allCustomersQuery = db.select().from(customers);

const result = await union(allUsersQuery, allCustomersQuery)
// Builder 방식
const result = await db.select().from(users).union(db.select().from(customers));

더 자세한 내용은 문서에서 확인하세요.

🎉 새로운 MySQL 프록시 드라이버

MySQL 데이터베이스를 사용하여 HTTP 드라이버를 직접 구현할 수 있는 새로운 드라이버가 출시되었습니다. 사용 예제는 ./examples/mysql-proxy 폴더에서 확인할 수 있습니다.

서버에 쿼리와 마이그레이션(마이그레이션 엔드포인트는 선택 사항이며, Drizzle 마이그레이션을 사용하려는 경우에만 필요)을 처리할 두 개의 엔드포인트를 구현해야 합니다. 서버와 드라이버 구현은 여러분의 자유에 맡겨져 있으므로, 커스텀 매핑, 로깅 등 다양한 기능을 추가할 수 있습니다.

서버와 드라이버 구현 예제는 ./examples/mysql-proxy 폴더에서 확인할 수 있습니다.

// 드라이버
import axios from 'axios';
import { eq } from 'drizzle-orm/expressions';
import { drizzle } from 'drizzle-orm/mysql-proxy';
import { migrate } from 'drizzle-orm/mysql-proxy/migrator';
import { cities, users } from './schema';

async function main() {
  const db = drizzle(async (sql, params, method) => {
    try {
      const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, {
        sql,
        params,
        method,
      });

      return { rows: rows.data };
    } catch (e: any) {
      console.error('프록시 서버에서 오류 발생:', e.response.data);
      return { rows: [] };
    }
  });

  await migrate(db, async (queries) => {
    try {
      await axios.post(`${process.env.REMOTE_DRIVER}/migrate`, { queries });
    } catch (e) {
      console.log(e);
      throw new Error('프록시 서버가 마이그레이션을 실행할 수 없습니다.');
    }
  }, { migrationsFolder: 'drizzle' });

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

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

  const usersToCityResponse = await db.select().from(users).leftJoin(
    cities,
    eq(users.cityId, cities.id),
  );
}

더 자세한 내용은 문서에서 확인할 수 있습니다.

🎉 새로운 PostgreSQL 프록시 드라이버

MySQL과 마찬가지로, 이제 PostgreSQL 데이터베이스를 위한 HTTP 드라이버를 직접 구현할 수 있습니다. 사용 예제는 ./examples/pg-proxy 폴더에서 확인할 수 있습니다.

서버에서 쿼리와 마이그레이션을 처리할 두 개의 엔드포인트를 구현해야 합니다. (마이그레이션 엔드포인트는 선택 사항이며, Drizzle 마이그레이션을 사용하려는 경우에만 필요합니다.) 서버와 드라이버 구현은 여러분의 자유에 맡겨져 있으므로, 커스텀 매핑, 로깅 등 다양한 기능을 추가할 수 있습니다.

서버와 드라이버 구현 예제는 ./examples/pg-proxy 폴더에서 확인할 수 있습니다.

import axios from 'axios';
import { eq } from 'drizzle-orm/expressions';
import { drizzle } from 'drizzle-orm/pg-proxy';
import { migrate } from 'drizzle-orm/pg-proxy/migrator';
import { cities, users } from './schema';

async function main() {
  const db = drizzle(async (sql, params, method) => {
    try {
      const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, { sql, params, method });

      return { rows: rows.data };
    } catch (e: any) {
      console.error('Error from pg proxy server:', e.response.data);
      return { rows: [] };
    }
  });

  await migrate(db, async (queries) => {
    try {
      await axios.post(`${process.env.REMOTE_DRIVER}/query`, { queries });
    } catch (e) {
      console.log(e);
      throw new Error('Proxy server cannot run migrations');
    }
  }, { migrationsFolder: 'drizzle' });

  const insertedCity = await db.insert(cities).values({ id: 1, name: 'name' }).returning();
  const insertedUser = await db.insert(users).values({ id: 1, name: 'name', email: 'email', cityId: 1 });
  const usersToCityResponse = await db.select().from(users).leftJoin(cities, eq(users.cityId, cities.id));
}

더 자세한 내용은 문서에서 확인할 수 있습니다.

🎉 D1 배치 API 지원

참고: https://developers.cloudflare.com/d1/platform/client-api/#dbbatch

배치 API 사용 예제:

const batchResponse = await db.batch([
  db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
    id: usersTable.id,
  }),
  db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
  db.query.usersTable.findMany({}),
  db.select().from(usersTable).where(eq(usersTable.id, 1)),
  db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(
    usersTable,
  ),
]);
type BatchResponse = [
  {
    id: number;
  }[],
  D1Result,
  {
    id: number;
    name: string;
    verified: number;
    invitedBy: number | null;
  }[],
  {
    id: number;
    name: string;
    verified: number;
    invitedBy: number | null;
  }[],
  {
    id: number;
    invitedBy: number | null;
  }[],
];

db.batch 내부에서 사용할 수 있는 모든 빌더:

`db.all()`,
`db.get()`,
`db.values()`,
`db.run()`,
`db.query..findMany()`,
`db.query..findFirst()`,
`db.select()...`,
`db.update()...`,
`db.delete()...`,
`db.insert()...`,

더 많은 사용 예제는 여기에서 확인할 수 있습니다: integration-tests/tests/d1-batch.test.ts 그리고 문서에서도 확인할 수 있습니다.


Drizzle Kit 0.20.0 업데이트

  1. defineConfig 함수를 사용하여 drizzle.config 정의하는 새로운 방식
    이제 defineConfig 함수를 사용해 설정 파일을 정의할 수 있습니다.

  2. wrangler.toml 파일을 통해 Drizzle Studio에서 Cloudflare D1 접근 가능
    Drizzle Studio에서 Cloudflare D1에 접근할 수 있게 되었습니다. 이를 위해 wrangler.toml 파일을 사용합니다.

  3. Drizzle Studio가 https://local.drizzle.studio/로 이전
    Drizzle Studio가 새로운 도메인인 https://local.drizzle.studio/로 이전했습니다.

  4. bigint unsigned 타입 지원
    이제 bigint unsigned 타입을 사용할 수 있습니다.

  5. primaryKeysforeignKeys에 커스텀 이름 지정 가능
    기본 키와 외래 키에 커스텀 이름을 지정할 수 있게 되었습니다.

  6. 환경 변수 자동으로 가져오기
    환경 변수를 자동으로 가져오는 기능이 추가되었습니다.

  7. 버그 수정 및 개선 사항
    여러 버그가 수정되고 기능이 개선되었습니다.

더 자세한 내용은 여기에서 확인할 수 있습니다.