Relational Queries 버전 2로 마이그레이션

WARNING

이 페이지는 drizzle 버전 1.0.0-beta.1 이상에서 사용 가능한 개념을 설명합니다.

npm
yarn
pnpm
bun
npm i drizzle-orm@beta
npm i drizzle-kit@beta -D

This guide assumes familiarity with:

API 변경사항

v1에서 달라진 동작 방식

가장 큰 업데이트 중 하나는 Relations 스키마 정의 부분입니다.

첫 번째 차이점은 더 이상 각 테이블에 대한 relations을 서로 다른 객체에서 개별적으로 지정한 다음 스키마와 함께 drizzle()에 모두 전달할 필요가 없다는 것입니다. Relational Queries v2에서는 이제 필요한 모든 테이블의 관계를 지정할 수 있는 전용 장소가 하나 있습니다.

콜백의 r 매개변수는 스키마의 모든 테이블과 one, many, through와 같은 함수를 포함하여 포괄적인 자동 완성 기능을 제공합니다. 본질적으로 관계를 지정하는 데 필요한 모든 것을 제공합니다.

// relations.ts
import * as schema from "./schema"
import { defineRelations } from "drizzle-orm"

export const relations = defineRelations(schema, (r) => ({
    ...
}));
// index.ts
import { relations } from "./relations"
import { drizzle } from "drizzle-orm/..."

const db = drizzle(process.env.DATABASE_URL, { relations })
무엇이 다른가요?

스키마 정의

import * as p from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text(),
	invitedBy: p.integer('invited_by'),
});

export const posts = p.pgTable('posts', {
	id: p.integer().primaryKey(),
	content: p.text(),
	authorId: p.integer('author_id'),
});

모든 관계를 한 곳에서 정의

❌ v1
import { relations } from "drizzle-orm/_relations";
import { users, posts } from './schema';

export const usersRelation = relations(users, ({ one, many }) => ({
  invitee: one(users, {
    fields: [users.invitedBy],
    references: [users.id],
  }),
  posts: many(posts),
}));

export const postsRelation = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
✅ v2
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  users: {
    invitee: r.one.users({
      from: r.users.invitedBy,
      to: r.users.id,
    }),
    posts: r.many.posts(),
  },
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  },
}));

여전히 여러 parts로 분리할 수 있으며, 원하는 크기로 만들 수 있습니다

import { defineRelations, defineRelationsPart } from 'drizzle-orm';
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  users: {
    invitee: r.one.users({
      from: r.users.invitedBy,
      to: r.users.id,
    }),
    posts: r.many.posts(),
  }
}));

export const part = defineRelationsPart(schema, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  }
}));

그런 다음 db 인스턴스에 제공할 수 있습니다

const db = drizzle(process.env.DB_URL, { relations: { ...relations, ...part } })

one 없이 many 정의하기

v1에서는 관계의 many 측면만 원하는 경우, 반대편에서 one 측면을 지정해야 했으며, 이는 좋지 않은 개발자 경험을 제공했습니다.

v2에서는 추가 단계 없이 many 측면만 사용할 수 있습니다

❌ v1
import { relations } from "drizzle-orm/_relations";
import { users, posts } from './schema';

export const usersRelation = relations(users, ({ one, many }) => ({
  posts: many(posts),
}));

export const postsRelation = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
✅ v2
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  users: {
    posts: r.many.posts({
      from: r.users.id,
      to: r.posts.authorId,
    }),
  },
}));

새로운 optional 옵션

타입 레벨에서 optional: falseposts 객체의 author 키를 필수로 만듭니다. 특정 엔티티가 항상 존재할 것이 확실할 때 사용해야 합니다.

❌ v1

v1에서는 지원되지 않았습니다

✅ v2
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  users: {
    posts: r.many.posts({
      from: r.users.id,
      to: r.posts.authorId,
      optional: false,
    }),
  },
}));

drizzle()에 모드 지정 불필요

모든 MySQL dialect에 대해 동일한 전략을 사용하는 방법을 찾았으므로, 더 이상 지정할 필요가 없습니다

❌ v1
import * as schema from './schema'

const db = drizzle(process.env.DATABASE_URL, { mode: "planetscale", schema });
// or
const db = drizzle(process.env.DATABASE_URL, { mode: "default", schema });
✅ v2
import { relations } from './relations'

const db = drizzle(process.env.DATABASE_URL, { relations });

fromto 개선

fieldsfrom으로, referencesto로 이름을 변경했으며, 둘 다 단일 값 또는 배열을 허용하도록 했습니다

❌ v1
...
author: one(users, {
  fields: [posts.authorId],
  references: [users.id],
}),
...
✅ v2
... 
author: r.one.users({
  from: r.posts.authorId,
  to: r.users.id,
}),
...
... 
author: r.one.users({
  from: [r.posts.authorId],
  to: [r.users.id],
}),
...

relationName -> alias

❌ v1
import { relations } from "drizzle-orm/_relations";
import { users, posts } from './schema';

export const postsRelation = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
	  relationName: "author_post",
  }),
}));
✅ v2
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
      alias: "author_post",
    }),
  },
}));

custom types의 새로운 함수

Relational Queries v2에서 데이터 매핑 방식을 제어할 수 있도록 custom types에 몇 가지 새로운 함수가 추가되었습니다:

fromJson

데이터베이스에서 JSON으로 변환된 데이터를 원하는 형식으로 변환하는 데 사용되는 선택적 매핑 함수입니다. 예를 들어, RQB 또는 JSON functions를 통해 bigint 컬럼을 쿼리할 때, 일반 쿼리의 bigint와 달리 결과 필드가 문자열 표현으로 반환됩니다. 이를 처리하기 위해 해당 필드의 매핑을 처리하는 별도의 함수가 필요합니다:

fromJson(value: string): bigint {
	return BigInt(value);
},

반환되는 데이터가 다음과 같이 변경됩니다:

{
	customField: "5044565289845416380";
}

에서:

{
	customField: 5044565289845416380n;
}

forJsonSelect

JSON functions 내에서 컬럼 선택을 수정하는 데 사용되는 선택적 선택 수정자 함수입니다. 이러한 시나리오에 필요할 수 있는 추가 매핑은 fromJson 함수를 사용하여 처리할 수 있습니다. relational queries에서 사용됩니다.

예를 들어, bigint를 사용할 때 데이터 무결성을 보존하기 위해 필드를 text로 캐스팅해야 합니다

forJsonSelect(identifier: SQL, sql: SQLGenerator, arrayDimensions?: number): SQL {
	return sql`${identifier}::text`
},

쿼리가 다음과 같이 변경됩니다:

SELECT
	row_to_json("t".*)
	FROM
	(
		SELECT
		"table"."custom_bigint" AS "bigint"
		FROM
		"table"
	) AS "t"

에서:

SELECT
	row_to_json("t".*)
	FROM
	(
		SELECT
		"table"."custom_bigint"::text AS "bigint"
		FROM
		"table"
	) AS "t"

쿼리로 반환되는 객체가 다음과 같이 변경됩니다:

{
	bigint: 5044565289845416000; // JSON 형식으로 직접 변환하여 부분적인 데이터 손실 발생
}

에서:

{
	bigint: "5044565289845416380"; // JSON 변환 전에 필드를 text로 변환하여 데이터 보존
}
✅ v2
const customBytes = customType<{
 	data: Buffer;
 	driverData: Buffer;
 	jsonData: string;
 }>({
 	dataType: () => 'bytea',
 	fromJson: (value) => {
 		return Buffer.from(value.slice(2, value.length), 'hex');
 	},
 	forJsonSelect: (identifier, sql, arrayDimensions) =>
 		sql`${identifier}::text${sql.raw('[]'.repeat(arrayDimensions ?? 0))}`,
 });
새로운 기능은 무엇인가요?

다대다 관계를 위한 through

이전에는 junction 테이블을 통해 쿼리한 다음 모든 응답에 대해 매핑해야 했습니다.

이제는 그럴 필요가 없습니다!

스키마

import * as p from "drizzle-orm/pg-core";

export const users = p.pgTable("users", {
  id: p.integer().primaryKey(),
  name: p.text(),
  verified: p.boolean().notNull(),
});

export const groups = p.pgTable("groups", {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const usersToGroups = p.pgTable(
  "users_to_groups",
  {
    userId: p
      .integer("user_id")
      .notNull()
      .references(() => users.id),
    groupId: p
      .integer("group_id")
      .notNull()
      .references(() => groups.id),
  },
  (t) => [p.primaryKey({ columns: [t.userId, t.groupId] })]
);
❌ v1
export const usersRelations = relations(users, ({ many }) => ({
  usersToGroups: many(usersToGroups),
}));

export const groupsRelations = relations(groups, ({ many }) => ({
  usersToGroups: many(usersToGroups),
}));

export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
  group: one(groups, {
    fields: [usersToGroups.groupId],
    references: [groups.id],
  }),
  user: one(users, {
    fields: [usersToGroups.userId],
    references: [users.id],
  }),
}));
// Query example
const response = await db.query.users.findMany({
  with: {
    usersToGroups: {
      columns: {},
      with: {
        group: true,
      },
    },
  },
});
✅ v2
import * as schema from './schema';
import { defineRelations } from 'drizzle-orm';

export const relations = defineRelations(schema, (r) => ({
  users: {
    groups: r.many.groups({
      from: r.users.id.through(r.usersToGroups.userId),
      to: r.groups.id.through(r.usersToGroups.groupId),
    }),
  },
  groups: {
    participants: r.many.users(),
  },
}));
// Query example
const response = await db.query.users.findMany({
  with: {
    groups: true,
  },
});

사전 정의된 필터

❌ v1

v1에서는 지원되지 않았습니다

✅ v2
import * as schema from './schema';
import { defineRelations } from 'drizzle-orm';

export const relations = defineRelations(schema,
  (r) => ({
    groups: {
      verifiedUsers: r.many.users({
        from: r.groups.id.through(r.usersToGroups.groupId),
        to: r.users.id.through(r.usersToGroups.userId),
        where: {
          verified: true,
        },
      }),
    },
  })
);
// 쿼리 예제: 인증된 모든 사용자가 있는 그룹 가져오기
const response = await db.query.groups.findMany({
  with: {
    verifiedUsers: true,
  },
});
where가 이제 객체입니다
❌ v1
const response = db._query.users.findMany({
  where: (users, { eq }) => eq(users.id, 1),
});
✅ v2
const response = db.query.users.findMany({
  where: {
    id: 1,
  },
});

전체 API 레퍼런스는 Select Filters 문서를 확인하세요.

RAW를 사용한 복잡한 필터 예제

// schema.ts
import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: integer("id").primaryKey(),
  name: text("name"),
  email: text("email").notNull(),
  age: integer("age"),
  createdAt: timestamp("created_at").defaultNow(),
  lastLogin: timestamp("last_login"),
  subscriptionEnd: timestamp("subscription_end"),
  lastActivity: timestamp("last_activity"),
  preferences: jsonb("preferences"),      // JSON column for user settings/preferences
  interests: text("interests").array(),     // Array column for user interests
});
const response = db.query.users.findMany({
  where: {
    AND: [
      {
        OR: [
          { RAW: (table) => sql`LOWER(${table.name}) LIKE 'john%'` },
          { name: { ilike: "jane%" } },
        ],
      },
      {
        OR: [
          { RAW: (table) => sql`${table.preferences}->>'theme' = 'dark'` },
          { RAW: (table) => sql`${table.preferences}->>'theme' IS NULL` },
        ],
      },
      { RAW: (table) => sql`${table.age} BETWEEN 25 AND 35` },
    ],
  },
});
orderBy가 이제 객체입니다
❌ v1
const response = db._query.users.findMany({
  orderBy: (users, { asc }) => [asc(users.id)],
});
✅ v2
const response = db.query.users.findMany({
  orderBy: { id: "asc" },
});
관계로 필터링하기
❌ v1

v1에서는 지원되지 않았습니다

✅ v2

예제: ID가 10보다 크고 “M”으로 시작하는 콘텐츠가 있는 게시물을 하나 이상 가진 모든 사용자 가져오기

const usersWithPosts = await db.query.usersTable.findMany({
  where: {
    id: {
      gt: 10
    },
    posts: {
      content: {
        like: 'M%'
      }
    }
  },
});
관련 객체에서 offset 사용하기
❌ v1

v1에서는 지원되지 않았습니다

✅ v2
await db.query.posts.findMany({
	limit: 5,
	offset: 2, // correct ✅
	with: {
		comments: {
			offset: 3, // correct ✅
			limit: 3,
		},
	},
});

v1에서 v2로 관계 스키마 정의 마이그레이션하기

옵션 1: drizzle-kit pull 사용하기

새 버전의 drizzle-kit pull은 새로운 구문으로 relations.ts 파일을 가져오는 것을 지원합니다:

1단계
npm
yarn
pnpm
bun
npx drizzle-kit pull

2단계

생성된 관계 코드를 drizzle/relations.ts에서 관계를 지정하는 데 사용하는 파일로 복사합니다

 ├ 📂 drizzle
 │ ├ 📂 meta
 │ ├ 📜 migration.sql
 │ ├ 📜 relations.ts ────────┐
 │ └ 📜 schema.ts            |
 ├ 📂 src                    │ 
 │ ├ 📂 db                   │
 │ │ ├ 📜 relations.ts <─────┘
 │ │ └ 📜 schema.ts 
 │ └ 📜 index.ts         
 └ …

drizzle/relations.tsdrizzle/schema.ts에서 모든 테이블의 import를 포함하며, 다음과 같이 보입니다:

import * as schema from './schema'

모든 스키마 테이블이 있는 파일로 이 import를 변경해야 할 수 있습니다.

여러 스키마 파일이 있는 경우 다음과 같이 할 수 있습니다:

import * as schema1 from './schema1'
import * as schema2 from './schema2'
...

3단계

drizzle 데이터베이스 인스턴스 생성을 변경하고 schema 대신 relations 객체를 제공합니다

이전
import * as schema from './schema'
import { drizzle } from 'drizzle-orm/...'

const db = drizzle('<url>', { schema })
이후
// 2단계의 파일에서 import해야 합니다
import { relations } from './relations'
import { drizzle } from 'drizzle-orm/...'

const db = drizzle('<url>', { relations })

MySQL dialect를 사용했다면 버전 2에서는 필요하지 않으므로 drizzle()에서 mode를 제거할 수 있습니다

수동 마이그레이션

수동으로 마이그레이션하려면 일대일, 일대다, 다대다 관계의 완전한 API 레퍼런스와 예제를 보려면 Drizzle Relations 섹션을 확인하세요.

v1에서 v2로 쿼리 마이그레이션하기

where 문 마이그레이션

예제와 완전한 API 레퍼런스를 보려면 Select Filters 문서를 확인하세요.

새로운 구문으로 AND, OR, NOT, RAW를 사용할 수 있으며, Relations v1에서 이전에 사용 가능했던 모든 필터링 연산자를 사용할 수 있습니다.

예제

simple eq
using AND
using OR
using NOT
complex example using RAW
const response = db.query.users.findMany({
  where: {
    age: 15,
  },
});
select "users"."id" as "id", "users"."name" as "name"
from "users" 
where ("users"."age" = $1)
orderBy 문 마이그레이션

Order by는 컬럼과 정렬 방향(asc 또는 desc)을 지정하는 단일 객체로 단순화되었습니다

❌ v1
const response = db._query.users.findMany({
  orderBy: (users, { asc }) => [asc(users.id)],
});
✅ v2
const response = db.query.users.findMany({
  orderBy: { id: "asc" },
});
다대다 쿼리 마이그레이션

Relational Queries v1은 다대다 쿼리를 관리하는 매우 복잡한 방법을 가지고 있었습니다. junction 테이블을 통해 명시적으로 쿼리한 다음 해당 테이블을 매핑해야 했습니다:

const response = await db.query.users.findMany({
  with: {
    usersToGroups: {
      columns: {},
      with: {
        group: true,
      },
    },
  },
});

Relational Queries v2로 업그레이드한 후 다대다 관계는 다음과 같이 보입니다:

import * as schema from './schema';
import { defineRelations } from 'drizzle-orm';

export const relations = defineRelations(schema, (r) => ({
  users: {
    groups: r.many.groups({
      from: r.users.id.through(r.usersToGroups.userId),
      to: r.groups.id.through(r.usersToGroups.groupId),
    }),
  },
  groups: {
    participants: r.many.users(),
  },
}));

쿼리를 마이그레이션하면 다음과 같이 됩니다:

// 쿼리 예제
const response = await db.query.users.findMany({
  with: {
    groups: true,
  },
});

부분 업그레이드 또는 업그레이드 후에도 RQB v1 유지하기

모든 이전 쿼리와 관계 정의가 여전히 사용 가능하도록 업그레이드를 만들었습니다. 이 경우 대규모 리팩토링 없이 쿼리별로 코드베이스를 마이그레이션할 수 있습니다

1단계: 관계 import 변경

Relational Queries v1을 사용하여 관계를 정의하려면 drizzle-orm에서 import해야 합니다

v1
import { relations } from 'drizzle-orm';

Relational Queries v2에서는 마이그레이션을 위한 시간을 주기 위해 drizzle-orm/_relations로 이동했습니다

v2
import { relations } from "drizzle-orm/_relations";
2단계: 쿼리를 ._query로 교체

Relational Queries v1을 사용하려면 db.query.를 작성해야 했습니다

v1
await db.query.users.findMany();

Relational Queries v2에서는 db.query를 새로운 구문에 사용할 수 있도록 db._query로 이동했으며, 여전히 db._query를 통해 이전 구문을 사용할 수 있는 옵션을 제공합니다.

db.query를 단순히 지원 중단하고 db.query2 또는 db.queryV2와 같은 것으로 교체해야 하는지에 대해 오랜 논의를 했습니다. 결국 모든 새로운 API는 db.query처럼 간단하게 유지되어야 하며, 이전 구문을 계속 사용하려면 모든 쿼리를 db._query로 교체하도록 요구하는 것이 향후 모든 사람이 db.queryV2, db.queryV3, db.queryV4 등을 사용하도록 강제하는 것보다 낫다고 결정했습니다.

v2
// RQBv1 사용
await db._query.users.findMany();

// RQBv2 사용
await db.query.users.findMany();
3단계

이 가이드를 사용하여 새로운 관계를 정의하거나 가져온 다음, 새 쿼리에서 사용하거나 기존 쿼리를 하나씩 마이그레이션하세요.

내부 변경사항

  1. 모든 drizzle database, session, migrator, transaction 인스턴스가 RQB v2 쿼리를 위한 2개의 추가 제네릭 인자를 얻었습니다

예제

migrator

이전
export async function migrate<
  TSchema extends Record<string, unknown>
>(
  db: NodePgDatabase<TSchema>,
  config: MigrationConfig,
) {
  ...
}
이후
export async function migrate<
 TSchema extends Record<string, unknown>,
 TRelations extends AnyRelations
>(
  db: NodePgDatabase<TSchema, TRelations>,
  config: MigrationConfig,
) {
  ...
}

session

이전
export class NodePgSession<
  TFullSchema extends Record<string, unknown>,
  TSchema extends V1.TablesRelationalConfig,
> extends PgSession<NodePgQueryResultHKT, TFullSchema, TSchema>
이후
export class NodePgSession<
  TFullSchema extends Record<string, unknown>,
  TRelations extends AnyRelations,
  TTablesConfig extends TablesRelationalConfig,
  TSchema extends V1.TablesRelationalConfig,
> extends PgSession<NodePgQueryResultHKT, TFullSchema, TRelations, TTablesConfig, TSchema>

transaction

이전
export class NodePgTransaction<
  TFullSchema extends Record<string, unknown>,
  TSchema extends V1.TablesRelationalConfig,
> extends PgTransaction<NodePgQueryResultHKT, TFullSchema, TSchema>
이후
export class NodePgTransaction<
  TFullSchema extends Record<string, unknown>,
  TRelations extends AnyRelations,
  TTablesConfig extends TablesRelationalConfig,
  TSchema extends V1.TablesRelationalConfig,
> extends PgTransaction<NodePgQueryResultHKT, TFullSchema, TRelations, TTablesConfig, TSchema>

driver

이전
export class NodePgDatabase<
  TSchema extends Record<string, unknown> = Record<string, never>,
> extends PgDatabase<NodePgQueryResultHKT, TSchema>
이후
export class NodePgDatabase<
  TSchema extends Record<string, unknown> = Record<string, never>,
  TRelations extends AnyRelations = EmptyRelations,
> extends PgDatabase<NodePgQueryResultHKT, TSchema, TRelations>
  1. DrizzleConfig 제네릭에 TRelations 인자와 relations: TRelations 필드가 추가되었습니다

예제

이전
export interface DrizzleConfig<
  TSchema extends Record<string, unknown> = Record<string, never>
> {
  logger?: boolean | Logger;
  schema?: TSchema;
  casing?: Casing;
}
이후
export interface DrizzleConfig<
  TSchema extends Record<string, unknown> = Record<string, never>,
  TRelations extends AnyRelations = EmptyRelations,
> {
  logger?: boolean | Logger;
  schema?: TSchema;
  casing?: Casing;
  relations?: TRelations;
}
  1. 다음 엔티티들이 drizzle-ormdrizzle-orm/relations에서 drizzle-orm/_relations로 이동되었습니다. 원본 import는 이제 Relational Queries v2에서 사용하는 새로운 타입을 포함하므로, 이전 타입을 사용하려면 import를 업데이트해야 합니다:

이동된 모든 엔티티 목록

  • Relation
  • Relations
  • One
  • Many
  • TableRelationsKeysOnly
  • ExtractTableRelationsFromSchema
  • ExtractObjectValues
  • ExtractRelationsFromTableExtraConfigSchema
  • getOperators
  • Operators
  • getOrderByOperators
  • OrderByOperators
  • FindTableByDBName
  • DBQueryConfig
  • TableRelationalConfig
  • TablesRelationalConfig
  • RelationalSchemaConfig
  • ExtractTablesWithRelations
  • ReturnTypeOrValue
  • BuildRelationResult
  • NonUndefinedKeysOnly
  • BuildQueryResult
  • RelationConfig
  • extractTablesRelationalConfig
  • relations
  • createOne
  • createMany
  • NormalizedRelation
  • normalizeRelation
  • createTableRelationsHelpers
  • TableRelationsHelpers
  • BuildRelationalQueryResult
  • mapRelationalRow
  1. 마찬가지로 ${dialect}-core/query-builders/query 파일이 ${dialect}-core/query-builders/_query로 이동되었으며 RQB v2의 대안이 그 자리에 배치되었습니다

예제

이전
import { RelationalQueryBuilder, PgRelationalQuery } from './query-builders/query.ts';
이후
import { _RelationalQueryBuilder, _PgRelationalQuery } from './query-builders/_query.ts';