第12章 ファイル保存の先にデータベースがある
第11章では、映画図鑑が動かないときに、画面、Network タブ、Sinatra のログ、ERB、JSON ファイルを分けて見る方法を学びました。
この章では、新しい機能を増やしません。映画図鑑で作ってきたものを振り返り、JSON ファイルによる保存の限界を、同時更新、データ量、関連、整合性の四つの観点から考えます。
映画図鑑でできるようになったこと
ここまでで、映画図鑑は小さな Web アプリケーションとして一通り動く状態になりました。
- 映画の一覧を表示する。
- 映画の詳細を表示する。
- フォームから映画を登録する。
- 登録済みの映画を編集する。
- 登録済みの映画を削除する。
- JSON ファイルへ保存する。
- GET、POST、PATCH、DELETE を使い分ける。
- 登録、更新、削除後にリダイレクトする。
- 利用者入力をエスケープして表示する。
- 見つからない映画や URL に 404 を返す。
- Network タブとログで動きを確認する。
この本の目的は、映画図鑑を実用的なサービスとして完成させることではありません。Sinatra を使って、Web アプリケーションがリクエストを受け取り、処理し、レスポンスを返す流れを自分の手で確かめることでした。
その目的に対して、JSON ファイル保存は十分に役立ちました。リクエストごとに消える Ruby の変数だけではなく、次のリクエストでも残るデータを扱えるようになったからです。
1 ファイル全体を読み書きしている
映画図鑑では、映画データを data/movies.json に保存しています。
保存処理は、次のような流れです。
def load_movies
JSON.parse(File.read(MOVIES_FILE))
end
def save_movies(movies)
File.write(MOVIES_FILE, "#{JSON.pretty_generate(movies)}\n")
end
この実装では、映画を 1 件だけ更新する場合でも、まず movies.json 全体を読み込みます。その後、Ruby の配列とハッシュを変更し、最後にファイル全体を書き戻します。
映画が数件なら、この方法は分かりやすく、学習用として扱いやすいです。しかし、映画が数千件、数万件に増えたらどうでしょうか。1 件を変更するだけでも、毎回すべてのデータを読み書きすることになります。
ファイル保存は、データが増えても必要な部分だけを効率よく探す仕組みを自分で用意しなければなりません。
同時に書き込むと壊れる可能性がある
本書の映画図鑑は、単一ユーザーがローカル環境で使う前提でした。
もし複数人が同時に使うアプリケーションにしたら、どうなるでしょうか。
例えば、次のような順番が起きるかもしれません。
1. A さんのリクエストが movies.json を読む。
2. B さんのリクエストも movies.json を読む。
3. A さんのリクエストが映画を追加して movies.json を書き戻す。
4. B さんのリクエストが、古い内容をもとに movies.json を書き戻す。
この場合、A さんが追加した映画が、B さんの書き戻しで失われる可能性があります。さらにタイミングによっては、ファイルの途中までしか書かれず、JSON として読めない状態になるかもしれません。
同時に書き込まれてもデータを壊さない仕組みは、Web アプリケーションでは重要です。ファイル保存にも対策の方法はありますが、本書では排他制御を扱いませんでした。ここでは問題を知るだけに留め、対策の実装は次の学習へ送ります。一般的な Web アプリケーションでは、このような問題を扱うためにデータベースを使うことが多くなります。
データが増えたときに探しにくい
映画図鑑では、ID で映画を探すときに配列を順番に見ています。
def find_movie(id)
load_movies.find { |movie| movie["id"] == id }
end
これも、映画が数件なら問題になりません。
しかし、データが増えると、探すたびに全件を確認することになります。タイトルで検索したい、公開年で絞り込みたい、ジャンルごとに並べたい、といった要求が出てくると、Ruby のコード側に検索や並び替えの処理が増えていきます。
本書では検索、絞り込み、並び替え、ページネーションを扱いませんでした。それらは、Web アプリケーションとして自然な機能ですが、ファイル保存のまま進めると、保存方式の限界と実装の複雑さが同時に出てきます。
データ同士の関連を扱いにくい
映画図鑑では、映画 1 件の中に監督名やジャンルを文字列として保存しました。
{
"id": "c55c1d37-f3cf-469e-a746-a3044279c716",
"title": "月面喫茶",
"director": "山田アキラ",
"year": "2042",
"genre": "SF",
"description": "月面にある小さな喫茶店を舞台にした物語。"
}
この形は、映画 1 件だけを表示するには分かりやすいです。
しかし、次のようなことをしたくなると、話が変わります。
- 同じ監督の映画を一覧したい。
- ジャンルを自由入力ではなく、別の一覧から選ばせたい。
- 映画ごとに複数のレビューを持たせたい。
- レビューを書いた人を管理したい。
このような場合、映画、監督、ジャンル、レビュー、ユーザーのように、データ同士の関係を考える必要があります。
ここで挙げたレビューやユーザーは、関連を考えるための例です。本書の映画図鑑には、レビュー投稿やログイン機能は追加しません。
JSON でも複雑な構造は作れます。けれども、どこに何を入れるか、同じ情報を重複して持たないか、片方を更新したときにもう片方も正しく変わるかを、自分で管理しなければなりません。
整合性を守る仕組みが足りない
整合性とは、データ同士のつじつまが合っていることです。「あるはずのデータがない」「同じ意味の値が場所によって違う」といった状態を避けるための考え方です。
例えば、映画が削除されたのに、その映画 ID を指すレビューだけが残っていたら、データのつじつまが合っていません。ジャンル名を「SF」から「サイエンスフィクション」へ変更したいとき、映画ごとに保存された文字列をすべて書き換えなければならないなら、書き換え漏れが起きるかもしれません。
本書の映画図鑑は、1 種類のデータだけを扱いました。そのため、整合性の問題を深く考えずに済みました。
アプリケーションが大きくなると、「データをどう分けるか」「どのデータがどのデータを参照するか」「消してよいデータと消してはいけないデータは何か」を考える必要があります。ここから先が、データベース設計の入口です。
ER 図や正規化はここでは扱わない
この章では、ER 図や正規化の説明には進みません。
ER 図は、データ同士の関係を図で表すためのものです。正規化は、データの重複や不整合を減らすために、データの分け方を整理する考え方です。どちらも重要ですが、本書の中心ではありません。
本書では、まず Web アプリケーションの流れを学びました。
- ブラウザからリクエストが送られる。
- Sinatra がルートを選ぶ。
paramsから入力値を受け取る。- Ruby の配列とハッシュを操作する。
- ERB で HTML を作る。
- レスポンスを返す。
- Network タブで HTTP を観察する。
この流れを自分で作ったからこそ、次にデータベースを学ぶ意味が分かります。データベースは、Web アプリケーションの外側に突然出てくるものではありません。保存、検索、更新、削除をより安全に、効率よく、関係を保って行うための仕組みです。
Rails で隠れて見えにくくなるもの
Rails では、データベース操作の多くを Active Record が扱います。
例えば、Rails では次のようなコードでデータを保存する場面があります。
movie = Movie.new(title: "月面喫茶")
movie.save
この短いコードの裏側では、データベースへ値を保存する処理が行われます。Active Record は、Ruby オブジェクトとデータベースのテーブルを対応させ、関連、バリデーション、データベース操作を扱うための仕組みです。
Rails を使うと、JSON ファイルを読み込んで、配列を探し、ファイル全体を書き戻すコードは見えにくくなります。これは便利です。しかし、見えないからといって、Web アプリケーションの基本が消えるわけではありません。
Rails に進んでも、次の考え方は残ります。
- リクエストには URL と HTTP メソッドがある。
- フォームから送られた値はサーバー側で受け取る。
- 保存後はリダイレクトして、再送信を避ける。
- 利用者入力は表示時に安全に扱う。
- 見つからないデータには適切なステータスを返す。
- データの保存先には、扱いやすさと限界がある。
Sinatra で手を動かして作った経験は、Rails で隠れている処理を想像する助けになります。
次に学ぶこと
映画図鑑は、本書の範囲では完成です。
ただし、Web アプリケーション開発の学習はここで終わりではありません。次に学ぶとよいのは、データベース設計です。
データベース設計では、例えば次のようなことを考えます。
- 映画、監督、レビューのようなデータをどう分けるか。
- それぞれのデータがどのような属性を持つか。
- データ同士がどのように関係するか。
- 重複や不整合をどう減らすか。
その後で Rails を学ぶと、Active Record の便利さだけでなく、その裏にあるデータの構造も理解しやすくなります。
本書で作った映画図鑑は、複雑なアプリケーションではありません。しかし、Web アプリケーションの基本を一つずつ見える形で作りました。別の題材を作るときも、URL、フォーム、保存、表示、リダイレクト、エスケープ、エラー応答の流れを同じように設計できます。この経験を、次の題材や次のフレームワークへ持っていってください。
確認しよう
app.rbのload_moviesとsave_moviesを読み、1 件の更新でもmovies.json全体を読み書きしていることを確認する。data/movies.jsonを開き、映画 1 件がどのようなハッシュとして保存されているかを確認する。- 映画にレビューを追加するとしたら、JSON のどこに入れるかを考える。すぐに実装しなくてよい。
考えてみよう
- JSON ファイル保存のまま、複数人が同時に映画を登録すると、どのような問題が起きそうでしょうか。
- 映画とレビューを別々のデータとして扱う場合、どの値で結びつける必要があるでしょうか。
- Rails で
movie.saveと書けるようになったとき、本書で書いたどの処理が見えにくくなるでしょうか。
この章の完成コード
この章では、完成コードに残す変更はありません。第10章までに完成した映画図鑑を使って、ファイル保存の限界と次の学習へのつながりを確認しました。
さらに学ぶ
- SQLite Appropriate Uses For SQLiteでは、SQLite が適する用途と、別のデータベースを選ぶ方がよい条件を学べます。
- Rails Guides Active Record Basicsでは、Rails でデータベースの表と Ruby のオブジェクトを結び付ける Active Record の役割を学べます。