[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
---
|
||||
id: db-sql-builder-vs-orm
|
||||
title: SQL Builder vs ORM — Drizzle / Kysely / Prisma
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [database, orm, sql-builder, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [SQL builder, Kysely, Drizzle, Prisma, raw SQL, query builder, type-safe SQL]
|
||||
---
|
||||
|
||||
# SQL Builder vs ORM
|
||||
|
||||
> ORM = object-relational mapping. SQL Builder = type-safe SQL. **Drizzle / Kysely (modern), Prisma (popular but binary)**. Raw SQL 도 valid.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- ORM: object → SQL.
|
||||
- Builder: type-safe SQL string.
|
||||
- Raw: 직접 SQL.
|
||||
- Type-safe: TS 가 schema → query type.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Raw SQL (가장 단순)
|
||||
```ts
|
||||
import postgres from 'postgres';
|
||||
const sql = postgres(url);
|
||||
|
||||
const users = await sql<User[]>`
|
||||
SELECT id, email FROM users WHERE created_at > ${since}
|
||||
`;
|
||||
|
||||
// Insert
|
||||
await sql`INSERT INTO users ${sql({ email, name })}`;
|
||||
|
||||
// Update
|
||||
await sql`UPDATE users SET email = ${email} WHERE id = ${id}`;
|
||||
```
|
||||
|
||||
→ 가장 빠름. Type-safety 약함 (manual generic).
|
||||
|
||||
### Drizzle (modern, 가장 인기)
|
||||
```ts
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { eq, and, gt } from 'drizzle-orm';
|
||||
|
||||
// Schema
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
email: text('email').unique().notNull(),
|
||||
name: text('name'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
|
||||
// Query
|
||||
const db = drizzle(sql);
|
||||
|
||||
// Select
|
||||
const allUsers = await db.select().from(users).where(eq(users.email, 'a@b.com'));
|
||||
|
||||
const recent = await db
|
||||
.select({ id: users.id, email: users.email })
|
||||
.from(users)
|
||||
.where(and(
|
||||
gt(users.createdAt, lastWeek),
|
||||
eq(users.deleted, false)
|
||||
))
|
||||
.orderBy(desc(users.createdAt))
|
||||
.limit(20);
|
||||
|
||||
// Insert
|
||||
const [user] = await db.insert(users).values({ email, name }).returning();
|
||||
|
||||
// Update
|
||||
await db.update(users).set({ email: newEmail }).where(eq(users.id, id));
|
||||
|
||||
// Delete
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
```
|
||||
|
||||
→ SQL-style + TS type-safe.
|
||||
|
||||
### Drizzle joins
|
||||
```ts
|
||||
const result = await db
|
||||
.select({
|
||||
userId: users.id,
|
||||
email: users.email,
|
||||
orderTotal: sum(orders.amount),
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(orders, eq(orders.userId, users.id))
|
||||
.groupBy(users.id, users.email);
|
||||
```
|
||||
|
||||
### Drizzle relations (eager load)
|
||||
```ts
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
orders: many(orders),
|
||||
}));
|
||||
|
||||
export const ordersRelations = relations(orders, ({ one }) => ({
|
||||
user: one(users, { fields: [orders.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
// Use
|
||||
const usersWithOrders = await db.query.users.findMany({
|
||||
with: { orders: true },
|
||||
where: eq(users.id, id),
|
||||
});
|
||||
```
|
||||
|
||||
→ N+1 자동 처리.
|
||||
|
||||
### Kysely (pure builder)
|
||||
```ts
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
|
||||
interface DB {
|
||||
users: { id: string; email: string; name: string | null };
|
||||
orders: { id: string; user_id: string; amount: number };
|
||||
}
|
||||
|
||||
const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool }) });
|
||||
|
||||
const users = await db
|
||||
.selectFrom('users')
|
||||
.where('email', '=', 'a@b.com')
|
||||
.select(['id', 'email'])
|
||||
.execute();
|
||||
|
||||
await db
|
||||
.insertInto('users')
|
||||
.values({ id: uuid(), email, name })
|
||||
.execute();
|
||||
```
|
||||
|
||||
→ 더 SQL-like. Schema 직접 정의.
|
||||
|
||||
### Kysely codegen (DB → types)
|
||||
```bash
|
||||
npx kysely-codegen --connection-string $DATABASE_URL
|
||||
```
|
||||
|
||||
→ DB schema 에서 자동 type generate.
|
||||
|
||||
### Prisma (전통적 ORM)
|
||||
```prisma
|
||||
// schema.prisma
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
name String?
|
||||
orders Order[]
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
amount Decimal
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 강력 + intuitive
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { orders: true },
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { email: newEmail },
|
||||
});
|
||||
```
|
||||
|
||||
→ Pros: 친숙. Cons: binary (rust query engine), Edge runtime 어려움.
|
||||
|
||||
### TypeORM (legacy)
|
||||
```ts
|
||||
@Entity()
|
||||
class User {
|
||||
@PrimaryGeneratedColumn('uuid') id!: string;
|
||||
@Column({ unique: true }) email!: string;
|
||||
}
|
||||
|
||||
const users = await User.find({ where: { email: '...' } });
|
||||
```
|
||||
|
||||
→ Java Hibernate 비슷. 새 프로젝트 권장 X.
|
||||
|
||||
### MikroORM (modern OO)
|
||||
```ts
|
||||
@Entity()
|
||||
class User {
|
||||
@PrimaryKey() id!: string;
|
||||
@Property() email!: string;
|
||||
}
|
||||
|
||||
const em = orm.em.fork();
|
||||
const user = await em.findOne(User, { email: '...' });
|
||||
user.email = 'new@email.com';
|
||||
await em.flush(); // 자동 dirty tracking
|
||||
```
|
||||
|
||||
→ Hibernate-like. Strong unit-of-work.
|
||||
|
||||
### Bun:sql (modern, fast)
|
||||
```ts
|
||||
import { sql } from 'bun';
|
||||
|
||||
const users = await sql`SELECT * FROM users WHERE id = ${id}`;
|
||||
```
|
||||
|
||||
→ Tagged template. Built-in.
|
||||
|
||||
### Comparison
|
||||
```
|
||||
Drizzle:
|
||||
+ Type-safe, SQL-style
|
||||
+ Edge friendly
|
||||
+ 작은 bundle
|
||||
- Schema 직접 정의
|
||||
|
||||
Kysely:
|
||||
+ Pure builder, no migration
|
||||
+ DB → type 자동
|
||||
- 더 verbose
|
||||
|
||||
Prisma:
|
||||
+ 친숙 / intuitive
|
||||
+ 강력 docs
|
||||
- Binary engine
|
||||
- Edge 어려움
|
||||
|
||||
Raw SQL:
|
||||
+ 가장 빠름
|
||||
+ Full SQL power
|
||||
- Manual type
|
||||
- Manual escape
|
||||
|
||||
MikroORM:
|
||||
+ Java-style
|
||||
+ Strong unit-of-work
|
||||
- Smaller community
|
||||
```
|
||||
|
||||
### Migration
|
||||
```ts
|
||||
// Drizzle Kit
|
||||
npx drizzle-kit generate // SQL 파일 generate
|
||||
npx drizzle-kit migrate // 실행
|
||||
|
||||
// Prisma Migrate
|
||||
npx prisma migrate dev
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
### Connection pooling (위 [[Backend_Connection_Handling]])
|
||||
```ts
|
||||
import { Pool } from 'pg';
|
||||
const pool = new Pool({ connectionString, max: 20 });
|
||||
|
||||
// Drizzle
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
const db = drizzle(pool);
|
||||
|
||||
// Kysely
|
||||
const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool }) });
|
||||
|
||||
// Prisma
|
||||
// Auto pool — connection_limit URL param
|
||||
```
|
||||
|
||||
### Edge runtime
|
||||
```ts
|
||||
// Drizzle + Neon HTTP (edge)
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-http';
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
const db = drizzle(sql);
|
||||
|
||||
// Cloudflare Workers + D1
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
const db = drizzle(env.DB);
|
||||
|
||||
// Prisma — driver adapter
|
||||
import { PrismaNeon } from '@prisma/adapter-neon';
|
||||
const adapter = new PrismaNeon(neonClient);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
```
|
||||
|
||||
### Transaction
|
||||
```ts
|
||||
// Drizzle
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(users).values(...);
|
||||
await tx.insert(orders).values(...);
|
||||
});
|
||||
|
||||
// Kysely
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.insertInto('users').values(...).execute();
|
||||
await trx.insertInto('orders').values(...).execute();
|
||||
});
|
||||
|
||||
// Prisma
|
||||
await prisma.$transaction([
|
||||
prisma.user.create({ data }),
|
||||
prisma.order.create({ data }),
|
||||
]);
|
||||
|
||||
// 또는 interactive
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create(...);
|
||||
await tx.order.create({ data: { userId: user.id, ... } });
|
||||
});
|
||||
```
|
||||
|
||||
### Raw SQL (escape hatch)
|
||||
```ts
|
||||
// Drizzle
|
||||
import { sql } from 'drizzle-orm';
|
||||
await db.execute(sql`UPDATE users SET balance = balance + ${amount}`);
|
||||
|
||||
// Kysely
|
||||
await sql`UPDATE users SET balance = balance + ${amount}`.execute(db);
|
||||
|
||||
// Prisma
|
||||
await prisma.$queryRaw`SELECT * FROM users WHERE balance > ${threshold}`;
|
||||
await prisma.$executeRaw`UPDATE users SET balance = ${val}`;
|
||||
```
|
||||
|
||||
### Type generation
|
||||
```ts
|
||||
// Drizzle — schema 가 truth
|
||||
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||
|
||||
type User = InferSelectModel<typeof users>;
|
||||
type NewUser = InferInsertModel<typeof users>;
|
||||
|
||||
// Kysely — DB schema 가 truth
|
||||
import type { Selectable, Insertable, Updateable } from 'kysely';
|
||||
|
||||
type User = Selectable<DB['users']>;
|
||||
type NewUser = Insertable<DB['users']>;
|
||||
```
|
||||
|
||||
### Schema migration
|
||||
```bash
|
||||
# Drizzle
|
||||
npx drizzle-kit generate # SQL diff
|
||||
npx drizzle-kit migrate
|
||||
|
||||
# Prisma
|
||||
npx prisma migrate dev --name add_email_index
|
||||
|
||||
# Kysely
|
||||
# Custom — kysely-migration-cli 등
|
||||
```
|
||||
|
||||
### Performance
|
||||
```
|
||||
Raw SQL: 가장 빠름.
|
||||
Bun:sql: raw 비슷.
|
||||
Drizzle: raw 와 거의 같음.
|
||||
Kysely: raw 와 거의 같음.
|
||||
Prisma: 5-20% slower (engine overhead).
|
||||
TypeORM: Variable.
|
||||
|
||||
→ 차이는 주로 미세. DB query 가 dominant.
|
||||
```
|
||||
|
||||
### Bundle
|
||||
```
|
||||
Raw SQL (postgres-js): ~30 KB
|
||||
Drizzle: ~40 KB
|
||||
Kysely: ~70 KB
|
||||
Prisma client: 10+ MB (binary engine)
|
||||
|
||||
→ Edge / lambda = Drizzle / Kysely.
|
||||
```
|
||||
|
||||
### When to choose
|
||||
```
|
||||
Drizzle:
|
||||
- New project + edge runtime
|
||||
- 빠른 + type-safe
|
||||
- SQL-style
|
||||
|
||||
Kysely:
|
||||
- Pure builder
|
||||
- 기존 DB → schema 자동
|
||||
- 빠른 dev
|
||||
|
||||
Prisma:
|
||||
- Familiar / 큰 team
|
||||
- Migration 강력
|
||||
- Edge 안 critical
|
||||
|
||||
Raw SQL:
|
||||
- Performance critical
|
||||
- 작은 query 수
|
||||
- 직접 control
|
||||
```
|
||||
|
||||
### N+1 detection
|
||||
```ts
|
||||
// Drizzle relations
|
||||
const usersWithOrders = await db.query.users.findMany({ with: { orders: true } });
|
||||
|
||||
// Without relations = N+1
|
||||
const users = await db.select().from(users);
|
||||
for (const u of users) {
|
||||
const orders = await db.select().from(orders).where(eq(orders.userId, u.id)); // N+1
|
||||
}
|
||||
```
|
||||
|
||||
→ Always relations / joins.
|
||||
|
||||
### DataLoader (GraphQL)
|
||||
```ts
|
||||
import DataLoader from 'dataloader';
|
||||
|
||||
const userLoader = new DataLoader(async (userIds: string[]) => {
|
||||
const users = await db.select().from(usersTable).where(inArray(usersTable.id, userIds));
|
||||
return userIds.map(id => users.find(u => u.id === id));
|
||||
});
|
||||
|
||||
// 자동 batch
|
||||
const a = await userLoader.load('1');
|
||||
const b = await userLoader.load('2');
|
||||
// 1 query (batched).
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Modern + edge | Drizzle |
|
||||
| 기존 DB 점진 도입 | Kysely |
|
||||
| 친숙 / quick start | Prisma |
|
||||
| Performance critical | Raw SQL |
|
||||
| Java background | MikroORM |
|
||||
| 단순 / 작음 | Bun:sql |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Raw SQL + escape 안 함**: SQL injection.
|
||||
- **모든 join 직접 multiple query**: N+1.
|
||||
- **ORM 의 lazy load 가정**: extra query.
|
||||
- **Type generate 무 manual**: drift.
|
||||
- **Big binary (Prisma) on edge**: 안 됨.
|
||||
- **Migration 없는 schema 변경**: drift.
|
||||
- **Connection pool 무**: 매 query 가 connect.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 새 = Drizzle.
|
||||
- 기존 DB = Kysely.
|
||||
- 친숙 + serverful = Prisma.
|
||||
- Raw SQL 도 OK.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[DB_ORM_Comparison]]
|
||||
- [[Backend_Connection_Handling]]
|
||||
- [[DB_Migration_Safety]]
|
||||
Reference in New Issue
Block a user