Drizzle ORM入門:初心者ガイド

Drizzle ORM入門のガイドのカバー画像

今日の記事では、マイグレーションの作成と適用、テーブルスキーマの定義、そしてDrizzle ORMを使ってデータベース自体とどうやってやりとりするかについての情報をお伝えします。

導入

これまでも何度かDrizzleを使ったことはありましたが、新しいユーザーのためのガイドとなるような具体的な記事を書いたことはありませんでした。様々なトピックについて、例とドキュメントへのリンクを交えて紹介します。

この記事でカバーする内容

  • Drizzle Kitを使ったマイグレーションの設定
  • Drizzle ORMを用いたデータモデリング
  • テーブル間の関連付けの定義
  • インデックスと制約の定義
  • データベースとのやりとり

必要条件

Node.jsの基本的な知識があり、過去にORMやクエリビルダーを使ったことがあること。また、リレーショナルデータベースに関する基本的な知識。

はじめに

まず必要な依存関係をインストールする必要があります。

npm install drizzle-orm better-sqlite3

前のコマンドでお気付きのように、今日の記事ではSQLite方言を使用するので、プロセスが実行されている必要がなく、できるだけ多くの人が試せるようになっています。

また、開発環境のために次の依存関係もインストールする必要があります。

npm install --dev drizzle-kit @types/better-sqlite3

依存関係がインストールされれば、データベーススキーマへのパスやマイグレーションを生成するパスを定義するためにdrizzle設定に進むことができます。

// drizzle.config.ts
import type { Config } from "drizzle-kit";

export default {
  schema: "./schema.ts",
  out: "./migrations",
  driver: "better-sqlite",
  dbCredentials: {
    url: "./local.db",
  },
  verbose: true,
  strict: true,
} satisfies Config;

デフォルトのファイル名はdrizzle.config.tsですが、ファイル名が違う場合でも、drizzle-kitを実行する際には--config=フラグを指定してファイルパスの設定をしなければなりません。

テーブルスキーマ

これでデータベーステーブルのスキーマ定義に移ることができます。Drizzleには、各方言に特有のプリミティブのコレクションが含まれています。例えば次のテーブルを見てみましょう。

// schema.ts
import {
  sqliteTable,
  integer,
  text,
} from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  username: text("username").unique().notNull(),
});

上記のコードではusersというテーブルがあり、2つの列が含まれています。autoIncrementが可能なprimary keyのデータ型が整数のid列と、テキストデータ型のusername列です。username列は一意でnullにできません。

今度は、一対多の関係を持つ2番目のテーブルを作成しましょう。次のテーブルを見てください。

// schema.ts
import {
  unique,
  sqliteTable,
  integer,
  text,
} from "drizzle-orm/sqlite-core";

// ...

export const tasks = sqliteTable(
  "tasks",
  {
    id: integer("id").primaryKey({ autoIncrement: true }),
    name: text("name").notNull(),
    start: integer("start", { mode: "timestamp" }).notNull(),
    end: integer("end", { mode: "timestamp" }).notNull(),
    userId: integer("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
  });

上記のコードスニペットでは、tasksというテーブルがあり、次の5つの列があります。

  • id はプライマリキーです。
  • name はテキストデータ型で、nullにできません。
  • startend はどちらもタイムスタンプで、nullにはできません。
  • user_iduserを参照する外部キーです。

次のユースケースを考慮に入れましょう。

"私たちはstartend列を考慮に入れて頻繁にクエリを行うことがよくあります。また、これらの列はuser_idを考慮して一意でなければなりません。"

上記の段落で指摘されたケースを考慮して、startendの列にはインデックスが必要であり、またこれらの列とuser_idとの間に制約を作成して一意であることを保証するのが理想的です。次のようにします。

// schema.ts
import {
  unique,
  sqliteTable,
  integer,
  text,
} from "drizzle-orm/sqlite-core";

// ...

export const tasks = sqliteTable(
  "tasks",
  {
    id: integer("id").primaryKey({ autoIncrement: true }),
    name: text("name").notNull(),
    start: integer("start", { mode: "timestamp" }).notNull(),
    end: integer("end", { mode: "timestamp" }).notNull(),
    userId: integer("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
  },
  (table) => ({
    startIndex: index("start_index").on(table.start),
    endIndex: index("end_index").on(table.end),
    timeUniqueConstraint: unique("time_unique_constraint").on(
      table.start,
      table.end,
      table.userId
    ),
  })
);

外部キーの定義では、ユーザーが削除された場合にユーザーに関連するすべての行が削除されるように指定しました。

テーブル関連付け

両方のテーブルが定義されたので、次にそれらの間の関連付けを明示する必要があります。先ほど一対多の関係であると言及しましたが、次のように定義できます。

// schema.ts
import { relations } from "drizzle-orm";

// ...

export const userRelations = relations(users, ({ many }) => ({
  tasks: many(tasks),
}));

export const tasksRelations = relations(tasks, ({ one }) => ({
  user: one(users, {
    fields: [tasks.userId],
    references: [users.id],
  }),
}));

上記のコードスニペットでは、テーブル間の関連付けを指定し、それぞれのキーをどの列へマッピングするかを示しました。

usersテーブルには複数のtasksを含めることができますが、タスクは1人のユーザーにのみ関連付けられるべきです。このように、tasksテーブル内のstartenduser_idの列との制約も正式に定式化されました。

マイグレーション

データベースのテーブルが定義され、それらの間の関連付けが指定されたので、最初のマイグレーションを作成することができます。それには次のコマンドを実行するだけです。

npm run drizzle-kit generate:sqlite

上述したコマンドは、当初作成したdrizzle.config.tsファイルを考慮しており、ファイル名が異なる場合はこのコマンドで--config=フラグを指定しなければなりません。

期待される動作は、/migrationsというフォルダが新しく作成されたマイグレーションとともに作成されることです。

成功した場合は、作成されたマイグレーションを適用することができます。

npm run drizzle-kit push:sqlite

期待される動作は、適用されるマイグレーションが端末に表示され、これら同じ変更を適用するかどうかを尋ねるプロンプトが出ることです。このため、ドリズル構成ファイルにverboseおよびstrictプロパティが追加されました。

データベースクライアント

データベースにマイグレーションが取り入れられたので、データベースクライアントを作成する次のステップに進むことができます。それは次のようなものになります。

// db.ts
import {
  drizzle,
  type BetterSQLite3Database,
} from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";

import * as schema from "./schema";

const sqlite = new Database("local.db");

export const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {
  schema,
});

上記のコードスニペットでは、データベースのスキーマをインポートすることが強調されており、テキストエディタでIntelliSenseを使用することができます。この最後の点の利点をすぐに感じるでしょう。

.insert()メソッドを使用することで、新しい行を追加したいテーブルを定義し、.values()メソッドを使用して挿入されるデータを定義できます。登録したいデータは、ただ一つの行を追加したい場合はObject、複数の行を追加したい場合はArrayになります。

// 一行の場合
await db.insert(users).values({ username: "Foo" });

// 複数行の場合
await db
  .insert(users)
  .values([
    { username: "Bar" },
    { username: "Baz" }
  ]);

上の例では、Promiseから返されるのは変更された行などのいくつかのメタデータだけです。挿入された行のデータを返したい場合は、.returning()メソッドを使用することができます。

await db
  .insert(users)
  .values({ username: "Foo" })
  .returning();

usersテーブルのスキーマに従うと、Fooという名前のユーザーの挿入に関するエラーが生じると予想されます。これは前の例ですでに追加されたからです。

この場合、.onConflictDoNothing()メソッドを使用して、競合が発生してもエラーが投げられないようにすることができます。これは、usernameが一意であるべきと指定されたからです。

await db
  .insert(users)
  .values({ username: "Foo" })
  .onConflictDoNothing();

特定のユーザーを更新したい場合は、.update()メソッドを使用して、どのテーブルで更新を行うべきかを指定できます。.set()メソッドを利用してどの列を変更するべきかと、.where()メソッドを利用してどの行かを定義すると、次のようになります。

await db
  .update(users)
  .set({ username: "Baz" })
  .where(eq(users.username, "Buzz Lightyear"))
  .returning();

一方で、行を削除したい場合は、.delete()メソッドを利用して、どのテーブルでこの操作を行うべきかを指定する必要があります。

また、.where()メソッドを使用しない場合、テーブルのすべての行が削除される点に注意が必要です。

// 一行を削除
await db.delete(users).where(eq(users.username, "Bar");

// テーブルをクリア
await db.delete(users);

テーブルのすべての行を取得するには、次の方法があります。

// SQLのような方法(最も一般的な)
await db.select().from(users);

// Drizzleクエリ
await db.query.users.findMany();

上記のコードスニペットでは、メソッドを連結して、どの列を選択すべきか(上記の例ではすべて)、どのテーブルで行うべきかを指定することができます。一方、2番目のアプローチは他のORMと非常に似た体験を提供します。

今後のクエリの例で、特定の列とデータを考慮に入れて行を取得したい場合は、次のようにします。

await db.query.users.findFirst({
  where: (user, { eq }) => eq(user.username, "Bar"),
});

他のテーブルの関連からデータもクエリで取得できるという点も興味深いです。たとえば、userとそのtasksを取得することができます。

await db.query.users.findFirst({
  where: (user, { eq }) => eq(user.username, "Bar"),
  with: {
    tasks: true,
  }
});

より細かいことをしたい場合、クエリでどのusersテーブルとtasksテーブルのどの列を返すかを選択できます。次のようにします。

await db.query.users.findFirst({
  where: (user, { eq<br><br>こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。<br>[https://dev.to/franciscomendes10866/getting-started-with-drizzle-orm-a-beginners-tutorial-4782](https://dev.to/franciscomendes10866/getting-started-with-drizzle-orm-a-beginners-tutorial-4782)