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

データベース

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

データベースサーバーは、データを保存したり、参照するための専用のサーバーです。Web の世界では、ブラウザが直接データベースサーバーと通信することはほとんどなく、基本的には Node.js などのウェブサーバーがデータベースサーバーと通信します。リレーショナルデータベースは、データベースの中でも最も多く使われる種類のもので、Excel のような表形式でデータを保持するという特徴があります。

次の図は、リレーショナルデータベースの基本的な概念である、テーブルカラムレコードについて整理した図です。リレーショナルデータベースを用いる一般的なアプリケーションでは、アプリケーション開発時にテーブルとカラムを作成しておき、ユーザーの操作に応じてレコードを追加・編集・削除していきます。

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

ElephantSQL で PostgreSQL を使用する

PostgreSQL は、代表的なリレーショナルデータベースです。ElephantSQL を利用すると、無料のアカウント登録のみで、PostgreSQL サーバーが利用できます。

アカウント登録が完了したら、早速 PostgreSQL サーバーを起動しましょう。設定項目は多くないので簡単です。

  • Plan: Tiny Turtle (Free)
  • Data center: AP-NorthEast-1 (Tokyo)

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

DBeaver は、無料で公開されている非常に高機能なデータベースクライアントです。ElephantSQL で表示できる認証情報を利用して、データベースに接続できることを確認しましょう。

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

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

Prisma は、Node.js から主にリレーショナルデータベースを使用するためのライブラリです。Prisma を便利に使用するための拡張機能が VS Code 向けに用意されているので、先にダウンロードしておきましょう。

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

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

npx prisma init

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

npx コマンド

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

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

prisma/schema.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 を用いてデータベースの操作を行う場合には、非同期処理の概念を理解しておく必要があります。

JavaScript のコードの実行やブラウザの画面への描写(レンダリング)は、メインスレッドと呼ばれる単一のスレッドにおいて順次処理されていきます。このため、ファイルの読み書きやネットワーク通信などの比較的時間がかかる処理があると、Web ページの読み込みに時間がかかってしまいます。非同期処理を使うと、メインスレッドを停止させずに時間のかかる処理を並行して行うことができ、処理効率が高まるので、なめらかな画面切り替えが可能になります。データベースの操作では、データベースサーバーとの通信やデータベースにあるデータの読み書きを行うため、非同期処理が必要となります。

まずは簡単な例で非同期処理を説明します。下のコードにおいて sample.txt を読み込む 3 つの関数 synccallbackasyncAwait の挙動を比較してみましょう。なお、sample.txt には これはサンプルです。 と記述されているものとします。

const fs = require("fs");
const fsPromises = require("fs/promises");

function sync() {
const buffer = fs.readFileSync("sample.txt");
console.log(buffer.toString());
}

function callback() {
fs.readFile("sample.txt", (error, buffer) => {
console.log(buffer.toString());
});
}

async function asyncAwait() {
const buffer = await fsPromises.readFile("sample.txt");
console.log(buffer.toString());
}

console.log("Before sync()");
sync();
console.log("After sync()");

console.log("Before callback()");
callback();
console.log("After callback()");

console.log("Before asyncAwait()");
asyncAwait();
console.log("After asyncAwait()");

このプログラムの結果は、次のようになります。

Before sync()
これはサンプルです。
After sync()
Before callback()
After callback()
Before asyncAwait()
After asyncAwait()
これはサンプルです。
これはサンプルです。

同期的な関数 sync では後続の console.log("After sync()"); がファイルの中身が表示された後に実行されているのに対し、その他の 2 つでは後回しになっています。これは、この 2 つはファイルの読み込みを非同期処理で行っているからです。

fs.readFile を用いる方法では、第 2 引数にコールバック関数を登録します。この関数はファイルの読み込みが完了したタイミングで実行されます。関数が呼び出されるまでの間は他の処理が割り込むことができます。

fs/promisesfs と同じく Node.js の標準モジュールですが、await キーワードを用いて非同期処理を記述できるようになっています。await キーワードをつけると非同期処理の完了を待つことができ、その間に他の処理が割り込めます。なお、 await キーワードは async キーワードを付けた関数内でしか使えません

非同期処理

(発展) Promise

async キーワードのついた関数は、戻り値として Promise クラスのインスタンスを返却するようになります。この Promise クラスを用いることにより、コールバック型の非同期関数を await を用いることができる形に変換できます。await キーワードをつけることで、resolve の引数 buffer の値が返ってきます。

const fs = require("fs");

// fsPromises.readFile を自分で実装する
function myReadFileAsync(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (error, buffer) => {
if (error) {
reject(error);
} else {
resolve(buffer);
}
});
});
}

async function myAsyncAwait() {
const buffer = await myReadFileAsync("sample.txt");
console.log(buffer.toString());
}

console.log("Before myAsyncAwait()");
myAsyncAwait();
console.log("After myAsyncAwait()");

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

Node.js から Prisma を利用してデータベースのデータを操作するためには、PrismaClient クラスを用います。こちらも fs/promises と同様に await キーワードを用いて非同期処理を記述することができます。データの取得には、次の 3 つのメソッドが利用できます。

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

const { PrismaClient } = require("@prisma/client");
const client = new PrismaClient();

async function main() {
const todos = await client.todo.findMany();
debugger;
}
main();

findMany の戻り値

また、PrismaClient#[テーブル名].create メソッドを用いることで、テーブルにレコードを作成することができます。

const { PrismaClient } = require("@prisma/client");
const client = new PrismaClient();

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

create の戻り値

課題

前頁で作成した掲示板システムのアプリケーションのデータが、データベース上に保存できるようにしてみましょう。