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

データベース

データ管理の限界

これまで作成してきたアプリケーションでは、次のように、データを全て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などで開発するサーバーであることが多いです。これまで開発してきたようなサーバーを、データベースサーバーと対比してアプリケーションサーバーと呼びます。

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

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

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

リレーショナルデータベースに対するクエリは、多くの場合、SQLと呼ばれる言語を用いて記述されます。しかし、SQLは人間が直接記述するための言語であり、プログラム中に直接記述するにはあまり適していません。そのため、データベースへのクエリをプログラムから発行しやすくするためのライブラリが数多く存在しています。その中でも、この節で用いるPrismaは、Node.jsにおける最も人気のあるライブラリの一つです。

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

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

PostgreSQLサーバーは自分で構築することもできますが、この節では、Supabaseというサービスが提供しているサーバーを利用します。まずは、Supabaseのアカウントを作成し、New Projectボタンから新しいPostgreSQLサーバーを起動させてください。入力が必要な情報は次の通りです。

  • Project name: 起動するサーバーにつける名前です。適当に設定して構いません。
  • Database Password: 起動するサーバーのパスワードです。Generate a passwordボタンを押して生成するのが良いでしょう。また、後でこのパスワードは使用することになるため覚えておきましょう。
  • Region: 起動するサーバーの地理的な場所です。ここではNortheast Asia (Tokyo)を選択しています。
  • Enable Data API: 外部からデータベースに直接アクセスできるようにするための、Supabase特有の機能を有効にするためのオプションです。本節では、一般的な方法でデータベースを利用するため、このオプションは無効にして構いません。
危険

SupabaseのData APIを有効にする場合、データベース内のデータが標準で全世界に公開された状態になります。本機能を利用する明確な理由がない限りは、このオプションは有効にしないようにしてください。

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

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

Prismaのドキュメント

最新のPrismaのドキュメントは、こちらから確認できます。

VS Code向けのPrisma拡張機能をインストールしましょう。これにより、VS Code上で、Prismaで使用する.prismaファイルを編集する際の補助機能が提供されるようになります。

Prisma拡張機能のインストール

続いて、新しいフォルダをVS Codeで開き、npm initコマンドを使用してpackage.jsonファイルを作成した後、次の2つのコマンドを実行して、計5つのnpmパッケージをインストールします。

npm install --save-dev prisma
npm install @prisma/client @prisma/adapter-pg pg dotenv

これらのパッケージは、それぞれ次のような役割を持ちます。

  • prisma: Prismaのコマンドを実行するためのnpmのパッケージです。
  • @prisma/client: Prismaの本体となるパッケージです。Node.jsのプログラムからインポートして使用します。
  • @prisma/adapter-pg: PrismaがPostgreSQLに接続できるようにするためのnpmのパッケージです。PostgreSQL以外のデータベースを使用する場合は、別のパッケージが必要になります。
  • pg: PostgreSQLサーバーとの通信を実行するためのパッケージです。@prisma/adapter-pgが内部で使用します。
  • dotenv: .envファイルに記述された環境変数を読み込むためのnpmのパッケージです。

その後、次のコマンドを実行し、prismaパッケージを用いて、テーブル構造を記述するprisma/schema.prismaファイルや、データベースへの接続情報を設定するprisma.config.tsファイルなどを生成します。

npx prisma init
--save-devオプション

npm install時の--save-devオプションは、npmのパッケージを開発環境でのみ使用することを示すためのオプションです。prismaパッケージは、後述するようにnpxコマンド等を通して直接実行するためのパッケージで、Node.jsのプログラムからインポートして使用するものではないため、開発環境でのみ使用されることになります。

npxコマンド

npxコマンドは、npmのパッケージを直接実行するためのコマンドです。prismaパッケージは、@prisma/clientパッケージとは異なり、プログラムからインポートして使用するのではなく、npxコマンド等を通して直接実行するためのパッケージとして設計されています。

.tsファイル

.tsファイルは、TypeScriptというプログラミング言語で記述されたファイルで、JavaScriptファイルと同様に、Node.jsにより実行可能です。詳細は、「TypeScript」の節で扱います。

続いて、Supabaseからデータベースへの接続情報を.envファイルにコピーします。パスワードの部分には、先ほどプロジェクトを作成した際に登録したパスワードを指定しましょう。これにより、PrismaはSupabase上のPostgreSQLサーバーと接続できるようになります。

環境変数

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

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

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

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

.envファイルは、環境変数の指定を簡略化するために慣習的に用いられるファイルです。node --env-file=.env main.jsのように指定することで、.envファイルに記述された環境変数を読み込ませることができます。npx prisma initコマンドを実行したときに作成されるprisma.config.tsファイルには、dotenvパッケージを用いて.envファイルを読み込むコードが記述されているため、Prismaのコマンドを実行する際には、.envファイルに記述された環境変数が自動的に読み込まれるようになっています。

prisma/schema.prismaファイルに、次のように追記し、ToDoテーブルとそのカラムを定義します。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Get a free hosted Postgres database in seconds: `npx create-db`

generator client {
provider = "prisma-client"
output = "../generated/prisma"
}

datasource db {
provider = "postgresql"
}

model Todo {
id Int @id @default(autoincrement())
name String
}

完了したら、

npx prisma db push

コマンドを実行しましょう。すると、データベースにschema.prismaに書かれた通りのテーブルとカラムが作成されるので、Supabaseから確認してみてください。また、このとき、後述する@prisma/clientパッケージが自動的にインストールされます。

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

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

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

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

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

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

Node.jsからPrismaを利用してデータベースのデータを操作するためには、@prisma/clientパッケージのPrismaClientクラスを用います。

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

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

import { PrismaClient } from "./generated/prisma/index.js";

const client = new PrismaClient();
const todos = await client.todo.findMany();
debugger;

findManyの戻り値

@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;

createの戻り値

演習問題

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

手順1

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

手順2

新しいプロジェクト用のディレクトリを作成し、npx prisma initコマンドを実行して、Prismaのセットアップをしましょう。.envファイルを編集し、Prismaがデータベースに接続できるようにしましょう。

手順3

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

テーブルの定義

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

prisma/schema.prisma の抜粋
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まで
main.jsの抜粋 (サーバーとして動作するJavaScript)
app.get("/posts", async (request, response) => {
const posts = await client.post.findMany();
response.json(posts);
});

手順7

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

解答例: 手順7まで
main.js (サーバーとして動作するJavaScript)
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);
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.jsの抜粋 (サーバーとして動作するJavaScript)
app.use(express.urlencoded({ extended: true }));
app.post("/posts", async (request, response) => {
debugger; // ここでrequestオブジェクトの中身を確認
});

手順9

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

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

手順10

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