Drizzle μ†Œν”„νŠΈ 관계

Drizzle κ΄€κ³„μ˜ μœ μΌν•œ λͺ©μ μ€ κ΄€κ³„ν˜• 데이터λ₯Ό κ°€μž₯ κ°„λ‹¨ν•˜κ³  κ°„κ²°ν•œ λ°©μ‹μœΌλ‘œ 쿼리할 수 μžˆλ„λ‘ ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€:

Relational queries
Select with joins
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,
    }
  ]
}]

μΌλŒ€μΌ

Drizzle ORM은 relations μ—°μ‚°μžλ₯Ό μ‚¬μš©ν•˜μ—¬ ν…Œμ΄λΈ” κ°„μ˜ μΌλŒ€μΌ 관계λ₯Ό μ •μ˜ν•˜λŠ” APIλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

μ‚¬μš©μžκ°€ λ‹€λ₯Έ μ‚¬μš©μžλ₯Ό μ΄ˆλŒ€ν•  수 μžˆλŠ” μ‚¬μš©μž κ°„ μΌλŒ€μΌ κ΄€κ³„μ˜ μ˜ˆμ‹œμž…λ‹ˆλ‹€ (이 μ˜ˆμ‹œλŠ” 자기 μ°Έμ‘°λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€):

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” ν…Œμ΄λΈ”μ— μ €μž₯λ˜μ–΄ 있기 λ•Œλ¬Έμ— μ‚¬μš©μž κ΄€κ³„μ—λŠ” 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  }

μΌλŒ€λ‹€

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은 μ†Œμœ„ 쀑간(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],
  }),
}));

μ™Έλž˜ ν‚€

relationsκ°€ μ™Έλž˜ 킀와 λΉ„μŠ·ν•΄ λ³΄μΈλ‹€λŠ” 것을 λˆˆμΉ˜μ±˜μ„ κ²ƒμž…λ‹ˆλ‹€ β€” references 속성도 μžˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ 차이점은 λ¬΄μ—‡μΌκΉŒμš”?

μ™Έλž˜ ν‚€λŠ” ν…Œμ΄λΈ” κ°„μ˜ 관계λ₯Ό μ •μ˜ν•œλ‹€λŠ” μ μ—μ„œ λΉ„μŠ·ν•œ λͺ©μ μ„ κ°€μ§€κ³  μžˆμ§€λ§Œ, relationsμ™€λŠ” λ‹€λ₯Έ μˆ˜μ€€μ—μ„œ μž‘λ™ν•©λ‹ˆλ‹€.

μ™Έλž˜ ν‚€λŠ” λ°μ΄ν„°λ² μ΄μŠ€ μˆ˜μ€€μ˜ μ œμ•½μ‘°κ±΄μœΌλ‘œ, λͺ¨λ“  insert/update/delete μž‘μ—…μ—μ„œ ν™•μΈλ˜λ©° μ œμ•½μ‘°κ±΄μ΄ μœ„λ°˜λ˜λ©΄ 였λ₯˜λ₯Ό λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. λ°˜λ©΄μ— 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"),
});

μ™Έλž˜ ν‚€ μž‘μ—…

μžμ„Έν•œ μ •λ³΄λŠ” PostgreSQL μ™Έλž˜ ν‚€ λ¬Έμ„œλ₯Ό ν™•μΈν•˜μ„Έμš”

λΆ€λͺ¨ ν…Œμ΄λΈ”μ˜ 참쑰된 데이터가 μˆ˜μ •λ  λ•Œ λ°œμƒν•΄μ•Ό ν•˜λŠ” μž‘μ—…μ„ μ§€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ μž‘μ—…μ„ β€œμ™Έλž˜ ν‚€ μž‘μ—…β€μ΄λΌκ³  ν•©λ‹ˆλ‹€. PostgreSQL은 μ΄λŸ¬ν•œ μž‘μ—…μ— λŒ€ν•œ μ—¬λŸ¬ μ˜΅μ…˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

μ‚­μ œ/μ—…λ°μ΄νŠΈ μž‘μ—…

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λ₯Ό μ‚­μ œν•  λ•Œ κ΄€λ ¨λœ λͺ¨λ“  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) => [
	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',
	}),
}));