drizzle ์ํฌํธ ๊ฒฝ๋ก๋ ์ฌ์ฉ ์ค์ธ **๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ผ์ด๋ฒ**์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค.
Drizzle ์ฟผ๋ฆฌ
Drizzle ORM์ SQL ์์ ์์ ํ์ ๊ณ์ธต์ผ๋ก ์ค๊ณ๋์์ต๋๋ค. ์ฐ๋ฆฌ๋ TypeScript์์ SQL ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ด์ํ๋ ์ต๊ณ ์ ๋ฐฉ๋ฒ์ ์ค๊ณํ๋ค๊ณ ์ง์ฌ์ผ๋ก ๋ฏฟ์ผ๋ฉฐ, ์ด๋ฅผ ๋์ฑ ๊ฐ์ ํ ๋์ ๋๋ค.
๊ด๊ณํ ์ฟผ๋ฆฌ๋ ์ฌ๋ฌ ์กฐ์ธ๊ณผ ๋ณต์กํ ๋ฐ์ดํฐ ๋งคํ์ ํผํ๋ฉด์ SQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ค์ฒฉ๋ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฅผ ์ฟผ๋ฆฌํ ์ ์๋ ํ๋ฅญํ ๊ฐ๋ฐ์ ๊ฒฝํ์ ์ ๊ณตํ๊ธฐ ์ํ ๊ฒ์ ๋๋ค.
์ด๊ฒ์ ๊ธฐ์กด ์คํค๋ง ์ ์ ๋ฐ ์ฟผ๋ฆฌ ๋น๋์ ํ์ฅ์ ๋๋ค. ํ์์ ๋ฐ๋ผ ์ ํ์ ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ต๊ณ ์์ค์ ๊ฐ๋ฐ์ ๊ฒฝํ๊ณผ ์ฑ๋ฅ์ ๋ชจ๋ ๋ณด์ฅํฉ๋๋ค.
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle({ schema });
const result = await 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,
}
]
}]โ ๏ธ ์ฌ๋ฌ ํ์ผ์ SQL ์คํค๋ง๋ฅผ ์ ์ธํ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ํ ์ ์์ต๋๋ค
import * as schema1 from './schema1';
import * as schema2 from './schema2';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle({ schema: { ...schema1, ...schema2 } });
const result = await db.query.users.findMany({
with: {
posts: true
},
});๋ชจ๋
Drizzle ๊ด๊ณํ ์ฟผ๋ฆฌ๋ ํญ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์คํํ ์ ํํ ํ๋์ SQL ๋ฌธ์ ์์ฑํ๋ฉฐ, ๋ช ๊ฐ์ง ์ฃผ์์ฌํญ์ด ์์ต๋๋ค.
๋ชจ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ํ ์ต๊ณ ์์ค์ ์ง์์ ์ ๊ณตํ๊ธฐ ์ํด **๋ชจ๋**๋ฅผ ๋์
ํ์ต๋๋ค.
Drizzle ๊ด๊ณํ ์ฟผ๋ฆฌ๋ ๋ด๋ถ์ ์ผ๋ก ํ์ ์ฟผ๋ฆฌ์ ์ธก๋ฉด ์กฐ์ธ(lateral joins)์ ์ฌ์ฉํ๋ฉฐ, ํ์ฌ PlanetScale์ ์ด๋ฅผ ์ง์ํ์ง ์์ต๋๋ค.
์ผ๋ฐ MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํจ๊ป mysql2 ๋๋ผ์ด๋ฒ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ โ mode: "default"๋ฅผ ์ง์ ํด์ผ ํฉ๋๋ค
PlanetScale๊ณผ ํจ๊ป mysql2 ๋๋ผ์ด๋ฒ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ โ mode: "planetscale"์ ์ง์ ํด์ผ ํฉ๋๋ค
import * as schema from './schema';
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
const connection = await mysql.createConnection({
uri: process.env.PLANETSCALE_DATABASE_URL,
});
const db = drizzle({ client: connection, schema, mode: 'planetscale' });์ฟผ๋ฆฌํ๊ธฐ
๊ด๊ณํ ์ฟผ๋ฆฌ๋ Drizzle์ ์๋ **์ฟผ๋ฆฌ ๋น๋**์ ํ์ฅ์
๋๋ค.
์คํค๋ง ํ์ผ์์ ๋ชจ๋ tables์ relations๋ฅผ drizzle() ์ด๊ธฐํ ์ ์ ๊ณตํ๊ณ
db.query API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle({ schema });
await db.query.users.findMany(...);// if you have schema in multiple files
import * as schema1 from './schema1';
import * as schema2 from './schema2';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle({ schema: { ...schema1, ...schema2 } });
await db.query.users.findMany(...);Drizzle๋ .findMany() ๋ฐ .findFirst() API๋ฅผ ์ ๊ณตํฉ๋๋ค.
์ฌ๋ฌ ๊ฐ ์ฐพ๊ธฐ
const users = await db.query.users.findMany();// result type
const result: {
id: number;
name: string;
verified: boolean;
invitedBy: number | null;
}[];์ฒซ ๋ฒ์งธ ์ฐพ๊ธฐ
.findFirst()๋ ์ฟผ๋ฆฌ์ limit 1์ ์ถ๊ฐํฉ๋๋ค.
const user = await db.query.users.findFirst();// result type
const result: {
id: number;
name: string;
verified: boolean;
invitedBy: number | null;
};๊ด๊ณ ํฌํจํ๊ธฐ
With ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๊ด๋ จ ํ
์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฒฐํฉํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ ์ ํ ์ง๊ณํ ์ ์์ต๋๋ค.
๋๊ธ๊ณผ ํจ๊ป ๋ชจ๋ ๊ฒ์๋ฌผ ๊ฐ์ ธ์ค๊ธฐ:
const posts = await db.query.posts.findMany({
with: {
comments: true,
},
});๋๊ธ๊ณผ ํจ๊ป ์ฒซ ๋ฒ์งธ ๊ฒ์๋ฌผ ๊ฐ์ ธ์ค๊ธฐ:
const post = await db.query.posts.findFirst({
with: {
comments: true,
},
});ํ์ํ ๋งํผ ์ค์ฒฉ๋ with ๋ฌธ์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค.
์ค์ฒฉ๋ with ์ฟผ๋ฆฌ์ ๊ฒฝ์ฐ Drizzle์ Core Type API๋ฅผ ์ฌ์ฉํ์ฌ ํ์
์ ์ถ๋ก ํฉ๋๋ค.
๊ฒ์๋ฌผ๊ณผ ํจ๊ป ๋ชจ๋ ์ฌ์ฉ์ ๊ฐ์ ธ์ค๊ธฐ. ๊ฐ ๊ฒ์๋ฌผ์๋ ๋๊ธ ๋ชฉ๋ก์ด ํฌํจ๋์ด์ผ ํฉ๋๋ค:
const users = await db.query.users.findMany({
with: {
posts: {
with: {
comments: true,
},
},
},
});๋ถ๋ถ ํ๋ ์ ํ
columns ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฐ์ ธ์ฌ ์ปฌ๋ผ์ ํฌํจํ๊ฑฐ๋ ์๋ตํ ์ ์์ต๋๋ค.
Drizzle์ ์ฟผ๋ฆฌ ์์ค์์ ๋ถ๋ถ ์ ํ์ ์ํํ๋ฏ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ถ๊ฐ ๋ฐ์ดํฐ๊ฐ ์ ์ก๋์ง ์์ต๋๋ค.
Drizzle์ ๋จ์ผ SQL ๋ฌธ์ ์ถ๋ ฅํ๋ค๋ ์ ์ ๊ธฐ์ตํ์ธ์.
id, content๋ง ํฌํจํ๊ณ comments๋ ํฌํจํ๋ ๋ชจ๋ ๊ฒ์๋ฌผ ๊ฐ์ ธ์ค๊ธฐ:
const posts = await db.query.posts.findMany({
columns: {
id: true,
content: true,
},
with: {
comments: true,
}
});content ์์ด ๋ชจ๋ ๊ฒ์๋ฌผ ๊ฐ์ ธ์ค๊ธฐ:
const posts = await db.query.posts.findMany({
columns: {
content: false,
},
});true์ false ์ ํ ์ต์
์ด ๋ชจ๋ ์๋ ๊ฒฝ์ฐ ๋ชจ๋ false ์ต์
์ ๋ฌด์๋ฉ๋๋ค.
name ํ๋๋ฅผ ํฌํจํ๊ณ id ํ๋๋ฅผ ์ ์ธํ๋ฉด id ์ ์ธ๋ ์ค๋ณต๋ฉ๋๋ค.
์ด์ฐจํผ name์ ์ ์ธํ ๋ชจ๋ ํ๋๊ฐ ์ ์ธ๋ฉ๋๋ค.
๋์ผํ ์ฟผ๋ฆฌ์์ ํ๋ ์ ์ธ ๋ฐ ํฌํจ:
const users = await db.query.users.findMany({
columns: {
name: true,
id: false //ignored
},
});// result type
const users: {
name: string;
};์ค์ฒฉ๋ ๊ด๊ณ์ ์ปฌ๋ผ๋ง ํฌํจ:
const res = await db.query.users.findMany({
columns: {},
with: {
posts: true
}
});// result type
const res: {
posts: {
id: number,
text: string
}
}[];์ค์ฒฉ๋ ๋ถ๋ถ ํ๋ ์ ํ
**๋ถ๋ถ ์ ํ**๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์ค์ฒฉ๋ ๊ด๊ณ์ ์ปฌ๋ผ์ ํฌํจํ๊ฑฐ๋ ์ ์ธํ ์ ์์ต๋๋ค:
const posts = await db.query.posts.findMany({
columns: {
id: true,
content: true,
},
with: {
comments: {
columns: {
authorId: false
}
}
}
});์ ํ ํํฐ
SQL๊ณผ ์ ์ฌํ ์ฟผ๋ฆฌ ๋น๋์ ๋ง์ฐฌ๊ฐ์ง๋ก,
๊ด๊ณํ ์ฟผ๋ฆฌ API๋ฅผ ์ฌ์ฉํ๋ฉด ์ฐ์ฐ์ ๋ชฉ๋ก์ผ๋ก ํํฐ์ ์กฐ๊ฑด์ ์ ์ํ ์ ์์ต๋๋ค.
drizzle-orm์์ ๊ฐ์ ธ์ค๊ฑฐ๋ ์ฝ๋ฐฑ ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค:
import { eq } from 'drizzle-orm';
const users = await db.query.users.findMany({
where: eq(users.id, 1)
})const users = await db.query.users.findMany({
where: (users, { eq }) => eq(users.id, 1),
})ํน์ ๋ ์ง ์ด์ ์ ์์ฑ๋ ๋๊ธ๊ณผ ํจ๊ป id=1์ธ ๊ฒ์๋ฌผ ์ฐพ๊ธฐ:
await db.query.posts.findMany({
where: (posts, { eq }) => (eq(posts.id, 1)),
with: {
comments: {
where: (comments, { lt }) => lt(comments.createdAt, new Date()),
},
},
});Limit ๋ฐ Offset
Drizzle ORM์ ์ฟผ๋ฆฌ ๋ฐ ์ค์ฒฉ๋ ์ํฐํฐ์ ๋ํ limit ๋ฐ offset API๋ฅผ ์ ๊ณตํฉ๋๋ค.
5๊ฐ์ ๊ฒ์๋ฌผ ์ฐพ๊ธฐ:
await db.query.posts.findMany({
limit: 5,
});๊ฒ์๋ฌผ์ ์ฐพ๊ณ ์ต๋ 3๊ฐ์ ๋๊ธ ๊ฐ์ ธ์ค๊ธฐ:
await db.query.posts.findMany({
with: {
comments: {
limit: 3,
},
},
});offset์ ์ต์์ ์ฟผ๋ฆฌ์์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค.
await db.query.posts.findMany({
limit: 5,
offset: 2, // correct โ
with: {
comments: {
offset: 3, // incorrect โ
limit: 3,
},
},
});5๋ฒ์งธ๋ถํฐ 10๋ฒ์งธ ๊ฒ์๋ฌผ๊น์ง์ ๋๊ธ๊ณผ ํจ๊ป ๊ฒ์๋ฌผ ์ฐพ๊ธฐ:
await db.query.posts.findMany({
limit: 5,
offset: 5,
with: {
comments: true,
},
});Order By (์ ๋ ฌ)
Drizzle์ ๊ด๊ณํ ์ฟผ๋ฆฌ ๋น๋์์ ์ ๋ ฌ์ ์ํ API๋ฅผ ์ ๊ณตํฉ๋๋ค.
๋์ผํ ์ ๋ ฌ **core API**๋ฅผ ์ฌ์ฉํ๊ฑฐ๋
์ํฌํธ ์์ด ์ฝ๋ฐฑ์์ order by ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
import { desc, asc } from 'drizzle-orm';
await db.query.posts.findMany({
orderBy: [asc(posts.id)],
});await db.query.posts.findMany({
orderBy: (posts, { asc }) => [asc(posts.id)],
});asc + desc๋ก ์ ๋ ฌ:
await db.query.posts.findMany({
orderBy: (posts, { asc }) => [asc(posts.id)],
with: {
comments: {
orderBy: (comments, { desc }) => [desc(comments.id)],
},
},
});์ปค์คํ ํ๋ ํฌํจ
๊ด๊ณํ ์ฟผ๋ฆฌ API๋ฅผ ์ฌ์ฉํ๋ฉด ์ปค์คํ ์ถ๊ฐ ํ๋๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค. ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๊ณ ์ถ๊ฐ ํจ์๋ฅผ ์ ์ฉํด์ผ ํ ๋ ์ ์ฉํฉ๋๋ค.
ํ์ฌ extras์์๋ ์ง๊ณ๊ฐ ์ง์๋์ง ์์ต๋๋ค. ์ด๋ฅผ ์ํด์๋ **core queries**๋ฅผ ์ฌ์ฉํ์ธ์.
import { sql } from 'drizzle-orm';
await db.query.users.findMany({
extras: {
loweredName: sql`lower(${users.name})`.as('lowered_name'),
},
})await db.query.users.findMany({
extras: {
loweredName: (users, { sql }) => sql`lower(${users.name})`.as('lowered_name'),
},
})ํค๋ก์ lowerName์ ๋ฐํ๋ ๊ฐ์ฒด์ ๋ชจ๋ ํ๋์ ํฌํจ๋ฉ๋๋ค.
.as("<name_for_column>")๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์ ํด์ผ ํฉ๋๋ค
๊ทธ๋ฃน๊ณผ ํจ๊ป ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ๊ฒ์ํ๋ fullName ํ๋(firstName๊ณผ lastName์ ์ฐ๊ฒฐ)๋ฅผ ํฌํจํ๋ ค๋ฉด, Drizzle ๊ด๊ณํ ์ฟผ๋ฆฌ ๋น๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
const res = await db.query.users.findMany({
extras: {
fullName: sql<string>`concat(${users.name}, " ", ${users.name})`.as('full_name'),
},
with: {
usersToGroups: {
with: {
group: true,
},
},
},
});// result type
const res: {
id: number;
name: string;
verified: boolean;
invitedBy: number | null;
fullName: string;
usersToGroups: {
group: {
id: number;
name: string;
description: string | null;
};
}[];
}[];
๋๊ธ๊ณผ ํจ๊ป ๋ชจ๋ ๊ฒ์๋ฌผ์ ๊ฒ์ํ๊ณ ๊ฒ์๋ฌผ ๋ด์ฉ์ ํฌ๊ธฐ์ ๊ฐ ๋๊ธ ๋ด์ฉ์ ํฌ๊ธฐ๋ฅผ ๊ณ์ฐํ๋ ์ถ๊ฐ ํ๋๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด:
const res = await db.query.posts.findMany({
extras: (table, { sql }) => ({
contentLength: (sql<number>`length(${table.content})`).as('content_length'),
}),
with: {
comments: {
extras: {
commentSize: sql<number>`length(${comments.content})`.as('comment_size'),
},
},
},
});// result type
const res: {
id: number;
createdAt: Date;
content: string;
authorId: number | null;
contentLength: number;
comments: {
id: number;
createdAt: Date;
content: string;
creator: number | null;
postId: number | null;
commentSize: number;
}[];
};์ค๋น๋ ๋ฌธ(Prepared statements)
์ค๋น๋ ๋ฌธ์ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ ๋ํญ ํฅ์์ํค๋๋ก ์ค๊ณ๋์์ต๋๋ค โ ์ฌ๊ธฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด ์น์ ์์๋ Drizzle ๊ด๊ณํ ์ฟผ๋ฆฌ ๋น๋๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ ์ด์คํ๋๋ฅผ ์ ์ํ๊ณ ์ค๋น๋ ๋ฌธ์ ์คํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ธ ์ ์์ต๋๋ค.
where์ ํ๋ ์ด์คํ๋
const prepared = db.query.users.findMany({
where: ((users, { eq }) => eq(users.id, placeholder('id'))),
with: {
posts: {
where: ((users, { eq }) => eq(users.id, placeholder('pid'))),
},
},
}).prepare('query_name');
const usersWithPosts = await prepared.execute({ id: 1 });limit์ ํ๋ ์ด์คํ๋
const prepared = db.query.users.findMany({
with: {
posts: {
limit: placeholder('limit'),
},
},
}).prepare('query_name');
const usersWithPosts = await prepared.execute({ limit: 1 });offset์ ํ๋ ์ด์คํ๋
const prepared = db.query.users.findMany({
offset: placeholder('offset'),
with: {
posts: true,
},
}).prepare('query_name');
const usersWithPosts = await prepared.execute({ offset: 1 });์ฌ๋ฌ ํ๋ ์ด์คํ๋
const prepared = db.query.users.findMany({
limit: placeholder('uLimit'),
offset: placeholder('uOffset'),
where: ((users, { eq, or }) => or(eq(users.id, placeholder('id')), eq(users.id, 3))),
with: {
posts: {
where: ((users, { eq }) => eq(users.id, placeholder('pid'))),
limit: placeholder('pLimit'),
},
},
}).prepare('query_name');
const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 });