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

データベース

データ管理の限界

これまで作成してきたアプリケーションでは、次のように、データを全て 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

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

DBeaver

多くのデータベースを直感的に操作できるソフトウェアです。PostgreSQL にも対応しています。

Prisma

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

  • @prisma/client パッケージ: アプリケーションサーバーから用いる npm のパッケージ。JavaScript プログラムから使用する。
  • prisma パッケージ: 開発時にコマンドとして用いる npm のパッケージ。npx コマンドを通して実行する。
  • .prisma ファイル: データベースのテーブル構造を記述するファイル。prisma パッケージのコマンドを用いて実際のデータベースサーバーに反映させる。
  • Prisma 拡張機能: VS Code の拡張機能。.prisma ファイルに対する補完やフォーマットの機能を提供する。

ElephantSQL

PostgreSQL サーバーを提供するサービスです。PostgreSQL サーバーは皆さんのコンピューター上にも構築できますが、ここではその手間を省くため、外部のサービスを利用します。

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

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

  • Name: 起動するサーバーにつける名前です。適当に設定して構いません。
  • Plan: 起動するサーバーの性能です。最も低い Tiny Turtle (Free) を選択すれば無料で使用できます。
  • Data center: 起動するサーバーの地理的な場所です。ここでは AP-NorthEast-1 (Tokyo) を選択しています。

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

DBeaver をインストールしましょう。続いて、ElephantSQL の管理画面で接続情報を表示し、その情報を DBeaver に入力して前項で起動した PostgreSQL サーバーに接続しましょう。

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

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

VS Code 向けの Prisma 拡張機能をインストールしましょう。

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

新しいフォルダを VS Code で開き、npm init コマンドを使用して package.json ファイルを作成した後、

npx prisma init

コマンドを実行します。パッケージを実行しても良いか尋ねられる場合は、y を入力して許可しましょう。

npx コマンド

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

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

prisma/schema.prisma ファイルを、次のように編集し、データベースのテーブルとカラムを定義します。

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

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

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

完了したら、

npx prisma db push

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

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

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

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

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

非同期処理

上記の 3 つのメソッドは、非同期処理を行います。JavaScript における非同期処理とは、ファイルの入出力やネットワーク通信など、JavaScript の外側の時間のかかる処理の完了を待つ間、ほかの処理を実行できるようにする仕組みです。非同期処理を行う関数を使用するためには、次の 2 つを行います。

  • 非同期処理を行う関数を呼び出す関数を定義する際、async キーワードをつけること
  • 非同期処理を行う関数の戻り値に対し、await 演算子を適用すること

非同期処理に関する詳細は、MDN の記事を参照してください。

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

import { PrismaClient } from "@prisma/client";

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

findMany の戻り値

続いて、PrismaClient#[テーブル名].create メソッドを用いて、テーブルにレコードを作成してみましょう。

import { PrismaClient } from "@prisma/client";

const client = new PrismaClient();
const todos = await client.todo.create({ data: { name: "買い物をする" } });
debugger;

create の戻り値

課題

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

手順 1

ElephantSQL で新しいデータベースを作成しましょう。作成したデータベースに DBeaver から接続できることを確認しましょう。

手順 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

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

手順 5

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

手順 6

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

ヒント

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

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

このメソッドの戻り値は、各カラムの値をプロパティとして持つオブジェクトの配列です。Array#map メソッドや Array#join メソッドを用い、適切な HTML に変換してレスポンスを生成しましょう。

解答例: 手順 6 まで
main.mjs の抜粋
const template = readFileSync("./index.html", "utf-8");
app.get("/", async (request, response) => {
const posts = await client.post.findMany();
const html = template.replace(
"<!-- messages -->",
posts.map((post) => `<li>${post.message}</li>`).join(""),
);
response.send(html);
});
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>掲示板</title>
</head>
<body>
<ul>
<!-- messages -->
</ul>
</body>
</html>

手順 7

掲示板を投稿するための HTML のフォームを表示できるようにしましょう。入力されたデータは /send への POST リクエストとして送信されるようにしてみましょう。

ヒント

手順 6 で作成したテンプレートとなる HTML ファイルを編集し、フォームを追加しましょう。

解答例: 手順 7 まで
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>掲示板</title>
</head>
<body>
<ul>
<!-- messages -->
</ul>
<form method="post" action="/send">
<input placeholder="メッセージ" name="message" />
<button type="submit">送信</button>
</form>
</body>
</html>

手順 8

前の手順で作成した HTML のフォームの送信先 (/send への POST リクエスト) を作成しましょう。送られてきたデータが正しいか、Node.js のデバッガを用いて確認してみましょう。

解答例: 手順 8 まで
main.mjs の抜粋
app.use(express.urlencoded({ extended: true }));
app.post("/send", async (request, response) => {
debugger; // ここで request オブジェクトの中身を確認
});

手順 9

送られてきたデータをデータベースに保存できるようにしましょう。データベースに投稿が保存された後、ブラウザは自動的に / に戻るようにしてみましょう。

ヒント

サーバー側からブラウザに対してページ遷移を指示するためには、ブラウザからのリクエストに対して、特殊なレスポンスを返します。Express を用いてこのようなレスポンスを生成するためには、express.Response#send メソッド の代わりに、express.Response#redirect メソッド を用います。

app.post("/send", async (request, response) => {
// 省略
response.redirect("/");
});
解答例: 手順 9 まで
main.mjs の抜粋
app.use(express.urlencoded({ extended: true }));
app.post("/send", async (request, response) => {
await client.post.create({ data: { message: request.body.message } });
response.redirect("/");
});

手順 10

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