プログラマーだけん、がんばる

神の国、島根のプログラマー。サーバ、Rubyまわりの技術(Ruby on Rails, Rhodes etc..)やiOS, Androidなどの開発を行っていくうえで、役だったことなどを共有できればいいなと思います。

ActiveRecordを継承しないモデルオブジェクトで検索機能の実装

こんにちは。今回はRailsについてです。

ActiveRecordを継承しないモデルオブジェクト

Railsを開発するうえで必須になってくるModelオブジェクト。
今回はActiveRecord::Baseを継承せずに、モデルと同じような動きをするRubyオブジェクトを作成して、検索機能を実装します。

作成したRubyオブジェクトにモデルと同じような動きを持たせるメリットとして、

  • バリデーション機能が使える
  • モデルオブジェクトとして振る舞うので、form_forの引数に渡せる
  • バリデーション時のエラーメッセージを簡単に他言語対応できる

上記の機能を持たせることによって、大きくその恩恵をうけることが出来るのが、フォームなどを使用する、検索機能などです。

今回は、productsリソースに基づいて、検索機能を実装します。
productsテーブルのカラムは1つで、"name"属性、タイプは"string"です。

実装

では、検索機能を実装する、Rubyオブジェクトの定義です。
クラスの名前は「SearchProduct」です。

・lib/search_product.rb

class SearchProduct
  # モデルの機能をincludeする
  include ActiveModel::Model

  # プロパティの定義
  attr_accessor :name

  # バリデートの設定
  validates :name, presence: true

  # 検索メソッドの実装
  def search
    self.valid? ? false : Product.where('name LIKE ?',  "%#{self.name}%")
  end
end

以上で、SearchProductの定義の終了です。
重要なのは下記の一行、

include ActiveModel::Model

ActiveModel::Modelをincludeすることによって、バリデート、属性名の翻訳といった機能をRubyオブジェクトに取り込みます。
あとは、普通のモデルのバリデートと同じように「validates」メソッドを使用してバリデートを宣言します。

  validates :name, presence: true

ここでは、presence: trueのみ設定しています。

以上で、検索オブジェクトの実装は終わりです。

次に、この検索機能とビューの橋渡しを行うコントローラーの作成を行います。

・app/controllers/products_controller.rb

class ProductsController < ApplicationController

  def index
    @search_product = SearchProduct.new
    @products = Product.all
  end

  def search
    # SearchProductインスタンスの作成
    @search_product = SearchProduct.new(product_params_as_search)
    
    # 検索メソッドの実行
    unless @products = @search_product.search
      # バリデーションに引っかかれば全件取得
      @products = Product.all
    end
    render :index
  end

  private

    def product_params_as_search
      params.require(:search_product).permit(:name)
    end
end

ここでは、ビューで検索ボタンを押下された時に、searchアクションに飛んでくることを想定します。
searchアクションの中では、先ほど作成したSearchProductオブジェクトを作成して、検索を実行しています。

・#search

    # SearchProductインスタンスの作成
    @search_product = SearchProduct.new(product_params_as_search)
    
    # 検索メソッドの実行
    unless @products = @search_product.search
      # バリデーションに引っかかれば全件取得
      @products = Product.all
    end


次にViewの実装です。

ここで、作成したSearchProductオブジェクトをform_forに渡しています。
このように、ActiveRecordを継承していなくても、ActiveModel::Modelをincludeするだけでform_forに渡せます。
こうするこで、パラメータなどの成形も簡単に出来ます。
さらにバリデーションを設定しているため、バリデートに引っかかった場合にエラーオブジェクトをとることが出来ます。

・app/views/products/index.html.erb

<!-- エラーがあればエラーメッセージを表示する -->
<% if @search_product.errors.any? %>
  <ul>
    <% @search_product.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

<!-- 作成したSearchProductオブジェクトを渡す -->
<%= form_for(@search_product, url: search_products_path) do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>

  <%= f.submit '検索' %>
<% end %>

<table>
  <thead>
    <tr>
      <th><%= Product.human_attribute_name(:name) %></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
      </tr>
    <% end %>
  </tbody>
</table>

最後に他言語対応です。ActiveRecordの場合と少し変わります。

ja:
  # activerecordではなく、activemodelとして定義する
  activemodel:
    models:
      search_product: 商品
    attributes:
      search_product:
        name: 名前

以上で、ActiveRecordを継承しないモデルオブジェクトで検索機能の実装の終了です。

画面

検索した時の画面はこのような感じです。
検索に一致したレコードのみ表示されています。

f:id:famtom:20131027010212p:plain


検索のテキストフィールに空文字を入れた場合、バリデートに引っかかりエラーが表示されます。

f:id:famtom:20131027010432p:plain

最後に

今回は、「ActiveRecordを継承しないモデルオブジェクト」について書きました。
Railsはどこに書いても大体動くので適材適所、コードを記述する場所を選んだり、役割分担をしっかりするべきです。
また、検索機能のロジックをコントローラーに書いた場合、パラメータの構造や、ビジネスロジックをコントローラーが知っていることになります。
「パラメータの構造をコントローラーが知っているビジネスロジック」は気持ち悪いです。
このような場合、今回のようなRubyオブジェクトなどは大活躍できるはずです。
実装が楽になり、テストが楽になり、再利用が可能になり、自分がかっこ良く見えて、みんなHappyです。

今回作成したサンプルのソースコードです。
https://github.com/tomomura/rails-search-sample