データベース
データ管理の限界
これまで作成してきたアプリケーションでは、次のように、データを全て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のアプリケーションサーバーで、Prismaと呼ばれるライブラリを用い、リレーショナルデータベースの一つであるPostgreSQLサーバーに保存されているデータを取得します。
使用する技術・サービス
PostgreSQL
現在最もよく用いられるリレーショナルデータベースのひとつです。豊富な機能を持ちます。
Prisma
主にリレーショナルデータベースを操作するためのNode.jsのライブラリです。複数の構成要素からなります。
@prisma/clientパッケージ: アプリケーションサーバーから用いるnpmのパッケージです。JavaScriptプログラムから使用します。prismaパッケージ: 開発時にコマンドとして用いるnpmのパッケージです。npxコマンドを通して実行します。.prismaファイル: データベースのテーブル構造を記述するファイルです。prismaパッケージのコマンドを用いて実際のデータベースサーバーに反映させます。Prisma拡張機能: VS Codeの拡張機能です。.prismaファイルに対する補完やフォーマットの機能を提供します。
Supabase
PostgreSQLサーバーを提供するサービスです。その他にデータベースを直感的に操作できる機能なども提供しています。PostgreSQLサーバーは皆さんのコンピューター上にも構築できますが、ここではその手間を省くため、外部のサービスを利用します。
Supabase で PostgreSQL サーバーを構築する
Supabaseのアカウントを作成しましょう。New Projectボタンを押して必要な情報を入力し、新しいPostgreSQLサーバーを起動させてください。入力が必要な情報は次の通りです。
- Project name: 起動するサーバーにつける名前です。適当に設定して構いません。
- Database Password: 起動するサーバーのパスワードです。
Generate a passwordボタンを押して生成するのが良いでしょう。また、後でこのパスワードは使用することになるため覚えておきましょう。 - Region: 起動するサーバーの地理的な場所です。ここでは
Northeast Asia (Tokyo)を選択しています。
この時点では、まだデータベース上にテーブルが作成されていません。Supabase上で作成することもできますが、今回はPrismaを使用して作成することにします。
Prismaでテーブル構造を作成する
VS Code向けのPrisma拡張機能をインストールしましょう。

新しいフォルダをVS Codeで開き、npm initコマンドを使用してpackage.jsonファイルを作成した後、
npx prisma init
コマンドを実行します。パッケージをインストールしても良いか尋ねられる場合は、yを入力して許可しましょう。
npxコマンドnpxコマンドは、npmのパッケージを、プログラムからではなく直接実行するためのコマンドです。npmにはprismaパッケージのように、直接実行専用のパッケージも存在します。
続いて、Supabaseからデータベースへの接続情報を.envファイルにコピーします。これにより、Prismaは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ファイルに記述された環境変数を読み込ませることができます。なお、Prismaは.envファイルの内容を自動的に読み込むため、--env-fileオプションを指定する必要はありません。
prisma/schema.prismaファイルに、次のように追記し、ToDoテーブルとそのカラムを定義します。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
name String
}
完了したら、
npx prisma db push
コマンドを実行しましょう。すると、データベースにschema.prismaに書かれた通りのテーブルとカラムが作成されるので、Supabaseから確認してみてください。また、このとき、後述する@prisma/clientパッケージが自動的にインストールされます。
Prismaが作成したテーブルにレコードを追加する
Prismaが作成したテーブルに、レコードを追加しましょう。
今回はSupabaseを利用してPostgreSQLサーバーを構築したため、Supabaseの機能を使用してデータベースを操作しましたが、DBeaverも便利です。DBeaverは、多くのデータベースを直感的に操作できるソフトウェアで、PostgreSQLにも対応しています。
DBeaverをインストールした後、次のようにすることでDBeaverを利用してデータベースを操作することができます。
Prismaでデータベースのデータを読み書きする
Node.jsからPrismaを利用してデータベースのデータを操作するためには、@prisma/clientパッケージのPrismaClientクラスを用います。
PrismaClient#[テーブル名].findManyメソッド: 条件を満たすレコードを全て取得PrismaClient#[テーブル名].findFirstメソッド: 条件を満たす最初のレコードを取得PrismaClient#[テーブル名].findUniqueメソッド: レコードを一意に識別できる条件を使用してレコードを1つだけ取得
上記の3つのメソッドは、非同期処理を行います。JavaScriptにおける非同期処理とは、ファイルの入出力やネットワーク通信など、JavaScriptの外側の時間のかかる処理の完了を待つ間、ほかの処理を実行できるようにする仕組みです。非同期処理を行う関数を使用するためには、次の2つを行います。
- 非同期処理を行う関数を呼び出す関数を定義する際、
asyncキーワードをつけること - 非同期処理を行う関数の戻り値に対し、
await演算子を適用すること
非同期処理に関する詳細は、MDNの記事を参照してください。
まずは、findManyメソッドの戻り値を、デバッガを用いて確認してみましょう。
import { PrismaClient } from "./generated/prisma/index.js";
const client = new PrismaClient();
const todos = await client.todo.findMany();
debugger;

@prisma/client パッケージのインポート元@prisma/clientパッケージによって提供されるPrismaClientクラスは、./generated/prisma/index.jsからインポートします。このファイルは、npx prisma db pushコマンドを実行した際に自動的に生成されるファイルで、schema.prismaファイルに記述したテーブルの構造を元に、Prismaが自動的に生成したものです。このファイルが生成される場所は、schema.prismaファイルのgeneratorセクションから変更できます。
続いて、PrismaClient#[テーブル名].createメソッドを用いて、テーブルにレコードを作成してみましょう。
import { PrismaClient } from "./generated/prisma/index.js";
const client = new PrismaClient();
const todos = await client.todo.create({ data: { name: "買い物をする" } });
debugger;

演習問題
PostgreSQLにデータを保存する掲示板サービスを作ってみましょう。
手順1
Supabaseで新しいデータベースを作成しましょう。
手順2
新しいプロジェクト用のディレクトリを作成し、npx prisma initコマンドを実行して、Prismaのセットアップをしましょう。.envファイルを編集し、Prismaがデータベースに接続できるようにしましょう。
手順3
作成された.prismaファイルを編集し、掲示板に投稿されたメッセージを保存するためのテーブルと、そのテーブルのカラムの定義を記述しましょう。npx prisma db pushコマンドでテーブルとカラムの定義をデータベースに反映させましょう。
掲示板サービスに必要なテーブルの構造を考えてみましょう。例えば、次の例では、掲示板の投稿を保存するためのPostテーブルを定義しており、このテーブルにはidとmessageの2つのカラムが存在しています。他にも、投稿のタイトルを保存するためのtitleカラムや、投稿者名を保存するためのauthorカラムなどを定義するなどの工夫が考えられます。
model Post {
id Int @id @default(autoincrement())
message String
}
手順4
掲示板の投稿のサンプルデータをデータベースに登録しましょう。
手順5
Node.jsのデバッガを用いて、データベースのデータがPrismaで取得できることを確認しましょう。
PrismaのfindManyメソッドを用いて、テーブル内にある全てのレコードを取得できます。
const posts = await client.post.findMany();
// [
// { id: 1, message: "おはようございます" },
// { id: 2, message: "こんにちは" },
// ]
このメソッドの戻り値は、各カラムの値をプロパティとして持つオブジェクトの配列です。
手順6
Expressをインストールし、/postsへのGETリクエストに対して、データベースのデータをJSON形式のレスポンスで返せるようにしましょう。
解答例: 手順6まで
app.get("/posts", async (request, response) => {
const posts = await client.post.findMany();
response.json(posts);
});
手順7
前頁での演習問題2と同様にして、ブラウザ側で、定期的に/postsにGETリクエストを発行し、受け取ったレスポンスに基づいてメッセージの一覧を表示するようにしてください。また、メッセージを入力し、送信ボタンを押すと、/sendに対してPOSTリクエストでメッセージの内容を送信するようにしてください。
解答例: 手順7まで
import express from "express";
import { PrismaClient } from "./generated/prisma/index.js";
const app = express();
const client = new PrismaClient();
app.use(express.json());
app.use(express.static("./public"));
app.get("/posts", async (request, response) => {
const posts = await client.post.findMany();
response.json(posts);
});
app.listen(3000);
<ul id="message-list"></ul>
<input id="message-input" placeholder="メッセージ" />
<button id="send-button" type="button">送信</button>
<script src="./script.js"></script>
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("/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: message }),
});
};
手順8
メッセージの送信先 (/sendへのPOSTリクエスト) を作成しましょう。送られてきたデータが正しいか、Node.jsのデバッガを用いて確認してみましょう。
解答例: 手順8まで
app.use(express.urlencoded({ extended: true }));
app.post("/send", async (request, response) => {
debugger; // ここでrequestオブジェクトの中身を確認
});
手順9
送られてきたデータをデータベースに保存できるようにしましょう。
解答例: 手順9まで
app.post("/send", async (request, response) => {
await client.post.create({ data: { message: request.body.message } });
response.send();
});
手順10
掲示板への投稿がデータベースに保存されていることを確認しましょう。また、Node.jsのサーバーを再起動しても、データが残っていることを確認しましょう。