Drizzle Soft Relations
Drizzle relations의 유일한 목적은 관계형 데이터를 가장 간단하고 간결한 방식으로 쿼리할 수 있도록 하는 것입니다:
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 관계형 쿼리를 사용하여 데이터를 쿼리하는 측면에서 정확히 동일하게 작동합니다.
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
-
CASCADE: 부모 테이블의 행이 삭제되면, 자식 테이블의 모든 해당 행도 함께 삭제됩니다. 이는 자식 테이블에 고아 행이 존재하지 않도록 보장합니다. -
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';
// 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 옵션을 제공합니다.
예를 들어, 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',
}),
}));