Drizzle Soft Relations

Drizzle relations의 유일한 목적은 관계형 데이터를 가장 간단하고 간결한 방식으로 쿼리할 수 있도록 하는 것입니다:

관계형 쿼리
조인을 사용한 Select
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/…';

const db = drizzle(client, { schema });

const result = db.query.users.findMany({
  with: {
    posts: true,
  },
});
[{
  id: 10,
  name: "Dan",
  posts: [
    {
      id: 1,
      content: "SQL is awesome",
      authorId: 10,
    },
    {
      id: 2,
      content: "But check relational queries",
      authorId: 10,
    }
  ]
}]

일대일 (One-to-one)

Drizzle ORM은 relations 연산자를 사용하여 테이블 간 일대일 관계를 정의하는 API를 제공합니다.

사용자가 다른 사용자를 초대할 수 있는 users와 users 간의 일대일 관계 예제입니다 (이 예제는 자기 참조를 사용합니다):

import { pgTable, serial, text, integer, boolean } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

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

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

다른 예제로는 사용자의 프로필 정보가 별도의 테이블에 저장되는 경우가 있습니다. 이 경우 외래 키가 “profile_info” 테이블에 저장되므로, user 관계에는 fields나 references가 없습니다. 이는 TypeScript에게 user.profileInfo가 nullable임을 알려줍니다:

import { pgTable, serial, text, integer, jsonb } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});

export const usersRelations = relations(users, ({ one }) => ({
	profileInfo: one(profileInfo),
}));

export const profileInfo = pgTable('profile_info', {
	id: serial('id').primaryKey(),
	userId: integer('user_id').references(() => users.id),
	metadata: jsonb('metadata'),
});

export const profileInfoRelations = relations(profileInfo, ({ one }) => ({
	user: one(users, { fields: [profileInfo.userId], references: [users.id] }),
}));

const user = await queryUserWithProfileInfo();
//____^? type { id: number, profileInfo: { ... } | null  }

일대다 (One-to-many)

Drizzle ORM은 relations 연산자를 사용하여 테이블 간 일대다 관계를 정의하는 API를 제공합니다.

사용자와 그들이 작성한 게시글 간의 일대다 관계 예제입니다:

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});

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

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

export const postsRelations = relations(posts, ({ one }) => ({
	author: one(users, {
		fields: [posts.authorId],
		references: [users.id],
	}),
}));

이제 게시글에 댓글을 추가해보겠습니다:

...

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

export const postsRelations = relations(posts, ({ one, many }) => ({
	author: one(users, {
		fields: [posts.authorId],
		references: [users.id],
	}),
	comments: many(comments)
}));

export const comments = pgTable('comments', {
	id: serial('id').primaryKey(),
	text: text('text'),
	authorId: integer('author_id'),
	postId: integer('post_id'),
});

export const commentsRelations = relations(comments, ({ one }) => ({
	post: one(posts, {
		fields: [comments.postId],
		references: [posts.id],
	}),
}));

다대다 (Many-to-many)

Drizzle ORM은 junction 또는 join 테이블이라고 불리는 중간 테이블을 통해 테이블 간 다대다 관계를 정의하는 API를 제공합니다. 이러한 중간 테이블은 명시적으로 정의되어야 하며, 관련 테이블 간의 연관 관계를 저장합니다.

사용자와 그룹 간의 다대다 관계 예제입니다:

import { relations } from 'drizzle-orm';
import { integer, pgTable, primaryKey, serial, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name'),
});

export const usersRelations = relations(users, ({ many }) => ({
  usersToGroups: many(usersToGroups),
}));

export const groups = pgTable('groups', {
  id: serial('id').primaryKey(),
  name: text('name'),
});

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

export const usersToGroups = pgTable(
  'users_to_groups',
  {
    userId: integer('user_id')
      .notNull()
      .references(() => users.id),
    groupId: integer('group_id')
      .notNull()
      .references(() => groups.id),
  },
  (t) => [
		primaryKey({ columns: [t.userId, t.groupId] })
	],
);

export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
  group: one(groups, {
    fields: [usersToGroups.groupId],
    references: [groups.id],
  }),
  user: one(users, {
    fields: [usersToGroups.userId],
    references: [users.id],
  }),
}));

외래 키 (Foreign keys)

relations가 외래 키와 유사하게 보인다는 것을 알아차렸을 수 있습니다 — 심지어 references 속성도 가지고 있습니다. 그렇다면 차이점은 무엇일까요?

외래 키는 테이블 간의 관계를 정의한다는 유사한 목적을 가지지만, relations와는 다른 수준에서 작동합니다.

외래 키는 데이터베이스 수준의 제약조건으로, 모든 insert/update/delete 작업마다 검사되며 제약조건이 위반되면 오류를 발생시킵니다. 반면에 relations는 더 높은 수준의 추상화로, 애플리케이션 수준에서만 테이블 간의 관계를 정의하는 데 사용됩니다. relations는 데이터베이스 스키마에 어떤 영향도 주지 않으며 외래 키를 암시적으로 생성하지도 않습니다.

이것이 의미하는 바는 relations와 외래 키는 함께 사용될 수 있지만, 서로 의존하지 않는다는 것입니다. 외래 키를 사용하지 않고도 relations를 정의할 수 있으며(그 반대도 가능), 이를 통해 외래 키를 지원하지 않는 데이터베이스에서도 사용할 수 있습니다.

다음 두 예제는 Drizzle 관계형 쿼리를 사용하여 데이터를 쿼리하는 측면에서 정확히 동일하게 작동합니다.

schema1.ts
schema2.ts
export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});

export const usersRelations = relations(users, ({ one, many }) => ({
	profileInfo: one(users, {
		fields: [profileInfo.userId],
		references: [users.id],
	}),
}));

export const profileInfo = pgTable('profile_info', {
	id: serial('id').primaryKey(),
	userId: integer("user_id"),
	metadata: jsonb("metadata"),
});

외래 키 액션 (Foreign key actions)

자세한 내용은 PostgreSQL foreign keys 문서를 확인하세요.

부모 테이블의 참조 데이터가 수정될 때 발생해야 하는 동작을 지정할 수 있습니다. 이러한 동작을 “외래 키 액션”이라고 합니다. PostgreSQL은 이러한 액션에 대해 여러 옵션을 제공합니다.

On Delete / Update Actions

ON DELETE와 유사하게 참조되는 컬럼이 변경(업데이트)될 때 호출되는 ON UPDATE도 있습니다. 가능한 액션은 동일하지만, SET NULL과 SET DEFAULT에는 컬럼 목록을 지정할 수 없습니다. 이 경우 CASCADE는 참조되는 컬럼의 업데이트된 값이 참조하는 행에 복사되어야 함을 의미합니다.

Drizzle에서는 references() 두 번째 인자를 사용하여 외래 키 액션을 추가할 수 있습니다.

액션 타입

export type UpdateDeleteAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';

// second argument of references interface
actions?: {
		onUpdate?: UpdateDeleteAction;
		onDelete?: UpdateDeleteAction;
	} | undefined

다음 예제에서 posts 스키마의 author 필드에 onDelete: 'cascade'를 추가하면, user를 삭제할 때 관련된 모든 게시글 레코드도 함께 삭제됩니다.

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});

export const posts = pgTable('posts', {
	id: serial('id').primaryKey(),
	name: text('name'),
	author: integer('author').references(() => users.id, {onDelete: 'cascade'}).notNull(),
});

foreignKey 연산자로 지정된 제약조건의 경우, 외래 키 액션은 다음 구문으로 정의됩니다:

import { foreignKey, pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});

export const posts = pgTable('posts', {
	id: serial('id').primaryKey(),
	name: text('name'),
	author: integer('author').notNull(),
}, (table) => [
	foreignKey({
		name: "author_fk",
		columns: [table.author],
		foreignColumns: [users.id],
	})
		.onDelete('cascade')
		.onUpdate('cascade')
]);

관계 명확화 (Disambiguating relations)

Drizzle은 동일한 두 테이블 간에 여러 관계를 정의할 때 관계를 명확히 구분하기 위한 방법으로 relationName 옵션을 제공합니다. 예를 들어, authorreviewer 관계를 가진 posts 테이블을 정의하는 경우입니다.

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
 
export const users = pgTable('users', {
	id: serial('id').primaryKey(),
	name: text('name'),
});
 
export const usersRelations = relations(users, ({ many }) => ({
	author: many(posts, { relationName: 'author' }),
	reviewer: many(posts, { relationName: 'reviewer' }),
}));
 
export const posts = pgTable('posts', {
	id: serial('id').primaryKey(),
	content: text('content'),
	authorId: integer('author_id'),
	reviewerId: integer('reviewer_id'),
});
 
export const postsRelations = relations(posts, ({ one }) => ({
	author: one(users, {
		fields: [posts.authorId],
		references: [users.id],
		relationName: 'author',
	}),
	reviewer: one(users, {
		fields: [posts.reviewerId],
		references: [users.id],
		relationName: 'reviewer',
	}),
}));