ユーザー環境を保存する。

調子良くCSVサーバーを運用していると、ユーザーからこんな要望が入りそう。「1ページの表示件数をもっと増やして欲しい、ページ切り替えが面倒だから100件くらい表示して。」と。開発者としても、現在の表示件数は開発段階でテストするのに都合のいい件数にしただけである。特に断る理由もなかったので、「わかりました。」と表示件数を100件に変更したとする。すると今度は別のユーザーから、すぐまた連絡が入る。「どうして表示件数を変更したのか、件数が多すぎて表示に時間がかかる」と。こんなとき、開発者としては一体どうすれば良いのだろう?
そうなのです。1ページの表示件数のような、ユーザーの好みや利用環境によってベストな設定値が変化する場合は、それぞれのユーザー固有の設定を、ユーザーごとに保持できる仕組みが必要なのです。

ユーザー環境保存の仕組み

それでは、どのような仕組みでユーザー固有の設定を保存すべきか?MacBookには、MacOSXという素晴らしいお手本がある。Cocoa開発環境にはユーザー設定を保存するユーザーデフォルトという仕組みがあるので、それを参考にすることに。以下は簡単だが、CSVサーバーでのデフォルト値の設定方法。

  • アプリケーションとしてのデフォルト値を設定する。
  • ユーザーがアプリケーションデフォルト値を変更した時は、その変更をデータベースにユーザーデフォルトとして保存する。
  • ユーザーデフォルトが存在しない場合は、アプリケーションデフォルトを利用して設定する。

1ページの表示件数をユーザーごとに設定

ここでは、まず1ページの表示件数をユーザーごとに設定することを目標にした。以下、自分の作業例を紹介。

デフォルトテーブル、モデル、コントローラーの作成

ユーザーデフォルトを保存するテーブルを作成する必要がある。デフォルト値をどんな形式で保存するかで悩む。思い浮かんだのは以下の2つの方法。

  • (A) 1つのフィールドに、1つの設定を保存する。
  • (B) 1つのフィールドに、複数の設定を保存する。

Railsで今まで経験した処理方法を活かすなら、(A)の方法が簡単そうだ。でも今後、ユーザー設定の項目は増え続けるだろう。そうなった時にその都度フィールドを追加するのは面倒な作業かもしれない...。そう考えて、(B)の方法で試してみることにした。
(B)の方法で実現するために、ユーザー設定は{:per_page=>100}のようなハッシュで設定して、データーベースに保存する時にyamlに変換して、テキストとして保存する予定。実際の作業は以下のようにした。

  1. defaultモデルの作成。
    • RadRailsの「ジェネレーター」で、「model」「default」を指定して実行。
  2. マイグレーションファイルの設定。
  3. マイグレーションの実行。
    • RadRailsの「Rakeタスク」で「db:migrate」を実行。
  4. userモデルとdefaultモデルの関連を設定。
  5. defaultコントローラーの作成。
    • RadRailsの「ジェネレーター」で、「controller」「defaults」を指定して実行。
      • ここで「default(単数形)」にすると、http://localhost:3000/default/アクション名でのURLでアクセス可能になる。
      • 今回は「defaults(複数形)」にしたので、http://localhost:3000/defaults/アクション名が有効になる。ちなみに、script/generate scaffoldを利用すると複数形でのアクセス、コントローラーにscaffold :defaultとしてメソッド呼出しした時は単数形でのアクセスになる。


db/migrate/009_create_defaults.rb
2.マイグレーションファイルの設定
class CreateDefaults < ActiveRecord::Migration
  def self.up
    create_table :defaults do |t|
      t.column :yaml,    :text
      t.column :user_id, :integer
    end
  end

  def self.down
    drop_table :defaults
  end
end
app/models/default.rb、user.rb
4.モデルの関連を設定
  • この関連付けによって、current_user.defaultとすれば、現在ログインしているユーザーのデフォルトを取得できるようになる。(current_userはlogin_engineが提供する現在のログインユーザーを取得するメソッド)
class Default < ActiveRecord::Base
  belongs_to :user
...(途中省略)...


class User < ActiveRecord::Base
  has_one :default
...(途中省略)...
環境設定のビューとコントローラー
app/views/defaults/edit_preference.rhtml
ビュー(ユーザー環境設定のページ)
  • お馴染みのscaffoldのコードを参考に、ユーザー環境設定のページを作る。
<h1>ユーザー環境の設定</h1>

<div>
<%= link_to 'アプリケーションの初期設定へ戻す', :action => 'set_app_default' %>
</div>
<br />

<%= start_form_tag :action => 'update_preference', :id => @default %>
  <%= render :partial => 'form' %>
  <br />
  
  <div>
  <%= link_to 'キャンセル', :controller => 'csvs', :action => 'list' %> |
  <%= submit_tag '更新' %>
  </div>
<%= end_form_tag %>
app/views/defaults/_form.rhtml
ビュー(ユーザー環境設定のフォームの部分)
  • フィールドにper_pageは存在しないので、データベースと連動したtext_fieldはそのまま使えない。ちょっと工夫が必要だ。text_field_tagヘルパメソッドを利用することにした。
  • 環境設定の送信パラメーターは、コントローラーではparams[:default]で一括して取得できるようにしたい。
  • :name => "default[per_page]"オプションを指定することで、{"default"=>{"per_page"=>"100"}}のようなパラメーターが送信されるようになる。
<table>
<tr>
  <th align="right">
  <label for="per_page">1ページ当りの表示件数:</label>
  </th>
  <td>
  <%= text_field_tag 'per_page', @default_hash['per_page'], :name => "default[per_page]" %>
  </td>
</tr>
</table>
app/controllers/defaults_controller.rb
コントローラー(ユーザー環境の設定ページ)
  • Csv.show_column_namesは、CSVファイルのリストページで表示する列を、配列で返してくれる。(今回、自分で定義したメソッド)以下のような配列が取得できる。
      • %w{file_name file_comment editable file_size created_at file_updated_at management_section_id user_id}
  • Rubyでは、{"per_page"=>"100"}.to_yamlとするだけで、ハッシュはyamlテキストに変換される。簡単だ!以下のように活用した。
      • current_user.default.update_attributes(:yaml => @default_hash.to_yaml)
class DefaultsController < ApplicationController
  def edit_preference
    @default_hash = default_user(true)
  end
  
  def set_app_default
    @default_hash = default_app
    render :action => 'edit_preference'
  end
  
  def update_preference
    current_user.create_default(:yaml => {}.to_yaml) if current_user.default.nil?
    @default_hash = default_user.merge(params[:default])
    
    if current_user.default.update_attributes(:yaml => @default_hash.to_yaml)
      flash[:notice] = 'ユーザー環境を更新しました。'
      redirect_to :controller => 'csvs', :action => 'list'
    else
      render :action => 'edit_preference'
    end
  end
end
  • default_user()は、defaultsテーブルからユーザーデフォルトを読み取るメソッド。アプリケーション全体からいつでも参照したいので、ApplicationControllerに定義してある。
  • 同じく、default_app()は、アプリケーションデフォルトを返すメソッド。以下のように定義してある。
app/controllers/application.rb
コントローラー(ユーザーデフォルト、アプリケーションデフォルトを返す)
  • ユーザーデフォルトとアプリケーションデフォルトを取得するメソッドを定義した。
  • ユーザーデフォルトが存在しない場合は、アプリケーションデフォルトが返される。
  • ユーザーデフォルトが存在する場合でも、アプリケーションデフォルトに上書き更新した状態でハッシュを返すようにした。(新しくユーザー設定項目を追加した場合に備えての対処)
  • Rubyでは、YAML.load(yamlテキスト)とすれば、yamlを元通りに復元したオブジェクトが取得できる。
  • Rails定義のメソッドではないので、ハッシュのキーはシンボルでなく、文字列で指定する必要あり。
  • ハッシュの値は常に文字列として保存して、設定値として利用する時にto_iメソッド等で変換することにした。
require 'login_engine'
class ApplicationController < ActionController::Base
...(途中省略)...
  # デフォルトデータベースからユーザー設定を読み取り、返す。
  # 値は全て文字列としてデータベースに保存されているので、ここで最適な形式に変換する。
  def default_user(force_reload = false)
    default_app.merge(YAML.load(current_user.default(force_reload).yaml)) rescue default_app
  end
  
  # アプリケーションのデフォルト値を返す。
  # ハッシュのキーは、シンボルでなく、文字列で指定する必要あり。:per_page...NG/'per_page'...OK
  # ハッシュの値も、全て文字列で指定する必要あり。'per_page'=>10...NG/'per_page'=>'10'...OK
  def default_app
    {'per_page'=>'10'}
  end
end
force_reloadについて

Railsでは、current_user.defaultで取得した結果(defaultsテーブルの1レコード)は、キャッシュされるらしい。同じユーザーで繰り返しcurrent_user.defaultを利用した時は、タイミングによっては、都度データベースにアクセスしないで、キャッシュされた情報を高速に読み取る。
しかし、CSVサーバーの環境設定のテストをしていると、このキャッシュの仕組みのためか、タイミングによっては反映されていないはずの件数でリスト表示されてしまうことがあった。
この現象を防ぐために、force_reloadという仕組みがあり、current_user.default(true)と指定することで、キャッシュを参照せずに、データベースを直接読み取ってくれる。default_userメソッドで引数にforce_reloadの設定が出来るようにしたのは、このメソッドの中でcurrent_user.defaultを呼び出しており、状況によってforce_reloadを利用したかったからなのだ。

メニューバーに環境設定のリンクを作成

以下のリンクを追記して、メニューバーから環境設定のページへ移動できるようにした。

app/views/layouts/_menu.rhtml
レイアウト(メニューバーの描画)
<%= link_if_authorized_current '設定', {:controller => 'defaults', :action => 'edit_preference'}, :wrap_in => "li" %>


以上の設定で、以下のような設定画面が出来上がる。

ユーザーデフォルトの利用

設定したユーザーデフォルトを利用してみる。1ページの表示件数は、csvs_controller.rbのpaginateメソッドの :per_page=> オプションで利用する。

app/controllers/csvs_controller.rb
コントローラー(1ページの表示件数を設定)
  • :per_pageオプションには(文字列でなく)数値を設定する必要があるので、default_user(true)['per_page'].to_iによって、整数に変換した。
...(途中省略)...
  def set_pagination
...(途中省略)...    
    # ハッシュのキーは、シンボルでなく、文字列で指定する必要あり。:per_page=>NG/'per_page'=>OK
    @csv_pages, @csvs = paginate :csvs, :per_page => default_user(true)['per_page'].to_i, 
                                 :conditions => @conditions, 
                                 :order => @order
...(途中省略)...

注意(自分が悩んだこと)

  • Railsに慣れてくると、ハッシュのキーの指定に、文字列とシンボルの区別を気にしなくなる。しかし、Rubyでは本来:per_page=>"シンボル"と'per_page'=>"文字列"のper_pageは、それぞれ異なるキーと認識される。この真理を見落としていて、単純なことに深く悩んでしまった...。Rails定義のメソッド以外では、注意が必要だ。
  • 同じようなことで、パラメーターでparamsで受け取る値は常に文字列なのだ。Railsはデータベースとやり取りする時には、フィールドに設定された適切な形式に気を利かせて変換してくれているようだ。今回のようにフィールドに直接保存せず、オブジェクトをまとめてyamlに変換して保存している場合は、yamlから復元した時の値も文字列のままだ。利用する時には、自分で適切な形式に変換する必要がある。(paginateの:per_pageオプションは数値で指定する必要があるので、to_iで数値に変換する必要があった。)(数値をyamlに変換した時は、yamlから復元した時も数値で取得できる。)


以上が、ユーザー設定を保存して活用する仕組みの原形だ。これをベースに拡張して行く予定。まだ、入力する時の値のチェック機能がない。