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

データベース

データ管理の限界

これまで作成してきたアプリケーションでは、次のように、データを全て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拡張機能をインストールしましょう。

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

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

npx prisma init

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

npxコマンド

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

続いて、Supabaseからデータベースへの接続情報を.envファイルにコピーします。これにより、PrismaはSupabase上の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パッケージが自動的にインストールされます。

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

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

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

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

DBeaverをインストールした後、次のようにすることで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

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で取得できることを確認しましょう。

手順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の抜粋
app.get("/", async (request, response) => {
const posts = await client.post.findMany();
response.send(`
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>掲示板</title>
</head>
<body>
<ul>
${posts.map((post) => `<li>${post.message}</li>`).join("")}
</ul>
</body>
</html>
`);
});

手順7

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

ヒント

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

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

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

ヒント

GETリクエストとPOSTリクエストの章で扱ったexpress.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

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