Drizzle 소프트 관계
Drizzle 관계의 유일한 목적은 여러분이 관계형 데이터를 가장 간단하고 명확하게 쿼리할 수 있도록 하는 것입니다.
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은 정말 멋져요",
authorId: 10,
},
{
id: 2,
content: "하지만 관계형 쿼리를 확인해보세요",
authorId: 10,
}
]
}]
일대일 관계
Drizzle ORM은 relations
연산자를 사용하여 테이블 간의 일대일 관계를 정의할 수 있는 API를 제공합니다.
다음은 사용자와 사용자 간의 일대일 관계 예제입니다. 한 사용자가 다른 사용자를 초대할 수 있는 경우를 보여줍니다. 이 예제는 자기 참조(self reference)를 사용합니다.
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
가 null
일 수 있음을 알려줍니다.
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();
//____^? 타입 { id: number, profileInfo: { ... } | null }
이 예제에서 user.profileInfo
는 null
일 수 있음을 TypeScript가 인식합니다. 이는 외래 키가 profile_info
테이블에 있기 때문입니다.
일대다(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],
}),
}));
이 코드는 사용자, 게시물, 댓글 간의 관계를 정의합니다. 사용자는 여러 게시물을 작성할 수 있고, 각 게시물은 여러 댓글을 가질 수 있습니다. 이러한 관계는 Drizzle ORM의 relations
연산자를 통해 쉽게 정의할 수 있습니다.
다대다 관계
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),
}));
// 사용자와 그룹을 연결하는 junction 테이블 정의
export const usersToGroups = pgTable(
'users_to_groups',
{
userId: integer('user_id')
.notNull()
.references(() => users.id),
groupId: integer('group_id')
.notNull()
.references(() => groups.id),
},
(t) => ({
pk: primaryKey({ columns: [t.userId, t.groupId] }),
}),
);
// junction 테이블과의 관계 정의
export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
group: one(groups, {
fields: [usersToGroups.groupId],
references: [groups.id],
}),
user: one(users, {
fields: [usersToGroups.userId],
references: [users.id],
}),
}));
이 예제에서는 users
와 groups
테이블 간의 다대다 관계를 정의하고, 이를 연결하는 usersToGroups
테이블을 생성합니다. usersToGroups
테이블은 userId
와 groupId
를 외래 키로 사용하여 두 테이블 간의 관계를 관리합니다.
외래 키(Foreign Keys)
relations
가 외래 키와 비슷해 보인다는 것을 눈치챘을 겁니다. 심지어 references
속성도 가지고 있죠. 그렇다면 둘의 차이는 무엇일까요?
외래 키도 비슷한 목적을 가지고 있지만, 테이블 간의 관계를 정의하는 방식에서 relations
와는 다른 수준에서 동작합니다.
외래 키는 데이터베이스 수준의 제약 조건입니다. 모든 insert
, update
, delete
작업에서 검사되며, 제약 조건이 위반되면 오류를 발생시킵니다.
반면에 **relations
**는 더 높은 수준의 추상화입니다. 애플리케이션 수준에서만 테이블 간의 관계를 정의하는 데 사용됩니다.
relations
는 데이터베이스 스키마에 영향을 미치지 않으며, 암묵적으로 외래 키를 생성하지도 않습니다.
이것이 의미하는 바는, relations
와 외래 키를 함께 사용할 수 있지만, 서로 의존하지 않는다는 것입니다.
외래 키를 사용하지 않고 relations
를 정의할 수 있고(그 반대도 가능), 이는 외래 키를 지원하지 않는 데이터베이스에서도 사용할 수 있게 해줍니다.
다음 두 예제는 Drizzle의 관계형 쿼리를 사용하여 데이터를 조회할 때 동일하게 동작합니다.
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) 동작
더 자세한 정보는 PostgreSQL 외래 키 문서를 참고하세요.
부모 테이블의 참조된 데이터가 수정될 때 발생할 동작을 지정할 수 있습니다. 이러한 동작을 “외래 키 동작”이라고 합니다. PostgreSQL은 이러한 동작에 대해 여러 옵션을 제공합니다.
삭제/업데이트 시 동작
-
CASCADE
: 부모 테이블의 행이 삭제되면, 자식 테이블의 모든 관련 행도 함께 삭제됩니다. 이는 자식 테이블에 고아(orphaned) 행이 남지 않도록 보장합니다. -
NO ACTION
: 기본 동작입니다. 자식 테이블에 관련 행이 있는 경우, 부모 테이블의 행 삭제를 방지합니다. 부모 테이블의 DELETE 작업이 실패합니다. -
RESTRICT
:NO ACTION
과 유사합니다. 자식 테이블에 종속된 행이 있는 경우, 부모 테이블의 행 삭제를 방지합니다.NO ACTION
과 동일하며, 호환성을 위해 포함되었습니다. -
SET DEFAULT
: 부모 테이블의 행이 삭제되면, 자식 테이블의 외래 키 컬럼이 기본값으로 설정됩니다. 기본값이 없는 경우, DELETE 작업이 실패합니다. -
SET NULL
: 부모 테이블의 행이 삭제되면, 자식 테이블의 외래 키 컬럼이 NULL로 설정됩니다. 이 동작은 자식 테이블의 외래 키 컬럼이 NULL 값을 허용한다고 가정합니다.
ON DELETE
와 유사하게, 참조된 컬럼이 변경(업데이트)될 때 호출되는ON UPDATE
도 있습니다. 가능한 동작은 동일하지만,SET NULL
과SET DEFAULT
의 경우 컬럼 목록을 지정할 수 없습니다. 이 경우,CASCADE
는 참조된 컬럼의 업데이트된 값이 참조하는 행에 복사됨을 의미합니다.
Drizzle에서는 references()
의 두 번째 인자를 사용하여 외래 키 동작을 추가할 수 있습니다.
동작 타입
export type UpdateDeleteAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
// references 인터페이스의 두 번째 인자
actions?: {
onUpdate?: UpdateDeleteAction;
onDelete?: UpdateDeleteAction;
} | undefined
다음 예제에서 posts
스키마의 author
필드에 onDelete: 'cascade'
를 추가하면, user
를 삭제할 때 관련된 모든 Post
레코드도 함께 삭제됩니다.
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) => ({
fk: foreignKey({
name: "author_fk",
columns: [table.author],
foreignColumns: [users.id],
})
.onDelete('cascade')
.onUpdate('cascade')
}),
);
관계 명확히 하기
Drizzle은 동일한 두 테이블 간에 여러 관계를 정의할 때, 이를 명확히 구분하기 위해 relationName
옵션을 제공합니다. 예를 들어, author
와 reviewer
관계를 모두 갖는 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',
}),
}));
위 예제에서 relationName
을 사용하여 author
와 reviewer
관계를 명확히 구분하고 있습니다. 이를 통해 동일한 두 테이블 간에 여러 관계를 정의할 때 발생할 수 있는 혼란을 방지할 수 있습니다.