Madogiwa Blog

主に技術系の学習メモに使っていきます。

railsのViewからVueの単一ファイルコンポーネントへモデルのインスタンスを受け渡すMEMO

railsのviewからVueの単一ファイルコンポーネントへモデルのインスタンスをいい感じに渡す方法を模索して、結構ハマってたのですが、自分なりに落ち着いたのでやり方をメモしておきます✍

今回やりたかったこと

やりたかったのは、下記のようにモデルのインスタンスの配列を、そのまま単一ファイルコンポーネントと渡すということがやりたかった。

しかし、viewから単一ファイルコンポーネントへ行くと、railsの世界から離れてしまうので、いい感じに渡す方法がイマイチ思いつかなかった。。。 ※ネットで調べるとviews側にscriptタグ使ってVueのinstanceを生成するとかも書いてあったけど、いまいちな気がしてしまい。。。

<div class="feed-show" id="feeds-entries">
  <h1 class="title"><%= @feed.title %></h1>
  <entry-card-collection entries="<%= @entries %>"></entry-card-collection>
</div>
<%= javascript_pack_tag 'feeds' %>
class FeedsController < ApplicationController
  def show
    @feed = Feed.find(params[:id])
    @entries = @feed.entries.order(published_at: :desc)
  end
end

どうやったか

いろいろハマったけど、やった方法は下記です。

<div class="feed-show" id="feeds-entries">
  <h1 class="title"><%= @feed.title %></h1>
  <entry-card-collection :entries="<%= @entries.to_json %>"></entry-card-collection>
</div>
<%= javascript_pack_tag 'feeds' %>

ポイントは2つ。

  • v-bindで値を受け渡すこと
  • to_jsonしてjson形式で値を渡すこと

あとは、単一ファイルコンポーネント側でpropsを定義して普通に使えばOKです🙌 ※取得する際にJSON.parseする必要があるかなと思いましたが、しなくてもいい感じにデフォルトでParseしてくれるみたいですね🤔

<template>
<div class="entries">
  <entry-card
    v-for="entry in entries"
    :key="entry.id"
    :entry="entry">
  </entry-card>
</div>
</template>
<script>
import EntryCard from './EntryCard'

export default {
  name: 'EntryCardCollection',
  components: { EntryCard },
  props: ['entries']
}
</script>
<style lang="scss">
</style>

参考

www.reddit.com

railsとruby標準ライブラリで作るRSSリーダー的なやつの作り方Memo📝

はじめに

みなさん、こんばんは。まどぎわです(・∀・)
rubyの標準ライブラリにRSS用のライブラリがあることを最近知り、railsと標準ライブラリを使ってRSSリーダー的なものを作ってみたので、作り方とかをメモしておきます✍

作るもの

はてなブログRSSフィードのエンドポイントを登録したら、そこからRSSをパースして表示出来るようなアプリを作っていきます👀

こんな感じのものをイメージしてもらえれば! f:id:madogiwa0124:20190203232035g:plain

使うもの

作り方

モデルを作る

とりあえずモデルを作ります。作るものを下記の2つです。

  • Feed.rb
  • Entry.rb

Feed.rbにはRSSフィードのタイトルとエンドポイントのURLを登録します。Entry.rbには、フィードから取得したエントリーを登録します。

schemeはこんな感じです。

  create_table "entries", force: :cascade do |t|
    t.bigint "feed_id"
    t.string "title"
    t.string "link"
    t.text "description"
    t.datetime "published_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["feed_id"], name: "index_entries_on_feed_id"
  end

  create_table "feeds", force: :cascade do |t|
    t.string "title", null: false
    t.string "endpoint", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

RSSを読み込んでParseする

それでは、RSSを読み込んでパースする部分は結構簡単で、ruby標準のrssライブラリをrequire 'rss'で読み、Net::HTTP.get(URI.parse(endpoint))xmlドキュメントを取得して、RSS::Parser.parseを使ってParseします。
私は、Feedモデルにparsed_xmlを定義してParseしたxmlを取得出来るようにしてみました👀

class Feed < ApplicationRecord
  require 'rss'

  has_many :entries

  def parsed_xml
    xml = Net::HTTP.get(URI.parse(endpoint))
    RSS::Parser.parse(xml)
  end
end

エントリーを取得して保存する

その後は、Parseしたxmlitemsに記事の一覧が入っているので、下記のようなEntryCreaterクラスを作成して、画面登録時に記事一覧を取得して登録するようにしました🙌

class FeedsController < ApplicationController
  def new
    @feed = Feed.new
  end

  def create
    @feed = Feed.new(feed_params)
    if @feed.save
      Feed::EntryCreater.new(@feed).execute
      redirect_to feed_path(@feed)
    else
      render :new
    end
  end

  private

  def feed_params
    params.require(:feed).permit(:title, :endpoint)
  end
end
class Feed::EntryCreater
  attr_reader :feed

  def initialize(feed)
    @feed = feed
  end

  def execute
    Entry.where(feed: feed).delete_all
    feed.parsed_xml.items.map do |item|
      Entry.create(
        feed: feed,
        title: item.title,
        description: strip_tags(item.description).truncate(300),
        published_at: item.pubDate,
        link: item.link
      )
    end
  end

  private

  def strip_tags(text)
    ActionController::Base.helpers.strip_tags text
  end
end

保存したエントリーを表示する

保存したエントリーを表示するのは普通に表示してあげればOKです🙆‍♂️

class FeedsController < ApplicationController
  def show
    @feed = Feed.includes(:entries).find(params[:id])
  end
end
<div class="feed-show">
  <h1 class="title"><%= @feed.title %></h1>
  <div class="entries">
    <%= render partial: 'entry_collection', collection: @feed.entries, as: 'entry' %>
  </div>
</div>
<div class="card">
  <header class="card-header">
    <p class="card-header-title"><%= entry.title %></p>
  </header>
  <div class="card-content">
    <div class="content">
      <%= entry.description %>
      <%= link_to 'サイトで読む', entry.link %>
    </div>
  </div>
  <footer class="card-footer">
    <p class="card-footer-item">
      <%= link_to entry.feed.title, feed_path(entry.feed) %>
    </p>
    <p class="card-footer-item">
      公開日: <%= l(entry.published_at, format: :long) %>
    </p>
  </footer>
</div>

おわりに

rubyの標準ライブラリを使うとRSSを簡単にParseしてRSSリーダー的なものがすぐ作れるんですねー。
他にも標準ライブラリは結構あるので、いろいろ見てみようかと思いました🙇‍♂️

参考

docs.ruby-lang.org

www.buildinsider.net

FactoryBotでtrait付きの関連(Association)を定義する

こんにちは、まどぎわです(・∀・)

今回はFactoryBotでtrait付きの関連(Association)を定義する方法を知ったのでメモしておきます✍

前提: Quizの正解数ランキングを集計する処理の検証

今回は下記のようなクイズへのユーザーの解答を集計してランキングを作成するようなテストコードを書いたものとします、そこまで悪くなさそうですがcreate(:quiz, :with_choices)が何度も出てきてしまっているのがイマイチですね😥

let(:no1_user) { create(:user) }
let(:no2_user) { create(:user) }
let(:no3_user) { create(:user) }

before do
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no1_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no1_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no2_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no2_user, correct: false)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no3_user, correct: false)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no3_user, correct: false)
end

it '正解数の降順で取得出来ること' do
  ranking = described_class.ranking
  expect(ranking.map(&:user_id)).to eq [no1_user.id, no2_user.id, no3_user.id]
end

trait付きの関連(Association)を定義してリファクタリング

こういう場合にtrait付きの関連をfactoryに定義すると便利です👀

FactoryBot.define do
  factory :quiz_user_answer do
    association :quiz, :with_choices
    user
    correct { [true, false].sample }
  end
end

こんな感じで繰り返しcreate(:quiz, :with_choices)書かなくていいのでスッキリしますね🙌

let(:no1_user) { create(:user) }
let(:no2_user) { create(:user) }
let(:no3_user) { create(:user) }

before do
  create(:quiz_user_answer, user: no1_user, correct: true)
  create(:quiz_user_answer, user: no1_user, correct: true)
  create(:quiz_user_answer, user: no2_user, correct: true)
  create(:quiz_user_answer, user: no2_user, correct: false)
  create(:quiz_user_answer, quiz: user: no3_user, correct: false)
  create(:quiz_user_answer, quiz: user: no3_user, correct: false)
end

it '正解数の降順で取得出来ること' do
  ranking = described_class.ranking
  expect(ranking.map(&:user_id)).to eq [no1_user.id, no2_user.id, no3_user.id]
end

参考

github.com

MeCabとRubyで形態素解析をやってみる👩‍🔬

今回は、MeCabを使って形態素解析を行ってみたので、やり方とかをメモしておきます✍

MeCabとは?

MeCab京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです。 言語, 辞書,コーパスに依存しない汎用的な設計を 基本方針としています。

MeCab: Yet Another Part-of-Speech and Morphological Analyzer

MeCabのインストール

インストールの方法は、公式サイトをご確認ください。

http://taku910.github.io/mecab/#install

※公式サイトに書いてないですが、apt-gethomebrewを使ってもインストール出来るようです。

私はMeCabrailsをインストールするDockerfileを作って構築しました👀

gist.github.com

MeCabの使い方

mecabコマンドを実行するとMeCabが起動し形態素解析を行うことが出来ます🙌

$ mecab
鳴かぬなら鳴くまで待とうホトトギス
鳴か  動詞,自立,*,*,五段・カ行イ音便,未然形,鳴く,ナカ,ナカ
ぬ 助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
なら  助動詞,*,*,*,特殊・ダ,仮定形,だ,ナラ,ナラ
鳴く  動詞,自立,*,*,五段・カ行イ音便,基本形,鳴く,ナク,ナク
まで  助詞,副助詞,*,*,*,*,まで,マデ,マデ
待と  動詞,自立,*,*,五段・タ行,未然ウ接続,待つ,マト,マト
う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
ホトトギス 名詞,一般,*,*,*,*,ホトトギス,ホトトギス,ホトトギス
EOS

RubyMecabを使う

mecabを扱うgemがあるので、Gemfileに追記してbundle installします。

gem 'mecab'

MeCab::Tagger.new.parseに文字列を渡すをmecab実行時と同様に形態素解析を行うことが出来ます。

irb(main):003:0> puts MeCab::Tagger.new.parse "鳴かぬなら鳴かせてみようホトトギス"
鳴か  動詞,自立,*,*,五段・カ行イ音便,未然形,鳴く,ナカ,ナカ
ぬ 助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
なら  助動詞,*,*,*,特殊・ダ,仮定形,だ,ナラ,ナラ
鳴かせ   動詞,自立,*,*,一段,連用形,鳴かせる,ナカセ,ナカセ
て 助詞,接続助詞,*,*,*,*,て,テ,テ
みよ  動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ
う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
ホトトギス 名詞,一般,*,*,*,*,ホトトギス,ホトトギス,ホトトギス
EOS
=> nil

今回は、下記のようなClassを使って扱いやすいような形で取得できるようにしてみました👀

class MecabClient
  attr_reader :text, :parsed_text, :words

  def initialize(text)
    @text = text
    parse
  end

  def parse
    @parsed_text = MeCab::Tagger.new.parse @text
    @words = parsed_text_rows.map { |row| build_word(row) }
    @parsed_text
  end

  private

  def parsed_text_rows
    rows = @parsed_text.split("\n")
    rows[0...(rows.length - 1)]
  end

  def build_word(row)
    text = row.split("\t")[0]
    properties = row.split("\t")[1].split(',')
    Word.new(
      text: text,
      category1: properties[0],
      category2: properties[1],
      category3: properties[2]
    )
  end

  class Word
    attr_reader :text, :category1, :category2, :category3

    def initialize(text: nil, category1: nil, category2: nil, category3: nil)
      @text = text
      @category1 = category1
      @category2 = category2
      @category3 = category3
    end

    def noun?
      @category1 == "名詞"
    end

    def number?
      @category2 == ''
    end
  end
end

こんな感じで使えます🙌

irb(main):010:0> pp keyword = MecabClient.new("鳴かぬなら鳴かせてみようホトトギス")
#<MecabClient:0x00005617509423f8
 @parsed_text=
  "鳴か\t動詞,自立,*,*,五段・カ行イ音便,未然形,鳴く,ナカ,ナカ\n" +
  "\t助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ\n" +
  "なら\t助動詞,*,*,*,特殊・ダ,仮定形,だ,ナラ,ナラ\n" +
  "鳴かせ\t動詞,自立,*,*,一段,連用形,鳴かせる,ナカセ,ナカセ\n" +
  "\t助詞,接続助詞,*,*,*,*,て,テ,テ\n" +
  "みよ\t動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ\n" +
  "\t助動詞,*,*,*,不変化型,基本形,う,ウ,ウ\n" +
  "ホトトギス\t名詞,一般,*,*,*,*,ホトトギス,ホトトギス,ホトトギス\n" +
  "EOS\n",
 @text="鳴かぬなら鳴かせてみようホトトギス",
 @words=
  [#<MecabClient::Word:0x0000561750941db8
    @category1="動詞",
    @category2="自立",
    @category3="*",
    @word="鳴か">,
   #<MecabClient::Word:0x0000561750941a20
    @category1="助動詞",
    @category2="*",
    @category3="*",
    @word="">,
   #<MecabClient::Word:0x0000561750941688
    @category1="助動詞",
    @category2="*",
    @category3="*",
    @word="なら">,
   #<MecabClient::Word:0x00005617509412f0
    @category1="動詞",
    @category2="自立",
    @category3="*",
    @word="鳴かせ">,
   #<MecabClient::Word:0x0000561750940f58
    @category1="助詞",
    @category2="接続助詞",
    @category3="*",
    @word="">,
   #<MecabClient::Word:0x0000561750940bc0
    @category1="動詞",
    @category2="非自立",
    @category3="*",
    @word="みよ">,
   #<MecabClient::Word:0x0000561750940828
    @category1="助動詞",
    @category2="*",
    @category3="*",
    @word="">,
   #<MecabClient::Word:0x0000561750940490
    @category1="名詞",
    @category2="一般",
    @category3="*",
    @word="ホトトギス">]>

おわりに

今回は、MeCabrubyを使って形態素解析を行う方法についてまとめてみました。 形態素解析と聞くと難しい感じがしますが、ツールを使うと意外と簡単に出来ますね🙏

Vue.js + marked.js + highlight.jsを使ってシンタックスハイライト機能付きマークダウンエディタを作ってみたのでMEMO

最近、Vue.jsとmarked.jsとhighlight.jsを使ってマークダウンエディタを作ったので、使い方等をBlogにまとめておこうと思います🙇

はじめに

まず作ろうとしているのは、こんな感じです。

f:id:madogiwa0124:20190103202247p:plain

普通のコードシンタックスハイライト機能付きのマークダウンエディタというような形です👀

まずはテンプレートを用意する。

まずは、今回のマークダウンエディタ用のテンプレートを用意します。 入力用のテキストエディタ(#editor)とプレビュー用(#preview)の要素を持つ単一ファイルコンポーネントを下記に記載しました。
また、テキストエディタには入力値をバインドするためにv-modelを記載しています。※スタイルは割愛してます。

<template>
  <form id="post-form">
    <div id="editor">
      <textarea v-model="markdownText">input</textarea>
    </div>
    <div id="preview">
    </div>
  </form>
</template>
<script>
  export default {
    name: 'postForm',
    data: function () {
      return {
        markdownText: ''
      }
    }
  }
</script>

marked.jsを使ってマークダウンテキスト→HTML変換を行う

それでは、marked.jsを使って入力値をHTMLに変換していきます。

https://github.com/markedjs/marked

まずはmarked.jsをインストールしましょう。

$ npm install marked --save

私は上記コマンドで行いましたが、詳しいインストール方法は下記を参考してください。

https://marked.js.org/#/README.md#installation

インストールが終わったらmarked.jsimportします。

import marked from 'marked';

そして下記のcomputedを追加します。markedの引数に変換したい文字列を渡せば、それに応じたHTMLを返してくれます👀

computed: {
  compiledMarkdown: function () {
    return marked(this.markdownText)
  }
},

あとは#previewv-html使ってバインドしてあげればOKです🙌

<div id="preview">
  <div v-html="compiledMarkdown"></div>
</div>

最終的にはこんな感じになります、これでエディタに応じたHTMLがバインドされるようになりました!意外と簡単ですね✨

<template>
  <form id="post-form">
    <div id="editor">
      <textarea v-model="markdownText">input</textarea>
    </div>
    <div id="preview">
      <div v-html="compiledMarkdown"></div>
    </div>
  </form>
</template>
<script>
  import marked from 'marked';

  export default {
    name: 'postForm',
    data: function () {
      return {
        markdownText: ''
      }
    },
    computed: {
      compiledMarkdown: function () {
        return marked(this.markdownText)
      }
    },
  }
</script>

highlight.jsを使ってシンタックスハイライト機能を追加する。

次にhighlight.jsを使ってコードシンタックスハイライト機能を追加していきましょう!

https://highlightjs.org/

まずは先程と同様にhighlight.jsをインストールしましょう!

$ npm install highlight.js --save 

私は上記コマンドで行いましたが、詳しいインストール方法は下記を参考してください。

https://highlightjs.org/usage/

インストールが終わったらhighlightjsimportします。

import hljs from 'highlightjs';

まずは、createdの中でhighlight.jsを使うようにmarkedのオプションを変更します。

created: function () {
  marked.setOptions({
    // code要素にdefaultで付くlangage-を削除
    langPrefix: '',
    // highlightjsを使用したハイライト処理を追加
    highlight: function(code, lang) {
      return hljs.highlightAuto(code, [lang]).value
    }
  });
},

最後にハイライトで使うスタイルシートを読み込みます。私は、github-gist.cssにしましたが、他にも色々あるようなのでお好きなスタイルを使ってみてください👀

<style src='highlightjs/styles/github-gist.css'></style>

最終的には、こんな感じになりました!これでハイライト機能が出来ました🙌

<template>
  <form id="post-form">
    <div id="editor">
      <textarea v-model="markdownText">input</textarea>
    </div>
    <div id="preview">
      <div v-html="compiledMarkdown"></div>
    </div>
  </form>
</template>
<script>
  import marked from 'marked';
  import hljs from 'highlightjs';

  export default {
    name: 'postForm',
    created: function () {
      marked.setOptions({
        langPrefix: '',
        highlight: function(code, lang) {
          return hljs.highlightAuto(code, [lang]).value
        }
      });
    },
    computed: {
      compiledMarkdown: function () {
        return marked(this.markdownText)
      }
    },
    data: function () {
      return {
        markdownText: ''
      }
    }
  }
</script>
<style src='highlightjs/styles/github-gist.css'></style>

おまけ:マークダウンのデザインを調整する

マークダウン用のスタイルを設定するのがめんどくさい人は、公開されているマークダウン用のスタイルがあるみたいなので、それを使ってみるのもありかと思いました!

ちなみに私はライセンスフリーの下記を使ってみました👀

github.com

おわりに

ライブラリを使うと結構簡単にマークダウンエディタが作れるんですね👀マークダウンエディタはシンプルかつ、いろいろ自分のアイデアを組み込めるので、勉強には良さそうな気がしました🤔
ここから色々機能を追加して、フロントエンド関係の知識も身に着けていきたいですね💪

参考

qiita.com

qiita.com

2018年の振り返り

2018年も終わりですね。年末ということで振り返り記事を書いてみる。

今年の振り返り

アウトプット

今年はBLOGを書いたり、ちょっとしたアプリを作ってみたり、初めてOSSにコントリビューションしたりしました👀

BLOG

今年は、1記事 / 2週を目標に学んだことをブログでアウトプットしようと思い、2018年は46記事(週に1回ぐらい)書いてました! 下記が印象深い記事を抜粋したものです✍

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

OSS

railsrspec-style-guideに、大したことない対応ですがコントリビュートしました🙇‍♂️

github.com

github.com

つくったもの

今年もしょうもないものや、Webサービス的なもの、LINE BOTとかGemとかそこそこ作りましたね👀

github.com

twitter.com

madogiwa0124.hatenablog.com

quiz-quiq.herokuapp.com

madogiwa0124.github.io

今年の目標

去年の目標と振り返り

目標 結果 コメント
誰かに使ってもらえるWebサービスをつくる rails勉強Botはフォロワー15人、sppは676回Downloadされてるみたいなので一応達成😅
ネイティブアプリと連動したWebサービスをつくる ここは出来なかった。。。
本を年間36冊読む 21冊しか読んでなかった😢
SIerからWeb系に転職する ここは達成!
OSSにコントリビュートする railsrspec-style-guideにコントリビュート🎉
スカイダイビング 出来なかった。。。

目標6個に対して達成3つ、未達3つということでそこそこだったけど、 今年は転職したりハワイ行ったりといろいろ出来てよかったなと🙂

今年の目標

目標 説明
web design系の本を3冊は読む 目的を持った読書目標にしようかなと、デザインも勉強したいので👩‍🎨
アプリを作るだけじゃなくて、ちゃんと運用してみる 作るだけじゃなくて、GAとか使って分析とか運用もやってみようかなと👀
rubyjavascript以外の言語に手を出す goとかelixirがちょっと気になっているので💻
勉強会で登壇する 話せることがあれば自社イベントとか、なんかしらの小さな勉強会で登壇にも挑戦してみれたら👩‍🏫
Webサービスを作るだけじゃなくて、ちゃんと運用してみる GAとかの使い方も覚えたいなと👩‍🔬

おわりに

今年は、Web系企業に転職したりOSSにコントリビュートしたり、エンジニアとしての第一歩を踏み出せた一年だったなと。
あっという間だったけど、良い一年だったなと思います。来年は、ただ作るだけじゃなくて、デザインを工夫したり、新しい技術を使ったり、運用をきちんとやったりと、そういうところまできちんと出来るようになれると良いなと思います。

それでは、皆さん来年もよろしくお願いいたします🙇‍♂️

良いお年を。

RubyonRails: 検索フォームから値を受け取って、値があれば検索するときの実装を考えてみる🤔

みなさん、こんばんは。まどぎわです。
今回は検索処理で値があるときだけ絞り込みを行うときにどうやって実装するのが良いのかなと、すこし考えたので検討過程をメモしておきます。

はじめに

今回は、下記のような前提で検索処理を実装することを想定してます。

  • 検索対象はPost、Postにはタイトルと本文がある。
  • Postにはタグが複数紐付いている。タグは、acts-as-taggable-onで実装されている。
  • 検索条件には、キーワード(タイトルの部分一致)とタグがある。
  • それぞれの検索条件が指定されていれば条件で絞り込みを行い、指定されていなければ検索処理を行わない。

まずは何も考えず実装してみる(かなり良くない)

なにも考えず実装すると下記のような感じになりますかね(;・∀・)
自分で書いといてあれですが、ごちゃごちゃしていて辛い・・・。

class PostsController
  def index
    @posts = if search_params[:keyword].present? && search_params[:tag].present?
      Post.where('title LIKE ?', "%#{search_params[:keyword]}%").tagged_with(search_params[:tag])
    elsif search_params[:keyword].present?
      Post.where('title LIKE ?', "%#{search_params[:keyword]}%")
    elsif search_params[:tag].present?
      Post.tagged_with(search_params[:tag])
    else
      Post.all
    end
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

とりあえず検索処理をModelに移してみる(あまり良くない)

Controllerはきれいになり、先程よりは良くなりましたが、ごちゃごちゃした部分がModelに移動しただけで根本的な問題は解決してないですね・・・。

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    if params[:keyword].present? && params[:tag].present?
      where('title LIKE ?', "%#{params[:keyword]}%").tagged_with(params[:tag])
    elsif params[:keyword].present?
      where('title LIKE ?', "%#{params[:keyword]}%")
    elsif params[:tag].present?
      tagged_with(search_params[:tag])
    else
      all
    end
  }
end

とりあえずModelの検索ロジックをリファクタリングしてみる(うーん)

さっきよりは断然良さそうですが、最初にallで取得しているところとか、毎回変数に入れているところに、少し違和感を感じますね(;・∀・)
※また最初、この書き方だとallwheretagged_withSQLが発行されてしまい非効率だと思ってたんですが、ちゃんと1回のSQLで取得出来るみたいですね、ActiveRecordすごい🙄

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    posts = all
    posts = posts.where('title LIKE ?', "%#{params[:keyword]}%") if params[:keyword].present?
    posts = posts.tagged_with(params[:tag]) if params[:tag].present?
    posts
  }
end

検索条件ごとにscopeを用意して個別に検索ロジックを実装してみる(良さそう)

今回は、これが良いんじゃないかと思ってるんですが、検索条件別にscopeを用意してあげて、その中で検索条件の有無でallを返すか絞り込みを行った結果を返すか分岐を行っています。
この方法であれば、メソッドチェーンを使って条件を適用出来るため分かりやすい、個別で検索条件を使うときに便利かなと思ったんですが、どうでしょうか?👀

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    search_by_keyword(params[:keyword]).search_by_tag(params[:tag])
  }
  scope :search_by_keyword, ->(keyword) {
    return all if keyword.blank?
    where('title LIKE ?', "%#{keyword}%")
  }
  scope :search_by_tag, ->(tag) {
    return all if tag.blank?
    tagged_with(tag)
  }
end

おわりに

今回は検索ロジックについて、ちょっと考えてみました。
Railsの良い感じの既存メソッドを使って、これよりも良い実装も全然あるような気もするので教えてください(;・∀・)