Astro กับ Drizzle ORM: Type-safe Database
#astro13 เม.ย. 2569
Astro กับ Drizzle ORM: Type-safe Database
Drizzle ORM เป็น TypeScript ORM รุ่นใหม่ที่กำลังได้รับความนิยมอย่างรวดเร็ว ด้วยแนวคิด SQL-first และ type-safety ที่สมบูรณ์แบบ Drizzle เหมาะอย่างยิ่งสำหรับการใช้งานกับ Astro โดยเฉพาะบน Edge environments
ทำไมต้องใช้ Drizzle ORM?
- Lightweight: ขนาดเล็กกว่า Prisma มาก เหมาะกับ Edge
- SQL-first: เขียน queries ที่ใกล้เคียง SQL จริงๆ
- Type-safe: TypeScript types ที่ infer จาก schema
- Edge Compatible: ทำงานได้บน Cloudflare Workers, Vercel Edge
- No Code Generation: ไม่ต้อง generate client
การติดตั้ง
สำหรับ PostgreSQL:
npm install drizzle-orm postgres
npm install -D drizzle-kit
สำหรับ SQLite (Cloudflare D1):
npm install drizzle-orm
npm install -D drizzle-kit
กำหนด Schema
สร้าง src/db/schema.ts:
import {
pgTable,
text,
integer,
boolean,
timestamp,
varchar,
serial,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
role: varchar('role', { length: 20 }).notNull().default('user'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
slug: varchar('slug', { length: 255 }).notNull().unique(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id').notNull(),
viewCount: integer('view_count').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 50 }).notNull().unique(),
});
export const postTags = pgTable('post_tags', {
postId: integer('post_id').notNull(),
tagId: integer('tag_id').notNull(),
});
// Relations
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
postTags: many(postTags),
}));
export const postTagsRelations = relations(postTags, ({ one }) => ({
post: one(posts, {
fields: [postTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postTags.tagId],
references: [tags.id],
}),
}));
สร้าง Database Connection
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL!;
// สำหรับ migrations
export const migrationClient = postgres(connectionString, { max: 1 });
// สำหรับ queries
const queryClient = postgres(connectionString);
export const db = drizzle(queryClient, { schema });
CRUD Operations
// src/lib/posts.ts
import { db } from '../db';
import { posts, users, postTags, tags } from '../db/schema';
import { eq, desc, like, and, sql } from 'drizzle-orm';
export async function getAllPosts(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const result = await db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
published: posts.published,
viewCount: posts.viewCount,
createdAt: posts.createdAt,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(limit)
.offset(offset);
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(posts)
.where(eq(posts.published, true));
return { posts: result, total: count };
}
export async function getPostBySlug(slug: string) {
const [post] = await db
.select()
.from(posts)
.where(and(eq(posts.slug, slug), eq(posts.published, true)))
.limit(1);
return post ?? null;
}
export async function createPost(data: {
title: string;
slug: string;
content: string;
authorId: number;
tagIds?: number[];
}) {
const [post] = await db
.insert(posts)
.values({
title: data.title,
slug: data.slug,
content: data.content,
authorId: data.authorId,
})
.returning();
if (data.tagIds?.length) {
await db.insert(postTags).values(
data.tagIds.map((tagId) => ({ postId: post.id, tagId }))
);
}
return post;
}
export async function incrementViewCount(id: number) {
await db
.update(posts)
.set({ viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, id));
}
ใช้งานใน Astro API Routes
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
import { getAllPosts, createPost } from '../../lib/posts';
export const GET: APIRoute = async ({ url }) => {
const page = parseInt(url.searchParams.get('page') ?? '1');
const limit = parseInt(url.searchParams.get('limit') ?? '10');
const data = await getAllPosts(page, limit);
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
const post = await createPost(body);
return new Response(JSON.stringify(post), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
};
Drizzle กับ Cloudflare D1
สำหรับ Cloudflare D1 (SQLite on Edge):
// src/db/d1.ts
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema-sqlite';
export function getDb(d1: D1Database) {
return drizzle(d1, { schema });
}
Schema สำหรับ SQLite:
// src/db/schema-sqlite.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
published: integer('published', { mode: 'boolean' }).default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
ใช้งานใน Astro กับ Cloudflare:
---
import { getDb } from '../../db/d1';
import { posts } from '../../db/schema-sqlite';
import { eq } from 'drizzle-orm';
const runtime = Astro.locals.runtime;
const db = getDb(runtime.env.DB);
const allPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true));
---
<ul>
{allPosts.map(post => <li>{post.title}</li>)}
</ul>
Drizzle Kit Migrations
สร้าง drizzle.config.ts:
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
# สร้าง migration files
npx drizzle-kit generate:pg
# Apply migrations
npx drizzle-kit push:pg
# เปิด Drizzle Studio
npx drizzle-kit studio
สรุป
Drizzle ORM เป็นตัวเลือกที่ยอดเยี่ยมสำหรับ Astro โดยเฉพาะเมื่อ deploy บน Edge environments ด้วยขนาดที่เล็ก, type-safety ที่สมบูรณ์, และ SQL-first approach ทำให้ developer มีความยืดหยุ่นสูงในการเขียน queries ที่ซับซ้อน