ユーザー別にデータベース管理

ユーザー別に管理する方法は以下の方法でやってみた。(表計算ソフトに例えれば、ユーザーを識別する列を追加して、フィルターで対象ユーザーだけ表示するようなイメージだ。)

  • ユーザー別に管理するためには、softwareテーブルにユーザーを識別するフィールドを追加する。
  • 新規作成した時は、ユーザー識別フィールドへ、ログインユーザーのidを記録する。
  • 一覧表示や詳細、編集する時は、ユーザー識別フィールドがログインユーザーと同じものしか照会できないようにする。

それでは、作業開始。

user_idフィールドの追加

ユーザーを識別するために、softwaresテーブルにuser_idフィールドを追加した。Login Engineがusersテーブルを利用しているので、そのテーブルと関連を持たせるために、フィールド名はRailsの規約に則りuser_idにする。

  • フィールドを追加するマイグレーションファイルを作成した。(script/generate migration add_user_id_to_softwaresと同等の操作をRadRailsで行う。)
    • RadRailsの「ジェネレーター」タブで、プルダウンメニューのmigrationを選択、右項目にadd_user_id_to_softwaresを入力、実行ボタンを押す。
    • db/migrate/005_add_user_id_to_softwares.rbというマイグレーションファイルが作成される。
class AddUserIdToSoftwares < ActiveRecord::Migration
  def self.up
    add_column :softwares, :user_id, :integer
  end

  def self.down
    remove_column :software, :user_id
  end
end
  • RadRailsの「Rake タスク」タブで、プルダウンメニューのdb:migrateを選択、実行ボタンを押す。

以上で、softwaresデーブルに、user_idフィールドを追加できた。

with_scopeの利用

最初は、conditionsでログインユーザーのidを条件に設定してみた。しかしその後、with_scopeというメソッドの存在を知り、嬉しくなって早速、利用。下記が追記したコードの全体像。オレンジ色の行が追記したコード。

softwares_controller.rbへの追記
class SoftwaresController < ApplicationController
  include SoftwaresHelper

...(途中省略)...

protected
  def login_user
    { :find   => {:conditions => ["user_id = ?", session[:user].id]},
      :create => {:user_id  => session[:user].id} }
  end
  
public
  
  def index
    list
    render :action => 'list'
  end

  def list
  Software.with_scope(login_user) do
    @keyword_id = params[:keyword_id] unless params[:keyword_id] == flash[:keyword_id]
    flash[:keyword_id] = @keyword_id
    
    @order_field = params[:order_field]
    @order_direction = params[:order_direction] if params[:order_field] == flash[:order_field]
    @order_next_direction = @order_direction == nil ? 'desc' : nil
    @direction_mark = @order_direction == nil ? '▲' : '▼'
    flash[:order_field] = @order_field
    
    #conditions = ["user_id = ?", session[:user].id]
    conditions = ["keyword_id = ?", @keyword_id] unless @keyword_id == nil
    order = "#{@order_field} #{@order_direction}" unless @order_field == nil
    @software_pages, @softwares = paginate :softwares, :per_page => 2,
                                           :conditions => conditions,
                                           :order => order
  end
  end

...(途中省略)...

  def show
  Software.with_scope(login_user) do
    @software = Software.find(params[:id])
  end
  end

  def new
    @software = Software.new
  end

  def confirm_create
    @software = Software.new(params[:software])
    render :action => 'new' unless @software.valid?
  end

  def create
    @software = Software.new(params[:software])
    @software.user_id = session[:user].id
    if params[:btn_cancel]
      render :action => 'new'
    elsif @software.save
      flash[:notice] = localize(:model, 'software') + localize(:command, :successfully_created)
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end

  def edit
  Software.with_scope(login_user) do
      @software = Software.find(params[:id])
  end
  end

  def confirm_update
    @software = Software.new(params[:software])
    render :action => 'edit' unless @software.valid?
  end

  def update
  Software.with_scope(login_user) do
    @software = Software.find(params[:id])
    if params[:btn_cancel]
      @software.attributes = params[:software]
      render :action => 'edit'
    elsif @software.update_attributes(params[:software])
      flash[:notice] = localize(:model, 'software') + localize(:command, :successfully_updated)
      redirect_to :action => 'show', :id => @software
    else
      render :action => 'edit'
    end
  end
  end

  def confirm_destroy
  Software.with_scope(login_user) do
    @software = Software.find(params[:id])
  end
  end

  def destroy
  Software.with_scope(login_user) do
    if params[:btn_cancel]
      redirect_to :action => 'list'
    else
      Software.find(params[:id]).destroy
      redirect_to :action => 'list'
    end
  end
  end
end


with_scopeのために追記した部分を詳しく見てみると...

  • login_userというメソッドを定義して、単にスコープの条件を返すようにしている。with_scope()..doを複数書く時に、条件が一箇所にまとまっている方が見通しが良い。
    • :findには、findやpaginateの読み出しの制限条件を書く。ここでは、ログインユーザーのデータだけ見ることが出来る条件。
    • :createには、新規作成createの制限条件を書く。ここではcreateした時に、ログインユーザーのidがデフォルトで設定される条件。
protected
  def login_user
    { :find   => {:conditions => ["user_id = ?", session[:user].id]},
      :create => {:user_id  => session[:user].id} }
  end
  • with_scope()..doのブロックで囲ってあげれば、そのブロックでは、データベースへのアクセスがwith_scopeの条件範囲に限定される。
  def show
  Software.with_scope(login_user) do
    @software = Software.find(params[:id])
  end
  end
  • しかし、ここで一つ問題が発生した。標準のscaffoldで確認してみると、下記のようにブロックで囲ってもwith_scopeの条件は無視されてしまう。どうやら、newやsaveはwith_scopeでは制限できないようだ。
  def create
  Software.with_scope(login_user) do
    @software = Software.new(params[:software])
    if @software.save
      flash[:notice] = 'Software was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end
  end
  • 新規作成で制限できるのはcreateだけ。with_scopeを利用するなら、以下のようにすれば制限されるようだ。
  def create
  Software.with_scope(login_user) do
    if @software = Software.create(params[:software])
      flash[:notice] = 'Software was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end
  end
  • ところが、jascaffoldではキャンセルボタンを押した時に、入力されたフォームの内容を保持して新規作成ページへ戻る必要がある。そのため、最初の@software = Software.new(params[:software])は、どうしても必要なコードになる。with_scopeを使うのはやめて、下記のようにuser_idにログインユーザーのidを代入することにした。
  def create
    @software = Software.new(params[:software])
    @software.user_id = session[:user].id
    if params[:btn_cancel]
      render :action => 'new'
    elsif @software.save
      flash[:notice] = localize(:model, 'software') + localize(:command, :successfully_created)
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end

jascaffold生成コードの修正

user_idフィールドを追加したので、その追加による修正も必要だ。修正したのは以下の2ファイル。

softwares_controller.rbへの追記
  • hide_feildに、:user_idを追加した。
class SoftwaresController < ApplicationController
  include SoftwaresHelper

  hide_field :id, :url, :user_id, 
    :only => [:index, :list, :list_by_keyword, :list_in_order]
  hide_field :id, :created_on, :updated_on, :user_id, 
    :only => [:new, :confirm_create, :create, :edit, :confirm_update, :update]

...(以下省略)...
views/models/softwares/_hidden.rhtmlへの追記
  • _hidden.rhtmlは、新規作成や修正の確認ページでキャンセルボタンを押した時に、直前の入力ページにフォームの内容を保持して戻るために存在している。*1
<%= localized_error_messages_for 'software' %>

<!--[:]-->
  
      <%- unless @controller.class.hidden_field?(@action_name, 'title') -%>
        <%= hidden_field "software", "title" %>
      <%- end -%>

...(途中省略)...

      <%- unless @controller.class.hidden_field?(@action_name, 'user_id') -%>
        <%= hidden_field "software", "user_id" %>
      <%- end -%>
<!--[:]-->


これでなんとか、ログインユーザー別に管理できるようになった。

参考リンク

以下のページをたいへん参考にさせて頂きました。感謝です!

*1:確認ページではフォームを利用してないので、入力データを保持するためにhidden_fieldで隠れたフォームを作成しているようだ。confirm_create.rhtmlやconfirm_update.rhtmlでは、render :partial => 'show2'とrender :partial => 'hidden'の両方を呼び出している。試しに_hidden.rhtmlを削除してしまったら、キャンセルボタンで戻った時に、今まで入力した内容がクリアされてしまった。