問題01の解答と解説
2015/12/08
解答
先月(2015年11月)の演習問題アプリケーションの設定をデータベースで管理するの出題者(黒田)による解答例です。
4 回に分けて GitHub のリポジトリにコミットしてありますので、ソースコードの差分をご覧ください。
- 04a201e ApplicationSetting モデルを作成
- 34a43fa 管理者によるアプリケーション設定機能
- 0149776 設定されたアプリケーション名の表示
- ea53399 設定に基づくセッションタイムアウト制御
このページは「Rails 演習問題」という連載の一部です。詳しくは、はじめにをお読みください。
解説
今回、演習問題を解いて送ってくれたのは 1 名だけ、という若干寂しい結果となりました。
その 1 名とはたかゆきさん。送っていただいたソースコードの差分は 43ab427 で見ることができます。
基本的な方針は、私の解答例と同じです。アプリケーションの 2 つの設定項目(アプリケーション名とセッションタイムアウトまでの時間)を記録するためのデータベーステーブルを作り、そこに 1 つだけ行を挿入するというものです。
私もたかゆきさんも、application_settings
というテーブルを作りました。
1 つだけの行を持つテーブルを定義するのは大げさなようですが、私はこれでいいと思います。設定項目の数が増えるたらデータベースマイグレーションを実行するだけです。設定項目が増えすぎて扱いづらくなったらテーブルを分割すればいいでしょう。
さて、私とたかゆきさんの実装で大きく異なる点がひとつあります。それは、application_settings
テーブルに「唯一の行」をどのように挿入するかです。
たかゆきさんは、Admin::Base
コントローラに次のようなプライベートメソッドを定義しました。
def initialize_application_setting
unless ApplicationSetting.exists?
ApplicationSetting.create(application_name: "Baukis", expiration_of_session: "60")
end
end
これを before_action
コールバックに登録しています。
悪くないのですが、改善の余地があります。
まず、この書き方だと application_settings
というテーブルに 2 つ以上の行が挿入される可能性がわずかながら存在してしまいます。このアプリケーションが最初に起動した後で、複数の管理者がほぼ同時にアクセスした場合です。そんな可能性はほとんどないと思いますが、ちょっと厳密さを欠くように思えます。
また、管理者ページにアクセスがあるたびに application_settings
というテーブルが空かどうかチェックするのは無駄です。
そこで、私は config/initializers
ディレクトリに初期化スクリプト applicationsettings.rb
を置き、その中で applicationsettings
テーブルに最初の(そして最後の)行を挿入することにしました。
はじめに私が書いたのは次のようなコードです:
if ApplicationSetting.count == 0
ApplicationSetting.create!(
application_name: 'Baukis',
session_timeout: 60
)
end
アプリケーションの初期化フェーズでは「同時アクセス」の問題は起きないので安全ですし、データベースアクセスの無駄も解消されます。
しかし、思いがけない問題に遭遇しました。
まだ application_settings
テーブルが存在しない場合にエラーが発生するという問題です。Rails は ApplicationSetting
という定数が出てきたところでデータベースからテーブル定義を取得しようとし、例外が発生します。
つまり、このアプリケーションを新たな環境に設置した直後に bin/rake db:setup
を実行できないのです。
そこで、私は StackOverflow のこの回答を参考にして、さきほどのコードを次のように書き換えることで問題を回避しました:
if ActiveRecord::Base.connection.tables.include?('application_settings')
ApplicationSetting.count == 0
ApplicationSetting.create!(
application_name: 'Baukis',
session_timeout: 60
)
end
この種のテクニックは、Rails の標準的な教科書には載っていませんね。勉強になりました。
解説(追記)
…と、以上のように書いて Web サイトに公開したわけであるが、実は公開した直後にもっとストレートで分かりやすい方法があることに気が付きました。
application_settings
テーブルの「唯一の行」をシードデータとして投入すればいい、ということに。
Rake タスク db:seed
はまさにそのためにあります。なぜ、そのことに気付かなかったのでしょう。
しかし、もう午前 3 時を過ぎていて疲れていたのでいったん寝ることにしました。
そして、朝起きてすぐに次のようにソースコードを修正しました。
せっかく覚えたテクニックは不要になってしまいましたが、この方がいいですね。
ただし、このアプリケーションがすでに運用中である場合は、別のアプローチが必要です。なぜなら、シードデータの投入はアプリケーションの設置直後に一度だけしか実行できないからです。
この場合は、マイグレーションスクリプトの中でデータを投入します。
- 4ae6214 マイグレーションで application_settings を初期化
class CreateApplicationSettings < ActiveRecord::Migration
def change
create_table :application_settings do |t|
t.string :application_name, null: false
t.integer :session_timeout
t.timestamps null: false
end
ApplicationSetting.create!(
application_name: 'Baukis',
session_timeout: 60
)
end
end
しかし、私の考えではマイグレーションスクリプトの中でモデルクラスを利用するのは得策ではありません。将来 application_settings
テーブルの名前を変えたり、削除したりする可能性があります。その場合、ApplicationSettings
クラス自体が存在しないことになるので、それを利用しているマイグレーションスクリプトが動かなくなるからです。
最新のデータベーススキーマが db/schema.rb
に記録されていることを考えれば、過去のマイグレーションスクリプトは動かなくてもよい、という考え方もあるかもしれません。しかし、私は開発中に bin/rake db:migrate:reset
コマンドをよく使うので、それでは困ります。
そこで、生の SQL を発行するようにマイグレーションスクリプトを書き換えます。
class CreateApplicationSettings < ActiveRecord::Migration
def change
create_table :application_settings do |t|
t.string :application_name, null: false
t.integer :session_timeout
t.timestamps null: false
end
timestamp = Time.current.to_s(:db)
execute(%Q{
INSERT INTO application_settings
(application_name, session_timeout, created_at, updated_at)
VALUES ('Baukis', 60, '#{timestamp}', '#{timestamp}')
})
end
end
しかし、単にこのように書くとマイグレーションをロールバックするときに execute
メソッドのところで例外 ActiveRecord::IrreversibleMigration
が発生してしまいます。
この問題を回避するには、次のように書いてください。
- f6539f9 生 SQL で application_settings を初期化
class CreateApplicationSettings < ActiveRecord::Migration
def change
create_table :application_settings do |t|
t.string :application_name, null: false
t.integer :session_timeout
t.timestamps null: false
end
reversible do |dir|
dir.up do
timestamp = Time.current.to_s(:db)
execute(%Q{
INSERT INTO application_settings
(application_name, session_timeout, created_at, updated_at)
VALUES ('Baukis', 60, '#{timestamp}', '#{timestamp}')
})
end
end
end
end
実を言えば、この追記を書くまで reversible
メソッドのことを私は知りませんでした。Google 検索で回避策を調べる過程で Reversible migrations with Active Record という記事に教えてもらいました。Rails 4.0 で導入されたのですね。またひとつ勉強になりました。