【第9回】データベース入門(2)

Webサーバーからデータベースを接続してみよう

Homeブログ一覧【第9回】データベース入門(2)

おさらい

データベース

なぜデータベースが必要なのか

  • データの永続化をしたい
  • データの整合性を保ちたい
  • データの検索を効率化したい

別にファイルとして保存すればよくない?

  • ファイルだとデータの整合性が取れない(多重書き込みなど)
    • DBはトランザクションという排他制御の仕組みを持っている
  • データの検索がしにくい
    • DBはインデックスキーやハッシュテーブルなど、高速に検索ができる仕組みを持っている
    • ファイルだと全てのデータを読み込んでから検索する必要があるため遅い

どんな種類があるの?

  • リレーショナルデータベース(RDB)
    • ファイルベース
      • SQLite
    • サーバーベース
      • MySQL, PostgreSQL

他にも

  • Key-Value Store
    • Redis
    • Cloudflare Workers KV
  • Vector DB
  • Document Store

...など

どうやって使うの?

  • 基本的にSQL(Structured Query Language)という言語を使って操作する
  • データベースに接続するためのライブラリを使ってプログラムからSQLをDBに送って操作する

何に使われてるの?

  • Webアプリケーション
  • ゲームサーバー
  • ビックデータ解析
  • スマホのデータ保存

少しでも「データ」を扱う場面であれば必ずと言っていいほどデータベースが使われています。 決してWebだけの技術というわけではありませんのでこの機会に慣れておくといいでしょう。

SQL

SQLとは

  • データベースを操作するための言語
  • データの操作(追加、更新、削除、取得)ができる
  • テーブルの作成や削除もできる

なぜSQLが必要なのか

サーバーはJavaScriptのほかに、GoやPythonなどの言語でも書くことができます。 例えば、JavaScriptだけがDBに直接命令をできるとしたら、他の言語で書かれたサーバーからはその命令を使うことはできませんよね? つまり、データベースにアクセスし操作するための共通の中間言語が必要なのです。

今回は紹介しませんが、SQLを書かなくてもうまくデータベースを操作できるライブラリもありますが、裏側では基本的にはSQLが使われています。

SQLの基本

前回の講義で学習したSQL文を復習してみましょう。詳しくは【第8回】データベース入門(1)を参照してください。

テーブル操作

  • CREATE TABLEでテーブルを作成
  • DROP TABLEでテーブルを削除

レコード操作

  • SELECTでデータを取得
  • INSERT INTOでデータを追加
  • UPDATEでデータを更新
  • DELETEでデータを削除

レコードの操作である、CREATEREADUPDATEDELETEの頭文字を取ってCRUDと呼ぶことがあります。

SQLが何をするかイメージしてみましょう。次のSQLは何をしそうだと思いますか?

CREATE TABLE users (
  id INT PRIMARY KEY,
  name TEXT,
  age INT
);

CREATE TABLEusersというテーブルを作成し、そのテーブルにはidnameageというカラムがあるんだなーくらいのイメージが湧いたらOKです。

次はどうでしょう?

SELECT * FROM users;

usersテーブルから全てのデータを取得するんだなーくらいのイメージが湧いたらOKです。

最後にこれは?

INSERT INTO users (id, name, age) VALUES (1, 'Alice', 20);

usersテーブルにid1nameAliceage20のデータを追加するんだなーくらいのイメージが湧いたらOKです。

プログラム(Node.js)からデータベースを操作する

おさらいの通り、今回はプログラムからデータベースを操作することを学びます。 そのまま自力でデータベースに接続するのは難しいので、better-sqlite3というライブラリを使ってSQLite3のデータベースをプログラムから操作してみましょう。

better-sqlite3 のインストール

3回前の講義から使い続けているsimple-todo-appフォルダへ移動してください。

Node.jsのプロジェクトにライブラリを追加する方法はnpm installコマンドでしたね。これを使ってbetter-sqlite3をインストールしましょう。

npm install better-sqlite3

もしうまくできてるか不安な場合は、package.jsonを確認してみましょう。

cat package.json

をして、dependenciesbetter-sqlite3が追加されていればOKです!

もしGitを使っている場合は

git diff

とすることで前回のコミットとの比較により、package.jsonに変更があったか確認するという方法もあります。

gitignore を使おう

これからデータベースファイルを作成しますが、データベースに入っているデータにもし個人情報などが含まれていた場合、パブリックリポジトリならそのデータが全世界に公開されてしまいますし、プライベートリポジトリでもそもそもGitHub社にデータを公開していることになるので問題です。

その後消すコミットをしても、Gitは履歴を保存するソフトウェアですからコミットを辿ればデータにアクセスできてしまいます。 そのような場合は、そもそも最初からデータベースファイルをGitで管理しないようにするために.gitignoreファイルを使いましょう。

.gitignoreファイルはGitで管理されるファイルの中で、Gitで管理しないファイルを指定するためのファイルです。

simple-todo-appフォルダに.gitignoreファイルを作成して、以下のように書いてください。

node_modules/
*.db

これはnode_modules/ディレクトリと.db拡張子のファイルをGitで管理しないようにするための設定です。 node_modules/ディレクトリはライブラリをインストールしたときに作成されるディレクトリで、ライブラリのインストールは各PCがやるべきなのでGitで管理する必要がないです。 逆にGitで管理してしまうと重すぎて大変になります。

そしたらこれをGitにコミットしておきましょう。

git add .gitignore
git commit -m ".gitignoreを追加"

これでデータベースファイルがGitで管理されないようになりました。

また、前々回の講義でnode_modules.gitignoreする指示を出し忘れていたので、こちらのコマンドを実行してGitからnode_modulesを削除しておいてください。

git rm -r --cached node_modules
git push -f

git push -fはリモートリポジトリを作成していれば (GitHub に上げていれば) 行なってください。

プログラムからDBを作る

control-db.jsというファイルをそのまま今いるフォルダ(simple-todo-app直下)に作成して、以下のコードを書いてください。

control-db.js
import Database from 'better-sqlite3';

const db = new Database('webken-9.db');

先ほどインストールしたbetter-sqlite3ライブラリから、Databaseクラスをインポートして、webken-9.dbというファイルを作成してデータベースとして扱うためのdbという変数を作成しています。

JS文法Tips - import

importはES Modulesという標準的なモジュールシステムを使って他のファイルから関数やクラスを読み込むための文法です。

importにはよく使う2つの書き方があります。

  1. デフォルトインポート

    デフォルトエクスポートされた関数やクラスを読み込むときに使います。

    some-module.js
    const hello = () => {
      console.log('Hello');
    };
    
    export default hello;
    
    main.js
    import hello from 'some-module';
    
    hello(); // Hello と表示される
    
  2. 名前付きインポート

    名前付きエクスポートされた関数やクラスを読み込むときに使います。

    some-module.js
    export const hello = () => {
      console.log('Hello');
    };
    
    main.js
    import { hello } from 'some-module';
    
    hello(); // Hello と表示される
    

Maximumは名前付きインポート勢が多いです。パフォーマンス的にも名前付きインポートの方がいい、ただしデフォルトインポートの方が簡潔だし統一性があるというメリットもあります。場合によって使い分けましょう。

詳しく説明すると挙動が全然違うので省略しますが、今回のbetter-sqlite3ライブラリはデフォルトインポートを使っているので、Databaseクラスをそのままimportしています。

このファイルを実行してみましょう。

node control-db.js

エラーが出なければ成功です。webken-9.dbというファイルが作成されているか確認してみてください。 できてたら次に進みましょう。

プログラムからテーブルを作る

そしたら、前回の講義でやったテーブル作成のSQL文をプログラムから実行してみましょう。 SQL文を実行するには文字列としてSQL文を書いて、db.exec()メソッドに渡すだけです。

control-db.js
import Database from 'better-sqlite3';

const createTweetTableQuery = `
CREATE TABLE tweets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content TEXT NOT NULL,
  created_at TEXT NOT NULL
);
`;

const db = new Database('webken-9.db');

db.exec(createTweetTableQuery);
JS文法Tips - 改行を含む文字列

JSで複数行の文字列を書くには、バッククォート(`)を使います。これはテンプレートリテラルと呼ばれる文法です。

const str = `
こんにちは
こんばんは
おはよう
`;

これは次のように解釈されます。

const str = "\nこんにちは\nこんばんは\nおはよう\n";

一番最初と最後の改行が邪魔ですね。これを取り除くにはtrim()メソッドを使います。

発展JS文法Tips - trim()

trim()はテンプレートリテラル構文のものではなく、文字列に対して使えるメソッドです。

もっと詳しくいうと、String.prototype.trim()という文字列オブジェクトのプロトタイプに生えているメソッドです。

const str = `
こんにちは
こんばんは
おはよう
`.trim();

これで改行が取り除かれます。

const str = "こんにちは\nこんばんは\nおはよう";

と解釈されることでしょう。

では、このファイルを実行してみましょう。

node control-db.js

エラーが出なければ成功です。 ちゃんとtweetsテーブルが作成されているかSQLite3のコマンドラインツールを使って確認してみましょう。

sqlite3 webken-9.db
.tables

とすると、tweetsテーブルが作成されていることが確認できるはずです。

プログラムからデータを挿入する

悪い例を紹介するのでこれを書く必要はありません

次に、前回の講義でやったデータの挿入をプログラムから行ってみましょう。

データの挿入はINSERT INTO文でしたね。これも文字列として書いて、db.exec()メソッドに渡すだけです。

INSERT INTO tweets (content, created_at) VALUES ('今日はいい天気ですね', '2024-07-01 12:00:00');
INSERT INTO tweets (content, created_at) VALUES ('おなかがすいたなー', '2024-07-01 12:30:00');

ただ、これをINSERT毎に書いていたら10000件とか挿入したくなったときに大変ですよね。 ということでこのINSERT文を作る関数を作る工夫をしてみます。 具体的にはこういう関数です。(まだ書く必要はありません)

const insertTweet = (content, createdAt) => {
  return `INSERT INTO tweets (content, created_at) VALUES ('${content}', '${createdAt}');`
};

こうすれば

console.log(insertTweet('今日はいい天気ですね', '2024-07-01 12:00:00'));
// INSERT INTO tweets (content, created_at) VALUES ('今日はいい天気ですね', '2024-07-01 12:00:00');

というように引数に好きな値を渡すだけでINSERT文を作成して実行できます。

SQLインジェクションとプレイスホルダ

ちょっと待って!

このままだと、SQLインジェクションという脆弱性が発生してしまいます。

SQLインジェクションとは、ユーザーが入力した値をそのままSQL文に埋め込んでしまうことで、意図しないSQL文が実行されてしまう脆弱性のことです。

試しに、content'); DROP TABLE tweets; --という文字列を入れてみましょう。

insertTweet("'); DROP TABLE tweets; --", '2024-07-01 12:00:00');

これを実行すると...

INSERT INTO tweets (content, created_at) VALUES(''); DROP TABLE tweets; --', '2024-07-01 12:00:00');

なんとtweetsテーブルが削除されてしまいます! INSERT文の第一引数に'); DROP TABLE tweets; --という文字列を渡すことで、INSERTが終わった後にDROP TABLEを実行するクエリを作れてしまいました。(--はそれ以降コメントアウトの記号です)

任意の値を埋め込めるということは、任意のSQL文を実行できてしまうということなのです。

この脆弱性発生を防ぐために色々な対策がありますが、その中でも一番簡単で効果的なのがプレイスホルダです。

プレイスホルダとは、SQL文の中に?という記号を使って、その後に渡す値を埋め込むという仕組みです。 この話は一応SQLite3もといSQLの話なのですが、前回紹介されていなかったのでここで紹介します。

better-sqlite3では、db.prepare()メソッドを使ってプレイスホルダを使ったSQL文を実行できます。

const insertTweetQuery = db.prepare(`
INSERT INTO tweets (content, created_at) VALUES (?, ?);
`);

insertTweetQuery.run('今日はいい天気ですね', '2024-07-01 12:00:00');

このように、?を使ってSQL文を書いて、run()メソッドに渡すことで、その?に対応する値を埋め込んで実行してくれます。 こうすれば、ユーザーが入力した値をそのままSQL文に埋め込むことがなくなるので、SQLインジェクションの脆弱性を防ぐことができます。

基本的にdb.prepare()メソッドを使ってSQL文を書きましょう。 (なるべくdb.exec()は使わないようにしましょう。わざと危険性を認識してもらうために使いました)

プログラムから色々操作を試してみる

後の操作はほぼ同じなので、一気に動かしてみましょう。

control-db.js
import Database from 'better-sqlite3';

const db = new Database('webken-9.db');

const createTweetTableQuery = db.prepare(`
CREATE TABLE tweets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content TEXT NOT NULL,
  created_at TEXT NOT NULL
);
`);

createTweetTableQuery.run();

const insertTweetQuery = db.prepare(`
INSERT INTO tweets (content, created_at) VALUES (?, ?);
`);

const getTweetsQuery = db.prepare(`
SELECT * FROM tweets;
`);

const deleteTweetQuery = db.prepare(`
DELETE FROM tweets WHERE id = ?;
`);

insertTweetQuery.run('今日はいい天気ですね', '2024-07-01 12:00:00');
insertTweetQuery.run('おなかがすいたなー', '2024-07-01 12:30:00');

console.log(getTweetsQuery.all());

deleteTweetQuery.run(1);

console.log(getTweetsQuery.all());

かなりファイルに変更があるのでコピーして全て上書きすることをおすすめします。

大変分かりづらくて申し訳ないんですが、INSERT文などをprepareするとき、操作先のテーブル(今回はtweetsテーブル)が存在しないとエラーが出ます。 createTweetTableQuery.run()はなるべく最初に実行するようにしましょう!

このファイルを実行してみましょう。

node control-db.js

エラーが出なければ成功です。もしSqliteError: table tweets already exists的なエラーが出た場合は、webken-9.dbを削除してから再度実行してみてください。

tweetsテーブルにデータが挿入されているか、SELECT文で取得できているか、DELETE文で削除できているか確認してみてください。

流れとしては

  1. tweetsテーブルを作成
  2. データを挿入 (idがAUTOINCREMENTなので12が付与される)
  3. データを取得 (id12のデータが取得される)
  4. id1のデータを削除
  5. データを取得 (id2のデータだけが取得される)

という流れになります。

つまり

sqlite3 webken-9.db
SELECT * FROM tweets;

を実行して、id2のデータだけが取得できていればOKです。

このファイルcontrol-db.jsはデモ用に作ったファイルですので今後使いません、消していただいても結構です。もし取っておきたい場合はGitにコミットしておくといいでしょう。

git add control-db.js
git commit -m "Add control-db.js"

simple-todo-app にデータベースを導入する

それでは、ついにsimple-todo-appにデータベースを導入していきましょう。

移行コードが長いので一部しか載せていません。最後に全て置き換えたコードを載せておくのでわからなくなったらそっちを見てください。

データベースファイルを作成する

上の方でまずDBを初期化しましょう。今回はtodo.dbというファイル名でDBを作成します。

server.js
import Database from 'better-sqlite3';
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
const db = new Database('todo.db');

app.use(cors());

const todoList = [
  { title: "JavaScriptを勉強する", completed: false },

server.jsの冒頭にbetter-sqlite3をインポートして、todo.dbというファイルを作成してデータベースとして扱うためのdbという変数を作成しています。

テーブルを作成する

次に、todoテーブルを作成するSQL文を書いて、db.exec()メソッドで実行します。

server.js
import Database from 'better-sqlite3';
import { serve } from "@hono/node-server";
import { Hono } from "hono";

const app = new Hono();
const db = new Database('todo.db');

// todoテーブルが存在しなければ作成するSQL
const createTodoTableQuery = db.prepare(`
CREATE TABLE IF NOT EXISTS todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  completed INTEGER NOT NULL
);
`);

// todoテーブルが存在しなければ作成
createTodoTableQuery.run();

const todoList = [
  { title: "JavaScriptを勉強する", completed: false },

ポイントはCREATE TABLE文の中でIF NOT EXISTSを使っているところです。 これを書くことで、2回目以降のサーバー起動などでもしすでにtodoテーブルが存在していた場合でもエラー(SqliteError: table todo already exists)が出なくなります。

completedカラムはtruefalseを表すため、BOOLEANとしたいところですが、SQLite3にはBOOLEAN型がないのでINTEGER型で01で表現します。

データを取得する

次に、データを取得する部分を書いてみましょう。

app.get()メソッドの中で、todoListをDBから取得する部分を書き換えます。

server.js
// `todo`テーブルから全てのデータを取得するSQL
const getTodoListQuery = db.prepare(`
SELECT * FROM todo;
`);

app.get("/", async (c) => {
  // `todo`テーブルから全てのデータを取得
  const todos = getTodoListQuery.all();

  return c.json(todos, 200);
});

サーバーを起動して、curlコマンドでデータが取得できるか確認してみましょう。

npm run start &
curl http://localhost:8000

もちろんまだ何もデータが入っていないので、空の配列が返ってくるはずです。

一度サーバーを停止します。

fg

を実行してから、Ctrl + Cでサーバーを停止してください。

データを挿入する

次にデータ挿入部分を書いてみましょう。

app.post()メソッドの中で、todoListにデータを追加する部分を書き換えます。

server.js
const insertTodoQuery = db.prepare(`
INSERT INTO todo (title, completed) VALUES (?, ?);
`);

app.post("/", async (c) => {
  const param = await c.req.json();

  if (!param.title) {
    throw new HttpException(400, { message: "Title must be provided" });
  }

  const newTodo = {
    completed: param.completed ? 1 : 0,
    title: param.title,
  };

  // リクエストに渡されたデータをDBに挿入
  insertTodoQuery.run(newTodo.title, newTodo.completed);

  return c.json({ message: "Successfully created" }, 200);
});

insertTodoQueryという変数にINSERT INTO文をprepare()メソッドで準備して、run()メソッドで実行しています。

サーバーを起動して、curlコマンドでデータが挿入できるか確認してみましょう。

サーバーを再度起動してください。

npm run start &

データを挿入するリクエストを送ってみましょう。

curl -X POST -H "Content-Type: application/json" -d '{"title": "次回のWeb研に出席する"}' http://localhost:8000

これでデータが挿入されているはずです。 さっき作ったGETリクエストを送って、データが取得できるか確認してみましょう。

curl http://localhost:8000

title"次回のWeb研に出席する"のデータが取得できていれば成功です。

一度サーバーを停止します。

fg

を実行してから、Ctrl + Cでサーバーを停止してください。

ここらへんで一旦コミットしておきましょう。

git add server.js
git commit -m "DBを使ってtodoデータを読み書きできるようにした"

データを編集する機能をDBを使って実装する

次に、データを編集する機能をDBを使って実装してみましょう。

app.put()メソッドの中で、todoListのデータを更新する部分を書き換えます。

server.js
// 指定されたIDのtodoを取得するSQL
const getTodoQuery = db.prepare(`
SELECT * FROM todo WHERE id = ?;
`);
// 指定されたIDのtodoのtitleとcompletedを更新するSQL
const updateTodoQuery = db.prepare(`
UPDATE todo SET title = ?, completed = ? WHERE id = ?;
`);


app.put("/:id", async (c) => {
  const param = await c.req.json();
  const id = c.req.param("id");

  if (!param.title && param.completed === undefined) {
    throw new HTTPException(400, { message: "Either title or completed must be provided" });
  }

  // 指定されたIDのtodoを取得
  const todo = getTodoQuery.get(id);
  if (!todo) {
    throw new HTTPException(400, { message: "Failed to update task title" });
  }

  if (param.title) {
    todo.title = param.title;
  }

  if (param.completed !== undefined) {
    todo.completed = param.completed ? 1 : 0;
  }

  // リクエストに渡されたデータをDBに更新
  updateTodoQuery.run(todo.title, todo.completed, id);

  return c.json({ message: "Task updated" }, 200);
});

getTodoQueryupdateTodoQueryという変数にそれぞれSELECT文とUPDATE文をprepare()メソッドで準備します。 getTodoQuery.get(id)で指定されたIDの更新前データを取得し、新たなデータになるようにtodoオブジェクトを更新して、updateTodoQuery.run()でDBを更新します。

サーバーを起動して、curlコマンドでデータが更新できるか確認してみましょう。

npm run start &

データを更新するリクエストを送ってみましょう。

curl -X PUT -H "Content-Type: application/json" -d '{"completed": true}' http://localhost:8000/1

さっき作ったGETリクエストを送って、データが更新されているか確認してみましょう。

curl http://localhost:8000

"completed": trueになっていれば成功です。

一度サーバーを停止します。

fg

を実行してから、Ctrl + Cでサーバーを停止してください。

データを削除する機能をDBを使って実装する

最後に、データを削除する機能をDBを使って実装してみましょう。

app.delete()メソッドの中で、todoListのデータを削除する部分を書き換えます。

server.js
// 指定されたIDのtodoを削除するSQL
const deleteTodoQuery = db.prepare(`
DELETE FROM todo WHERE id = ?;
`);

app.delete("/:id", async (c) => {
  const id = c.req.param("id");

  // 指定されたIDのtodoを取得
  const todo = getTodoQuery.get(id);

  // もし指定されたIDのtodoが存在しない場合はエラーを返す
  if (!todo) {
    throw new HTTPException(400, { message: "Failed to delete task" });
  }

  // 指定されたIDのtodoを削除
  deleteTodoQuery.run(id);

  return c.json({ message: "Task deleted" }, 200);
});

deleteTodoQueryという変数にDELETE文をprepare()メソッドで準備します。

サーバーを起動して、curlコマンドでデータが削除できるか確認してみましょう。

npm run start &

データを削除するリクエストを送ってみましょう。

curl -X DELETE http://localhost:8000/1

さっき作ったGETリクエストを送って、データが削除されているか確認してみましょう。

curl http://localhost:8000

"id": 1のデータが削除されていれば成功です。

一度サーバーを停止します。

fg

を実行してから、Ctrl + Cでサーバーを停止してください。

コミットしましょう。

git add server.js
git commit -m "DBを使ってtodoデータの更新と削除ができるようにした"

これでsimple-todo-appにデータベースを導入することができました!

リファクタリング

最後に、要らなくなったtodoListcurrentIdを削除して終わりです。最終的にはこうなりました。

server.js
import Database from 'better-sqlite3';
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
const db = new Database('todo.db');

app.use(cors());

// todoテーブルが存在しなければ作成するSQL
const createTodoTableQuery = db.prepare(`
CREATE TABLE IF NOT EXISTS todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  completed INTEGER NOT NULL
);
`);

// todoテーブルが存在しなければ作成
createTodoTableQuery.run();

// `todo`テーブルから全てのデータを取得するSQL
const getTodoListQuery = db.prepare(`
SELECT * FROM todo;
`);

app.get("/", async (c) => {
  // `todo`テーブルから全てのデータを取得
  const todos = getTodoListQuery.all();

  return c.json(todos, 200);
});

// `todo`テーブルにデータを挿入するSQL
const insertTodoQuery = db.prepare(`
INSERT INTO todo (title, completed) VALUES (?, ?);
`);

app.post("/", async (c) => {
  const param = await c.req.json();

  if (!param.title) {
    throw new HttpException(400, { message: "Title must be provided" });
  }

  const newTodo = {
    completed: param.completed ? 1 : 0,
    title: param.title,
  };

  // リクエストに渡されたデータをDBに挿入
  insertTodoQuery.run(newTodo.title, newTodo.completed);

  return c.json({ message: "Successfully created" }, 200);
});

// 指定されたIDのtodoを取得するSQL
const getTodoQuery = db.prepare(`
SELECT * FROM todo WHERE id = ?;
`);
// 指定されたIDのtodoのtitleとcompletedを更新するSQL
const updateTodoQuery = db.prepare(`
UPDATE todo SET title = ?, completed = ? WHERE id = ?;
`);

app.put("/:id", async (c) => {
  const param = await c.req.json();
  const id = c.req.param("id");

  if (!param.title && param.completed === undefined) {
    throw new HTTPException(400, { message: "Either title or completed must be provided" });
  }

  // 指定されたIDのtodoを取得
  const todo = getTodoQuery.get(id);
  if (!todo) {
    throw new HTTPException(400, { message: "Failed to update task title" });
  }

  if (param.title) {
    todo.title = param.title;
  }

  if (param.completed !== undefined) {
    todo.completed = param.completed ? 1 : 0;
  }

  // リクエストに渡されたデータをDBに更新
  updateTodoQuery.run(todo.title, todo.completed, id);

  return c.json({ message: "Task updated" }, 200);
});

// 指定されたIDのtodoを削除するSQL
const deleteTodoQuery = db.prepare(`
DELETE FROM todo WHERE id = ?;
`);

app.delete("/:id", async (c) => {
  const id = c.req.param("id");

  // 指定されたIDのtodoを取得
  const todo = getTodoQuery.get(id);

  // もし指定されたIDのtodoが存在しない場合はエラーを返す
  if (!todo) {
    throw new HTTPException(400, { message: "Failed to delete task" });
  }

  // 指定されたIDのtodoを削除
  deleteTodoQuery.run(id);

  return c.json({ message: "Task deleted" }, 200);
});

app.onError((err, c) => {
  return c.json({ message: err.message }, 400);
});

serve({
  fetch: app.fetch,
  port: 8000,
});

DBを使うことでサーバーを止めてもデータが消えることがなくなりました。 また、実はデータの検索がO(logN)O(\log N) (NN はレコード数) でできるようになっていたり、AUTOINCREMENTを使うことでidを自動で付与できるようになっていたりします。

これでsimple-todo-appは完成です!お疲れ様でした!