ume

rails7 開発環境でメールを送信機能実装

やりたいこと

  • ECサイトで商品購入した際に購入商品の明細をメールで送りたい.

注意点: 今回はactive mailerの設定とどのようなメールが届くのかというのを開発環境で見ていこうと思います。実際のメールに届くわけではありません。

完成像

全体像

メーラーを作成する.
メーラーの編集.
③viewを作成と編集する.
④好きなコントローラーでメーラーのアクションを呼び出す.
⑤letter_opener_webというgemをインストール.
railsでletter_opener_webの設定をする.

メーラーを作成する

下記のコマンドでメーラーを作成

bin/rails generate mailer Customer #ここは任意の名前でOK.今回はCustomerとします

⚠️メーラーとはコントローラーのようなものとお考えください(メール専用のコントローラーと認識していただいても大丈夫です)

このコマンドを実行すると

create  app/mailers/customer_mailer.rb

app/mailers/配下にメーラーが作成されます。

メーラーの編集

メーラーの中にアクションを追加します(この時点でコントローラーと構造は同じことがわかると思います)

class CustomerMailer < ApplicationMailer

  def send_invoice
    @customer = params[:customer]
    @purchased_products = @customer.purchased_products
    mail(to: @customer.email, subject: '購入ありがとうございます')
  end
end

今回は購入処理の後に明細書を送信したいのでsend_invoiceというアクション名で作成していきます。
コントローラーと同じでこのアクション内で使用しているインスタンス変数(@customerや@purchased_productsなど)はこの後作成するメーラーのviewで使用可能です.
目新しいのは以下の部分だと思います。

 mail(to: @customer.email, subject: '購入ありがとうございます')

mail()メソッドで実際にメールを送ります。引数のto:で「どこのメールアドレス」にメールを送信するかを指定して、subject:でメールのタイトルを指定します。

③viewを作成と編集する.

app/views/customer_mailer/配下に メーラーの中のアクション名.html.erbでhtmlファイルを作成します.
app/views/ここは人それぞれ異なる/ 僕の場合下記を作成.

app/views/customer_mailer/send_invoice.html.erb
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1><%= @customer.first_name %>様、ご購入いただき誠にありがとうございます</h1>
    <p>
     こちら今回のお買い物の購入明細でございます
    </p>
    <table class="table table-striped-columns"> 
    <tr>
     <th scope="col">商品名</th>
     <th scope="col">料金</th>
     <th scope="col">数量</th>
     <th scope="col">合計</th>
    </tr>
    <% @purchased_products.each do |purchased_product|  %>
    <tr>
      <td><%=purchased_product.name%></td>
      <td><%=purchased_product.price%>円</td>
      <td><%=purchased_product.quantity%>個</td>
    </tr>
    <% end %>
     <tr>
      <td></td>
      <td></td>
      <td><%=calc_mail_total_quantity(@purchased_products)%>個</td>
      <td><%=calc_mail_total_price(@purchased_products)%>円</td>
    </tr>


    </table>
 
  </body>
</html>

このようにhtml形式で記載していきます。ここまででメールを送る下準備ができました。

④好きなコントローラーでメーラーのアクションを呼び出す.

下記のアクションで呼び出す

メーラー名.with(キー:値).アクション名.deliver_nowメソッド

withメソッドでキーと値を指定することでメーラーでparams[:キー]とすると値が取り出せます

 def create
    @customer = Customer.new(customer_params)
    if @customer.save 
      create_purchased_products
      @cart.destroy
      redirect_to   products_path, flash: { primary: '購入ありがとうございます' }
      CustomerMailer.with(customer: @customer).send_invoice.deliver_now
end 

このままではまだメールは送ることでができません。smtpサーバーを立てないと実際にメールを送ることはできません。しかし、今回は開発環境でどんなメールが送られてくるのかを見たいだけなので次のgemを使うことでサーバーを立てずに済みます。

⑤letter_opener_webというgemをインストール.

group :development do
  gem 'letter_opener_web', '~> 2.0'
end
bundle install

railsでletter_opener_webの設定をする.

railsのconfig/environments/development.rbこのファイルを開き下記2行を追記します

config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true

routes.rbに下記一行追記

Rails.application.routes.draw do
  mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development? #この行を追記
end 

最後にlocalhost:3000/letter_openerこのurlをブラウザーから飛べば完成画像のような画面が表示されます

参考情報

railsguides.jp

GitHub - ryanb/letter_opener: Preview mail in the browser instead of sending.

rails7 メンターさんにコードレビューしてもらったフィードバック

前書き

これは記事というより個人的な忘却録として残します。

指摘箇所

⇨find_or_create_by()メソッドを使おう

モデル.find_or_create_by(引数).  
⇨引数の条件でモデルからレコードを探しあれば戻り値として返すが、見つからない場合は(引数)をモデルに新規作成する。

私のコード. ↓では

 def set_customer
  @customer = Customer.find_by(id: 
    cookies.signed[:customer_id])
    return unless @customer.nil?
    @customer = Customer.create
  
   cookies.permanent.signed[:customer_id] = @customer.id
end

ファクタリング後

  def set_customer
    @customer = Customer.find_or_create_by(id: cookies.signed[:customer_id])
    cookies.permanent.signed[:customer_id] = @customer.id if @customer.id.nil?
  end 

本番環境でデータベースリセットしマイグレーションファイルの適用と初期データ挿入

前書き

  • 本番環境でデータベースをリセットし再びマイグレーションファイルを適用させたいのですがはまったので記事に残します

環境

  • rails7
  • ruby 3.0.8
  • heroku
  • postgresSQL

結論

heroku pg:reset -a <アプリ名>
heroku run rails db:migrate
heroku run rails db:seed

この3つのコマンド実行

rails7 from_withのオプションの違い(model:オプションとurl:オプション)

結論

フォームのデータをコントローラーでparams[:モデル名][:キー]でデータを受け取るかparams[:キー]で受け取るかの違い

modelオプション

⇨params[:モデル名][:キー]でデータを受け取る

urlオプション

⇨params[:キー]でデータを受け取る

modelオプション

例 

 <%= form_with model: CartProduct.new do |f| %>
  <%= f.number_field :quantity, class: "form-control text-center me-3", style: "max-width: 4rem", min: 1, max: 100%>
省略
<% end %>

modelオプションでモデルのインスタンス(@cart_productなど)を渡すかModel名.newで設定すると.
コントローラー内で

params{
 "cart_product"=>{"quantity"=>"4"}
}

このようになります。 paramsの中に[:モデル名][:key]のようになります.

urlオプション

 <%= form_with url: "/cart_products/1" do |f| %>
  <%= f.number_field :quantity, class: "form-control text-center me-3", style: "max-width: 4rem", min: 1, max: 100%>
省略
<% end %>

このようにurlで指定すると

params{
{"quantity"=>"4","id"=>"1"}
}

このようになります。modelオプションの時とは違い[:model名]が入らずにシンプルになります。またform_with url: "/cart_products/1" do |f| %>で設定した/cart_products/1の1の部分が{id:1}のようにparamsの中に格納されます

まとめ

modelオプションもurlオプションもしていることは指定したサーバーにフォームのデータを送ることが目的ですが、それぞれparamsの中の中にデータを入れる方法が異なることがわかりました。

Rails7メンターさんにしていただいたこと(Active StorageのN+1問題)

前書き

現在ECサイトを作成しています。

以下のコードで上記の表示をしています.
index.html(↓わかりやすいように余計なdivタグなど省略).
products_controller.rb

class ProductsController < ApplicationController
  def index
    @products = Product.all   #productテーブルからデータを全て取得
end 

index.html.erb

<% @products.each do |product| %> #Productテーブルのデータを一つずつ取り出す(みかんなど)
<%= link_to product_path(product.id)  do %>
 <%= image_tag product.image,class:"w-100 h-100", style:" aspect-ratio: 2 / 1;"   if product.image.attached? %>#product.imageでproduct(みかんなど)の関連データ(みかんの画像など)を取得表示する
<% end %>
<% end %>

データベースには Productテーブルに商品に関するデータが入っておりblobテーブルには商品に関連のある画像が格納されている

指摘箇所

Products_controller.rb

class ProductsController < ApplicationController
私のコード↓
  def index
    @products = Product.all
  end
メンターさんに↓のように修正するようにと言われました。
  def index
    @products = Product.with_attached_image
  end
end

解説(なぜこのように修正する必要があるか)

⇨N+1問題を解決するため

N+1問題とは?

shuttodev.hatenablog.com

https://pikawaka.com/rails/includes

N+1問題を一言でいうと「無駄にSQLが発行される」状態

どういう時にN+1問題が発生する?

⇨モデルオブジェクト.関連名.

関連名とは?

models/product.rbファイル

class Product < ApplicationRecord
  has_one_attached :image #⇦ここが
関連名
end 

下のコードを実行するとN+1問題発生する

@products = Product.all
@product.each do |product|
product.image  #⇦ここで無駄なSQLが発行される
end 
 解決策(active storageを使用している場合)

⇨ with_attached_imageメソッドを使う

@products = Product. with_attached_image

まとめ

  • N+1問題とは無駄なSQLを発行してしまう問題

  • モデルオブジェクト.関連名で発生する

正規表現を翻訳

前書き

下記の正規表現の意味を一つ一つ分解し理解を深める

/\A(?=.*\d)(?=.*[a-zA-Z])[a-zA-Z0-9]+\z/

\A

⇨先頭文字を評価

()

→グループ化. 評価する文字に()内で指定した複数文字をがあるか調べる.
例(じゃ)+ーん.
マッチする文字

じゃじゃーん
じゃじゃじゃーん

+は直前の一文字以上が評価する文字にあるのか調べる.
例じゃ+-ん⇨「ゃ」が一文字以上あるのか調べる

?

⇨直前の文字がないか、1つだけある.
Windows?
マッチする値

window 
windows

例 wind?ows マッチしない

winddows

.

→一文字だったらなんでも良い マッチしない例がなぜいけないかというと

saru 先頭がzではない
maru 先頭がzではない
zau 文字数が足りない

*

アスタリスクの前の文字(u)が評価する文字の中になくてもいいし1文字以上あってもいい

例. maru*にマッチするかどうかを調べる

マッチする例

mar  #アスタリスクの前のuが評価する文字(mar)の中になくてもいい
maruuuuuuuuuu #アスタリスクの前のuが評価する文字(mar)の中に一文字以上あってもいい

マッチしない例

mavu

\d

⇨半角数字

例 3 マッチする例

342142

マッチしない例

12314

⇨はブランケットという。[]内の文字や数字にマッチするか調べる.
例 [a].
マッチする例

a  
aaaaa

マッチしない例

b
1

-を使うことで範囲を示す.
例. [a-z] ⇦a~zまでのアルファベットにマッチするか調べる.
マッチする例

b
cb

マッチしない例

1
23

+

⇨+の直前の文字(u)が評価する文字の中に一文字以上ある.
例 maru+.
マッチする文字

maru #評価する文字(maru)の中にuが入っているのでマッチする
maruuuuuuu #評価する文字の中に一文字以上uがあるのでマッチする

マッチしない文字

mar
marf

\z

⇨\z 文字列の末尾にマッチします.
⇨\Z 文字列の末尾にマッチします。 ただし文字列の最後の文字が改行ならばそれの手前にマッチします.
要は語尾が改行だったら改行を評価するのが\zで開業の直前まで評価するのが\Z

# 末尾が改行文字、正規表現は小文字の \z
"03-1234-5678\n" =~ /\A\d+-\d+-\d+\z/
# => nil (Rubyの世界では偽)

# 末尾が改行文字、正規表現は大文字の \Z
"03-1234-5678\n" =~ /\A\d+-\d+-\d+\Z/
# => 0 (Rubyの世界では真)

参考情報

https://www.youtube.com/watch?v=T7Y2kfMm7kk

regex-checker.com

rails7 ルーティングの記載の順番で挙動が変わる件について

前書き

正しいルーティングを記載しているのに想定していたコントローラーに繋がらなくハマったので記事に残します。

ハマったこと

new_admin_product(admin/products#new ) というprefixでリクエストを送るとBのadmin/productsコントローラーのnewアクションに遷移すると想定していました。ただ実際はAのproductsコントローラーのshowアクションに遷移してしまう。 上記のルーティングで実際作成されるパスは以下になります↓

| ルート名            | HTTPメソッド | パス                         | コントローラー#アクション   |
|---------------------|--------------|------------------------------|-----------------------------|
| admin_products      | GET          | /admin/products(.:format)    | products#index              |
| admin_product       | GET          | /admin/products/:id(.:format)| products#show  ⇦このアクションにつながる             |
|                     | POST         | /admin/products(.:format)    | admin/products#create      |
| new_admin_product   | GET          | /admin/products/new(.:format)| admin/products#new ⇦このアクションにつなげたい       |
| edit_admin_product  | GET          | /admin/products/:id/edit     | admin/products#edit      |
|                     | PATCH        | /admin/products/:id(.:format)| admin/products#update      |
|                     | PUT          | /admin/products/:id(.:format)| admin/products#update      |
|                     | DELETE       | /admin/products/:id(.:format)| admin/products#destroy     |

結論

⇨namespace :admin doとscope 'admin' doのルーティングの記載の順番を入れ替える

| ルート名            | HTTPメソッド | パス                             | コントローラー#アクション   |
|---------------------|--------------|----------------------------------|-----------------------------|
| admin_products      | POST         | /admin/products(.:format)        | admin/products#create      |
| new_admin_product   | GET          | /admin/products/new(.:format)    | admin/products#new ⇦このアクションにつながる    |
| edit_admin_product  | GET          | /admin/products/:id/edit.        | admin/products#edit        |
| admin_product       | PATCH        | /admin/products/:id(.:format)    | admin/products#update      |
|                     | PUT          | /admin/products/:id(.:format)    | admin/products#update      |
|                     | DELETE       | /admin/products/:id(.:format)    | admin/products#destroy     |
|                     | GET          | /admin/products(.:format)        | products#index              |
|                     | GET          | /admin/products/:id(.:format)    | products#show               |

解説

new_admin_product(/admin/products/new) というリクエストを送信するとroutes.rbの上から順番に一致するものを探す

| ルート名            | HTTPメソッド | パス                         | コントローラー#アクション   |
|---------------------|--------------|------------------------------|-----------------------------|
| admin_products      | GET          | /admin/products(.:format)    | products#index              |
| admin_product       | GET          | /admin/products/:id(.:format)| products#show  ⇦このアクションにつながる             |
|                     | POST         | /admin/products(.:format)    | admin/products#create      |
| new_admin_product   | GET          | /admin/products/new(.:format)| admin/products#new ⇦このアクションにつなげたい       |
| edit_admin_product  | GET          | /admin/products/:id/edit     | admin/products#edit      |
|                     | PATCH        | /admin/products/:id(.:format)| admin/products#update      |
|                     | PUT          | /admin/products/:id(.:format)| admin/products#update      |
|                     | DELETE       | /admin/products/:id(.:format)| admin/products#destroy     |

上のハマったルーティングを見ると /admin/products/newが/admin/products/:idと一致すると判断されたため想定外のコントローラーのアクションに遷移しました.
なぜなら:idの中にnewが代入される. :idと聞くとadmin/products/2などのように数値 が入るイメージでしたが数値以外も代入されるそうです

まとめ

  • ルーティングの記載の順番で想定外のコントローラーにつながる

  • /admin/products/:idなどの:idには数値以外も代入される

  • ルーティングは上から順番に一致するものを探す