Rails で 検索機能を実装する際にフルネームで検索できるように実装したい

こんにちは!kossyです!




さて、今回はRailsで検索機能を実装する際に、フルネームで検索できるようにする方法を
ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.0
Mysql 5.7.23




前提
usersテーブルがあり、カラムとして

  • last_name
  • firlst_name
  • last_name_kana
  • first_name_kana

があるとします。
ので、full_nameというカラムを用意するのであれば今回紹介する方法を取る必要はありません。



実装手順

まずは全コード晒してみます。

app/controllers/users_controller.rb

require 'nkf'

class UsersController < ApplicationController
  KEYWORD_SIZE = 6

  def search
    _params = params.permit(:keyword)
    keywords = ["%#{NKF.nkf('-w --katakana', _params[:keyword].gsub(' ', ''))}%"] * KEYWORD_SIZE 
    query_str = 'concat(last_name, first_name) like ? OR last_name LIKE ? OR last_name LIKE ? OR concat(last_name_kana, first_name_kana) like ? OR first_name_kana LIKE ? OR last_name_kana LIKE ?'
    
    @users = User.where(query_str, *keywords)
  end
end

require 'nkf' と記述しているのは、ひらがなが検索フォームに入力されても、カタカナに変換するためです。
https://qiita.com/y_minowa/items/c204992e4665a8687d4a

gsubは、文字列中の第一引数に合致するものを、第二引数に置き換えるStringクラスのメソッドです。
今回の場合は、空白(スペース)があった場合は取り除くようにしています。

%を入れているのは、あいまい検索を行うために入れています。
https://qiita.com/nakanishi03/items/2a6dbd72f9793b7e0ce4

keywordsは配列になっていて、例えばkeywordが田中だった場合、

["%田中%", "%田中%", "%田中%", "%田中%", "%田中%", "%田中%"]

のような返り値となります。

query_strはSQL句を文字列にしているもので、その中で使っているconcatは、文字列を連結させることができるmysqlの機能になります。(PostgreSQLの場合は || )
last_nameとfirst_nameをconcatする事で、フルネームでの検索を可能にしています。

\*keywordは配列を展開している動作になります。*は配列の前につけると配列を展開できる、Rubyのショートハンドになってます。
javascriptだとスプレッド構文がそれに当たりますかね。


これでひらがなでもカタカナでも漢字でもフルネームで検索ができると思います。

Railsでenumを定義している時、自動でscopeが定義される

こんにちは!kossyです!




さて、今回はRailsenumを定義すると、自動でscopeが定義されるので、
その使い方についてブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3
MacOS Mojave



使い方

例えば、Articleモデルがあって、

class Article
  enum status: [:published, :draft, :ignore]
end

のようにenumでstatusを定義しているとします。

このとき、「Articleのなかでstatusがdraftになっているレコードを抽出したい」とした場合、

Article.where(status: :draft)
=> #<ActiveRecord::Relation [#<Article id: 1, status: "draft"]

のように取得できますが、enumで定義している場合、

Article.draft
=> #<ActiveRecord::Relation [#<Article id: 1, status: "draft"]

とする事で、whereを使った時と同様の結果を得られるようになります。

where.notで取りたい場合も、

Article.not_draft

とするだけで取得することができます。

これは、enumを定義すると自動的にクラスに対してscopeが定義されるためです。
https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Enum.html


勉強になりました。

Railsでsendgridのmessage_idを取得したい

こんにちは!kossyです!



さて、今回はRailsでsendgridをメール配信サービスとして使用していた時に、
message_idを取得する方法についてブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




なお、sendgridの設定周りについての説明は割愛します。
configは下記の記事が参考になるかと思います。
https://sendgrid.kke.co.jp/docs/Integrate/Frameworks/rubyonrails.html



ActionMailer::DeliveryJobではなくNet::SMTP::Responseを返り値にする

通常、RailsでActionMailerを使ってメールを送信するときは、
deliver_nowかdeliver_laterを使うと思いますが、
https://edgeapi.rubyonrails.org/classes/ActionMailer/MessageDelivery.html

上記2つのメソッドでメールを送信した場合、返り値がActionMailer::DeliveryJobになります。

これだと、sendgridのmessage_idを受け取ることができないので、
smtp_configと実行するメソッドを変更する必要があります。

まずは config/environment/各環境のenvファイル のsmtpのセッティングを変更します。

  config.action_mailer.smtp_settings = {
    user_name: ENV['sendgrid_user_name'],
    password: ENV['sendgrid_password'],
    domain: 'hogehoge.com',
    address: 'smtp.sendgrid.net',
    port: 587,
    authentication: :plain,
    enable_starttls_auto: true,
    return_response: true # 追加
  }

return_responseをtrueにすると、サーバーからのresponseをそのまま返すようになります。

参考: http://aligach.net/diary/20180725.html



そして、deliver_later(またはdeliver_now)ではなく、deliver_now!を使うようにします。

すると、

response = UserMailer.notify_private_announcement(user).deliver_now!

response.class
=> Net::SMTP::Response

ActionMailer::DeliveryJobではなくNet::SMTP::Responseを返り値にすることができます。

Net::SMTP::Responseインスタンスはattrとして、

response
=> #<Net::SMTP::Response:0x005652f6301890 @status="250", @string="250 Ok: queued as 1dMBeSp6Rj-Uh5TdzJKgmq\n">

statusとstringを持っています。
内、sendgridのmessage_idは、1dMBeSp6Rj-Uh5TdzJKgmq の部分になります。

この文字列を取得するには、

# この書き方なんとかしたい、、、

response.string[18..39]
=> "1dMBeSp6Rj-Uh5TdzJKgmq"

とすることで取得できます。

このmessage_idを取得し、sendgridのEventWebhookのsg_message_idと前方一致させることで、
メールの開封状況を取得することもできるようになります。

EventWebhookについては、
https://sendgrid.kke.co.jp/docs/API_Reference/Webhooks/event.html
https://sendgrid.kke.co.jp/docs/Tutorials/C_Manage_Events/using_event_webhook.html

が参考になるかと思います。

RailsにおいてDate.todayで日本時間が返らない原因を探った話

こんにちは!kossyです!



さて、今回は個人開発でLineBotをRailsを使って作成した時に、
Date.todayで日本時間が返らなかった時の備忘録を残してみたいと思います。



環境
Ruby 2.6.3
Rails 6.0.2
MacOS Mojave



config.active_record.default_timezoneの設定忘れ

単純なミスでした、、、

しかし、よく考えると時刻周りのconfigについて理解が曖昧だったので、調べてみました。



まずは公式ドキュメントをみました。
https://railsguides.jp/configuring.html

config.active_record.default_timezone:
データベースから日付・時刻を取り出した際のタイムゾーンをTime.local (:localを指定した場合)と
Time.utc (:utcを指定した場合)のどちらにするかを指定します。
デフォルトは:utcです。

引用: https://railsguides.jp/configuring.html


ふむ、デフォルトはutcだったから日本時間が返らなかったんですね。


他にはどんな影響があるのでしょうか。

config.active_record.default_timezoneの設定はDBを読み書きする際に、DBに記録されている時間をTime.utcで読むかTime.localで読むかを設定する。
:utcの場合DBに記録されている時間はUTC扱いで、この時DBサーバのタイムゾーン設定は考慮しない。
ActiveRecordインスタンスが持っているTimeWithZoneの値をUTCに変換し、その時刻をDBに書き込む。
:localの場合は、DBに記録されている時間はシステムのタイムゾーンとして扱う。
ActiveRecordインスタンスが持っているTimeWithZoneの値をシステムのタイムゾーンに変換し、その時刻をDBに書き込む。

config.active_record.default_timezoneは、DBのタイムゾーンと一致させておくべきだ。
DBのタイムゾーンUTCなら:utcにDBのタイムゾーンがSYSTEMなら:localにしておく。

新しくRailsのシステムを開発する場合は、DBのタイムゾーン設定とconfig.active_record.default_timezoneを合わせるように注意した方が良い。
もし、その設定に気付かず(忘れて)運用を開始してしまった場合は、MySQLタイムゾーンの設定をUTCにした方が良いかもしれない。

引用: https://qiita.com/joker1007/items/2c277cca5bd50e4cce5e

上記の記事が詳しく説明されていました。

時刻周りに無知の状態でWebアプリを開発していたら、事故りますねこれ、、、


皆さんも同じ轍を踏まないよう、くれぐれもご注意ください。

Railsでsendgridを使ってメールを一斉送信したい

こんにちは!kossyです!



さて、今回はRailsでメール配信サービスとしてsendgridを使っている場合に、
メールを一斉送信する方法について、ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




なお、sendgridでメール送信を行う設定周りについては説明を割愛します。
configには以下の記事が参考になると思います。
https://sendgrid.kke.co.jp/docs/Integrate/Frameworks/rubyonrails.html



X-SMTPAPIを使う!

sendgridにはメール送信APIとして、SMTP-APIとWeb-APIがあるのですが、
SMTP-APIの機能に複数の宛先を選択する機能が用意されています。

今回はこのSMTP-API(別名X-SMTPAPI)の「複数の宛先を選択する機能」を使って、一斉送信をする例を紹介します。


例としてApplicationMailerクラスを継承したUserMailerクラスが定義してあるとします。

class UserMailer < ApplicationMailer

  def send_announcement_mail(subject, content, emails)
    @content = content
    xsmtp_api_params = { to: emails }
    headers['X-SMTPAPI'] = JSON.generate(xsmtp_api_params)
    mail(to: emails, subject: subject)
  end
end


headers['X-SMTPAPI']にjsonを格納する事で、簡単に一斉送信が可能になります。

send_announcement_mailメソッドの引数には、emailsが格納された配列を渡します。

呼び出し元のコードはこんな感じ。

class AnnouncementsController < ApplicationController
  def create
    @announcement = Announcement.new(announcement_params)
    if @announcement.save
      UserMailer.send_announcement_email(@announcement.subject, @announcement.content, User.all.pluck(:email)).deliver_later
    else
      # 例外処理
    end
  end
end

@announcementオブジェクトはattrとしてsubjectとcontentを持っていて、@announcementがDBに保存されたあと、
send_announcement_emailを呼び出し、subjectとcontentとUser全員のemailの配列をpluckメソッドを使って取り出して引数として渡しています。



to: emails とすると、宛先が見えてしまうのでは?と懸念された方がいるかもしれません。
が、公式リファレンスにその辺りのことがきちんと書かれていました。

このリクエストはSendGridで分解され、各宛先にはToヘッダに個別に宛先指定(他の人の宛先が見えることはありません)されたメールが届きます。

引用: https://sendgrid.kke.co.jp/blog/?p=4232


このように、sendgridのX-SMTPAPIを使えば、簡単にメールの一斉配信機能を実装することができます。
X-SMTPAPIには他にも機能があるので、興味のある方は、

https://qiita.com/wawoon/items/6de229ca5e891c00452b
https://sendgrid.kke.co.jp/docs/API_Reference/SMTP_API/building_an_smtp_email.html

上記の記事を読んでみてください。

Rubyのsendとinjectを使って短くスッキリしたコードを書く

こんにちは!kossyです!



1ヶ月ほど更新できていませんでした。。。
大きめの機能追加に目処がついたため、少し余裕ができました。
これから可能な限り、ブログを更新したいと思います。




さて、今回はRubyのsendメソッドとinjectメソッドを使って、短いコードを書く方法をブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




sendメソッドとinjectメソッド

まず、両メソッドについて説明します。

と言っても、公式リファレンスのリンクを貼るだけですが、、、

sendメソッド
https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

オブジェクトのメソッド name を args を引数にして呼び出し、メソッドの実行結果を返します。

ブロック付きで呼ばれたときはブロックもそのまま引き渡します。

send が再定義された場合に備えて別名 __send__ も用意されており、ライブラリではこちらを使うべきです。また __send__ は再定義すべきではありません。

send, __send__ は、メソッドの呼び出し制限にかかわらず任意のメソッドを呼び出せます。 クラス/メソッドの定義/呼び出し制限 も参照してください。

引用: https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

メソッドを動的に実行したい時なんかに使ったりしますね。
「黒魔術」と揶揄されたりもしてます。https://qiita.com/Yuki_Nakagami/items/21319c10e833cd0efc12

今回のブログでは、ゴリゴリ使います。


injectメソッド
https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

リストのたたみこみ演算を行います。

最初に初期値 init と self の最初の要素を引数にブロックを実行します。 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。

要素が存在しない場合は init を返します。

初期値 init を省略した場合は、最初に先頭の要素と 2 番目の要素をブロックに渡します。また要素が 1 つしかなければブロックを実行せずに最初の要素を返します。要素がなければブロックを実行せずに nil を返します。

引用: https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

金額の合計値とかを出したい時によく使ったりします。
当たり前ですが、配列やハッシュをカスタマイズして新しい値を返したい時がユースケースとしては一番多いですね。


前提

家計簿アプリ的な物を例にしてみます。

・Accountsテーブル(家計簿そのもの)
・コストごとにテーブルが分かれているとします。(今回は「食費」「光熱費」「交際費」のみとします。)



食費モデル(MealCost)
光熱費モデル(UtilityCost)
交際費モデル(EntertainmentCost)

は、

家計簿モデル(Account)に属し(belongs_to)、

家計簿モデル(Account)はそれぞれのコストをhas_manyする関連があるとします。

それぞれのコストは、dateとpriceとnameをattrとし持っているとします。

前提は以上です。


メソッド定義

その月のコスト全てを合計した総額を返すようなメソッドを定義してみます。

普通に考えると、

class Account
  def total_price(target_month)
    meal_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, meal| sum += meal.price } + utility_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, utility| sum += utility.price } + entertainment_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, entertainment| sum += entertainment.price }
  end
end

みたいなメソッドを定義することになるかなぁ、、、(異論は認めます)


でも、コストは上記の3種類以外にもありますし、コスト項目が増えると見通しも悪くなります。

そこで、sendを駆使してみます。

class Account
  ITEM_COSTS = [
    'meal',
    'utility',
    'entertainment'
  ]

  def total_price(target_month)
    ITEM_COSTS.inject(0) do |total, item|
      total += self.send("#{item}_costs").where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, _item| sum += _item.price }
    end
  end
end

中身がコスト名の定数ITEM_COSTSを定義し、total_priceメソッド内でsendメソッドとinjectメソッドを使って合計額を算出する形に切り替えました。

これなら、コスト項目が増えてもITEM_COSTSに足すだけでいいので、変更にも強いかなと思います。

Railsのenumで定義した値がenum内の値かどうかを検証する

こんにちは!kossyです!




さて、今回はRailsにおいて、enumで定義した値がenum内の値かどうかを検証する方法について、
ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3



サンプル


例えば下記の実装があったとします。

class Article
  enum status: [:draft, :published, :dropped]
end

このような実装をしていた時に、
「ブラウザから送られてきたパラメータがenumで定義した値のいずれかでなければ、エラーを返す」
という実装を行いたいとします。

普通に実装すると

raise StandardError unless [:draft, :published, :dropped].include?(params[:status])

みたいになるかと思うのですが、

Enumで定義した値は、
Article.statuses(モデル名.enumで定義したカラムの複数形)とすると、

{ "draft": 0, "published": 1, "dropped": 2 }

という返り値を得ることができるので、keysメソッドと合わせて、

raise StandardError unless Article.statuses.keys.include?(params[:status])

と書くことができます。
Railsっぽくていいですね!

RailsでObject.has_many.has_manyという関連が組んである場合のデータ取得

こんにちは!kossyです!



さて、今回は、Object.has_many.has_manyという関連が組んである場合のデータ取得の方法を
ブログに残したいと思います。



環境
Rails 5.2.3
Ruby 2.5.1
MacOS Mojave



mapやflattenを使う!



「学生は講義をいっぱい持っていて、講義は科目をいっぱい持っている」とします。

Student has_many rectures
recture has_many subjects

ここで、
「その学生が持つ講義の科目を全て取得したい」とします。

その場合、

$ student = Student.first

$ student.rectures.map{|rec| rec.subjects }.flatten

こうすることで実現可能です。

要件が変わって、
「その学生が持つ講義の科目名を全て取得したい」となっても、

$ student = Student.first

$ student.rectures.map{|rec| rec.subjects }.flatten.pluck(:name)

とすることで対応可能です。

Rubyでオブジェクトをハッシュ化して特定のkeyを取り除いて新たなハッシュを返す

こんにちは!kossyです!



さて、今回は、Rubyでオブジェクトをハッシュ化して特定のkeyを取り除いて新たなハッシュを返す方法について、
ブログに残してみたいと思います。







下記のようなオブジェクトがあるとします。

=> #<Article:0x00007fe76b8d89c0
 id: 1,
 title: "練習試合の結果0",
 body:
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 released_at: Sat, 22 Jun 2019 12:02:35 JST +09:00,
 expired_at: Fri, 28 Jun 2019 12:02:35 JST +09:00,
 member_only: true,
 created_at: Sun, 30 Jun 2019 12:02:35 JST +09:00,
 updated_at: Sun, 30 Jun 2019 12:02:35 JST +09:00>

このオブジェクトをハッシュ化したい場合、
attriburesメソッドを使うと、ハッシュ化できます。

rails/attribute_methods.rb at master · rails/rails · GitHub

pry(main)> article.attributes
=> {"id"=>1,
 "title"=>"練習試合の結果0",
 "body"=>
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 "released_at"=>Sat, 22 Jun 2019 12:02:35 JST +09:00,
 "expired_at"=>Fri, 28 Jun 2019 12:02:35 JST +09:00,
 "member_only"=>true,
 "created_at"=>Sun, 30 Jun 2019 12:02:35 JST +09:00,
 "updated_at"=>Sun, 30 Jun 2019 12:02:35 JST +09:00}


$ article.attributes.class
=> Hash

ハッシュ化したあと、特定のkeyを取り除いて新たなハッシュを作成したい場合は、
reject(またはdelete_if)を使います。

$ article.attributes.reject{|key| ['id', 'body', 'released_at', 'expired_at', 'created_at', 'updated_at'].include?(key) }
=> {"title"=>"練習試合の結果0", "member_only"=>true}


特定のkeyを取り除いて、新たなハッシュを返すことができました。

rejectにブロック引数を複数渡すこともできます。
例えば、特定のkeyを取り除きつつ、valuenilの値を取り除きたいとします。

=> #<Article:0x00007fe76b8d89c0
 id: 1,
 title: "練習試合の結果0",
 body:
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 released_at: Sat, 22 Jun 2019 12:02:35 JST +09:00,
 expired_at: nil,
 member_only: true,
 created_at: Sun, 30 Jun 2019 12:02:35 JST +09:00,
 updated_at: Sun, 30 Jun 2019 12:02:35 JST +09:00>

$ article.attributes.reject{|k, v| ['id', 'body', 'released_at', 'created_at', 'updated_at'].include?(k) || v.nil?}

include?やnil?メソッドを駆使して、目的を達成することができました。

Rubyってほんと書きやすいですね。

FactoryBotで多対多の関連を組んでいる時のモックデータを作成する

こんにちは!kossyです!



さて、今回はFactoryBotで多対多の関連を組んでいる時のモックデータを作成するやり方について、ブログに残してみたいと思います。



例えば、

  • 学生(students)は授業(rectures)を複数持っている
  • 授業は学生を複数持っている

という関連があるとしましょう。

この場合、

# FactoryBotの省略構文を用いています。

student = create  :student
recture = create :recture
create :student_recture, student: student, recture: recture

上記のような定義方法になるかと思いますが、

student = create  :student
recture = create :recture, students: [student]

関連先のオブジェクトを配列で指定することによって、
中間テーブルのレコードを同時に作成することができます。


勉強になりました。