Madogiwa Blog

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

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の良い感じの既存メソッドを使って、これよりも良い実装も全然あるような気もするので教えてください(;・∀・)

minimagickで生成した画像をDBに保存したときに`ArgumentError (invalid byte sequence in UTF-8)`が発生したときの対処法

minimagickを最近ちょっと使っているのですが、合成した画像を保存する際に、めちゃめちゃハマったので対応方法をメモしておきますφ(・

事象

画像に文字を合成してMiniMagick::Imageインスタンスを返すImageBuilder::PostThumbnail.buildの返り値からバイナリ文字列を取得し、保存しようとしたところArgumentError: invalid byte sequence in UTF-8が発生した。

[3] pry(main)> p.thumbnail = ImageBuilder::PostThumbnail.build("hoge").tempfile.open.read
[4] pry(main)> p.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
ArgumentError: invalid byte sequence in UTF-8

解決策

返り値からバイナリ文字列を取得する際にbinmodeを呼び出してからバイナリ文字列を取得することで解決できました🙌

[6] pry(main)> p.thumbnail = ImageBuilder::PostThumbnail.build("hoge").tempfile.open.binmode.read
[7] pry(main)> p.save
   (0.2ms)  BEGIN
  PostImage Load (4.7ms)  SELECT "post_images".* FROM "post_images" WHERE "post_images"."post_id" = $1  [["post_id", 5]]
  Post Update (2.2ms)  UPDATE "posts" SET "thumbnail" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["thumbnail", "<17691 bytes of binary data>"], ["updated_at", "2018-12-01 12:58:39.176813"], ["id", 5]]
  ActsAsTaggableOn::Tagging Load (0.9ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2  [["taggable_id", 5], ["taggable_type", "Post"]]
   (4.7ms)  COMMIT
=> true

binmodeを使うとバイナリモードでファイルを開くことが出来るみたいですね👀
詳細はよくわかってないのですが、今回は画像のバイナリ文字列が取得したかったので、binmodeを使ったほうがよかったみたいです🤔

参考

class IO (Ruby 2.5.0)#binmode

RubyonRails:whereでサブクエリを使って効率よく対象を絞って取得する方法👀

みなさん、こんにちは。まどぎわです(・∀・)
今回は、ちょっと複雑な条件をサブクエリを使って効率よく取得する方法について書こうと思います✍

今回のケース

今回は、「公開中の投稿に紐づくコメントのみを取得する」というケースで考えてみようと思います🤔
前提事項を下記に整理したので、参考までに。

  • 投稿:Post、コメント:Comment
  • 投稿とコメントは 1:N(has_many)の関係
  • 公開済みの投稿はopened: true

対応案を考えてみる

公開中の投稿を変数に入れて紐づくコメントを取得する(いまいち😭)

これはあんまり良くなさそうだなーというのを書いてみました😥
公開中の投稿を取得してpostsに格納したあとに、それに紐づくコメントを取得しています。 これは、コード行数も2行でコメントを取得する際に毎回SQLが発行されしまうため非効率です。。。

$ posts = Post.where(opened: true)
# => (0.7ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."opened" = "true"
$ comments = posts.map(&:comments)
=> # 公開中の投稿の数だけ、コメントを取得するSQLが発行される・・・。

pluckを使ってみる(スッキリ書けるけど、効率いまいち🤔)

では、ActiveRelation#pluckを使ってみるのはどうでしょうか?🤔
pluckを使って公開中の投稿のidの配列を取得し、コメントを取得条件に渡します。 1行でスッキリ書けて、一見良さそうに見えますが公開中の投稿のidを取得するSQL公開中の投稿に紐づくコメントを取得するSQLの2回、SQLが発行されてしまっています😥

$ PostComment.where(post_id: Post.where(opened: true).pluck(:id))
=> (0.7ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."opened" = "true"
   PostComment Load (9.9ms)  SELECT "post_comments".* FROM "post_comments" WHERE "post_comments"."post_id" IN ($1, $2, $3, $4)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]

※👇pluckの詳しい説明はこちら
Active Record クエリインターフェイス #pluck

selectを使う(スッキリ書けて効率よい🙌)

こういうケースは、ActiveRecord:: QueryMethods#selectを使うと、きれいに効率よく書くことが出来ます💡
書き方はpluckのときとほとんど一緒です。selectを使うことによって、公開中の投稿に紐づくコメントを取得するSQLのIN句に公開中の投稿のidを取得するサブクエリが発行されるようになり一回のSQLで取得することが出来ます!スッキリかけて効率的ですね🙌

$ PostComment.where(post_id: Post.where(opened: true).select(:id))
=> PostComment Load (2.2ms)  SELECT "post_comments".* FROM "post_comments" WHERE "post_comments"."post_id" IN (SELECT "posts"."id" FROM "posts" WHERE "posts"."opened" = "true")

※👇selectの詳しい説明はこちら
Active Record クエリインターフェイス #select

おわりに

ActiveRecord:: QueryMethods#selectをwhereで使うとサブクエリを使って効率よくスッキリ目的のレコードを取得することが出来ます🙌
今まで結構書いてしまっていた気もしますが、pluckだと毎回SQLが発行されてしまうので注意ですね👀

railsの勉強のためのTwitterBotをリリースしました📢

f:id:madogiwa0124:20181111222201p:plain

みなさん、こんばんは。まどぎわです(・∀・)
本日、Rails勉強BotというTwitterBotをリリースしました!🎊
railsを勉強している人には役に立つと思うので、この記事で使い方など紹介します📢✨

rails勉強Botとは?

rails勉強Botは、railsのメソッドとGithub上のソースコードへのリンクを投稿するTwitterBotです🐦
現在は、10分に一回ActiveSupportのメソッドについて呟いてくれています📢

twitter.com

rails勉強Botの活用方法

rails勉強botをフォローすると10分に一回タイムラインに、こんな感じで投稿がされます✍

urlのリンクをクリックするとGithub上のソースコードを確認できます🙌 f:id:madogiwa0124:20181111220631g:plain

メソッドと実際のコードを関連付けて知ることで、より深く処理の中身を知ることが出来るんじゃないかなぁと思ってます👀

どうやって作ってるの?

APIモードのrailsとherokuを使って作っています🔨
ソースコードGithub上に公開してますので、よろしければ見てみてください🐾 github.com

そんなに難しいことはしてなくて仕組みとしては、こんな感じです。

  • public_methodsでpublicなmethodの一覧を取得
  • source_locationで該当methodが記載されたファイルとその行番号を取得
  • Github上のURLに合わせて、取得結果を整形
  • TwitterApiに作成したクラスとメソッドとGithub上のURLの文字列を渡す

👇実際に上記を行っているコードです。
rails_study_bot/active_support.rb at master · Madogiwa0124/rails_study_bot · GitHub

そもそものTwitterBotの作り方は、この辺を参考にしてもらえれば🙌

madogiwa0124.hatenablog.com

おわりに

まだActiveSupportのメソッドしか呟けていないのですが、今後はActiveRecordActionView等、対応クラスを増やして行く予定です💪
自分でも使っていますが、なかなか勉強になることも多いのでrails勉強中の方は、使ってみて感想等頂けると嬉しいです😊

PS. TwitterAPIの承認申請めんどくさすぎた😇

twitter.com

RubyOnRails:FormObjectを使って複雑なフォームの処理を良い感じに実装するメモ✍

みなさん、こんにちは!まどぎわです(・∀・)
今回は、Railsデザインパターン(?)の一つのFormObjectについて学んだので、使い方とかをメモしておきます✍

FormObjectってなに?

FormObjectとは、ActiveModelincludeしたClassにフォームで扱うプロパティをもたせたものです。

私は実際に使ってみて、下記のようなメリットを感じました!!

  • 複数モデルにまたがったバリデーション等、記載に箇所に迷う実装をスッキリ書ける。
  • Controller側の実装がスッキリして、コードの見通しがよくなる。

ちょっと、具体的な例と合わせて見ていこうと思います👀

FormObjectってどういうときに使う?

例えば、タイトルと本文とタグ(10件まで)登録するブログサービスのようなものがあったとします。
Blogに紐づくTagの個数チェックや、タグを含めたブログの登録処理等、実装箇所に迷う部分が多かったですが下記のような実装(イメージ)としてみました。。。

この状態ではコントローラーの中にエラーチェックがあったり、タグのインスタンスを作る処理があったり、トランザクションを貼って複数モデルにまたがった作成処理を行っていたり等、見通しが悪く、またコントローラーに書きたくないような処理があって、ごちゃちゃしているように感じますね(;・∀・)

class BlogsController < ApplicationController
  TAGS_LIMIT = 10
  
  def create
    @post = BlogPost.new(blog_post_form)
    ActiveRecord.transaction do
      check_up_to_limit_tags_can_be_set!
      blog = Blog.create!(title: blog_params[:title], body: blog_params[:body])
      build_tags.each do |tag|
        tag.save!
        Tagging.create!(blog: blog, tag: tag)
      end
    end
    redirect_to blogs_path
  rescue ActiveRecord::RecordInvalid
    render :new
  end

  private
  
  def check_up_to_limit_tags_can_be_set!
    return if tags.length <= TAGS_LIMIT
    @post.errors.add(:tags, 'は、10個まで設定可能です。')
  end
  
  def build_tags
    blog_params[:tags].map { |tag| Tag.new(name: tag) }
  end
end

複数モデルにまたがったエラーチェックやモデルの生成等、ちょっと複雑な機能だと結構ありがちで、実装を迷う部分な気がするのですが、こういうときにFormObjectを使うと結構スッキリかけます🙌

FormObjectを使ってリファクタリングしてみる

先程話した例をFormObjectを使ってリファクタリングしてみると下記のような感じになります!!
BlogPostFormに実装に迷った処理が集約されて、コントローラー内もスッキリし、フォーム側に登録処理を寄せることで、複数モデルの登録も違和感無い形になっているので良さそうに見えますね👀✨

class BlogsController< ApplicationController
  def create
    @post = BlogPostForm.new(blog_params)
    post.save!
    redirect_to blogs_path
  rescue ActiveRecord::RecordInvalid
    render :new
  end
end

class BlogPostForm
  TAGS_LIMIT = 10
  include ActiveModel::Model
  attr_accessor :title, :body, :tags
  validates :title, :body, presence: true
  valudate :up_to_10_tags_can_be_set
  
  def save!
    raise ActiveRecord::RecordInvalid if invalid?
    ActiveRecord.transaction do
      blog = Blog.create!(title: title, body: body)
      build_tags.each do |tag|
        tag.save!
        Tagging.create!(blog: blog, tag: tag)
      end
    end
  end
  
  private
  
  def build_tags
    tags.map { |tag| Tag.new(name: tag) }
  end
    
  def up_to_limit_tags_can_be_set
    return if tags.length <= TAGS_LIMIT
    errors.add(:tags, 'は、10個まで設定可能です。')
  end
end

FormObjectをもっと知る

FormObjectについて、すごく分かりやすく記載されてる記事です👀
※多大に参考にさせて頂きました・・・!🙇‍♂️

tech.medpeer.co.jp

FormObjectのためのGemもあるみたいです👀

github.com

おわりに

今回はFormObjectについて、少し整理してみました。
このようなデザインパターンのような知識を学ぶと自分の実装の引き出しが増える感じがして良いですね🙌
これからも学んでいきたみが強い・・・!!

【Ruby on Rails】Rails GuidesにPRを出して、Railsのコントリビューターになったよって話

はじめに

みなさん、こんにちは、まどぎわです(・∀・)
この前Railsのコントリビューターになりました!🎉

実際にマージされたのが下記PRです🙋

github.com

内容としては、Rails Guidesを1文字修正しただけなのですが😅

# bofore
If it says something like "Rails 5.1.1", you are ready to continue.
# after
If it says something like "Rails 5.2.1", you are ready to continue.

それでもRails Contributorsに名前が乗るのは、嬉しいですね🙌

contributors.rubyonrails.org

RailsのPRを見てると機能追加だけじゃなくて、docfix typoが、結構Mergeされているみたいなので、PRチャンスは結構ありそうだなと思いました👀

[ci skip] Fix typo by frodsan · Pull Request #34119 · rails/rails · GitHub

fix broken link in Action Cable guides and readme [ci skip] by gregmolnar · Pull Request #34126 · rails/rails · GitHub

Rails GuidesにPRを出すまで

というわけでRails GuidesにPRを出すまでの手順とかまとめておきます📝
手順は下記の通りです。

  1. RailsをFork
  2. Forkした自分のリポジトリguides/source配下を修正
  3. RailsのmasterにPRを出す

詳しいやり方とか注意事項は、Rails Guidesの下記ページに記載されているので、読んでみてください👀

Contributing to Ruby on Rails — Ruby on Rails Guides

https://guides.rubyonrails.org/ruby_on_rails_guides_guidelines.html

おわりに

RailsGuidesを読みながら、気づいたところをPR出すようなことやると英語とRailsの勉強にもなるし良さそうですね✨
今回出したPRの1行の英文を書くのにもだいぶ時間がかかってしまったので、英語も勉強しないといけないなと。。。

今回は、Rails GuidesのしょぼいPRでしたが、いずれは機能追加系のPRも出せるように勉強していきたいですね💪

herokuとrubyでゴミ出しを通知するLINE BOTを作ってみた🔔

みなさんこんにちは、まどぎわです(・∀・)
今回は、いつも忘れてしまっていたので、毎日明日出せるゴミを通知してくれるLINE BOT作ってみました🙌

f:id:madogiwa0124:20180930232656p:plain

やってみたら意外と簡単に出来たので手順等をメモしておきます📝

LineBotの作り方

流れは下記のような感じです👇

  1. LineDevelopersに登録
  2. チャンネル作成
  3. BOTを実装
  4. herokuにアプリを作成

LineDevelopersに登録

下記からLineDevelopersに登録できます👀

LINE Developers

f:id:madogiwa0124:20180930232920p:plain

画面に従って行って貰えれば、特に躓くところは無いと思います。

チャンネル作成

BOTを作成するには、まずはチャンネルを作る必要があります。
チャンネルとは、実際にBOTを運用するにあって必要な情報、Tokenや表示する名前等を登録します。詳しいやり方は、まとめてくださっている方がいらっしゃいますので、下記等を参考にしてみてください👇

qiita.com

BOTを実装

今回は、sinatraと公式のgemline-bot-sdk-rubyを使って作成しました。 軽く動かすだけだったら、公式チュートリアルをコピペするとオウム返しのLineBotが作成出来ます🐦

github.com

私のBotの返信処理は下記の通りです👇
メッセージが来たら、明日がなんのゴミの日かを取得し返信しています。

app.rb

post '/callback' do
  // リクエストボディの取得
  body = request.body.read

  // リクエストのチェック
  signature = request.env['HTTP_X_LINE_SIGNATURE']
  unless client.validate_signature(body, signature)
    error 400 do 'Bad Request' end
  end

  // リクエストボディをパース
  events = client.parse_events_from(body)
  events.each do |event|
    // メッセージを受信したら
    case event
    when Line::Bot::Event::Message
      // 通知メッセージを返信する
      client.reply(event['replyToken'], GomiChecker.notice_message)
    end
  end
end

ちなみに、返信等のcallbackを受けずにBOT側から通知を送信する場合は、下記のような実装になります。※ユーザーIDを指定して通知を行う必要があるので、環境変数で管理するようにしてます。

batch.rb

def push_ids
  ENV['PUSH_TO_ID'].split(',')
end

begin
  // 環境変数に設定されたすべてのユーザーに通知を送信
  push_ids.each { |id| client.push(id, GomiChecker.notice_message) }
rescue => e
  puts "batch exec error ..."
  p e
end

詳しい実装はGithubにコードを上げているので、そちらをご確認ください。

github.com

herokuにアプリを作成してデプロイ

下記のような感じでherokuにデプロイすると実際にLINEBOTが動き出すと思います🙌

$ heroku create
# hoge.gitは自分のアプリに合わせて変える
$ git remote add heroku https://git.heroku.com/hoge.git
$ git push heroku master

これでLINEから自分のBOTを友達登録して、通知を受け取れるはずです!🔔

おわりに

今回は、いつも忘れる明日のゴミの日を通知するBOTを作ってみました、実際に自分の生活を豊かにするアプリは良いですね🏠 LINEBOTを使うのは初めてでしたが結構簡単に作れたので、また何か日常で困ったことがあったら使ってみようかなと思いました🙌