- 投稿日:2021-06-15T22:16:32+09:00
【Rails】カートモデルの代わりにsessionを使ってカート機能を実装する
はじめに 簡単な自己紹介 はじめまして、shunと申します。 善良で健康な23歳です。 現在、「私立探求学園」というプログラミングスクールの「Rails共同開発プログラム」に2期生として参加しております。 プログラミング歴は執筆時点で3ヶ月ほどです。 Qiitaに記事を投稿するのは今回が初めてになります。 そのため、専門性に欠ける部分や読みづらい点が多々あるかとは思いますがご容赦ください。 本記事の内容 カート機能を実装する ECサイトでよく目にするカート機能を実装します。 今回実装するカート機能では以下の事が可能です。 本稿では主にコントローラに焦点を当てて解説します。 ① 「カートへ」ボタン押下で商品をカートに入れる。 ② カート内商品を表示する。 ③ カート内商品の個数変更1、商品削除ができる。 ④ 「購入する」ボタンを押下でカート内を空にして購入完了画面を表示する。 ⑤ 購入完了画面で注文番号を表示する。 本記事の特徴 本記事では、カート内で商品情報を出し入れする際にはカートモデルを使用しません。 その代わりに、sessionに商品情報を保存し、運用します。 sessionを使う理由 そもそもカート情報をDBに保存しなくてよいのでは?? 「カート機能の実装にはカートモデルを作ること」という前提が多くの記事で見られるかと思います。では、実際にカートモデルの情報をどう扱うのか考えてみると、 「あるユーザーのカート内に何の商品が入っているか・いたかを表示する」 くらいしか僕には思いつきませんでした。 「何の商品が入っているか」についてはsession機能で事足りますし、「いたか」についてはそもそもユーザーからの需要が感じられません。 Amazonや楽天市場を見ていただければ一目瞭然ですが、大手ECサイトでも過去にカートに何の商品が入っていたか確認できるページはおそらく存在しないはずです。注文履歴は必須ですが、カート履歴は需要ないのでは??と考え、カート情報を保存するモデルは不要だと判断しました。 ストレージの節約 sessionを使ってカート機能を実装するとカート内の商品情報をDBに保存しなくて済むため、ストレージ容量の浪費をその分抑えることができます。サーバーのレンタル料にも直結するので、できるだけ無駄は省きたいですよね。。 参考記事 Qiita マークダウン記法 一覧表・チートシート 【Laravel】誰でも出来る!sessionを使用したカート機能実装の完全解説!! - 前編 - Rails5でカート機能を作るためのロジックを作ってみた Ruby 配列関連のいろいろ instance method Time#to_i RubyのString入門 環境 ruby 2.7.1 rails 6.0.3.6 bootstrap 5.0.0.beta3 完成イメージ ①)「カートへ」ボタン押下で商品をカートに入れる。 ユーザーは「商品詳細ページ」から「カートへ」のボタン押下で任意の個数の商品をカートに入れることが可能です。 ②) カート内商品を表示する。 ③) カート内商品の数量変更、商品削除ができる。 ①の「カートへ」ボタン押下で「カート内商品一覧画面」に遷移します。 一覧ページでは、カート内商品の確認、数量変更、商品削除、注文確定などが可能です。2 ④) 「購入する」ボタンを押下でカート内を空にして購入完了画面を表示する。 ⑤) 購入完了画面で注文番号を表示する。 「注文を確定する」ボタンを押すと注文が確定されます。 その間はカート内の商品が空になり注文番号が表示され、その後「購入完了画面」に遷移します。 前準備 user(ユーザー)がproduct(商品)を購入すると、order(注文)が発生し、orderにはorder_details(注文詳細)が紐付きます。 (1)モデル作成 userモデル、 productモデル、 orderモデル、 order_detailモデル productモデルには商品名や価格、カテゴリーid(商品をカテゴリーに分類する際に使用)などが、 orderモデルには注文日時や注文番号などが、 order_detailsモデルには商品名、注文個数、発送日など、注文の詳細なデータが入ります。 (2)helperメソッドのcurrent_userの定義 以下のようにcurrent_userメソッドを定義しておくと非常に便利です。 app/helpers/sessions_helper.rb module SessionsHelper . . . def current_user @current_user ||= User.find_by(id: session[:user_id]) end . . . end 完成形 routes周り routes Rails.application.routes.draw do . . . get 'carts/show', to: 'carts#show', as: 'carts_show' post 'carts/add_cart', to: 'carts#add_cart' patch 'carts/change_quantity', to: 'carts#change_quantity', as: 'change_item_quantity' delete 'carts/destroy_carts_item', to: 'carts#destroy_carts_item', as: 'destroy_carts_item' get '/perchase_completed/:id', to: 'orders#perchase_completed', as: 'perchase_completed' resources :products resources :users resources :orders end view周り (尺の都合上、本題から外れる部分を除いています) app/views/carts/show.html.erb(商品詳細画面) ・ ・ ・ <% if @cart.blank? %> <h1 class="text-center">カートに商品はありません</h1> <% else %> ・ ・ ・ <tbody> <% @cart.each.with_index(1) do |cart, i| %> <tr class="text-center"> <th class="align-middle"> <%= i %> </th> <td class="align-middle"> <%= cart[:name] %> </td> <td class="align-middle"> <%= cart[:category_name] %> </td> <td class="align-middle"> <%= cart[:price] %> 円 </td> <% #数量変更 %> <%= form_with url: change_item_quantity_path, method: :patch, local: true do |f| %> <td class="align-middle"> <%= f.number_field :quantity, value: cart[:quantity], min: 1, class: "btn btn-outline-dark"%> 個 </td> <td class="align-middle"> <%= cart[:sub_total] %> 円 </td> <td> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "更新", class: "btn btn-primary" %> </td> <% end %> <% #商品削除 %> <%= form_with url: destroy_carts_item_path, method: :delete, local: true do |f| %> <td class="border-0 align-middle"> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "削除", class: "btn btn-danger" %> </td> <% end %> </tr> <% end %> <tr class="text-center"> <td class="border-bottom-0 align-right"> 合計 </td> <td class="border-bottom-0 align-right"> <%= @cart_total_price %>円 </td> </tr> ・ ・ ・ </tbody> ・ ・ ・ <% end %> ・ ・ ・ app/views/orders/perchase_completed.html.erb(購入完了画面) <main> <div class="blockquote mt-5 text-center"> <h1 style="font-weight: bolder">購入完了しました</h1> </div> <div class="container-sm mt-5 text-center"> <p class="h1">ご購入ありがとうございます!</p> <p class="h1"> <%= @display_number %> </p> </div> <div class="container text-center mt-5"> <%= link_to "Topに戻る", root_path, class: "btn btn-primary mt-5" %> </div> </main> コントローラ周り app/controllers/carts_controller.rb class CartsController < ApplicationController def show return if session[:cart].blank? @cart = [] session[:cart].each do |cart| product = Product.find_by(id: cart["product_id"]) sub_total = product.price * cart["quantity"].to_i next unless product @cart.push({ product_id: product.id, name: product.product_name, category_name: product.category.category_name, price: product.price, quantity: cart["quantity"].to_i, sub_total: sub_total }) end @cart_total_price = cart_total_price(@cart) end def add_cart # session内の商品の有無で条件分岐 if session[:cart].blank? # 商品が入っていない場合 session[:cart] = [{ product_id: params["product_id"], quantity: params["quantity"].to_i }] return redirect_to carts_show_path end # 商品が既に入っている場合、追加する商品が重複するかで条件分岐 match = session[:cart].select {|cart| cart["product_id"] == params["product_id"] } # 重複が発生する場合 if match.present? match[0]["quantity"] += params["quantity"].to_i # 重複が発生しない場合 else session[:cart].push({ product_id: params["product_id"], quantity: params["quantity"].to_i }) end redirect_to carts_show_path end # カート内商品の数量変更 def change_quantity array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart][array_index[0]]["quantity"] = params["quantity"] redirect_to carts_show_path end # カート内商品の削除 def destroy_carts_item array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart].delete_at(array_index[0]) redirect_to carts_show_path end # カート内商品の合計金額の計算 def cart_total_price(cart) cart.sum { _1[:sub_total] } end end app/controllers/orders_controller.rb class OrdersController < ApplicationController . . . def create if session[:cart].blank? return redirect_to carts_show_path end order = current_user.orders.create!( order_date: Time.current, order_number: ランダムな数字を16桁出す, ) session[:cart].each do |cart| order.order_details.create( product_id: cart["product_id"], shipment_status_id: 1, order_detail_number: "#{cart["product_id"]}#{complex_data}".rjust(16, "0"), order_quantity: cart["quantity"], ) end session[:cart].clear redirect_to perchase_completed_path(order.id) end def perchase_completed @display_number = Order.find_by(id: params[:id]).order_number end . . . end 注文番号について 3 作成手順 ① 「カートへ」ボタン押下で商品をカートに入れる。 について見ていきます。 以下のような流れです。 商品詳細ページ(products/:id)で任意の個数を入力して「カートへ」ボタンを押下 ↓ 分岐)session[:cart]内は空? true → session[:cart]にそのまま追加 「カート内商品一覧ページ」にリダイレクト false → 分岐)商品を追加することで重複する? true → 既にカート内にある商品に入力値を加算 false → カートに追加 「カート内商品一覧ページ」にリダイレクト ※sessionに商品情報を格納するイメージを簡単に説明します。 session[:cart]に {商品id: params["商品id"], 個数: params[個数]} のハッシュのセットを追加して配列を作成します。 以下のようなイメージです。 session内部のイメージ session[:cart] = [{"商品id"=>"1", "個数"=>4}, {"商品id"=>"8", "個数"=>7}, {"商品id"=>"4", "個数"=>1}, ... ] 流れを詳しく見ていきます。 まずは商品詳細ページについてですが、 個数入力欄と「カートへ」の箇所をformタグを適用させます。 「カートへ」ボタンはf.submitで設置しますが、その際に@poduct.idを:product_idとしてparamsに渡すためにhidden_field_tagを追加します。 ボタン押下でカートコントローラのadd_cartメソッドが走ります。 app/views/products/show.html.erb <%= form_with url: carts_add_cart_path, method: :post, local: true do |f| %> <%= f.label :quantity, "購入個数", class: "pur-number-box"%>> <%= f.number_field :quantity, min: 1, class: "pur-number" %>> <%= hidden_field_tag :product_id, @product.id %> <%= f.submit "カートへ", class: "cart-button" %>> <% end %> add_cartsが走るためのroutes Rails.application.routes.draw do . . . post 'carts/add_cart', to: 'carts#add_cart' . . . resources :products resources :users resources :orders end add_cartメソッドが走った段階でparamsには商品id(product_id)と個数(quantity)が渡されています。 (商品idが"1"である商品「りんご」を10個選択した状態で、add_cartメソッドに渡されたparamsをコンソール上で取得した画像) "product_id"=>"1","quantity"=>"10"の表記が確認できます。 次は、cartsコントローラのadd_cartメソッド内部についてです。 前提として、既にカートに追加されている商品と同じ商品をカートに追加しようとした場合(重複している場合)、加算処理が必要になります。 cartsコントローラ内のadd_cartメソッド def add_cart # session内の商品の有無で条件分岐 if session[:cart].blank? # 商品が入っていない場合 session[:cart] = [{ product_id: params["product_id"], quantity: params["quantity"].to_i }] return redirect_to carts_show_path end # 商品が既に入っている場合、追加する商品が重複するかで条件分岐 match = session[:cart].select {|cart| cart["product_id"] == params["product_id"] } # 重複が発生する場合 if match.present? match[0]["quantity"] += params["quantity"].to_i # 重複が発生しない場合 else session[:cart].push({ product_id: params["product_id"], quantity: params["quantity"].to_i }) end redirect_to carts_show_path end 条件分岐が2つあるので、前半後半に分けてみていきます。 まずは前半からです。 blank?メソッドを使って、session[:cart]が空かどうかを判断します。 空の場合は素直にsession[:cart] に [{ product_id: params["product_id"], quantity: params["quantity"].to_i }]を代入し、 カート内商品一覧画面にリダイレクトさせます。 cartsコントローラ内のadd_cartメソッド def add_cart # session内の商品の有無で条件分岐 if session[:cart].blank? # 商品が入っていない場合 session[:cart] = [{ product_id: params["product_id"], quantity: params["quantity"].to_i }] return redirect_to carts_show_path end . . . end 次は後半を見ていきます。 空でない場合、商品が重複するか否かで条件分岐させますが、その前に条件と一致しているか否かを示す変数を定義します。変数をmatchとし、 match = session[:cart].select {|cart| cart["product_id"] == params["product_id"] } とします。 ここでは、paramsに渡された商品idと同一の商品をsession[:cart]の中からselectメソッドで特定しようとしています。なので、特定でき、matchに値が返る場合重複していることを意味し、特定できずに値が返らない場合重複していないことを意味します。。 それを踏まえ、条件分岐を見ていきます。 present?メソッドを使ってmatchの中身があるかどうかを見ています。 trueの場合、重複しているので加算処理を行い、match[0]["quantity"]にparamsで取得した値を加算して返しています。 falseの場合、pushメソッドでsession[:cart]にparamsで取得した商品idと数量をそのまま格納します。 条件分岐後、カート内商品一覧ページにリダイレクトさせます。 cartsコントローラ内のadd_cartメソッド def add_cart . . . # 商品が既に入っている場合、追加する商品が重複するかで条件分岐 match = session[:cart].select {|cart| cart["product_id"] == params["product_id"] } # 重複が発生する場合 if match.present? match[0]["quantity"] += params["quantity"].to_i # 重複が発生しない場合 else session[:cart].push({ product_id: params["product_id"], quantity: params["quantity"].to_i }) end redirect_to carts_show_path end 次は ②カート内商品一覧ページを表示する。 についてです。 まず@cartに商品情報が格納されているかどうかで条件分岐します。 (@cartについての説明はコントローラの方でします。) if @cart.blank? で 格納されていない場合、 「カートに商品はありません」 とviewで表示するのみです。 格納されている場合、else内の処理が行われます。 carts/show.html.erb(カート内商品一覧画面) ・ ・ ・ <% if @cart.blank? %> <h1 class="text-center">カートに商品はありません</h1> <% else %> ・ ・ ・ <tbody> <% @cart.each.with_index(1) do |cart, i| %> <tr class="text-center"> <th class="align-middle"> <%= i %> </th> <td class="align-middle"> <%= cart[:name] %> </td> <td class="align-middle"> <%= cart[:category_name] %> </td> <td class="align-middle"> <%= cart[:price] %> 円 </td> <% #数量変更 %> <%= form_with url: change_item_quantity_path, method: :patch, local: true do |f| %> <td class="align-middle"> <%= f.number_field :quantity, value: cart[:quantity], min: 1, class: "btn btn-outline-dark"%> 個 </td> <td class="align-middle"> <%= cart[:sub_total] %> 円 </td> <td> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "更新", class: "btn btn-primary" %> </td> <% end %> <% #商品削除 %> <%= form_with url: destroy_carts_item_path, method: :delete, local: true do |f| %> <td class="border-0 align-middle"> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "削除", class: "btn btn-danger" %> </td> <% end %> </tr> <% end %> <tr class="text-center"> <td class="border-bottom-0 align-right"> 合計 </td> <td class="border-bottom-0 align-right"> <%= @cart_total_price %>円 </td> </tr> ・ ・ ・ </tbody> ・ ・ ・ <% end %> ・ ・ ・ 肝心の@cartについてです。 session[:cart]に保存する段階では、[商品idと個数]の組合せだけで良かったのですが、カート内商品一覧ページでは商品の詳細な情報を表示する必要があります。そのため、session[:cart]に保存されている各商品の[商品idと個数]の配列に対しeachメソッドを使って取り出し、空の配列@cartに詳細な商品情報が反映された配列を新しく作って表示させます。 cartsコントローラ内のadd_cartメソッド def show return if session[:cart].blank? @cart = [] session[:cart].each do |cart| product = Product.find_by(id: cart["product_id"]) sub_total = product.price * cart["quantity"].to_i next unless product @cart.push({ product_id: product.id, name: product.product_name, category_name: product.category.category_name, price: product.price, quantity: cart["quantity"].to_i, sub_total: sub_total }) end @cart_total_price = cart_total_price(@cart) end . . . end viewの説明に戻ります。viewでも同様、@cartに保存されている配列をeachで取り出します。この際、各商品の行番号がほしいので、with_indexのオプションを追加しています。 これで②番が完了です。 次は ③カート内商品の数量変更、商品削除ができる。 についてです。 数量変更と商品削除はそれぞれmethodが違うので、それぞれ別のルーティングを設定し、formタグで値を渡してあげます。 routesは上が数量変更で下が商品削除です。 数量変更と商品削除に関してのroutes patch 'carts/change_quantity', to: 'carts#change_quantity', as: 'change_item_quantity' delete 'carts/destroy_carts_item', to: 'carts#destroy_carts_item', as: 'destroy_carts_item' 数量変更と商品削除についてのview <% #数量変更 %> <%= form_with url: change_item_quantity_path, method: :patch, local: true do |f| %> <td class="align-middle"> <%= f.number_field :quantity, value: cart[:quantity], min: 1, class: "btn btn-outline-dark"%> 個 </td> <td class="align-middle"> <%= cart[:sub_total] %> 円 </td> <td> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "更新", class: "btn btn-primary" %> </td> <% end %> <% #商品削除 %> <%= form_with url: destroy_carts_item_path, method: :delete, local: true do |f| %> <td class="border-0 align-middle"> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "削除", class: "btn btn-danger" %> </td> <% end %> 数量変更と商品削除のコントローラ # カート内商品の数量変更 def change_quantity array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart][array_index[0]]["quantity"] = params["quantity"] redirect_to carts_show_path end # カート内商品の削除 def destroy_carts_item array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart].delete_at(array_index[0]) redirect_to carts_show_path end まずは数量変更について見ていきます。 「個数」の入力欄(f.number_field)に任意の数字を入力し、「更新」ボタンを押すと個数が変更されます。 「更新」ボタンはf.submitで設置しますが、その際にcart[:product_id]をparamsに:product_idとして渡すためにhidden_field_tagを追加します。 数量変更のパス patch 'carts/change_quantity', to: 'carts#change_quantity', as: 'change_item_quantity' メソッド内部について見ていきます。 array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } では、each_indexメソッドとselectメソッドを使っています。 https://www.sukerou.com/2018/06/blog-post_80.html 数量を変更したい商品のidと同一の商品をsession[:cart]内のからselectで指定し、each_indexで該当商品があった配列のindexを取得してarray_indexに代入しています。 次の行では、 配列のindexが入ったarray_indexを配列番号に当たる箇所に持ってきて、paramsの["quantity"]をsession[:cart]の["quantity"]に代入します。 そして最後に同ページにリダイレクトします。 数量変更のメソッド # カート内商品の数量変更 def change_quantity array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart][array_index[0]]["quantity"] = params["quantity"] redirect_to carts_show_path end これで数量は変更できるはずです。 続いて商品削除について見ていきます。 流れは数量変更に近いため、重複している箇所は端折りながら説明していきます。 まず、数量変更同様にhidden_field_tagを追加し、paramsに:product_idを渡すように設定します。 商品削除のview <% #商品削除 %> <%= form_with url: destroy_carts_item_path, method: :delete, local: true do |f| %> <td class="border-0 align-middle"> <%= hidden_field_tag :product_id, cart[:product_id] %> <%= f.submit "削除", class: "btn btn-danger" %> </td> <% end %> 「削除ボタン」押下でcartsコントローラのdestroy_carts_itemメソッドに飛びます。 商品削除のパス delete 'carts/destroy_carts_item', to: 'carts#destroy_carts_item', as: 'destroy_carts_item' メソッド内部では、 数量変更の時同様、selectメソッド、each_indexメソッドを使い、変数array_indexに該当の配列番号を代入します。 そして次の行で、session[:cart]のarray_indexを持つ配列をdelete_atメソッドで削除したのちに同ページにリダイレクトします。 商品削除 # カート内商品の削除 def destroy_carts_item array_index = session[:cart].each_index.select {|i| session[:cart][i]["product_id"] == params["product_id"] } session[:cart].delete_at(array_index[0]) redirect_to carts_show_path end これで商品を削除できるはずです。 次は ④「購入する」ボタンを押下でカート内を空にして購入完了画面を表示する。 についてです。 「注文を確定する」ボタン押下 ↓ orderコントローラのcreateメソッドが走り、session[:cart]を反映したorderデータとorder_detailsデータを作成 ↓ session[:cart]を空にする ↓ 購入完了画面に遷移 詳しく見ていきます。 まず、link_toメソッドを使って、「注文確定ボタン」を押すと、ordersコントローラのcreateメソッドに飛ぶように設定します。 app/views/carts/show.html.erbの「注文を確定する」ボタン . . . <td class="border-0"> <%= link_to "注文を確定する", orders_path, method: :post, class: "btn btn-primary"%> </td> . . . ルーティングの設定はresourcesで事足りるように設定しています。 order#createに対応するroute Rails.application.routes.draw do . . . resources :orders end createメソッド内部を詳しく見ていきます。 まず、blank?メソッドでsession[:cart]が空かどうかで条件分岐させます。 空の場合、returnで値を返してそれ以降無駄な処理が走らないようにしています。 次にuser_idに紐づくorderをcreateで作成し、その後、orderに紐づくorder_detailsをカート内の各商品ごとにeachメソッドを使って取得し、それぞれにcreateを使って作成します。 orderコントローラ内のcreateメソッド class OrdersController < ApplicationController . . . def create if session[:cart].blank? return redirect_to carts_show_path end order = current_user.orders.create!( order_date: Time.current, order_number: ランダムな数字を16桁出す, ) session[:cart].each do |cart| order.order_details.create( product_id: cart["product_id"], shipment_status_id: 1, order_detail_number: ランダムな数字を16桁出す, order_quantity: cart["quantity"], ) end session[:cart].clear redirect_to perchase_completed_path(order.id) end . . . end ※例) りんご(商品idが1)の商品を10個、 食パン(商品idが2)の商品を20個 がカートに入っている状態で「注文を確定する」ボタンを押すと、画像のようなorderデータ、order_detailsデータがDBに作成されます。 その後、clearメソッドでsession[:cart]を空にします。 購入完了画面にリダイレクトします。(ここで⑤で使うorder.idを渡しています。) 最後に ⑤購入完了画面で注文番号を表示する。 についてです。 ④でsession[:cart].clearが動いた後、購入完了画面にリダイレクトし、注文番号が表示されます。 以下のような流れになります。 購入完了画面にリダイレクト ↓ 購入完了画面のviewが表示され、インスタンス変数の@display_numberの定義元を辿る。 ↓ 定義元のorderコントローラ内のperchase_completedメソッドが働き、該当する注文番号を表示する。 詳しく見ていきます。 session[:cart].clearでカートを空にした後、 redirect_to perchase_completed_path(order.id)で購入完了画面にリダイレクトする際に、order.idを渡します。 次に購入完了画面のviewが表示され、@displayの定義元であるorderコントローラが走ります。 app/views/orders/perchase_completed.html.erb(購入完了画面) <main> <div class="blockquote mt-5 text-center"> <h1 style="font-weight: bolder">購入完了しました</h1> </div> <div class="container-sm mt-5 text-center"> <p class="h1">ご購入ありがとうございます!</p> <p class="h1"> <%= @display_number %> </p> </div> <div class="container text-center mt-5"> <%= link_to "Topに戻る", root_path, class: "btn btn-primary mt-5" %> </div> </main> orderコントローラのperchase_completedメソッドへ。 渡されたorder.idに紐づくorder_numberを特定して@display_numberに代入します。 これでviewに適切なorder_numberが@display_numberとして表示されます。 orderコントローラ内のperchase_completedメソッド class OrdersController < ApplicationController . . . def perchase_completed @display_number = Order.find_by(id: params[:id]).order_number end . . . end これで⑤が完了し、晴れて①~⑤の機能を兼ね備えたカート機能の完成です! 最後まで読んでいただき、ありがとうございました! ※訂正箇所など教えていただけると泣いて喜びます、、 本稿では個数と数量は同じような意味として扱っています。 ↩ 「買い物を続ける」ボタンを押下で商品検索画面に遷移可能ですが、商品検索画面はカート機能の対象外であるため説明は割愛させていただきます。 ↩ 一意な注文番号を作成するロジックについては割愛させていただきます。 ↩
- 投稿日:2021-06-15T22:16:03+09:00
本番環境でActiveAdminを導入しようと思ったら苦労した話
簡単だと思っていたActiveAdminの導入が想像以上に苦労した話 deviseも導入しないといけない 本番のアプリではユーザー機能がなかったので、deviseを導入してなかったのですが、当然Adminとは言えユーザーであることは変わらないので、deviseの導入が不可欠だった。 utf8mb4であるが故、マイグレーション時にエラー 本番のアプリは絵文字たっぷりなアプリ名ため、文字コードはutf8mb4を採用していました。 で、詳しく話すと長くなるのですが、要はutf8mb4とutf8の最大バイト数に違いがあるため、マイグレーション時にエラーが多発しました。 そのため、一旦MySQLの最大バイト数を減らす必要があります。 方法は上記サイトにて。 アセットパイプラインにactive_admin.cssがないですよという状態 さて、実際にインストールしたら、今度はCSSがアセットされてないですよというエラー。 これは下記記事のとおりコマンドを実行することで解消 CSSの汚染 さて、ここまできたら無事Adminのログイン画面が表示されて、ほっと一息。 と思ったらちょっと嫌な予感がしました。 というのもrailsはapplication.cssでassets/stylesheets配下のcssを全て読み込みます。 つまり表の環境でも読み込んじゃうのでは・・・? と思ったら予想的中。 案の定、CSS読み込んでしまって、見た目がぐちゃぐちゃになりました。 ということで、adminだけ別でCSS,JSを読み込みたい。 この記事をもとに汚染除去。 ここまできてようやく、無事管理者ログインができました・・・・
- 投稿日:2021-06-15T21:41:05+09:00
Railsによるリクエストからレスポンスまでの流れ
Railsによるリクエストからレスポンスまでの流れについて はじめまして、新卒でWeb開発エンジニアへの就職を目指して、昨年の10月から勉強を始めている情報系大学院生のAtusと申します。 Railsで簡単な掲示板アプリは作成出来るようになりましたので 、今まで勉強したことを復習するためにRailsによるリクエストからレスポンスまでの流れを記したいと思います。 GETリクエストからレスポンスまでの流れ ①コントローラが呼ばれる 例えば静的なサイトに対しては、ブラウザのアドレスバーにhttps://localhost:3000/usersと送るとホストサーバーのポート3000番に対してusersフォルダ配下のindex.htmlファイルをhttps通信で取得してきてという内容になる。 Railsのアプリでは、サーバーに対してGETリクエスト送ると、リクエストの処理を行うコントローラが呼ばれる。 どのコントローラが呼ばれるかはroute.rbファイルに書かれてあるルーティングによって来まる。 ②データベースが参照される場合はモデルを静的なページを返す場合にはビューが呼ばれる データベースとの通信が必要な場合はコントローラがモデルを介してデータベースから必要なデータを取得する。 取得したデータを元にビューファイルを作成しクライアントに返す。 データベースを介さない静的なページを取得したい場合、コントローラから直接ビューが呼ばれ、クライアントのブラウザにレンダリングされる。 概念図 最後に 拙い説明でしたが、ご覧頂きありがとうございました。 これからも、学んだことを日々アップしていきたいと思います。
- 投稿日:2021-06-15T21:24:16+09:00
【Rails】お気に入り機能の追加
完成GIF映像 User(会員)はPost(投稿)に対して、Like(お気に入り)をすることができる。 Userは1つのPostに対して、1つしかお気に入り出来ない状態にする。 非同期通信で行えるようにする。 1.routesの編集 お気に入りの詳細などは必要ないのでresourseでルーティング作成を行う。 config/routes.rb Rails.application.routes.draw do resources :posts do resource :likes, only: [:create, :destroy] end end resoursesは、:idを付与する。 resourseは、:idを付与しない。 2.モデルの編集 user.rb has_many :posts, dependent: :destroy has_many :likes, dependent: :destroy attachment :profile_image カラム名 データ型 説明 id integer PK last_name string 苗字 first_name string 名前 profile_image_id string 画像 post.rb belongs_to :user has_many: likes attachment :post_image カラム名 データ型 説明 id integer PK title string タイトル content text 内容 post_image_id string 画像 GIF動画ではジャンルがありましたが、今回は関係ないので端折ります。 like.rb belongs_to :user belongs_to :post validates_uniqueness_of :post_id, scope: :user_id 中間テーブルとなるLikeテーブル カラム名 データ型 説明 id integer PK user_id integer FK post_id integer FK validates_uniquness_ofは属性の値が一意であることをバリデーションする。 :scope で一意制約を決めるカラムを選択。 ターミナル $ rails g model user $ rails g model post $ rails g model like //migrateファイルを編集する $ rails db:migrate 2.PostモデルにLikeに関わるメソッドを追記 post.rb class Post < ApplicationRecord def liked_by?(user) likes.where(user_id: user.id).exists? end end Postに付けられているLikeにWhereで検索をかけて、User_idが存在するかどうかを確認。 existsは条件にマッチするレコードがあれば、true。無ければfalseを返すメソッドです。 3.コントローラーの編集 likes_controller.rb class Public::LikesController < ApplicationController before_action :authenticate_user! def create @post = Post.find(params[:post_id]) //どの投稿にお気に入りするかの情報(post_id) like = @post.likes.new(user_id: current_user.id) //誰がお気に入りにしたかの情報(user_id) like.save redirect_to request_referer //直前のページへ遷移 end def destroy @post = Post.find(params[:post_id]) //どの投稿のお気に入りを削除するか(post_id) like = @post.likes.find_by(user_id: current_user.id) //誰のお気に入り情報を削除するか一件のみ取得(user_id) like.destroy redirect_to request_referer end end 4.viewの作成 views/likes/_like_btn.html.erb <% if post.liked_by?(current_user) %> <%= link_to "#{ post.likes.count }", post_likes_path(post), method: :delete, class: "fas fa-star",style:"color: #DAA520;" %> <% else %> <%= link_to "#{ post.likes.count }", post_likes_path(post), method: :post, class: "far fa-star", style:"color:black;"%> <% end %> if文でcurrent_userがお気に入りをしているかどうかを識別。 liked_by?はPost.rbに作成したメソッドですね。 #{ post.likes.count }はお気に入りのカウントを表示させます。 投稿一覧の部分に表示させてみます。 views/posts/index.html.erb <div class="container"> <div class="row"> <div class="col-md-2"> <%= link_to "投稿する", new_post_path, class:"btn btn-info btn-lg"%> </div> </div> <div class="row mt-4"> <%= render "public/posts/index", posts: @posts%> </div> </div> views/posts/_index.html.erb <% posts.each do |post|%> <div class="col-12 col-md-6 col-lg-4"> <div class="card my-2" style="width: 18rem; "> <%= link_to post_path(post) do%> <%= attachment_image_tag(post, :post_image,size:"250x200",fallback: "no_image.png",class:"card-img-top")%> <% end %> <div class="card-body"> <div class="border-bottom pb-2"> <p class="card-title font-weight-bold"><%= post.title%></p> <span>ジャンル :<p class="btn btn-light btn-sm mt-2 ml-2"><%= post.genre.name %></p></span> <div class="d-flex"> <div><%= render "public/likes/like_btn", post: post%></div> </div> </div> <p class="d-inline">from:<%= full_name(post.user)%></p> </div> </div> </div> <% end %> Bootstrapを使うと比較的簡単にレイアウトを整えます。 5.Ajax(非同期通信化)する 非同期通信とは、ページ全体の再読み込み無しでページを更新する方法です。 同期通信は、redirect_toなどを用いてサーバーとのやり取りを行ってページを表示させる方法になります。 非同期通信にすることで、レスポンスを待つ必要がなくなるのでユーザーにとって使いやすい形になります。 今回は、お気に入りする時に使ったlink_toにremote: trueを追記することでAjaxさせます。 viewにremote_trueの追記 views/likes/_like_btn.html.erb <% if post.liked_by?(current_user) %> <%= link_to "#{ post.likes.count }", post_likes_path(post), method: :delete, remote: true, class: "fas fa-star",style:"color: #DAA520;" %> <% else %> <%= link_to "#{ post.likes.count }", post_likes_path(post), method: :post, remote: true, class: "far fa-star", style:"color:black;"%> <% end %> 「どこのお気に入りを非同期するか」を判断させるため id="post_<%= post.id %>" => 「どこの投稿?」 id="like_btn" => 「お気に入りボタン」 を追加。 post.idが識別されないと、全てに対してお気に入りすることになってしまうので忘れないように。 views/posts/_index.html.erb <% posts.each do |post|%> <div class="col-12 col-md-6 col-lg-4"> <div class="card my-2" style="width: 18rem; "> <%= link_to post_path(post) do%> <%= attachment_image_tag(post, :post_image,size:"250x200",fallback: "no_image.png",class:"card-img-top")%> <% end %> <div class="card-body"> <div class="border-bottom pb-2"> <p class="card-title font-weight-bold"><%= post.title%></p> <span>ジャンル :<p class="btn btn-light btn-sm mt-2 ml-2"><%= post.genre.name %></p></span> <div id="post_<%= post.id %>" class="d-flex"> <div id="like_btn"><%= render "public/likes/like_btn", post: post%></div> </div> </div> <p class="d-inline">from:<%= full_name(post.user)%></p> </div> </div> </div> <% end %> controllerの編集 redirect_toの記述を削除。 これで同期通信ではなく、非同期通信するための準備が出来ました。 likes_controller.rb def create @post = Post.find(params[:post_id]) like = @post.likes.new(user_id: current_user.id) like.save end def destroy @post = Post.find(params[:post_id]) like = @post.likes.find_by(user_id: current_user.id) like.destroy end js.erbファイルの作成 viewで非同期化させて、controllerにrenderやredirect_toがない場合にcontroller名/アクション名.js.erbを読みに行きます。 なので、今回はapp/views/likes/create.jsとapp/views/likes/destroy.jsを作成しましょう。 create&destroy.js.erb $("#post_<%= @post.id%> #like_btn").html("<%= j(render 'public/likes/like_btn', post: @post )%>"); .html()で対象の内容を変更。 j()はescape_javascript()の省略形で、js.erbファイルで部分テンプレートを呼び出す際に必要。
- 投稿日:2021-06-15T20:41:38+09:00
OpenStructを用いる オブジェクト指向設計実践ガイドより
はじめに 本題はここOpenStructからです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 OpenStruct 今回はOpenStructについて説明します。 OpenStructを用いて、よりコードを簡潔にしていきましょう。 オブエジェクト指向実践ガイドの第8章 「コンポジションでオブジェクトを組み合わせる」、 この章を理解していると言えるでしょう。 リファクタリング前 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end require 'forwardable' class Parts extend Forwardable def_delegators :@parts, :size,:each include Enumerable def initialize(parts) @parts = parts end def spares select{|part| part.needs_spare} end end class Part attr_reader :name, :description, :needs_spare def initialize(args) @name = args[:name] @description = args[:description] @needs_spare = args.fetch(:needs_spare,true) end end module PartsFactory def self.build(config,part_class = Part,parts_class = Parts) parts_class.new( config.collect{ |part_config| part_class.new( name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2,true) ) } ) end end road_config = [ ["chain","10-speed"], ["tire_size","23"], ["tape_color","Fox"] ] mountain_config = [ ["chain","10-speed"], ["tire_size","Manitou",false ], ["rear_shock","Fox"] ] road_bike = Bicycle.new( size: 'L', parts: PartsFactory.build(road_config) ) road_bike.spares mountain_bike = Bicycle.new( size: "L", parts: PartsFactory.build(mountain_config) ) mountain_bike.spares mountain_bike.parts.size 考察 PartsFactoryモジュールのbuildメソッドが複雑ですっきりさせたいですね。 また、PartクラスとPartsFactoryモジュールが重複しています。 リファクタリング後 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end require 'forwardable' class Parts extend Forwardable def_delegators :@parts, :size,:each include Enumerable def initialize(parts) @parts = parts end def spares select {|part| part.needs_spare} end end require 'ostruct' module PartsFactory def self.build(config,parts_class = Parts) parts_class.new( config.collect{|part_config| create_part(part_config) } ) end def self.create_part(part_config) OpenStruct.new( name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2,true) ) end end road_config = [ ["chain","10-speed"], ["tire_size","23"], ["tape_color","Fox"] ] mountain_config = [ ["chain","10-speed"], ["tire_size","Manitou",false ], ["rear_shock","Fox"] ] road_bike = Bicycle.new( size: 'L', parts: PartsFactory.build(road_config) ) road_bike.spares mountain_bike = Bicycle.new( size: "L", parts: PartsFactory.build(mountain_config) ) mountain_bike.spares mountain_bike.parts.size OpenStruct 新たにcreate_partメソッドを作り、buildメソッドを簡潔にしました。 そして、OpenStructを用いることによって、Partクラスを全て削除できました! 後記 OpenStructのより詳しい説明を後日加筆しようか考えてます 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T20:18:53+09:00
モジュールを適切に使う オブジェクト指向設計実践ガイド
はじめに 本題はここモジュールからです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 モジュール 今回はモジュールについて説明します。 下記リファクタリング前のコードはコンポジションで作られ、十分に使えるコードです。 今回は、インスタンスの処理をモジュール使ってすっきりさせましょう。 オブエジェクト指向実践ガイドの第8章 「コンポジションでオブジェクトを組み合わせる」、 この章を一部理解していると言えるでしょう。(残りの知識は別記事に書きます) リファクタリング前 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end class Parts attr_reader :parts def initialize(parts) @parts = parts end def spares parts.select{|part| part.needs_spare} end end class Part attr_reader :name, :description, :needs_spare def initialize(args) @name = args[:name] @description = args[:description] @needs_spare = args.fetch(:needs_spare,true) end end chain = Part.new(name: "chain",description: "10-speed") road_tire = Part.new(name: "tire_size",description: "23") tape = Part.new(name: "tape",description: "red") mountain_tire = Part.new(name: "tire_size",description: "2.1") rear_shock = Part.new(name: "rear_shock",description: "Foc") front_shock = Part.new(name: "front_shock", description: "Manitou", needs_spare: false) road_bike_parts = Parts.new([chain,road_tire,tape]) road_bike = Bicycle.new( size: "L", parts: road_bike_parts ) road_bike.size road_bike.spares mountain_bike_parts = Parts.new([chain,mountain_tire,front_shock,rear_shock]) mountain_bike = Bicycle.new( size: "L", parts: mountain_bike_parts ) mountain_bike.size mountain_bike.spares モジュールを用いる chain = Part.new(name: "chain",description: "10-speed") road_tire = Part.new(name: "tire_size",description: "23") tape = Part.new(name: "tape",description: "red") 上記の処理をすっきりさせたいですね。 モジュールを使ってリファクタリングしてみましょう。 リファクタリング後 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end class Parts attr_reader :parts def initialize(parts) @parts = parts end def spares parts.select{|part| part.needs_spare} end end class Part attr_reader :name, :description, :needs_spare def initialize(args) @name = args[:name] @description = args[:description] @needs_spare = args.fetch(:needs_spare,true) end end module PartsFactory def self.build(config,part_class = Part,parts_class = Parts) parts_class.new( config.collect{ |part_config| part_class.new( name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2,true) ) } ) end end road_config = [ ["chain","10-speed"], ["tire_size","23"], ["tape_color","Fox"] ] mountain_config = [ ["chain","10-speed"], ["tire_size","Manitou",false ], ["rear_shock","Fox"] ] road_bike = Bicycle.new( size: 'L', parts: PartsFactory.build(road_config) ) road_bike.spares mountain_bike = Bicycle.new( size: "L", parts: PartsFactory.build(mountain_config) ) mountain_bike.spares モジュール モジュールを新たに作り、インスタンスの処理がすっきりしました。 注意点!! PartsFactoryモジュールはまだ改善の余地はあります。(後日、別記事で書きます) 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T19:45:21+09:00
コンポジション オブジェクト指向設計実践ガイドより
はじめに 本題はここコンポジションからです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 コンポジション 今回は第8章コンポジションについて説明します。 下記リファクタリング前のコードは継承、フックメッセージが適切に使われおり、十分に使えるコードです。 今回は、継承を使わない観点、コンポジションという観点からリファクタリングして見て下さい。 オブエジェクト指向実践ガイドの第8章 「コンポジションでオブジェクトを組み合わせる」、 この章を一部理解していると言えるでしょう。(残りの知識は別記事に書きます) リファクタリング前 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end class Parts attr_reader :chain, :tire_size def initialize(args={}) @chain = chain || default_chain @tire_size = tire_size || default_tire_size post_initialize(args) end def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end def post_initialize(args) nil end def local_spares {} end def default_chain "10-speed" end def default_tire_size raise NotImplementedError end end class RoadBikeParts < Parts attr_reader :tape_color def post_initialize(args) @tape_color = args[:tape_color] end def local_spares {tape_color: tape_color} end def default_tire_size "23" end end class MountainBikeParts < Parts attr_reader :front_shock, :rear_shock def post_initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def local_spares {rear_shock: rear_shock} end def default_tire_size "2.1" end end road_bike = Bicycle.new(size: "L",parts: RoadBikeParts.new(tape_color: "red")) road_bike.size road_bike.spares 継承を使わない書き方 Partsクラスを各サブクラスが継承しています。 継承を使わない観点からリファクタリングしてみましょう。 リファクタリング後 class Bicycle attr_reader :size, :parts def initialize(args = {}) @size = args[:size] @parts = args[:parts] end def spares parts.spares end end class Parts attr_reader :parts def initialize(parts) @parts = parts end def spares parts.select{|part| part.needs_spare} end end class Part attr_reader :name, :description, :needs_spare def initialize(args) @name = args[:name] @description = args[:description] @needs_spare = args.fetch(:needs_spare,true) end end chain = Part.new(name: "chain",description: "10-speed") road_tire = Part.new(name: "tire_size",description: "23") tape = Part.new(name: "tape",description: "red") mountain_tire = Part.new(name: "tire_size",description: "2.1") rear_shock = Part.new(name: "rear_shock",description: "Foc") front_shock = Part.new(name: "front_shock", description: "Manitou", needs_spare: false) road_bike_parts = Parts.new([chain,road_tire,tape]) road_bike = Bicycle.new( size: "L", parts: road_bike_parts ) road_bike.size road_bike.spares コンポジション かなりすっきりしましたね! ただ、インスタンス作成の処理がごちゃごちゃしているので、 まだ改善の余地はあります。(後日、別記事で書きます) 継承使って書くか? コンポジションで書くか? 私の現在のレベルではうまく説明できません。 引用します。 一般的なルールとしては、直面した問題がコンポジションによって解決できるなら、 コンポジションで解決することを優先するべきです。(中略) 継承がより良い選択肢であるのは、継承が低いリスクで高い利益を生み出してくれるときです。 『オブジェクト指向設計実践ガイド』 p229 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T18:57:21+09:00
フックメッセージを使う オブジェクト指向実践ガイドより
読者対象 読者対象はプログラミング初学者です。 オブエジェクト指向実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 オブエジェクト思考実践ガイド 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクトし思考実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブエジェクト思考実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 フックメッセージ 今回は第6章フックメッセージについて説明します。 下記コードを読んで、このコードの危うさを説明し、自分なりにリファクタリングしたコードを書けるレベルに達しているなら、 オブエジェクト指向実践ガイドの第6章 「継承によって振る舞いを獲得する」、 この章を理解していると言えるでしょう。 注意点 下記コードは第6章の後半とあって、それなりにリファクタリングされています。 絶対にダメな書き方というわけではありませんので注意して下さい。 リファクタリング前 class Bicycle attr_reader :size,:chain,:tire_size def initialize(args) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def spares { tire_size: tire_size, chain: chain } end def default_chain "10-speed" end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares super.merge({tape_color: tape_color}) end def default_tire_size "23" end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge({rear_shock: rear_shock}) end def default_tire_size "2.1" end end bike = RoadBike.new(size: "L",tape_color: "red") bike.size bike.spares このコードの危うさ 新たにサブクラスが作られたとします。 このサブクラスでdefault_tire_sizeが書かれてなかったら、エラーが発生します。 また、superの書き忘れで思わぬエラーが発生します。 サブクラスどんどん増えていくにつれて、上記のエラー発生率が高くなります。 リファクタリング後 class Bicycle attr_reader :size,:chain,:tire_size def initialize(args) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end def post_initialize(args) nil end def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end def local_spares {} end def default_chain "10-speed" end # 適切なエラーを表示するため def default_tire_size raise NotImplementedError end end class RoadBike < Bicycle attr_reader :tape_color def post_initialize(args) @tape_color = args[:tape_color] end def local_spares {tape_color: tape_color} end def default_tire_size "23" end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def post_initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def local_spares {rear_shock: rear_shock} end def default_tire_size "2.1" end end bike = RoadBike.new(size: "L",tape_color: "red") bike.size bike.spares フックメッセージ default_tire_sizeのエラー処理を親クラスに記載しました。 ここから本題です。 親クラスのBicycleが各サブクラスにフックメッセージを送るようにしました。(post_initialize(args)) フックメッセージを使う目的は、 サブクラスからアルゴリズムの知識を取り除き、代わりにスーパークラスに制御を戻すことです。 (オブジェクト指向実践ガイド p172) 今回実装によって、superの書き忘れによるエラーを防ぐことができます。 また各サブクラスのコードの可読性も増しました。 新しいサブクラスを作る際も、リファクタリング前より簡潔に書けるようになります。 参考文献 オブエジェクト指向実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T18:57:21+09:00
フックメッセージを使う オブジェクト指向設計実践ガイドより
はじめに 本題はここフックメッセージからです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 フックメッセージ 今回は第6章フックメッセージについて説明します。 下記コードを読んで、このコードの危うさを説明し、自分なりにリファクタリングしたコードを書けるレベルに達しているなら、 オブエジェクト指向設計実践ガイドの第6章 「継承によって振る舞いを獲得する」、 この章を理解していると言えるでしょう。 注意点 下記コードは第6章の後半とあって、それなりにリファクタリングされています。 絶対にダメな書き方というわけではありませんので注意して下さい。 リファクタリング前 class Bicycle attr_reader :size,:chain,:tire_size def initialize(args) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def spares { tire_size: tire_size, chain: chain } end def default_chain "10-speed" end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares super.merge({tape_color: tape_color}) end def default_tire_size "23" end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge({rear_shock: rear_shock}) end def default_tire_size "2.1" end end bike = RoadBike.new(size: "L",tape_color: "red") bike.size bike.spares このコードの危うさ 新たにサブクラスが作られたとします。 このサブクラスでdefault_tire_sizeが書かれてなかったら、エラーが発生します。 また、superの書き忘れで思わぬエラーが発生します。 サブクラスどんどん増えていくにつれて、上記のエラー発生率が高くなります。 リファクタリング後 class Bicycle attr_reader :size,:chain,:tire_size def initialize(args) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end def post_initialize(args) nil end def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end def local_spares {} end def default_chain "10-speed" end # 適切なエラーを表示するため def default_tire_size raise NotImplementedError end end class RoadBike < Bicycle attr_reader :tape_color def post_initialize(args) @tape_color = args[:tape_color] end def local_spares {tape_color: tape_color} end def default_tire_size "23" end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def post_initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def local_spares {rear_shock: rear_shock} end def default_tire_size "2.1" end end bike = RoadBike.new(size: "L",tape_color: "red") bike.size bike.spares フックメッセージ default_tire_sizeのエラー処理を親クラスに記載しました。 ここから本題です。 親クラスのBicycleが各サブクラスにフックメッセージを送るようにしました。(post_initialize(args)) フックメッセージを使う目的は、 サブクラスからアルゴリズムの知識を取り除き、代わりにスーパークラスに制御を戻すことです。 (オブジェクト指向実践ガイド p172) 今回実装によって、superの書き忘れによるエラーを防ぐことができます。 また各サブクラスのコードの可読性も増しました。 新しいサブクラスを作る際も、リファクタリング前より簡潔に書けるようになります。 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T18:22:16+09:00
クラス継承 オブエジェクト指向実践ガイドより
読者対象 読者対象はプログラミング初学者です。 オブエジェクト指向実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 オブエジェクト思考実践ガイド 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクトし思考実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブエジェクト思考実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 クラス継承 今回は第6章クラスによる継承について説明します。 下記コードを読んで、このコードの危うさを説明し、自分なりにリファクタリングしたコードを書けるレベルに達しているなら、 オブエジェクト指向実践ガイドの第6章 「継承によって振る舞いを獲得する」、 この章を半分理解していると言えるでしょう。(残りは後日書きます) リファクタリング前 class Bicycle attr_reader :style,:size,:tape_color,:front_shock,:rear_shock def initialize(args) @style = args[:style] @size = args[:size] @tape_color = args[:tape_color] @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def spares if style == :road { chain: "10-speed", tire_size: "23", tape_color: tape_color } else { chain: "10-speed", tire_size: "2.1", rear_shock: rear_shock } end end end bike = Bicycle.new( style: :mountain, size: "s", front_shock: "Manitou", rear_shock: "Fox" ) bike.spares このコードの危険性 Bicycleクラスが肥大している。 特に、機能が拡張するたびに、sparesメソッド内のif文を修正しないといけない。←バグのもと リファクタリング class Bicycle attr_reader :size def initialize(args = {}) @size = args[:size] end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares { chain: "10-speed", tire_size: "23", tape_color: tape_color } end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge(rear_shock: rear_shock) end end road_bike = RoadBike.new( size: "M", tape_color: "red" ) road_bike.spares mountain_bike = MountainBike.new( size: "s", front_shock: "Manitou", rear_shock: "Fox" ) mountain_bike.size クラスの継承 Bicycleクラスの下に、新たにサブクラスを設置することによって、 Bicycleクラスがすっきりしました! 注意点 このリファクタリング後のコードがベストではありません。 より改善できます。サブクラスを設置しない方法もあります。 あくまでリファクタリング前よりかはよくなったと捉えて下さい。 参考文献 オブエジェクト指向実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T18:22:16+09:00
クラス継承 オブエジェクト指向設計実践ガイドより
はじめに 本題はここクラス継承からです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 クラス継承 今回は第6章クラスによる継承について説明します。 下記コードを読んで、このコードの危うさを考え説明して見て下さい。 そして、自分なりにリファクタリングしたコードを書いてみましょう。 この記事を理解したなら、オブエジェクト指向設計実践ガイドの第6章 「継承によって振る舞いを獲得する」、 この章を半分理解していると言えるでしょう。(残りは後日書きます) リファクタリング前 class Bicycle attr_reader :style,:size,:tape_color,:front_shock,:rear_shock def initialize(args) @style = args[:style] @size = args[:size] @tape_color = args[:tape_color] @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def spares if style == :road { chain: "10-speed", tire_size: "23", tape_color: tape_color } else { chain: "10-speed", tire_size: "2.1", rear_shock: rear_shock } end end end bike = Bicycle.new( style: :mountain, size: "s", front_shock: "Manitou", rear_shock: "Fox" ) bike.spares コードの危険性 Bicycleクラスが肥大している。 特に、機能が拡張するたびに、sparesメソッド内のif文を修正しないといけない。←バグのもと リファクタリング後 class Bicycle attr_reader :size def initialize(args = {}) @size = args[:size] end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares { chain: "10-speed", tire_size: "23", tape_color: tape_color } end end class MountainBike < Bicycle attr_reader :front_shock,:rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge(rear_shock: rear_shock) end end road_bike = RoadBike.new( size: "M", tape_color: "red" ) road_bike.spares mountain_bike = MountainBike.new( size: "s", front_shock: "Manitou", rear_shock: "Fox" ) mountain_bike.size クラスの継承 Bicycleクラスの下に、新たにサブクラスを設置することによって、 Bicycleクラスがすっきりしました! 注意点 このリファクタリング後のコードがベストではありません。 より改善できます。サブクラスを設置しない方法もあります。 あくまでリファクタリング前よりかはよくなったと捉えて下さい。 後記 コードの危険性の説明を加筆しようか検討中 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T16:46:23+09:00
ActiveHashを使用する上での命名規則
注意点 ActiveHashを使用する上で個人的に沼ったところを書き起こします。 マイグレーションファイルは○○_idで記述する db/migrate/2021********_create_items.rb class CreateItems < ActiveRecord::Migration[6.0] def change create_table :items do |t| t.string :name, null: false t.text :info, null: false t.integer :category_id, null: false t.integer :status_id, null: false t.integer :fee_status_id, null: false t.integer :prefecture_id, null: false t.integer :schedule_delivery_id, null: false t.integer :price, null: false t.references :user, null: false, foreign_key: true t.timestamps end end end こんな感じで、idと記述します。 モデル名はマイグレーションファイルで記述した○○にする これが個人的にめっちゃ沼ってしまった。 特に考えずに、例えばfee_status_idであればモデル名をfee.rbとかにしてました。 ちゃんとfee_status.rbでモデルを作成しましょう。 紐付け先のモデルには○○で記述する 今回はitemモデルにfee_statusモデルが紐づいているので、 app/models/item.rb belongs_to :fee_status と記述します。 あまり考えずに適当に名前つけてたら、変なところでハマってしまった....
- 投稿日:2021-06-15T16:22:18+09:00
Can't resolve image into URL: to_model delegated to attachment, but attachment is nilの解決
indexメソッドを使って一覧表示しようとしたときのエラーでした。 文章を訳すと画像をURLに変換できなかった、モデルと紐づけられているが、nilだよって感じですかね。 要するにitem.imageがnilなんです。 1モデルの紐付けを見直す app/models/item.rb class Item < ApplicationRecord extend ActiveHash::Associations::ActiveRecordExtensions belongs_to :user has_one_attached :image という感じで確かに紐付けしてる。 2画像がない投稿がある 今回はこっちでした。 Sequel Proで見てみると、itemの投稿は8個あったのに対して、画像は6個しかなかったんですね。 ということは2個は画像が紐づけられてないってことで、該当のレコードを削除して解決。
- 投稿日:2021-06-15T15:42:07+09:00
[Rails] テストコードでFakerを使う
はじめに 前回はテストコードでFactoryBotの導入と使い方をアウトプットさせていただきました。 今回は似たような感じで、テストコードでよく使用されるであろう、値をランダムに生成してくれるFakerの導入と使い方を説明できればと思います。よろしくお願いします。 説明に使用しているソースコードやファイルは前回の続きです。 Fakerとは メールアドレスや人名、パスワードなど、様々な意図に応じた値を生成してくれます。なぜ、このランダムな値が必要になるかは、複数のテストを行う際に、一意性の値などは、意図せず弾かれてしまう可能性があるからです。 Fakerは毎回ランダムに値を生成してくれるのでその心配がなくなります。 Fakerを導入 FactoryBot同様、FakerもGemfileのgroup :development, :test doに記述します。気をつける点ですね。 Gemfile group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails', '~> 4.0.0' gem 'factory_bot_rails' gem 'faker' end からのbundle install ターミナル % bundle install これで導入完了です。 コンソールから試してみましょう。rails cでコンソールを起動させます。 ターミナル [1] pry(main)> Faker::Name.initials(number: 2) => "TJ" [2] pry(main)> Faker::Internet.free_email => "ruthanne_thompson@gmail.com" [3] pry(main)> Faker::Internet.password(min_length: 6) => "O5y0Ru" このように毎回ランダムに生成してくれます。 早速Fakerの記述をしていきます。 前回使用したspec/factories/users.rbの記述を変更します。 spec/factories/users.rb FactoryBot.define do factory :user do nickname {Faker::Name.initials(number: 2)} email {Faker::Internet.free_email} password {Faker::Internet.password(min_length: 6)} password_confirmation {password} end end これで完了です。前回と同じようにテストを実行しても変わらず成功するはずです。 おまけ Fakerの公式GitHubを見ていたら、本当に様々なランダムな値を作れるみたいで面白かったので少し遊んでました。例えば下記をみてください ターミナル [1] pry(main)> Faker::JapaneseMedia::DragonBall.character => "Super Saiyan Goten" [2] pry(main)> Faker::JapaneseMedia::DragonBall.character => "Raditz" [3] pry(main)> Faker::JapaneseMedia::DragonBall.character => "Goten" [4] pry(main)> Faker::JapaneseMedia::DragonBall.character => "Demon King Piccolo" ドラゴンボールのキャラクターの名前をランダムで生成してくれます笑 こんなのはどうでしょうか ターミナル [1] pry(main)> Faker::JapaneseMedia::StudioGhibli.movie => "Porco Rosso" [2] pry(main)> Faker::JapaneseMedia::StudioGhibli.movie => "The Cat Returns" [3] pry(main)> Faker::JapaneseMedia::StudioGhibli.movie => "Porco Rosso" [4] pry(main)> Faker::JapaneseMedia::StudioGhibli.movie => "Castle in the Sky" さすがジブリ!Fakerでランダムに生成できます でも、英語表記だとなんだか馴染みがなくてわからない。。。 Porco Rossoは紅の豚ですね The Cat Rerurnsは猫の恩返しでしょうか、なんか違う気が、、、 Castle in the Skyはラピュタですかね笑 最後にポケモン ターミナル [1] pry(main)> Faker::Games::Pokemon.name => "ホウオウ" [2] pry(main)> Faker::Games::Pokemon.name => "シェルダー" [3] pry(main)> Faker::Games::Pokemon.name => "ケンタロス" [4] pry(main)> Faker::Games::Pokemon.name => "マリル" [5] pry(main)> Faker::Games::Pokemon.name => "ハネッコ" まさかの日本語で返ってきました笑 一発目、ホウオウが出たのなんか嬉しかったです。ガチャみたい こちらのFakerの公式GitHubのREADMEに色々あるのでみて見てください。結構面白いですよ。 https://github.com/faker-ruby/faker
- 投稿日:2021-06-15T15:06:41+09:00
Thinreportsのアップグレードに際して注意する点
対象バージョン:Thinreports 0.11 元々メモ書きで残していたものに若干の追記修正を行っただけなので一部表現がおかしくなっているかもしれません アップグレード Thinreports自体のアップグレードはGemを更新するだけで可能 ただ、テンプレートファイルについても同様にアップグレードを行う必要がある テンプレートファイルのアップグレード方法はバージョンを上げたエディタで開き直す この時、一気に最新バージョンのエディタで開こうとするとそもそもファイル自体開くことができない 一つ一つバージョンを上げて確認しないと上手くいかないので要注意 Thinreportsのバージョンが0.7 <= 0.8のThinreports Editorで開いてバージョンアップ ↓ Thinreportsのバージョンが0.8 <= 0.9のThinreports Editorで開いてバージョンアップ ↓ Thinreportsのバージョンが0.9 <= 0.11のThinreports Editorで開いてバージョンアップ バージョンアップに伴う修正部分 帳票の作成 0.7系での書き方は0.11系では以下のように変換できる reports_path = File.join(Rails.root, 'app', 'reports') # 帳票の作成 reports = Thinreports::Report.generate do |report| ~ end # あるいは report = Thinreports::Report.new layout: File.join(reports_path, 'test.tlf') # generate時のレイアウト指定 report.use_layout File.join(reports_path, 'test.tlf'), default: true # 新しいページの作成 report.start_new_page do |page| ~ end # ページを作成するたびに実行されるcallback report.on_page_create do |page| ~ end Report.newで帳票の作成を行う場合、処理完了後にreport.generateをしないとsend_fileなどでファイル送信しても正しくファイルが読み込まれないため注意 callback eventsが廃止されているため、該当部分を置き換える作業も必要となる events.on :page_create do |e| if e.page.layout.id == :test_id e.page.list(:details).add_row do (1..10).each do |cnt| item("test_#{cnt}").hide end ↓ report.on_page_create do |page| if page.layout.id == :test_id page.list(:details).add_row do |row| (1..10).each do |cnt| row.item("test_{cnt}").hide end events部分はすべて必要なくなり、代わりにitemで選択する際にrow.指定が必要となった ページ番号について item(:hogehoge).value(hoge) itemはテンプレートファイルに記載した変数指定 valueは変数に挿入する値のことで、"hoge"などで直接文字列を挿入することも可能 report.on_page_create do |page| page.item(:page).value(page.no) end ヘッダ・フッタについて list(:hogehoge) do |list| リスト表⽰された変数に順番に値を⼊れる リスト表⽰は⾏で区切られた表形式のようなもの on_footer_insert リストの最後に挿⼊される合計等に値を挿⼊する際に使⽤するもの page.list(:details) do |list| list.on_footer_insert do |footer| footer.values total_count: user.size, price: price, total_price: total_price end end また、以下のようにlambda式を用いることで一括でデータを入れることも可能 report_header_value = lambda do |page| page.values date_start date_st, date_end: data_ed, section: section, name: user.name, title: "HOGE" end lambda式で入れられたデータはreport_header_value.call(page)のように記述することで呼び出すことができる 参考 Thinreport - Github
- 投稿日:2021-06-15T13:55:44+09:00
Rails × Docker環境内でmysqlサーバーに接続するまでの流れ
SQLの学習目的で、Dockerコンテナ内でSQLサーバーに接続する際に生じたエラーの原因とその解決方法を記します。 1. コンテナ内に接続 → ERROR 2002 (HY000) ~ (2) → ERROR 2002 (HY000) ~ (111) docker-compose exec web bundle exec /bin/bash root@b671078fa536:/myapp# mysql ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) root@b671078fa536:/myapp# mkdir /var/run/mysqld root@b671078fa536:/myapp# touch /var/run/mysqld/mysqld.sock root@b671078fa536:/myapp# mysql ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (111) ERROR 2002 (HY000) ~ (2) このエラーは、mysqlに接続する為のソケットが存在しない事に由来している(らしい)。 なので上記コードでソケットを作成している。 ERROR 2002 (HY000) ~ (111) このエラーは、mysqlが起動していないことが原因(らしい)。 $ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------- myapp_chrome_1 /opt/bin/entry_point.sh Up 0.0.0.0:4444->4444/tcp myapp_db_1 docker-entrypoint.sh --def ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp myapp_web_1 entrypoint.sh bash -c rm - ... Up 0.0.0.0:3000->3000/tcp myapp_db_1は起動しているけどな? ここで少し時間を有する。思考錯誤の結果、以下の記事を参考にしました。 コンテナ指定接続 → mysqlサーバーへログイン 結果として自分の場合は、コンテナを指定してログインする事でmysqlサーバーへ正常にログインできるようになりました。 $ docker exec -it myapp_db_1 bash # コンテナ指定でログイン root@d9ca806f810a:/# mysql -u root # mysqlサーバーにログイン Enter password: Welcome to the MySQL monitor. Commands end with ; or \g.# 正常にログイン!! Your MySQL connection id is 8 Server version: 8.0.25 MySQL Community Server - GPL Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | myapp_development | | myapp_test | | mysql | | performance_schema | | sys | +--------------------+ DB名を指定してログインする事で時間節約 $ docker exec -it myapp_db_1 bash root@d9ca806f810a:/# mysql -u root -p myapp_development Enter password: Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 10 Server version: 8.0.25 MySQL Community Server - GPL Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select works.title, SUM(footprints.counts) as total_footprint_counts from works inner join footprints where works.id = 1; +--------------+------------------------+ | title | total_footprint_counts | +--------------+------------------------+ | Blade Runner | 1 | +--------------+------------------------+ 1 row in set (0.00 sec) やはりDocker周辺は知識不足により、難しく感じました。
- 投稿日:2021-06-15T13:19:01+09:00
[Ruby on Rails] RSpecテストの導入
1.Gem group :test do ~ endに編集。 Gemfile // ---------- // group :test do gem "capybara" gem "rspec-rails" gem "factory_bot_rails" gem "faker" end // ---------- // $ bundle $ rails g rspec:install //RSpecの準備 config/enviroments/test.rb config.active_support.deprecation = :silence // stderrからsilenceに変更する end 2.FactoryBotのデータ作成 specフォルダの直下にfactroiesフォルダを作成 user.rbを作成 spec/factories/user.rb FactoryBot.define do factory :user do last_name { Faker::Lorem.characters(number: 5) } first_name { Faker::Lorem.characters(number: 5) } email { Faker::Internet.email("kinpachi") } password { Faker::Internet.password(min_length: 6) } password_confirmation { password } end end Faker::~~に続くのは他にも色々ある。 FactoryBotが使用できるように設定。 spec/rails_helper.rb // ---------- // config.include FactoryBot::Syntax::Methods end 3.単体テストと結合テスト 単体テスト クラスやメソッド(関数)といった単位でのテスト。 空白チェック 文字列チェック 数値チェック 結合テスト 単体テストの組み合わせで期待とする動作が行えているかをテストする。
- 投稿日:2021-06-15T11:15:45+09:00
コードレビュー時に指摘されたこと
必要な機能の実装だけ行う 主にルーティングやcontrollerの記述 コメントアウトやconsole.logの削除 不要なものは削除する viewで表示したい時は<%= %>にする link_toでは可能な限りPrefixを使う rails routesで確認 Factorybotの記述 画像を添付するテストの場合は spec/factories/items.rb after(:build) do |item| item.image.attach(io: File.open("public/images/test_image.png"), filename: "test_image.png") end これを記述する。 ActiveHashを導入したときのテストコード 登録できない選択肢もテストする (---とか) 選択肢はクォーテーションで囲まない テストコード全般 数値型はシングルクォーテーションで囲まない 全角、半角、英字、数字のバリデーションをしている場合はそれ以外の文字列、混合の時のテストも記述する モデル バリデーションは要件定義に従って行う(画面は無視) presence: trueをwith_optionsでまとめてあげる has_one_attachedは1つのファイルを追加するために使用。そのため、単数系で記述。 controller encrypted_passwordについてはdeviseにてデフォルトで扱われるためpermitに含める必要はない
- 投稿日:2021-06-15T10:50:31+09:00
ダックタイピング オブエジェクト指向設計実践ガイドより
はじめに 本題はここダックタイピングからです 読者対象 読者対象はプログラミング初学者です。 といっても全くの初学者ではなく、Railsチュートリアルを終えたレベルと定義します。 オブジェクト指向設計実践ガイド オブエジェクト指向設計実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向設計実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクト指向設計実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブジェクト指向設計実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 最後に、もう一度、リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 ダックタイピング 今回はダックタイピングというコードの書き方を説明します。 下記コードを読んで、このコードの危うさを説明し、自分なりにリファクタリングしたコードを書けるレベルに達しているなら、 オブエジェクト指向設計実践ガイドの第五章 「ダックタイピングでコストを削減する」、 この章を理解していると言えるでしょう。 リファクタリング前 class Trip attr_reader :bicycles,:customers,:vehicle def prepare(prepares) prepares.each do |preparer| case preparer when Mechanic preparer.prepare_bicycles(bicycles) when TripCordinator preparer.buy_food(customers) when Driver preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle) end end end end class Mechanic def prepare_bicycles(bicycles) bicycles.each {|bicycle| prepare_bicycle(bicycle)} end def prepare_bicycle(bicycle) end end class TripCordinator def buy_food(customers) ; end end class Driver def gas_up(vehicle) ;end def fill_water_tank(vehicle) ; end end trip =Trip.new prepares = [Mechanic.new,TripCordinator.new] trip.prepare(prepares) このコードの危険性 Tripクラスのprepareメソッドが各クラスに依存しすぎている。 機能が拡張するたびに、prepareメソッドを修正しないといけない。←バグのもと リファクタリング後 class Trip attr_reader :bicycles,:customers,:vehicle def prepare(prepares) prepares.each do |preparer| preparer.prepare_trip(self) end end end class Mechanic def prepare_trip(trip) trip.bicycles.each do |bicycle| prepare_bicycle(bicycle) end end def prepare_bicycle(bicycle) ;end end class TripCordinator def prepare_trip(trip) buy_food(trip.customer) end def buy_food(customers) ; end end class Driver def prepare_trip(trip) vehicle = trip.vehicle gas_up(vehicle) fill_water_tank(vehicle) end def gas_up(vehicle) ;end def fill_water_tank(vehicle) ; end end trip =Trip.new prepares = [Mechanic.new,TripCordinator.new] trip.prepare(prepares) ダックタイピング 各クラスにprepare_trip(trip)メソッドを書くことによって、 Tripクラスのprepareメソッドがすっきりしました!! このような書き方をダックタイピングといいます。 詳しくはオブジェクト指向設計実践ガイドに書いてあります。 参考文献 オブエジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T10:50:31+09:00
ダックタイピング オブエジェクト指向実践ガイドより
読者対象 読者対象はプログラミング初学者です。 オブエジェクト指向実践ガイドの記事は、他の方も書いてありますが、 一度に全部まとめたものが多かったです。 この記事は、オブエジェクト指向らしく、各章ごとに、単一責任に、シンプルさを意識して書きました。 リファクタリング前のコードと、リファクタリング後のコードを読み比べ、 抽象的な思考、抽象的な書き方を共に学んでいきましょう。 一問一答 一問一答風の構成にしています。 リファクタリング前のコードを読み、コードの危うさ、嗅覚を養っていきましょう。 その後、リファクタリング前のコードの危うさを説明し、自分なりにリファクタリングしたコードを書いてみましょう。 オブエジェクト思考実践ガイド 余談ですが、とある弁護士が弁護士は六法全書を丸暗記してないが、各条文を見ただけで、関係ある条文、知識を思い出すそうです。 この記事は、オブジェクト指向実践ガイドってこんなこと書いてあったなーと思いだすための記事でもあります。 また、オブエジェクトし思考実践ガイドを読んだことがなくても、こういうことが書いてあるとのかー、とさわりだけでも理解して頂けたら幸いです。 オブエジェクト思考実践ガイドで得た3つのポイント リファクタリング前のコードを読み、コードの危うさを察知できる。 リファクタリングするために、より抽象的なコードの書き方を学べる。 gemなどの抽象的なコードを読解できる。 ダックタイピング 今回はダックタイピングというコードの書き方を説明します。 下記コードを読んで、このコードの危うさを説明し、自分なりにリファクタリングしたコードを書けるレベルに達しているなら、 オブエジェクト指向実践ガイドの第五章 「ダックタイピングでコストを削減する」、 この章を理解していると言えるでしょう。 リファクタリング前 class Trip attr_reader :bicycles,:customers,:vehicle def prepare(prepares) prepares.each do |preparer| case preparer when Mechanic preparer.prepare_bicycles(bicycles) when TripCordinator preparer.buy_food(customers) when Driver preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle) end end end end class Mechanic def prepare_bicycles(bicycles) bicycles.each {|bicycle| prepare_bicycle(bicycle)} end def prepare_bicycle(bicycle) end end class TripCordinator def buy_food(customers) ; end end class Driver def gas_up(vehicle) ;end def fill_water_tank(vehicle) ; end end trip =Trip.new prepares = [Mechanic.new,TripCordinator.new] trip.prepare(prepares) このコードの危険性 Tripクラスのprepareメソッドが各クラスに依存しすぎている。 機能が拡張するたびに、prepareメソッドを修正しないといけない。←バグのもと リファクタリング class Trip attr_reader :bicycles,:customers,:vehicle def prepare(prepares) prepares.each do |preparer| preparer.prepare_trip(self) end end end class Mechanic def prepare_trip(trip) trip.bicycles.each do |bicycle| prepare_bicycle(bicycle) end end def prepare_bicycle(bicycle) ;end end class TripCordinator def prepare_trip(trip) buy_food(trip.customer) end def buy_food(customers) ; end end class Driver def prepare_trip(trip) vehicle = trip.vehicle gas_up(vehicle) fill_water_tank(vehicle) end def gas_up(vehicle) ;end def fill_water_tank(vehicle) ; end end trip =Trip.new prepares = [Mechanic.new,TripCordinator.new] trip.prepare(prepares) ダックタイピング 各クラスにprepare_trip(trip)メソッドを書くことによって、 Tripクラスのprepareメソッドがすっきりしました!! このような書き方をダックタイピングといいます。 詳しくはオブエジェクト思考実践ガイドに書いてあります。 参考文献 オブエジェクト指向実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 投稿日:2021-06-15T06:34:03+09:00
【Rails】Herokuでデプロイしたアプリの削除
目的 Herokuでデプロイしたアプリを削除する。 開発環境 macOS: Big Sur Rubyバージョン: 2.6.5 Railsバージョン: 6.0.0 前提 アプリをHerokuにてデプロイ済み。 【Rails】Herokuでアプリをデプロイする方法 手順 はじめに 削除方法 はじめに 今回はHerokuにデプロイしたアプリの削除をしていきます。 削除方法 それでは早速始めていきます! 今回はターミナルから削除する方法で行っていきます。 ターミナル % heroku apps:destroy --app アプリ名 このコマンドによって、Heroku上のアプリもリモートリポジトリの設定もすべて消してくれます。 上記コマンドですと、確認のためにアプリ名の入力を求められますが、 下記コマンドを実行すれば、アプリ名の入力を求められず削除することができます。 % heroku apps:destroy --app アプリ名 --confirm アプリ名 これでアプリを削除することができました! 最後に 以上で、Herokuでデプロイしたアプリ削除は完了です。 では。
- 投稿日:2021-06-15T02:12:00+09:00
whereメソッドで複数条件指定してみた。(boolean真偽値とintegerの比較条件)
はじめに Whereメソッドで複数条件で事前にフィルターにかけて、 ransackに渡すことをしたく調べたけれどひとつはboolean型の真偽値、ひとつはinteger型の値を比較するand検索が載っている記事がなかったので、色々試してできたので書いておきます。 Whereメソッドの使い方 基本的なWhereメソッドの使い方はこう Whereメソッド基本形 モデル名.where(カラム名: 条件) # nameカラムから「yamada_kun」という文字が入っているレコードを全て取得 User.where(name: "yamada_kun") これでyamada_kunのレコードが取り出せます。 複数条件の基本形はこう(ANDでの検索) 複数条件(AND) モデル名.where(カラム名: 条件 , カラム名: 条件) #または モデル名.where(カラム名: 条件).where(カラム名: 条件) # nameカラムから「yamada_kun」と「ageが20」が入っているレコードを全て取得 User.where(name: "yamada_kun" , age: 20) User.where(name: "yamada_kun").where(age: 20) 比較するWhereメソッドの書き方はこう Whereメソッド(比較) モデル名.where("カラム名 > ?" , 数字 ) # idが10以上のレコードを全て取得 User.where("id > ?", 10) boolean型とinteger型でAND検索 それではこの基本形と比較して条件を定義してやる方法を組み合わせてやってみましょう。 あれ、エラーになりました・・・。 できない、できない、できないと色々と試してみてできたのがこちら。 booleanとinteger(比較)でAND検索 モデル名.where("カラム名 条件 and カラム名 > 数字") # nameがyamadakunでidが10以上のレコードを全て取得 User.where("name= "yamada_kun" and id > 10) ちなみに僕がransackを使ってやったコードがこちら @q = Item.where("onlist = true and stock >= 1").ransack(params[:q]) みなさんの探し物が見つかりますように! 参考サイト Pikawaka whereメソッドを使って欲しいデータの取得をしよう! SamuraiEngineerBlog スマートに検索!Railsのwhere使い方まとめ!
- 投稿日:2021-06-15T01:01:24+09:00
ActiveAdminを使用してみる
個人的に作成したアプリが PythonでYouTubeとTwitterのAPIの情報を取得する ↓ CSVでRailsのテーブルに登録 という手順。 今まではコマンドで実行していたが、視覚的にインポートをやりたいというのと、デプロイ後の管理方法が面倒なので、管理画面を実装したいなぁと思っていました。 そこで、ActiveAdminを使用することで課題がほぼ解決に。 gem "activeadmin", github: "gregbell/active_admin" gem "active_admin_importable" インポートもするので、二つともGemfileに記述。 でいつもの bundle install rake db:migrate rails s を順番に実行。 db/seeds.rb AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development? と記述がある通り、emailとpasswordの記載があります。 にアクセス。 rails g active_admin:resource モデル名 にて管理画面専用のモデルを作成。 今回はツイートモデルを例に app/admin/tweets.rb ActiveAdmin.register Tweet do active_admin_importable do |model, hash| model.create(text: hash[:text], image: hash[:image]) end permit_params :text, :image end これで手動で登録することもできるようになりますし、インポートも可能。 課題 今回の使用用途としては困らないですが、アソシエーションしている時に登録がうまくいかなかったです。 多分paramsの設定を○○_idって感じにしなきゃいけないっぽい。 とりあえず今回の目的としてはインポートができるので、すべてOK。 備考 日本語表記に変更したり、タイムゾーンを変更できたりもするらしい。 個人的な最終形としては Pythonで集計も定期実行にして、このインポートも定期実行にしてってやれば、ほぼメンテナンス要らずで管理ができるので、楽かなと。 そんな幻想を抱きながら、今日の作業は終了です、お疲れ様でした。
- 投稿日:2021-06-15T00:02:54+09:00
ridgepoleした後に、annotateを同時に実行する。
ridgepoleをした後に、annotateしてくれる流れを記載します。 以下のように annotate を実行するようにします。 Rake::Task['annotate_models'].invoke if Rails.env.development? annotate_models は rails g annotate:install 実行後に利用可能となります。 お忘れなく。 cf: https://github.com/ctran/annotate_models#configuration-in-rails ridgepoleをtaskにまとめて実行します。 以下のように annotate_models を追加したら終了です。 $ cat lib/tasks/ridgepole.rake # frozen_string_literal: true namespace :ridgepole do desc 'Apply database schema' task apply: :environment do ridgepole('--apply', "-E #{Rails.env}", "--file #{schema_file}") Rake::Task['db:schema:dump'].invoke if Rails.env.development? Rake::Task['annotate_models'].invoke if Rails.env.development? # これを追加する。 end desc 'Export database schema' task export: :environment do ridgepole('--export', "-E #{Rails.env}", '--split', "--output #{schema_file}") end private def schema_file Rails.root.join('db/schemas/Schemafile') end private def config_file Rails.root.join('config/database.yml') end private def ridgepole(*options) command = ['bundle exec ridgepole', "--config #{config_file}"] system [command + options].join(' ') end end スキーマを適応する。 $ bundle exec rake ridgepole:apply 【新卒】→ SIer入社、ドキュメントの日々 【転職】→ IT上場企業、主にインフラ担当 【転職】→ ベンチャーIT企業、主にインフラ担当 【現在】→ 起業 IT業界限定で全ての方に向けて、 今だけ限定で無料キャリア相談実施中。 オンライン面談予約は http://nekakoshi.youcanbook.me