メインコンテンツまでスキップ

データベース

データ管理の限界

これまで作成してきたアプリケーションでは、次のように、データを全てNode.jsアプリケーション上の変数に記録していました。しかし、このような方法では、サーバーが終了するたびにデータが消えてしまいます。

const messages = [];
app.post((request, response) => {
messages.push(request.body.message);
// 省略
});

データをファイルに記録することはできますが、後述するような複数の問題があります。

import { writeFileSync } from "node:fs";
app.post((request, response) => {
writeFileSync("./messages.txt", request.body.message);
// 省略
});

ひとつは、複数のサーバー間でデータの共有ができないことです。Webアプリケーションの利用者が増えてくると、1台のサーバーではリクエストを処理しきれなくなります。このような場合、リクエストを複数のサーバーに分散させます。このとき、サーバー内に保存されているファイルは共有されないため、データに不整合が生じてしまいます。

複数のサーバーで負荷を分散する

また、データのサイズが大きくなってくると、データをファイルに保存することが難しくなってきます。これは、ファイルの読み書きは、変数の読み書きと比べ大幅に時間がかかるためです。高速なデータの読み書きを実現するためには、ファイルの読み書きが最小限になるよう、データの配置を工夫する必要があります。

データベースは、このようなデータに関する諸問題を解決するためのシステムです。

データベースが動作する仕組み

データベースは、通常サーバーとして動作します。つまり、データベースサーバーは、保持しているデータに対する参照や更新のためのリクエスト (クエリ) を受け、その結果をレスポンスとしてクライアントに返します。

データベースサーバーのクライアントは、通常Webサービスの使用者ではなく、皆さんがNode.jsなどで開発するサーバーです。これまで開発してきたようなサーバーを、データベースサーバーと対比してアプリケーションサーバーと呼びます。

データベースとアプリケーションサーバー

データベースの中でも、リレーショナルデータベースは、最も多く使われる種類のもので、データをExcelのような表形式でとらえます。次の図は、リレーショナルデータベースの基本的な概念である、テーブルカラムレコードについて整理した図です。リレーショナルデータベースを用いる一般的なアプリケーションでは、アプリケーション開発時にテーブルとカラムを作成しておき、ユーザーの操作に応じてレコードを追加・編集・削除していきます。

リレーショナルデータベース

リレーショナルデータベースに対するクエリは、通常SQLと呼ばれる言語を用いて記述します。データベースクライアントとして用いるライブラリによっては、SQLを直接用いることなく、そのライブラリが提供する専用の関数等を用いてデータベースに対してクエリを発行できることがあります。

データベースを用いるアプリケーション

ここでは、Node.jsのアプリケーションサーバーで、Drizzleと呼ばれるライブラリを用い、リレーショナルデータベースの一つであるPostgreSQLサーバーに保存されているデータを取得します。

使用する技術・サービス

PostgreSQL

現在最もよく用いられるリレーショナルデータベースのひとつです。豊富な機能を持ちます。

Drizzle ORM

リレーショナルデータベースを操作するためのNode.jsのライブラリです。複数の構成要素からなります。

  • drizzle-ormパッケージ: アプリケーションサーバーから用いるnpmのパッケージです。JavaScriptプログラムから使用します。
  • pgパッケージ: PostgreSQLサーバーに接続するためのドライバです。
  • drizzle-kitパッケージ: 開発時にコマンドとして用いるnpmのパッケージです。npxコマンドを通して実行します。テーブル構造のデータベースへの反映などに使用します。

Supabase

PostgreSQLサーバーを提供するサービスです。その他にデータベースを直感的に操作できる機能なども提供しています。PostgreSQLサーバーは皆さんのコンピューター上にも構築できますが、ここではその手間を省くため、外部のサービスを利用します。

Supabase で PostgreSQL サーバーを構築する

Supabaseのアカウントを作成しましょう。New Projectボタンを押して必要な情報を入力し、新しいPostgreSQLサーバーを起動させてください。入力が必要な情報は次の通りです。

  • Project name: 起動するサーバーにつける名前です。適当に設定して構いません。
  • Database Password: 起動するサーバーのパスワードです。Generate a passwordボタンを押して生成するのが良いでしょう。また、後でこのパスワードは使用することになるため覚えておきましょう。
  • Region: 起動するサーバーの地理的な場所です。ここではNortheast Asia (Tokyo)を選択しています。

この時点では、まだデータベース上にテーブルが作成されていません。Supabase上で作成することもできますが、今回はDrizzleを使用して作成することにします。

Drizzleでテーブル構造を作成する

新しいフォルダをVS Codeで開き、npm initコマンドを使用してpackage.jsonファイルを作成した後、必要なパッケージをインストールします。

npm install drizzle-orm pg
npm install -D drizzle-kit
npxコマンド

npxコマンドは、npmのパッケージを、プログラムからではなく直接実行するためのコマンドです。npmにはdrizzle-kitパッケージのように、直接実行専用のパッケージも存在します。

続いて、Supabaseからデータベースへの接続情報を.envファイルにコピーします。これにより、DrizzleがSupabase上のPostgreSQLサーバーと接続できるようになります。

環境変数

データベースへの接続情報は、プログラム内に直接記述するのではなく、環境変数を用いて指定することが一般的です。環境変数は、アプリケーションの実行時に、アプリケーション自体を変更することなく外側から動作を変更するために用いることができる仕組みで、キーと値の組み合わせによって定義されます。環境変数は、主に次のような情報をプログラム内に記述することを避けるために用いられます。

  • 機密情報
  • 環境ごとに異なる設定情報

アプリケーションの実行時に環境変数を指定するには、コマンドの前にKEY=VALUEの形式の文字列を記述します。例えば、ターミナル上で次のコマンドを実行すると、main.mjsでは、process.env.DATABASE_URLを通して環境変数DATABASE_URLの値を取得できます。

DATABASE_URL=postgresql://user:password@example.com:5432/db node main.mjs

.envファイルは、環境変数の指定を簡略化するために慣習的に用いられるファイルです。node --env-file=.env main.mjsのように指定することで、.envファイルに記述された環境変数を読み込ませることができます。Drizzleは.envファイルの内容を自動的には読み込まないため、Node.jsの実行時には--env-fileオプションを指定する必要があります。ただし、drizzle-kitのコマンドは.envファイルを自動的に読み込みます。

プロジェクトのルートディレクトリにschema.mjsファイルを作成し、次のようにToDoテーブルとそのカラムを定義します。

schema.mjs
import { pgTable, serial, text } from "drizzle-orm/pg-core";

export const todos = pgTable("Todo", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
});

また、drizzle.config.mjsファイルを作成し、Drizzle Kitの設定を記述します。

drizzle.config.mjs
import { defineConfig } from "drizzle-kit";

export default defineConfig({
schema: "./schema.mjs",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL,
},
});

完了したら、

npx drizzle-kit push

コマンドを実行しましょう。すると、データベースにschema.mjsに書かれた通りのテーブルとカラムが作成されるので、Supabaseから確認してみてください。

Drizzleが作成したテーブルにレコードを追加する

Drizzleが作成したテーブルに、レコードを追加しましょう。

DBeaverでPostgreSQLサーバーに接続する

今回はSupabaseを利用してPostgreSQLサーバーを構築したため、Supabaseの機能を使用してデータベースを操作しましたが、DBeaverも便利です。DBeaverは、多くのデータベースを直感的に操作できるソフトウェアで、PostgreSQLにも対応しています。

DBeaverをインストールした後、次のようにすることでDBeaverを利用してデータベースを操作することができます。

Drizzleでデータベースのデータを読み書きする

Node.jsからDrizzleを利用してデータベースのデータを操作するためには、drizzle-ormパッケージのdrizzle関数でデータベース接続を作成し、そのオブジェクトのメソッドを用います。

これらのメソッドは、非同期処理を行います。

まずは、selectメソッドの戻り値を、デバッガを用いて確認してみましょう。

import { drizzle } from "drizzle-orm/node-postgres";
import { todos } from "./schema.mjs";

const db = drizzle(process.env.DATABASE_URL);
const allTodos = await db.select().from(todos);
debugger;

selectの戻り値

drizzle関数の使い方

drizzle-ormパッケージのdrizzle関数にDATABASE_URL環境変数を渡すことで、データベースへの接続が作成されます。schema.mjsで定義したテーブルオブジェクトをselectinsertのメソッドに渡すことで、データベースを操作できます。

続いて、insertメソッドを用いて、テーブルにレコードを作成してみましょう。

import { drizzle } from "drizzle-orm/node-postgres";
import { todos } from "./schema.mjs";

const db = drizzle(process.env.DATABASE_URL);
const result = await db.insert(todos).values({ name: "買い物をする" }).returning();
debugger;

insertの戻り値

演習問題

PostgreSQLにデータを保存する掲示板サービスを作ってみましょう。

手順1

Supabaseで新しいデータベースを作成しましょう。

手順2

新しいプロジェクト用のディレクトリを作成し、npm initコマンドでpackage.jsonを作成した後、npm install drizzle-orm pgnpm install -D drizzle-kitコマンドを実行して、Drizzleのセットアップをしましょう。.envファイルを作成・編集し、Drizzleがデータベースに接続できるようにしましょう。

手順3

schema.mjsファイルとdrizzle.config.mjsファイルを作成し、掲示板に投稿されたメッセージを保存するためのテーブルと、そのテーブルのカラムの定義を記述しましょう。npx drizzle-kit pushコマンドでテーブルとカラムの定義をデータベースに反映させましょう。

テーブルの定義

掲示板サービスに必要なテーブルの構造を考えてみましょう。例えば、次の例では、掲示板の投稿を保存するためのPostテーブルを定義しており、このテーブルにはidmessageの2つのカラムが存在しています。他にも、投稿のタイトルを保存するためのtitleカラムや、投稿者名を保存するためのauthorカラムなどを定義するなどの工夫が考えられます。

schema.mjs の抜粋
import { pgTable, serial, text } from "drizzle-orm/pg-core";

export const posts = pgTable("Post", {
id: serial("id").primaryKey(),
message: text("message").notNull(),
});

手順4

掲示板の投稿のサンプルデータをデータベースに登録しましょう。

手順5

Node.jsのデバッガを用いて、データベースのデータがDrizzleで取得できることを確認しましょう。

ヒント

Drizzleのselectメソッドを用いて、テーブル内にある全てのレコードを取得できます。

const allPosts = await db.select().from(posts);
// [
// { id: 1, message: "おはようございます" },
// { id: 2, message: "こんにちは" },
// ]

このメソッドの戻り値は、各カラムの値をプロパティとして持つオブジェクトの配列です。

手順6

Expressをインストールし、/postsへのGETリクエストに対して、データベースのデータをJSON形式のレスポンスで返せるようにしましょう。

解答例: 手順6まで
main.mjsの抜粋 (サーバーとして動作するJavaScript)
app.get("/posts", async (request, response) => {
const allPosts = await db.select().from(posts);
response.json(allPosts);
});

手順7

前頁での演習問題2と同様にして、ブラウザ側で、定期的に/postsにGETリクエストを発行し、受け取ったレスポンスに基づいてメッセージの一覧を表示するようにしてください。また、メッセージを入力し、送信ボタンを押すと、/postsに対してPOSTリクエストでメッセージの内容を送信するようにしてください。

解答例: 手順7まで
main.mjs (サーバーとして動作するJavaScript)
import express from "express";
import { drizzle } from "drizzle-orm/node-postgres";
import { posts } from "./schema.mjs";

const db = drizzle(process.env.DATABASE_URL);
const app = express();
app.use(express.json());
app.use(express.static("./public"));

app.get("/posts", async (request, response) => {
const allPosts = await db.select().from(posts);
response.json(allPosts);
});

app.listen(3000);
public/index.htmlの抜粋
<ul id="message-list"></ul>
<input id="message-input" placeholder="メッセージ" />
<button id="send-button" type="button">送信</button>
<script src="./script.js"></script>
public/script.js (ブラウザ上で動作するJavaScript)
setInterval(async () => {
const response = await fetch("/posts");
const posts = await response.json();

const messageList = document.getElementById("message-list");
messageList.innerHTML = "";

for (const post of posts) {
const li = document.createElement("li");
li.textContent = post.message;
messageList.appendChild(li);
}
}, 1000);

document.getElementById("send-button").onclick = async () => {
const messageInput = document.getElementById("message-input");
const message = messageInput.value;
await fetch("/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: message }),
});
};

手順8

メッセージの送信先 (/postsへのPOSTリクエスト) を作成しましょう。送られてきたデータが正しいか、Node.jsのデバッガを用いて確認してみましょう。

解答例: 手順8まで
main.mjsの抜粋 (サーバーとして動作するJavaScript)
app.post("/posts", async (request, response) => {
debugger; // ここでrequestオブジェクトの中身を確認
});

手順9

送られてきたデータをデータベースに保存できるようにしましょう。

解答例: 手順9まで
main.mjsの抜粋 (サーバーとして動作するJavaScript)
app.post("/posts", async (request, response) => {
await db.insert(posts).values({ message: request.body.message });
response.sendStatus(201); // Created(新しいメッセージを作成)
});

手順10

掲示板への投稿がデータベースに保存されていることを確認しましょう。また、Node.jsのサーバーを再起動しても、データが残っていることを確認しましょう。