読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでVirtual Attributes(仮想的な属性)をする

Rails中級 Rails Model Rails View

Virtual Attributesとは、モデルのDBに存在しない仮想的な属性のことです。
モデルにVirtual Attributesを追加することで、DBを変更しないでフォームを変更することができるようになります。

言葉ではいまいち分かりづらいので、次のような、「名前」の入力フィールドと「グループ」のセレクトボックスがある画面を、
f:id:nipe880324:20141206212234p:plain:w420


Virtual Attributesを使って、DBのカラムを変更しないで、「名前」が「苗字」と「氏名」に分かれ、グループを追加する入力フィールドを追加できます。
f:id:nipe880324:20141206212239p:plain:w420


Virtual Attributesの使いどころとして、DBをマイグレートするのが大変、その他なんらかの理由によりDBをいじれない場合に、モデルにVirtual Attributesを追加することでフォームの内容を変更することが可能です。
もちろん、バリデーションも行えます。


動作確認

  • Rails 4.1
  • ActiveRecord 4.1.7

目次

  1. 名前入力欄を苗字と氏名入力欄に分ける
  2. 新規グループ入力欄を追加する


1. 名前入力欄を苗字と氏名入力欄に分ける

まず、フォームを修正し、「苗字」と「氏名」の入力フィールドを追加します。

# users/_form.html.erb

<!-- 変更前
<div class="field">
  <%= f.label :name %><br>
  <%= f.text_field :name %>
</div>
-->

<!-- 変更後 -->
<div class="field">
  <%= f.label :last_name, "苗字" %><br>
  <%= f.text_field :last_name %>
</div>
<div class="field">
  <%= f.label :first_name, "氏名" %><br>
  <%= f.text_field :first_name %>
</div>


この状態で画面にアクセスすると、undefined method `last_name' for Userという例外が発生します。
そのため、モデルにVirtual Attributes(仮想的な属性)として、last_namefirst_nameを追加します。

  • フォームからの first_name と last_name という入力を、結合し、DBのnameカラムに保存しています。
  • また、DBのnameカラムを取得し、分割し、フォームの first_name と last_name として返しています。
  • あとは、バリデーションエラー時の新規画面(new.html.erb)を再表示した際にデータを保持しておく必要があるので、インスタンス変数の値を使っています。
# app/models/user.rb

class User < ActiveRecord::Base
  belongs_to :group

  validates :name,       presence: true

  # 追加箇所開始
  # セッターを定義。new, create, updateなどのときに設定される
  attr_writer :last_name, :first_name

  validates :last_name,  presence: true
  validates :first_name, presence: true

  before_validation :set_name

  # 画面表示時に呼ばれる
  def last_name
    @last_name || self.name.split(" ").first if self.name.present?
  end

  # 画面表示時に呼ばれる
  def first_name
    @first_name || self.name.split(" ").last if self.name.present?
  end

  # DBのカラムはnameのため、last_nameとfirst_nameを
  # バリデーションの前に結合させて、設定しておく必要がある
  def set_name
    self.name = [@last_name, @first_name].join(" ")
  end
  # 追加箇所終了
end


これで、例外は発生しなくなりました。
では、コントローラのStrongParametersでlast_namefirst_nameの入力を許可させます。

def user_params
  params.require(:user).permit(:last_name, :first_name, :group_id)
end


では、サーバーを起動して動作を確認します。
「苗字」や「氏名」が入力されていないとバリデーションエラーになります。
f:id:nipe880324:20141206213818p:plain:w420


もちろん、「苗字」と「氏名」を入力すれば、「苗字」と「氏名」を結合した値が名前(name)としてDBに保存され、画面に表示されます。
f:id:nipe880324:20141206213934p:plain:w420




2. 新規グループ入力欄を追加する

次は、新規グループの入力欄を追加します。
まずは、フォームに入力欄を追加します。

# app/views/users/_form.html.erb

<div class="field">
  <%= f.label :group_id %><br>
  <%= f.select :group_id, Group.order(:name).map { |g| [g.name, g.id] }, include_blank: true %>
</div>
<!-- 追加箇所 開始 -->
<div class="field">
  <%= f.label :new_group_name, "グループを追加:" %>
  <%= f.text_field :new_group_name %>
</div>
<!-- 追加箇所 終了 -->


この状態で画面にアクセスすると、undefined method `new_group_name' for Userという例外が発生します。
そのため、モデルにVirtual Attributes(仮想的な属性)としてnew_group_nameを追加します。

# app/models/user.rb

class User < ActiveRecord::Base
  attr_writer :last_name, :first_name

  belongs_to :group

  validates :name,       presence: true
  validates :last_name,  presence: true
  validates :first_name, presence: true

  before_validation :set_name

  def last_name
    @last_name || self.name.split(" ").first if self.name.present?
  end

  def first_name
    @first_name || self.name.split(" ").last if self.name.present?
  end

  def set_name
    self.name = [@last_name, @first_name].join(" ")
  end

  # 追加箇所 開始
  # データの変換は必要ないので単純にattr_accessorで読み書きを可能にしている
  attr_accessor :new_group_name

  # バリデーション前にグループのバリデーションをする
  before_validation :build_group

  def build_group
    # new_group_nameが設定されたいない場合は、グループを作成しない
    # ため、アーリーリターンをする
    return if new_group_name.blank?

    @group = Group.new name: new_group_name
    if @group.valid?
      # グループのバリデーションエラーが発生しなかった場合、
      # 値を設定しておく。Userを保存するときにグループも一緒に作成される 
      self.group = @group
    else
      # グループのバリデーションエラーが発生した場合、
      # グループのバリデーションエラーのメッセージをUserモデルに設定
      @group.errors.each do |_, message|
        errors.add :new_group_name, message
      end
    end
  end
  # 追加箇所 終了
end


ちなみに、グループ(Group)のバリデーションは次のようになっています。
>|ruby|
class Group < ActiveRecord::Base
  has_many :users
  validates :name, presence: true, uniqueness: true
end


では、StrongParametersでnew_group_nameを許可するようにします。

def user_params
  params.require(:user).permit(:last_name, :first_name, :group_id, :new_group_name)
end


では、動作確認をします。既に登録済みのグループ名を追加しようとするとバリデーションエラーが発生します。
f:id:nipe880324:20141206220312p:plain:w420


登録されていないグループ名を入力し、登録すると、新規でグループが作成され、そのグループが設定されます。
f:id:nipe880324:20141206220403p:plain:w420


以上です。