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 ที่ซับซ้อน