@kood/claude-code 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/hono/docs/library/drizzle/cloudflare-d1.md +247 -0
- package/templates/hono/docs/library/drizzle/config.md +167 -0
- package/templates/hono/docs/library/drizzle/index.md +259 -0
- package/templates/tanstack-start/docs/library/drizzle/cloudflare-d1.md +147 -0
- package/templates/tanstack-start/docs/library/drizzle/config.md +118 -0
- package/templates/tanstack-start/docs/library/drizzle/crud.md +205 -0
- package/templates/tanstack-start/docs/library/drizzle/index.md +79 -0
- package/templates/tanstack-start/docs/library/drizzle/relations.md +202 -0
- package/templates/tanstack-start/docs/library/drizzle/schema.md +154 -0
- package/templates/tanstack-start/docs/library/drizzle/setup.md +96 -0
- package/templates/tanstack-start/docs/library/drizzle/transactions.md +127 -0
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +204 -0
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +195 -0
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +203 -0
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +213 -0
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Drizzle - 관계 정의
|
|
2
|
+
|
|
3
|
+
## 관계 유형
|
|
4
|
+
|
|
5
|
+
### 1:N (One-to-Many)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core'
|
|
9
|
+
import { relations } from 'drizzle-orm'
|
|
10
|
+
|
|
11
|
+
// 사용자
|
|
12
|
+
export const users = pgTable('users', {
|
|
13
|
+
id: serial('id').primaryKey(),
|
|
14
|
+
name: text('name').notNull(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// 사용자 관계
|
|
18
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
19
|
+
posts: many(posts),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
// 게시글
|
|
23
|
+
export const posts = pgTable('posts', {
|
|
24
|
+
id: serial('id').primaryKey(),
|
|
25
|
+
title: text('title').notNull(),
|
|
26
|
+
authorId: integer('author_id').references(() => users.id),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// 게시글 관계
|
|
30
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
31
|
+
author: one(users, {
|
|
32
|
+
fields: [posts.authorId],
|
|
33
|
+
references: [users.id],
|
|
34
|
+
}),
|
|
35
|
+
}))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 1:1 (One-to-One)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// 사용자
|
|
42
|
+
export const users = pgTable('users', {
|
|
43
|
+
id: serial('id').primaryKey(),
|
|
44
|
+
name: text('name').notNull(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const usersRelations = relations(users, ({ one }) => ({
|
|
48
|
+
profile: one(profiles),
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
// 프로필
|
|
52
|
+
export const profiles = pgTable('profiles', {
|
|
53
|
+
id: serial('id').primaryKey(),
|
|
54
|
+
bio: text('bio'),
|
|
55
|
+
userId: integer('user_id').unique().references(() => users.id),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const profilesRelations = relations(profiles, ({ one }) => ({
|
|
59
|
+
user: one(users, {
|
|
60
|
+
fields: [profiles.userId],
|
|
61
|
+
references: [users.id],
|
|
62
|
+
}),
|
|
63
|
+
}))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### M:N (Many-to-Many)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// 사용자
|
|
70
|
+
export const users = pgTable('users', {
|
|
71
|
+
id: serial('id').primaryKey(),
|
|
72
|
+
name: text('name').notNull(),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
76
|
+
usersToGroups: many(usersToGroups),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
// 그룹
|
|
80
|
+
export const groups = pgTable('groups', {
|
|
81
|
+
id: serial('id').primaryKey(),
|
|
82
|
+
name: text('name').notNull(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
export const groupsRelations = relations(groups, ({ many }) => ({
|
|
86
|
+
usersToGroups: many(usersToGroups),
|
|
87
|
+
}))
|
|
88
|
+
|
|
89
|
+
// 조인 테이블
|
|
90
|
+
export const usersToGroups = pgTable('users_to_groups', {
|
|
91
|
+
userId: integer('user_id').notNull().references(() => users.id),
|
|
92
|
+
groupId: integer('group_id').notNull().references(() => groups.id),
|
|
93
|
+
}, (table) => [
|
|
94
|
+
primaryKey({ columns: [table.userId, table.groupId] }),
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
|
|
98
|
+
user: one(users, {
|
|
99
|
+
fields: [usersToGroups.userId],
|
|
100
|
+
references: [users.id],
|
|
101
|
+
}),
|
|
102
|
+
group: one(groups, {
|
|
103
|
+
fields: [usersToGroups.groupId],
|
|
104
|
+
references: [groups.id],
|
|
105
|
+
}),
|
|
106
|
+
}))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 관계 포함 조회
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// 1:N 관계 포함
|
|
113
|
+
const userWithPosts = await db.query.users.findFirst({
|
|
114
|
+
where: eq(users.id, 1),
|
|
115
|
+
with: {
|
|
116
|
+
posts: true,
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// 중첩 관계
|
|
121
|
+
const userWithAll = await db.query.users.findFirst({
|
|
122
|
+
where: eq(users.id, 1),
|
|
123
|
+
with: {
|
|
124
|
+
posts: {
|
|
125
|
+
with: {
|
|
126
|
+
comments: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// 관계 필터링 + 정렬
|
|
133
|
+
const result = await db.query.users.findFirst({
|
|
134
|
+
with: {
|
|
135
|
+
posts: {
|
|
136
|
+
where: eq(posts.published, true),
|
|
137
|
+
orderBy: desc(posts.createdAt),
|
|
138
|
+
limit: 5,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// M:N 관계
|
|
144
|
+
const userWithGroups = await db.query.users.findFirst({
|
|
145
|
+
where: eq(users.id, 1),
|
|
146
|
+
with: {
|
|
147
|
+
usersToGroups: {
|
|
148
|
+
with: {
|
|
149
|
+
group: true,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## SQL Join으로 조회
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Inner join
|
|
160
|
+
const result = await db.select({
|
|
161
|
+
userId: users.id,
|
|
162
|
+
userName: users.name,
|
|
163
|
+
postTitle: posts.title,
|
|
164
|
+
})
|
|
165
|
+
.from(users)
|
|
166
|
+
.innerJoin(posts, eq(users.id, posts.authorId))
|
|
167
|
+
.where(eq(users.id, 1))
|
|
168
|
+
|
|
169
|
+
// Left join + count
|
|
170
|
+
const result = await db.select({
|
|
171
|
+
userId: users.id,
|
|
172
|
+
userName: users.name,
|
|
173
|
+
postCount: count(posts.id),
|
|
174
|
+
})
|
|
175
|
+
.from(users)
|
|
176
|
+
.leftJoin(posts, eq(users.id, posts.authorId))
|
|
177
|
+
.groupBy(users.id)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 자기 참조 관계
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { type AnyPgColumn, pgTable, serial, text, integer } from 'drizzle-orm/pg-core'
|
|
184
|
+
|
|
185
|
+
// 사용자 (초대 관계)
|
|
186
|
+
export const users = pgTable('users', {
|
|
187
|
+
id: serial('id').primaryKey(),
|
|
188
|
+
name: text('name').notNull(),
|
|
189
|
+
invitedBy: integer('invited_by').references((): AnyPgColumn => users.id),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
export const usersRelations = relations(users, ({ one, many }) => ({
|
|
193
|
+
inviter: one(users, {
|
|
194
|
+
fields: [users.invitedBy],
|
|
195
|
+
references: [users.id],
|
|
196
|
+
relationName: 'inviter',
|
|
197
|
+
}),
|
|
198
|
+
invitees: many(users, {
|
|
199
|
+
relationName: 'inviter',
|
|
200
|
+
}),
|
|
201
|
+
}))
|
|
202
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Drizzle - 스키마 정의
|
|
2
|
+
|
|
3
|
+
## 필수 규칙
|
|
4
|
+
|
|
5
|
+
1. **파일별 분리** 구조 사용
|
|
6
|
+
2. **모든 요소에 한글 주석** (파일, 테이블, 컬럼)
|
|
7
|
+
|
|
8
|
+
## 구조
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/db/schema/
|
|
12
|
+
├── index.ts # export all
|
|
13
|
+
├── user.ts # User 테이블
|
|
14
|
+
├── post.ts # Post 테이블
|
|
15
|
+
└── enums.ts # Enum 정의
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 예시
|
|
19
|
+
|
|
20
|
+
### index.ts
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// 모든 스키마 export
|
|
24
|
+
export * from './user'
|
|
25
|
+
export * from './post'
|
|
26
|
+
export * from './enums'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### enums.ts
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { pgEnum } from 'drizzle-orm/pg-core'
|
|
33
|
+
|
|
34
|
+
// 사용자 역할
|
|
35
|
+
export const roleEnum = pgEnum('role', ['USER', 'ADMIN'])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### user.ts
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core'
|
|
42
|
+
import { relations } from 'drizzle-orm'
|
|
43
|
+
import { roleEnum } from './enums'
|
|
44
|
+
import { posts } from './post'
|
|
45
|
+
|
|
46
|
+
// 사용자 테이블
|
|
47
|
+
export const users = pgTable('users', {
|
|
48
|
+
id: serial('id').primaryKey(),
|
|
49
|
+
email: text('email').notNull().unique(), // 로그인 이메일
|
|
50
|
+
name: text('name'), // 표시 이름
|
|
51
|
+
role: roleEnum('role').default('USER'), // 사용자 역할
|
|
52
|
+
verified: boolean('verified').notNull().default(false),
|
|
53
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
54
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// 사용자 관계
|
|
58
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
59
|
+
posts: many(posts), // 작성 게시글 (1:N)
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// 타입 추론
|
|
63
|
+
export type User = typeof users.$inferSelect
|
|
64
|
+
export type NewUser = typeof users.$inferInsert
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### post.ts
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { pgTable, serial, text, integer, boolean, timestamp } from 'drizzle-orm/pg-core'
|
|
71
|
+
import { relations } from 'drizzle-orm'
|
|
72
|
+
import { users } from './user'
|
|
73
|
+
|
|
74
|
+
// 게시글 테이블
|
|
75
|
+
export const posts = pgTable('posts', {
|
|
76
|
+
id: serial('id').primaryKey(),
|
|
77
|
+
title: text('title').notNull(), // 제목
|
|
78
|
+
content: text('content'), // 본문
|
|
79
|
+
published: boolean('published').notNull().default(false),
|
|
80
|
+
authorId: integer('author_id').references(() => users.id), // 작성자
|
|
81
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
82
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 게시글 관계
|
|
86
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
87
|
+
author: one(users, {
|
|
88
|
+
fields: [posts.authorId],
|
|
89
|
+
references: [users.id],
|
|
90
|
+
}),
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
export type Post = typeof posts.$inferSelect
|
|
94
|
+
export type NewPost = typeof posts.$inferInsert
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 컬럼 타입 (PostgreSQL)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import {
|
|
101
|
+
serial, // auto-increment
|
|
102
|
+
integer, // 정수
|
|
103
|
+
bigint, // 큰 정수
|
|
104
|
+
text, // 문자열
|
|
105
|
+
varchar, // 길이 제한 문자열
|
|
106
|
+
boolean, // 불린
|
|
107
|
+
timestamp, // 날짜/시간
|
|
108
|
+
date, // 날짜
|
|
109
|
+
numeric, // 소수점
|
|
110
|
+
json, // JSON
|
|
111
|
+
jsonb, // JSONB
|
|
112
|
+
uuid, // UUID
|
|
113
|
+
} from 'drizzle-orm/pg-core'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 인덱스
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { pgTable, serial, text, index, uniqueIndex } from 'drizzle-orm/pg-core'
|
|
120
|
+
|
|
121
|
+
export const users = pgTable('users', {
|
|
122
|
+
id: serial('id').primaryKey(),
|
|
123
|
+
name: text('name'),
|
|
124
|
+
email: text('email').notNull(),
|
|
125
|
+
}, (table) => [
|
|
126
|
+
index('name_idx').on(table.name),
|
|
127
|
+
uniqueIndex('email_idx').on(table.email),
|
|
128
|
+
])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 복합 키
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { pgTable, integer, primaryKey } from 'drizzle-orm/pg-core'
|
|
135
|
+
|
|
136
|
+
export const usersToGroups = pgTable('users_to_groups', {
|
|
137
|
+
userId: integer('user_id').notNull(),
|
|
138
|
+
groupId: integer('group_id').notNull(),
|
|
139
|
+
}, (table) => [
|
|
140
|
+
primaryKey({ columns: [table.userId, table.groupId] }),
|
|
141
|
+
])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 테이블 매핑
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// snake_case 컬럼명 사용
|
|
148
|
+
export const users = pgTable('users', {
|
|
149
|
+
id: serial('id').primaryKey(),
|
|
150
|
+
firstName: text('first_name'), // DB: first_name, JS: firstName
|
|
151
|
+
lastName: text('last_name'),
|
|
152
|
+
createdAt: timestamp('created_at').defaultNow(),
|
|
153
|
+
})
|
|
154
|
+
```
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Drizzle - 설치 및 설정
|
|
2
|
+
|
|
3
|
+
## 설치
|
|
4
|
+
|
|
5
|
+
### PostgreSQL
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install drizzle-orm pg
|
|
9
|
+
npm install -D drizzle-kit @types/pg
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### MySQL
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install drizzle-orm mysql2
|
|
16
|
+
npm install -D drizzle-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### SQLite
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install drizzle-orm better-sqlite3
|
|
23
|
+
npm install -D drizzle-kit @types/better-sqlite3
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 폴더 구조
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
프로젝트/
|
|
32
|
+
├── drizzle.config.ts
|
|
33
|
+
├── drizzle/
|
|
34
|
+
│ └── migrations/
|
|
35
|
+
├── src/
|
|
36
|
+
│ ├── db/
|
|
37
|
+
│ │ └── schema/
|
|
38
|
+
│ │ ├── index.ts # export all
|
|
39
|
+
│ │ ├── user.ts
|
|
40
|
+
│ │ └── post.ts
|
|
41
|
+
│ └── lib/
|
|
42
|
+
│ └── db.ts # Drizzle client
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Drizzle Client
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// src/lib/db.ts
|
|
51
|
+
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
52
|
+
import * as schema from '@/db/schema'
|
|
53
|
+
|
|
54
|
+
export const db = drizzle(process.env.DATABASE_URL!, { schema })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## TanStack Start 연동
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// app/services/user.ts
|
|
63
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
64
|
+
import { db } from '@/lib/db'
|
|
65
|
+
import { users } from '@/db/schema'
|
|
66
|
+
import { eq } from 'drizzle-orm'
|
|
67
|
+
import { z } from 'zod'
|
|
68
|
+
|
|
69
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
70
|
+
.handler(async () => {
|
|
71
|
+
return db.select().from(users)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export const getUserById = createServerFn({ method: 'GET' })
|
|
75
|
+
.inputValidator(z.coerce.number())
|
|
76
|
+
.handler(async ({ data: id }) => {
|
|
77
|
+
const [user] = await db.select().from(users).where(eq(users.id, id))
|
|
78
|
+
return user
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
82
|
+
.inputValidator(z.object({ email: z.email(), name: z.string() }))
|
|
83
|
+
.handler(async ({ data }) => {
|
|
84
|
+
const [user] = await db.insert(users).values(data).returning()
|
|
85
|
+
return user
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 환경 변수
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# .env
|
|
95
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
96
|
+
```
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Drizzle - 트랜잭션
|
|
2
|
+
|
|
3
|
+
## 기본 트랜잭션
|
|
4
|
+
|
|
5
|
+
하나라도 실패하면 모두 롤백.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { db } from '@/lib/db'
|
|
9
|
+
import { users, accounts } from '@/db/schema'
|
|
10
|
+
import { eq, sql } from 'drizzle-orm'
|
|
11
|
+
|
|
12
|
+
await db.transaction(async (tx) => {
|
|
13
|
+
await tx.update(accounts)
|
|
14
|
+
.set({ balance: sql`${accounts.balance} - 100` })
|
|
15
|
+
.where(eq(accounts.userId, senderId))
|
|
16
|
+
|
|
17
|
+
await tx.update(accounts)
|
|
18
|
+
.set({ balance: sql`${accounts.balance} + 100` })
|
|
19
|
+
.where(eq(accounts.userId, recipientId))
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 반환값 사용
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const newBalance = await db.transaction(async (tx) => {
|
|
27
|
+
await tx.update(accounts)
|
|
28
|
+
.set({ balance: sql`${accounts.balance} - 100` })
|
|
29
|
+
.where(eq(accounts.userId, senderId))
|
|
30
|
+
|
|
31
|
+
await tx.update(accounts)
|
|
32
|
+
.set({ balance: sql`${accounts.balance} + 100` })
|
|
33
|
+
.where(eq(accounts.userId, recipientId))
|
|
34
|
+
|
|
35
|
+
const [account] = await tx.select({ balance: accounts.balance })
|
|
36
|
+
.from(accounts)
|
|
37
|
+
.where(eq(accounts.userId, senderId))
|
|
38
|
+
|
|
39
|
+
return account.balance
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 조건부 롤백
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
await db.transaction(async (tx) => {
|
|
47
|
+
const [account] = await tx.select({ balance: accounts.balance })
|
|
48
|
+
.from(accounts)
|
|
49
|
+
.where(eq(accounts.userId, senderId))
|
|
50
|
+
|
|
51
|
+
if (account.balance < 100) {
|
|
52
|
+
tx.rollback() // 예외 발생, 트랜잭션 롤백
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await tx.update(accounts)
|
|
56
|
+
.set({ balance: sql`${accounts.balance} - 100` })
|
|
57
|
+
.where(eq(accounts.userId, senderId))
|
|
58
|
+
|
|
59
|
+
await tx.update(accounts)
|
|
60
|
+
.set({ balance: sql`${accounts.balance} + 100` })
|
|
61
|
+
.where(eq(accounts.userId, recipientId))
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 에러 처리
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
try {
|
|
69
|
+
await db.transaction(async (tx) => {
|
|
70
|
+
const [user] = await tx.insert(users)
|
|
71
|
+
.values({ email: 'user@example.com' })
|
|
72
|
+
.returning()
|
|
73
|
+
|
|
74
|
+
if (someCondition) {
|
|
75
|
+
throw new Error('Rollback')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await tx.insert(posts)
|
|
79
|
+
.values({ title: 'Post', authorId: user.id })
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// 전체 롤백됨
|
|
83
|
+
console.error('Transaction failed:', error)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 중첩 트랜잭션 (PostgreSQL)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
await db.transaction(async (tx) => {
|
|
91
|
+
await tx.insert(users).values({ email: 'user1@example.com' })
|
|
92
|
+
|
|
93
|
+
await tx.transaction(async (tx2) => {
|
|
94
|
+
await tx2.insert(users).values({ email: 'user2@example.com' })
|
|
95
|
+
// 여기서 에러 발생해도 외부 트랜잭션은 영향 없음 (savepoint)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 복잡한 예시
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { db } from '@/lib/db'
|
|
104
|
+
import { users, posts, comments } from '@/db/schema'
|
|
105
|
+
import { eq } from 'drizzle-orm'
|
|
106
|
+
|
|
107
|
+
const createUserWithPost = async (userData: NewUser, postData: NewPost) => {
|
|
108
|
+
return db.transaction(async (tx) => {
|
|
109
|
+
// 사용자 생성
|
|
110
|
+
const [user] = await tx.insert(users).values(userData).returning()
|
|
111
|
+
|
|
112
|
+
// 게시글 생성
|
|
113
|
+
const [post] = await tx.insert(posts)
|
|
114
|
+
.values({ ...postData, authorId: user.id })
|
|
115
|
+
.returning()
|
|
116
|
+
|
|
117
|
+
// 사용자 + 게시글 반환
|
|
118
|
+
return { user, post }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 사용
|
|
123
|
+
const result = await createUserWithPost(
|
|
124
|
+
{ email: 'user@example.com', name: 'John' },
|
|
125
|
+
{ title: 'First Post', content: 'Hello World' }
|
|
126
|
+
)
|
|
127
|
+
```
|