

Railsで検索機能を作りたいと思っているのですが、ハマってしまっているのでご教授賜りたく思います。  
Railsで
ArticleとItemというモデルがあり、Itemの中にはURLカラムがあり、そのURLをもとに検索を部分一致検索をかけられる検索フォームを作りたいと思っています。  
現状は`@items = Item.where("url like
%#{params[:search]}").uniq`でフォームに入力されたパラメーター(:search)をもとにlikeで含む検索を可能にしています。  
ここまではできているのですが、ここからItemに紐付いたArticleのレコードを引っ張りたいのですが、方法が思い浮かびません。ArticleとItemはItemにarticle_idがAricleのidと紐付いています。


CC BY-SA 3.0
2015-10-15T02:49:07.707
17665
2015-10-15T05:44:01.297
2015-10-15T05:03:05.960
2646
1065
question
ruby-on-rails ruby
Railsで関連テーブルのカラムを対象に検索したい


モデルには以下のように`has_many :items`が定義されているとします

[code]

    class Article < ActiveRecord::Base
      has_many :items
    end
    
    class Item < ActiveRecord::Base
    end
    
[/code]

その時`joins`を利用することで実現できます

[code]

    articles = Article.joins(:items).where("items.url like '%#{params[:search]}'").uniq
    
[/code]


CC BY-SA 3.0
2015-10-15T03:26:56.017
17667
2015-10-15T05:04:13.237
2015-10-15T05:04:13.237
85
2646
17665
answer


joinsを使えば関連するテーブルに対する検索を実行できます。

が、文字列に直接パラメータを埋め込むとSQLインジェクションを引き起こすので大変危険です。  
次のように?を使ってバインドするようにしてください。

[code]

    articles = Article.joins(:items).where("items.url LIKE ?", "%#{params[:search]}").uniq
    
[/code]

SQLインジェクションはこちらに詳しい説明があります。

[Rails セキュリティガイド | Rails
ガイド](http://railsguides.jp/security.html#sql%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)

**P.S.**

余談ですが、SQLインジェクションが何かを理解していると以下の4コママンガの面白さが理解できるようになります。

[xkcd: Exploits of a Mom](https://xkcd.com/327/)

[![画像の説明をここに入力](https://i.stack.imgur.com/NOXkO.png)](https://i.stack.imgur.com/NOXkO.png)

**翻訳**

  1. 「もしもし。息子さんの小学校の者です。ちょっと今コンピュータトラブルが発生しています。」
  2. 「あら、たいへん。息子が何か壊したんでしょうか？」「ある意味そうですね。。」
  3. 「お母さん、あなたは本当に息子さんに "Robert'); DROP TABLE Students; --" という名前を付けたんでしょうか？」「ええ。私たちは "ボビーテーブルちゃん" って呼んでます。」
  4. 「とりあえず、我々は今年度の生徒データを喪失しました。あなたにとっては、してやったりでしょうね。」「それといい勉強になったかしら。データベースの入力値はサニタイズすべきだって。」

### 追記

Rails 4.2だと`sanitize_sql_like`というメソッドが使えます。  
使い方はこんな感じです。

[code]

    class Article < ActiveRecord::Base
      has_many :items
      scope :keyword_search, ->(keyword) {
        joins(:items).where("items.url LIKE ?", "%" + sanitize_sql_like(keyword))
      }
    end
    
    # controller
    articles = Article.keyword_search(params[:search])
    
[/code]

LIKE検索では'%'や'_'が特殊な意味を持ちます。  
sanitize_sql_likeを使うと、こうした文字列をエスケープしてくれます。

例: '100%'というキーワードが入力された場合のSQL

[code]

    SELECT ... WHERE (items.url LIKE '%100\%')
    
[/code]


CC BY-SA 3.0
2015-10-15T04:26:19.513
17670
2015-10-15T05:44:01.297
2015-10-15T05:44:01.297
85
85
17665
answer


まず、

[code]

    where("url like %#{params[:search]}")
    
[/code]

はいろいろまずいのでそこを直します。まず根本的なところで検索文字列がリテラルになっていないのでもとのコードはきっと

[code]

    where("url like '%#{params[:search]}'")
    
[/code]

だったのでしょう。で、これには

  * SQLインジェクションが可能
  * 検索キー中に%や_があった時にLIKEのワイルドカードとして扱われる

という結構重大な問題があるので、Arelを使ってこう書きかえます。

[code]

    search_key = "%" + params[:search].gsub(/[%_]/, "\\\\\0")
    Item.where(Item.arel_table[:url].matches(search_key, "\\"))
    
[/code]

これで上記の問題は無くなります。ただし、Railsが古いと、`#matches`が第二引数をサポートしていないかもしれません。そのときは

[code]

    Item.where(%q!"items.url" LIKE ? ESCAPE '\\'!, search_key)
    
[/code]

でお茶を濁します。`joins`などを使った場合にカラム名の重複が起きてエラーになることがあるので、カラム名はテーブル修飾をしておいた方がいいです。

その上で本題ですが、検索対象の主眼はどちらでしょうか。

## `Item`を検索したい。検索された`Item`から関連する`Article`も参照したい

`Item`に`belongs_to :article`のリレーションを追加しておきます。

[code]

    @item = Item.where(...)
    @items[0].article
    
[/code]

でarticleを参照します。

## `Item`の要素を使って`Article`を検索したい

`Article`に`has_many :items`のリレーションを追加しておきます。

[code]

    Article.joins(:items).where(...).uniq
    
[/code]

もとのコードの`#uniq`はあまり意味が無かったのですが、この場合は`#uniq`を付けておかないと、同じ`article_id`を持つ`Item`が複数あった場合に`Article`が重複して結果に出てきます。


CC BY-SA 3.0
2015-10-15T04:46:52.240
17671
2015-10-15T05:04:54.780
2015-10-15T05:04:54.780
5793
5793
17665
answer