20200210のRailsに関する記事は19件です。

Railsにおける単体テストの解説(Railsチュートリアル)

はじめに

自分が作成したWebサービスに単体テストを何気なく導入していましたので、ここで一旦立ち止まって、詳しく分かりやすくアウトプットしていこうと思います。

そもそも単体テストとは何か??

めちゃくちゃ簡単に言うと

合体前の部品(メソッド、モデル、ビューヘルパー)が、それぞれちゃんと動くかを確認するテストのこと。

下記の記事が用語自体の理解をするには非常に分かりやすいので参考にしてみてください。
単体テストとは

コードの解説

ここではRailsチュートリアルの「3.3 テストから始める」編を例にとってみていきます。
Railsチュートリアル3.3テストから始める

まずコントローラの作成で

$ rails generate controller StaticPages home help

を実行した時点でコントローラーのファイルに付随してテスト用のファイルも作成されるため、このテスト用のファイルにテストコードを書いていくことを理解する。

作成されたテスト用ファイルはこちら。

test/controllers/static_pages_controller_test.rb

中身を見ていきましょう。

require 'test_helper'   #test_helper.rbが読み込まれる

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do     #homeメソッドに対するテストコード
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do     #helpメソッドに対するテストコード
    get static_pages_help_url
    assert_response :success
  end
end

ここではコントローラを作成したと同時に作成されたhomeとhelpのメソッドに対するテストが書かれています。ここで書かれているテストコードは自動で作成されたものなので、これを場合によっては編集して実際にテストコードとして有効なものへと自分でいじっていかなければなりません。

上のコードを理解する前に
まずは下記の2つのコードが同等に振る舞うということを理解します。
これはRailsガイドにも書かれています。
Railsガイド

test "the truth" do
  assert true
end
----------------------------------------------------------------------------------
def test_the_truth 
  assert true 
end

上記のことから先ほどのhome,helpメソッドは下記のようになります

def test_should_get_home       #homeメソッドに対するテストコード
    get static_pages_home_url
    assert_response :success
end

def test_should_get_help       #helpメソッドに対するテストコード
    get static_pages_help_url
    assert_response :success
end

ここでなぜわざわざdefじゃなくてtestというコードを使うの?
と思われた方もいるのではないでしょうか。僕も思いました。
おそらくメソッド名の命名に気を使わなくて良くなり、扱いやすいからだと僕は解釈しています。

では次にメソッドの中身を見ていきましょう。

get static_pages_home_url
assert_response :success

簡単に訳すと、
「static_pages_home_urlを取得して、取得したものを評価し、成功と返す」
となります。

詳しく書きます。

  • 1行目にstatic_pages_home_urlをgetする(取得する)ということをあらわしています。

  • 2行目にその取得したurlから返ってきたHTTPステータスコードが200なら成功ということをあらわしています。

assert_responseに関してはRailsガイドを参照します

assert_response(type, message = nil)
レスポンスが特定のステータスコードを持っていることを主張する。:successを指定するとステータスコード200-299を指定したことになり、同様に:redirectは300-399、:missingは404、:errorは500-599にそれぞれマッチする。ステータスコードの数字や同等のシンボルを直接渡すこともできる。

ステータスコードとは、「PCやスマホの画面にこのウェブサイトを表示させたい」という<リクエスト>に対し、「現在このサイトは表示できません」「このサイトはURLが変更になりました」などのウェブサーバーからの<レスポンス>を表す数字のことで下記のような組み合わせとなっています。

以上のテストコードからここではhome,helpメソッドが単体でテストされているという解釈になります。

終わりに

ここではRailsチュートリアルにおける単体テストをほんの一部ではありますが記述しました。
また編集と改善を繰り返して行きたいと考えています。
私も勉強中であるため誤りがあるかもしれませんが、指摘していただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ransackでラベル検索をチェックボックス検索にしたい

Ransackを使ったラベル(label)検索をどうしてもチェックボックス(check_box)で実装したいことがあったのでメモ

スクリーンショット 2020-02-10 18.25.55.png

ラベル機能がある状態からスタート!!!
※ラベル機能の作成まではこの記事で完結できます↓
【gemなし】Railsでラベル機能を作る

gemのransackをインストール

rb.Gemfile
gem 'ransack'
$ bundle install
posts_controller.rb
  def index
    @search = Post.ransack(params[:q])
    @posts = @search.result(distinct: true)
  end


  private
    def post_params
      params.require(:post).permit(:title, :details, label_ids: [] )
    end
view.posts.index.html.erb
  <%= search_form_for @search do |f| %>
  <% Label.all.each do |label| %>
    <%= f.check_box :labels_id_eq_any, { multiple: true, checked: label[:checked], disabled: label[:disabled], include_hidden: false }, label[:id] %>
    <label><%= label.title %></label>
  <% end %>
      <%= f.submit "検索" %>
  <% end %>

以上です!

参考記事
https://www.tom08.net/entry/2016/11/11/144813

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]paranoiaを使わずに退会機能を実装する(ユーザーを論理削除する)

実装すること

paranoiaを使わずに、ユーザーの退会機能を作成するために下記のコードを書いていきます。
①ユーザーを論理削除する。
②退会ユーザーをログアウトさせる。
③退会ユーザーはログインできなくする。

ページ設計

マイページ(users/show.html.erb)で退会できるようにします。

Userテーブル定義

is_deletedカラムを作成し、デフォルトをfalseにしておきます。
退会ユーザーのis_deletedをtrueにすることで退会ユーザーのデータは残したまま、退会しているかどうかを区別できるようにします。

カラム名 カラム説明 データ型 デフォルト
ID ユーザーID integer
name ユーザー名 string
is_deleted 削除フラグ datetime FALASE
created_at 登録日 datetime NOW
updated_at 更新日 boolean NOW

Userモデルの作成

Userモデルはdeviseを使用してモデルを作成していきます。

deviseの導入・Userモデルの作成

Gemfile.
gem 'devise'
$ bundle install
$ rails g devise:install
$ rails g devise user name:string is_deleted:boolean
$ rails g devise:views

コントローラの作成・編集

$ rails g controller users

users_controller.rb

hideアクションを作成します。
@user.update(is_deleted: true)で、@userのis_deletedカラムをtrueにupdateします。
reset_sessionで、ユーザーをログアウトさせます。
redirect_to root_pathで、ログアウト後ルートパスに飛ばします。

app/controllers/users_controller.rb
    def show
        @user = User.find(params[:id])
    end

    def hide
        @user = User.find(params[:id])
        #is_deletedカラムにフラグを立てる(defaultはfalse)
        @user.update(is_deleted: true)
        #ログアウトさせる
        reset_session
        flash[:notice] = "ありがとうございました。またのご利用を心よりお待ちしております。"
        redirect_to root_path
    end

ルーティングの作成

hideアクションのルーティングを作成します。
as: 'users_hide'で、showアクションと間違われないようURL名を指定します。

config/routes.rb
Rails.application.routes.draw do

  devise_for :users

  resources :users
  put "/users/:id/hide" => "users#hide", as: 'users_hide'
end

退会ボタンの作成

マイページ(users/show.html.erb)

if user_signed_in? && @user.id == current_user.idで、ユーザーがログインしていて且つログインユーザーであれば退会ボタンを表示するようにしています。
"data-confirm" => "本当に退会しますか?"で退会ボタンクリック時にアラートが出るようにしています。

app/views/users/show.html
<% if user_signed_in? && @user.id == current_user.id %>
        <%= link_to "退会", users_hide_path(current_user), method: :put, "data-confirm" => "本当に退会しますか?", class: "btn btn-outline-danger" %>
<% end %>

退会ユーザーはログインできなくする

user.rb

ログインする時に退会済み(is_deleted==true)のユーザーを弾くためのメソッドを作成します。
super && (self.is_deleted == false)で、userのis_deletedがfalseならtrueを返すようにしています。

app/models/user.rb
class User < ApplicationRecord

  def active_for_authentication?
    super && (self.is_deleted == false)
  end

end

sessions_controller.rb

if (@user.valid_password?(params[:user][:password])で、入力されたパスワードが正しいことを確認しています。
(@user.active_for_authentication? == false))で、@userのactive_for_authentication?メソッドがfalseであるかどうかを確認しています。
・上記の2点が当てはまれば、ログインページにリダイレクトし、エラーメッセージを表示するようにしています。

app/controllers/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController

  before_action :reject_user, only: [:create]

  protected

  def reject_user
    @user = User.find_by(email: params[:user][:email].downcase)
    if @user
      if (@user.valid_password?(params[:user][:password]) && (@user.active_for_authentication? == false))
        flash[:error] = "退会済みです。"
        redirect_to new_user_session_path
      end
    else
      flash[:error] = "必須項目を入力してください。"
    end
  end
end

sessions/new.html.erb

エラーメッセージを表示するため、<%= flash[:error] %>をお好きなところに追記してください。

app/views/sessions/new.html
<%= flash[:error] %>

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。

参考

特定のユーザのアカウントは残しつつ、ログインできないようにしたい
https://blog.edwardkenfox.com/post/90435229476/%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%AE%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AF%E6%AE%8B%E3%81%97%E3%81%A4%E3%81%A4%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%9F%E3%81%84

RailsでDeviseを使っていて、入力されたパスワードが正しいかどうかを自前で実装したい場合
https://qiita.com/knt45/items/49f8ad2bdef906dca302

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】「都道府県」を表示させるため、View側でDB参照した部分のテストでハマった

はじめに

まとめをかきます。

  • テスト実行時には、DBのレコードは空っぽです。マスターテーブルをView側で参照した部分のテストでは事前にレコードをつくりましょう。
  • さもないと「ブラウザで動いてるのに、テスト時だけコケる」ので注意しましょう。
  • ActiveHashに置き換えると、db:seedの手間もなくなり、変更も容易になります

    -------✁-------✁-------✁-------✁-------

※以下は、解決までにどのようなことを考えて試したかの話になります。
筆者自身への備忘録な成分が多めですのでご了承ください。

開発の環境

  • Rails 6.0.2
  • Ruby 2.7
  • simple_form 5.0.2
  • RSpec 3.9.1

住所の都道府県の選択をPrefectureテーブルとsimple_formで実装

Prefectureはnameだけをもつ、都道府県のマスターテーブルです。

app/views/addresses/_form.html.haml
= f.input :prefecture, selected: "東京都", collection: Prefecture.all, label_method: :name, value_method: :id

こうすると、こう。

image.png

Prefectureテーブル用に以下のseedをつかいました。

prefecture_seed.rb
prefs = %w[北海道 青森県 岩手県 宮城県 秋田県 山形県 福島県 茨城県 栃木県 群馬県 埼玉県 千葉県 東京都 神奈川県 新潟県 富山県 石川県 福井県 山梨県 長野県 岐阜県 静岡県 愛知県 三重県 滋賀県 京都府 大阪府 兵庫県 奈良県 和歌山県 鳥取県 島根県 岡山県 広島県 山口県 徳島県 香川県 愛媛県 高知県 福岡県 佐賀県 長崎県 熊本県 大分県 宮崎県 鹿児島県 沖縄]

insert_prefs = prefs.map {|pref| {name: pref, created_at: Time.current, updated_at: Time.current } }    

Prefecture.insert_all!(insert_prefs)

System Testでセレクト要素を選択する部分がコケた【今回の問題点】

「新規住所を登録する」Specをつくりました。

addresses_spec.rb
visit new_user_address_path(user)
select "高知県", from: "都道府県"

すると、ElementNotFoundがでました。("高知県" がみつかっていない)

ブラウザで実行すると表示される

ブラウザではたしかに表示されているし、期待通りの動きをしているのに、、、、なぜだ。。。

スクショを確認すると、セレクト要素が空白

System Specが落ちたときのスクショを確認すると、セレクト要素が空白でした。(ここで気づきたかった!)

頭の中は「?? :thinking: ??」

IDEを起動したりしました。

ためしたこと 1.配列を直接わたすとパスする

simple_formのcollectionが動いていない?

app/views/addresses/_form.html.haml
= f.input :prefecture, selected: "東京都", collection: ["東京都", "高知県"]

書き換えてみると、テストはパスしました。

ためしたこと 2.ActiveHashにするとパスする

gem 'active_hash'
bundle install

したあとに、2都県の日本でためしてみました。

app/models/prefecture.rb
class Prefecture < ActiveHash::Base
  field :name
  add id: 1 , name: "北海道"
  add id: 2 , name: "高知県"
end

パスします。
あとで調べてわかったのですが、ActiveHash化した Prefecture.all は単に配列を返すので、1.の検証とおなじ挙動になります。

テスト実行時は「レコード0件の世界」であることに気づく【解決】

要素が表示されない。

Prefecture.allはエラーではない。

つまりレコードが存在しない。

なぜ・・・

時計の長針が1周しようかというところで、ついにテスト実行時は「レコード0件の世界」であることに気づくわけです。長かった。

スペック内でPrefectureにレコードをつくるとパスする

検証。

addresses_spec.rb
Prefecture.create!(name: "東京都")
Prefecture.create!(name: "高知県")

visit new_user_address_path(user)

select "高知県", from: "都道府県"

パスしました。

さいごに

ここまで読んでいただきありがとうございました。

普段の記事では、問題に対する答えだけをかくようにしていますが、今回は「どのようにして解決に至ったか」までの道のりをかいてみました。

「simple_formの使い方がまちがっているんだろう」というところから、おなじ問題がIssueにあがってないかと公式レポジトリを検索したり、迷走の極みでした。

人に聞くと解決するメソッド

気づいてしまえばなんでもないことほど深くハマってしまうことがあります。筆者の場合は、チャットで質問しようと文章を書いているときに :qiitan::bulb:解決しました。

ハマったときこそ考え込まず、文章 :writing_hand: にしたり、他の人に説明 :speaking_head: してみるといいかもしれません。

追伸:PrefectureはActiveHashにしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

1. クレジットカード番号などの情報登録:view
2. ”トークン”作成:Javascript
3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller
4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について(カスタムフォーム使用)

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。
イメージとしては以下のようになっており、一時的なトークンを作成し、取引をすることで、加盟店はクレジットカード情報などの重要な情報を扱う必要がないまま、取引ができます。

payjpimage.png

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

1. クレジットカード番号などの情報登録:view
2. ”トークン”作成:Javascript
3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller
4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で一時的な”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について/カスタムフォーム版

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。
イメージとしては以下のようになっており、一時的なトークンを作成し、取引をすることで、加盟店はクレジットカード情報などの重要な情報を扱う必要がないまま、取引ができます。

payjpimage.png

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

  1. 下準備(gemインストール+application.html.hamlに追記) 1. クレジットカード番号などの情報登録:view 2. ”トークン”作成:Javascript 3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller 4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

0. 下準備(gemインストール+application contorollerに追記)

下準備として、pay.jpのgemをインストールします。

Gemfile
gem 'payjp'

記載後、"bundle install"を忘れずに実行しましょう!

また、viewにあるapplication.html.hamlに以下の記述を行いましょう。

application.html.haml
%script{src: "https://js.pay.jp/", type: "text/javascript"} 
%script{type:"text/javascript"}Payjp.setPublicKey('公開鍵'); 

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で一時的な”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Ajaxを用いて非同期でフォロー機能の実装

実装すること

Ajaxを用いて非同期通信を行い、フォロー機能を書いていきます。
Ajaxについては下記リンク先で説明しています。
[Rails]Ajaxを用いて非同期でコメント機能の実装:https://qiita.com/yuto_1014/items/c7d6213139a48833e21a

ER図

Userテーブル同士で「多対多」の関係を作ります。なぜなら、フォローしているユーザーもフォロワーもユーザになるからです。そこでUserテーブル同士の中間テーブルとしてRelationshipテーブルを作成します。
フォローER図.png

ページ設計

ユーザー詳細ページ(user/show.html.erb)でフォローができる。

モデルの作成

Userモデルは作成した前提で進めていきます。
作成手順は下記リンク先で説明しております。
[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
それではRelationshipモデルを作成していきます。

$ rails g model Relationship follower_id:integer following_id:integer
$ rails db:migrate

マイグレーションファイルの追記

t.index [:follower_id, :following_id], unique: trueで、一度フォローしたユーザーを2度フォローしてしまわないようにするための一意の設定をしています。

20200109085109_create_relationships_.rb
class CreateRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :following_id

      t.index [:follower_id, :following_id], unique: true ←追記

      t.timestamps
    end
  end
end


アソシエーションの確認

Userモデル

『following_relationships』
following_relationshipsモデルを架空で作成しています。
class_name: "Relationship"でRelationshipモデルの、foreign_key: "follower_id" で、follower_idを参考に、following_relationshipsモデルへアクセスするようにしています。
フォロワーに関しても同じです。
『following』
through: :following_relationshipsで、中間テーブルにfollowing_relationshipsテーブルを指定しています。その結果、user.following と打つだけで、userが中間テーブルfollowing_relationships を取得し、その1つ1つのfollowing_idから、「フォローしているUser達」を取得できるようになります。
『def following?』
following_relationshipsテーブルのfollowing_idにuserのidが存在するか確認しています。
『def follow?』
フォローするときのメソッドを作成しています。
このメソッドが呼び出されたときには、following_idにuser.idを代入します。
『def unfollow?』
フォローを外すときのメソッドを作成しています。
このメソッドが呼び出されたときには、following_idのuser.idを削除します。

app/models/user.rb
class User < ApplicationRecord

   has_many :following_relationships, foreign_key: "follower_id", class_name: "Relationship",  dependent: :destroy
   has_many :following, through: :following_relationships
   has_many :follower_relationships, foreign_key: "following_id", class_name: "Relationship", dependent: :destroy
   has_many :followers, through: :follower_relationships

  #フォローしているかを確認するメソッド
  def following?(user)
    following_relationships.find_by(following_id: user.id)
  end

  #フォローするときのメソッド
  def follow(user)
    following_relationships.create!(following_id: user.id)
  end

  #フォローを外すときのメソッド
  def unfollow(user)
    following_relationships.find_by(following_id: user.id).destroy
  end

Relationshipsモデル

class_name: "User" と補足設定することで、followerクラス、followingクラスという存在しないクラスを参照することを防ぎ、Userクラスであることを明示しています。

app/models/relationship.rb
    #自分をフォローしているユーザー
    belongs_to :follower, class_name: "User"
    #自分がフォローしているユーザー
    belongs_to :following, class_name: "User"
    #バリデーション
    validates :follower_id, presence: true
    validates :following_id, presence: true

コントローラーの作成

$ rails g controller relationships

ルーティングの作成

フォローユーザーとフォロワーを取れるようにしています。
following_user GET /users/:id/following(.:format) users#following
followers_user GET /users/:id/followers(.:format) users#followers

config/routes.rb
Rails.application.routes.draw do

    resources :users do
      member do
          get :following, :followers
      end
    end
    resources :relationships, only: [:create, :destroy]

end

コントローラーの編集

users_controller.rb

フォローユーザー一覧とフォロワー一覧を表示するためのアクションを作成します。
@user.following@user.followersでは、Userモデルのメソッドを呼び出しています。

app/controllers/users_controller.rb
class UsersController < ApplicationController

    def following
        #@userがフォローしているユーザー
        @user  = User.find(params[:id])
        @users = @user.following
        render 'show_follow'
    end

    def followers
        #@userをフォローしているユーザー
        @user  = User.find(params[:id])
        @users = @user.followers
        render 'show_follower'
    end
end

ralationships_controller.rb

フォローする・フォロー解除するためのアクションを作成します。
followunfollowでは、Userモデルのメソッドを呼び出しています。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController

    def create
      @user = User.find(params[:following_id])
      current_user.follow(@user)
    end

    def destroy
      @user = User.find(params[:id])
      current_user.unfollow(@user)
    end
end

ビューの編集

ユーザー詳細ページ(users/show.html.erb)

followers.countでフォロワーの人数、following.countでフォローユーザーの人数を取っています。
・フォローボタンは、パーシャルにしています。

app/views/users/show.html
  <div class="follower">
      <%= link_to followers_users_user_path(user.id) do %>
          <h5 style="color: black;">フォロワー<%= user.followers.count %></h5>
      <% end %>
  </div>
  <div class="follow">
      <%= link_to following_users_user_path(user.id) do %>
                <h5 style="color: black;">フォロー<%= user.following.count %></h5>
      <% end %>
  </div>
  <div>
      <%= render "follow_form" %>
  </div>

フォローボタン(users/_follow_form.html.erb)

if user_signed_in? && @user != current_userでは、ユーザーがログインしていて且つユーザー詳細ページ(users/show.html.erb)に表示されているユーザーが、ログインユーザーでなければ、フォローボタンを表示するようにしています。
if current_user.following?(@user)で、ログインユーザーが@userをすでにフォローしているかどうかをUserモデルのメソッドを呼び出して確認しています。フォローしているか否かで呼び出されるパーシャルが変わってきます。

app/views/users/_follow_form.html
<!-- フォローボタン(MYPAGEのユーザーがcurrent_userでなければ表示------------------------------------------------------>
<% if user_signed_in? && @user != current_user %>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render "unfollow" %>
    <% else %>
      <%= render "follow" %>
    <% end %>
  </div>
<% end %>

まだフォローしていなかった場合

users/_follow.html.erb
remote: trueで、relationshipsコントローラーのcreateアクションに飛んだ後、create.js.erbに飛ぶようにしています。
hidden_field_tag :following_id, @user.idで、following_idに@user.idを代入しています。

app/views/users/_follow.html
<!-- フォローボタン ------------------------------------------------------------------>
<%= form_for(current_user, url: relationships_path, method: :post, remote: true) do |f| %>
  <%= hidden_field_tag :following_id, @user.id %>
  <%= f.submit "フォローする", class: "btn btn-outline-secondary" %>
<% end %>

relationships/create.js.erb
relationshipsコントローラのcreateアクションの処理後、
users/_follow_form.htmlのid=follow_formをターゲットに、unfollowパーシャルを差し替えています。

app/views/relationships/create.js
$("#follow_form").html("<%= j(render("users/users/unfollow")) %>");

既にフォローしていた場合

users/_unfollow.html.erb
remote: trueで、relationshipsコントローラーのdestroyアクションに飛んだ後、destroy.js.erbに飛ぶようにしています。

app/views/users/_unfollow.html
<!-- フォロー解除ボタン ------------------------------------------------------------------>
<%= form_for(current_user, url: relationship_path(@user), method: :delete, remote: true) do |f| %>
  <%= f.submit "フォロー解除", class: "btn btn-outline-secondary" %>
<% end %>

relationships/destroy.js.erb
relationshipsコントローラのdestroyアクションの処理後、
users/_follow_form.htmlのid=follow_formをターゲットに、followパーシャルを差し替えています。

app/views/relationships/destroy.js
$("#follow_form").html("<%= j(render("users/users/follow")) %>");

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
Ajax関連でいいねとコメントの非同期も実装しています。

[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装
https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
[Rails]Ajaxを用いて非同期でコメント機能の実装
https://qiita.com/yuto_1014/items/c7d6213139a48833e21a

参考

・railsでフォロー機能をつける。
https://qiita.com/kitaokeita/items/59b625e0c43a62f5fe6a
・Railsでフォロー機能を作る方法
https://qiita.com/MitsuguSueyoshi/items/e41e2ff37f143db81897
・フォロー機能、完成版
https://qiita.com/Kaisyou/items/86869db6345c9cc1413f

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト

test

filename.rb
test
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書いたことないPdMやPOに捧ぐ、Rails on Dockerハンズオン vol.5 - Model and CRUD -

この記事はなにか?
この記事はが社内のプログラミング未経験者、ビギナー向けに開催しているRuby on Rails on Dockerハンズオンの内容をまとめたものです。ていうかこの記事を基にそのままハンズオンします。ハンズオンは
1回の内容は喋りながらやると大体40~50分くらいになっています。お昼休みに有志でやっているからです。
現在進行形なので週1ペースで記事投稿していけるように頑張ります。
ビギナーの方のお役にたったり、同じように有志のハンズオンをしようとしている人の参考になれば幸いです。

他のハンズオンへのリンク
Vol.1 - Introduction -
Vol.2 - Hello, Rails on Docker -
Vol.3 - Scaffold, RESTful, MVC -
Vol.4 - Static pages -
・ Vol.5 - Model and CRUD -
Vol.6 - Model validation -

$, #, >について
$: ローカルでコマンドを実行するときは、頭に$をつけています。
#: コンテナの中でコマンドを実行するときは、頭に#をつけています。
>: Rails console内でコマンド(Rubyプログラム)を実行するときは、頭に>をつけています。

はじめに

第5回は、Modelを作って遊んでみます!
Modelはデータベースと密に関係していますので、メソッドをつかってCRUDを試してみます。

Userモデルを作ろう

今回作るModelは以下の通り。
image.png
ER図を描くツールはdbdiagram.ioを使ってます。Entityしか書いてないから何ともですが、便利なツールです。

Userモデルがどんなモデルかといえば、Integer型のidをprimary keyとして、String型のname, email、Datetime型(年月日時分秒)のcreated_at(作成日時), updated_at(更新日時)を持っています。

rails generate model

モデルを作成するコマンドはrails generate modelです。Modelに必要なファイルとそのModelのデータをDBに登録するためにDB側にテーブルを作る必要があるのでマイグレーションファイルを作成してくれます。

rails generate model NAME [field:type field:type]が型ですね。
NAMEがモデルの名前です。fieldがモデルのattribute(属性)、typeが型です。

早速、ER図のモデルを作ってみましょう!

# rails generate model user name:string email:string

注目点はid, created_at, updated_atのことはコマンドで定義していないところです。ここはあとで説明。

マイグレーションファイル

先ほどのコマンド実行でマイグレーションファイルが生成されています。

db/migrate/YYYYMMDDhhmmss_create_user.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.timestamps
    end
  end
end

Scaffoldの時も実は同じようにファイルができていたんですが、中身を見るのは初めてですね。
rails db:migrateをするとRailsがこのファイルを読み込んでDBにSQLを発行してくれているんですね。
先ほどコマンドで定義したname, emailと一緒にtimestampsというものがあります。これがcreated_at, updated_atを作ってくれるやつでモデルを作成するときにRailsがデフォルトで付けてくれています。

また、create_tableはデフォルトでprimary keyとしてInteger型のidを付けてくれます。
なので、このままマイグレーションファイルをdb:migrateすれば先ほどのER図通りのテーブルを作成してくれます。

# rails db:migrate

Userモデルで遊んでみる

ここからは作成したUserモデルを使ってCRUDで遊んでみます。
CRUDとはデータを操作する上で基本となるCreate(作成)、Read(参照)、Update(更新)、Delete(削除)の頭文字をとったものです。Railsの場合はSQL文をコーディングするのではなく、モデルのメソッドを使うだけでCRUDができちゃうのでそれを体感しましょう!

ここからはRails consoleを使っていきます。これでRailsアプリケーションとコマンドラインで対話式にやりとりができます。
Rails consoleはそのままでもいいのですが、pryというツールをインストールすることでRails consoleがみやすくなるのでそうします。

Gemfile
...
group :development, :test do
  ...
  gem 'pry-rails'
  ...
end
...

gemを追加でインストールする場合は、再度Docker imageをビルドします。今立っているコンテナは古いイメージをもとに作られたコンテナなので一回落として、新しくビルドしたイメージで再度コンテナを作ってあげましょう。

$ docker-compose down
$ docker-compose build
$ docker-compose up -d

コンテナを起動できたら、コンテナの中でrails consoleコマンドを使ってRails consoleを立ち上げましょう!

$ docker-compose exec web ash
# rails console
Running via Spring preloader in process 335
Loading development environment (Rails 6.0.2.1)
[1] pry(main)>

Rails console内では接頭に>がつくので、>がついている時はRails consoleの中でコマンドを実行しているんだなと思ってくださいね。

Create

モデルの作成の方法は大きく2種類あります。

  • newメソッドでモデルオブジェクトを作成し、saveメソッドでデータ保存する
  • createメソッドでオブジェクト作成とデータ保存を同時に行う

Create 1: new+save

例でtanaka@sample.comのメアドのTaro Tanakaさんを作ってみましょう!

> user = User.new(name: "Taro Tanaka", email: "tanaka@sample.com")
=> #<User:0x000055c0f665aab0 id: nil, name: "Taro Tanaka", email: "tanaka@sample.com", created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Create (11.4ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "Taro Tanaka"], ["email", "tanaka@sample.com"], ["created_at", "2020-01-23 17:08:36.027245"], ["updated_at", "2020-01-23 17:08:36.027245"]]
   (2.6ms)  COMMIT
=> true

まず、User.newで例の属性を持つUserモデルオブジェクトを作成し変数userに代入しています。

その後、saveメソッドを実行。実行後のコンソールからDBにINSERTしているのがわかると思います。
RailsではModelのメソッドを使うことでとても簡単にSQLの操作ができるようになります。
saveメソッドはデータ作成に成功したらtrueを失敗したらfalseを返却するメソッドです。

また、.の形で属性の情報を取得したり設定したりできるので、以下のようなやり方でもデータを作成することができます。
yamada@sample.comのメアドのHanako Yamadaさんを作ってみます!

> user = User.new
=> #<User:0x000055c0f693a488 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil>

> user.name = "Hanako Yamada"
=> "Hanako Yamada"

> user.email = "yamada@sample.com"
=> "yamada@sample.com"

> user.save
   (0.3ms)  BEGIN
  User Create (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "Hanako Yamada"], ["email", "yamada@sample.com"], ["created_at", "2020-01-23 17:10:31.545772"], ["updated_at", "2020-01-23 17:10:31.545772"]]
   (1.4ms)  COMMIT
=> true

Create 2: create

createメソッドはnewメソッドとsaveメソッドを同時に行うメソッドっす。
john@sample.comのメアドのJohn Smithを作ります。突然の外国人ですが、『John Smith』は日本でいう『名無しの権兵衛』です。

> User.create(name: "John Smith", email: "john@sample.com")
   (0.3ms)  BEGIN
  User Create (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "John Smith"], ["email", "john@sample.com"], ["created_at", "2020-01-23 17:11:03.731916"], ["updated_at", "2020-01-23 17:11:03.731916"]]
   (0.7ms)  COMMIT
=> #<User:0x000055c0f4a511e8
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

createは基本的にはnew+saveなのですが、作成が成功した場合はそのモデルオブジェクト、失敗した場合はfalseを返却するところが大きく違うポイントです。

Read

データを確認するメソッドはかなりあります。ここでは特に利用頻度が高いであろうメソッドをご紹介します。

all

allメソッドはそのモデルの全てのレコードをオブジェクトの配列として取得します。

> User.all
  User Load (2.5ms)  SELECT "users".* FROM "users"
=> [#<User:0x000055c0f6702508
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f6702378
  id: 2,
  name: "Hanako Yamada",
  email: "yamada@sample.com",
  created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>,
 #<User:0x000055c0f6702260
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>]

find

findメソッドはprimary keyの値を指定して1件のオブジェクトを取得するメソッドです。

> User.find(1)
  User Load (1.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User:0x000055c0f5da80e8
 id: 1,
 name: "Taro Tanaka",
 email: "tanaka@sample.com",
 created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>

マッチするレコードがない場合は、ActiveRecord::RecordNotFound Exceptionを発生させます。

> User.find(5)
  User Load (2.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=5
from /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/core.rb:177:in `find'

find_by

find_byメソッドは指定したカラムの条件にマッチした1件のオブジェクトを取得するメソッドです。

> User.find_by(email: "john@sample.com")
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "john@sample.com"], ["LIMIT", 1]]
=> #<User:0x000055c0f65cb108
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

マッチするレコードが存在しない場合はnilを返却します。

> User.find_by(email: "hoge@sample.com")
  User Load (1.7ms)  SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "hoge@sample.com"], ["LIMIT", 1]]
=> nil

また、find_byメソッドでは複数の条件全てにヒットする1レコードを取得する書き方もできます。

> User.find_by(name: "John Smith", email: "john@sample.com")
=> #<User:0x000055c0f6af3040
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

こんな感じでユーザーを特定することができます!

where

whereメソッドはSQLのWHERE句と同様、レコードの検索条件を指定し、条件にマッチしたレコードをオブジェクトの配列として取得します。
条件はハッシュ型で書くのがわかりやすくてオススメ。

等値条件

あるカラムが特定の値と同一であるレコードを取得する場合、単にkey: valueの形で条件を指定するだけです。

> User.where(id: 1)
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 1]]
=> [#<User:0x000055c0f4d7ace8
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>]

範囲条件

valueには範囲を指定することもできます。Rubyでは0 ≦ x ≦ 20..20 ≦ x < 20...2と表現します。

> User.where(id: 1..2)
  User Load (2.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" BETWEEN $1 AND $2  [["id", 1], ["id", 2]]
=> [#<User:0x000055c0f638f740
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f638f5d8
  id: 2,
  name: "Hanako Yamada",
  email: "yamada@sample.com",
  created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>]

サブセット条件

valueには配列を指定することも可能です。

> User.where(id: [1, 3])
  User Load (2.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" BETWEEN $1 AND $2  [["id", 1], ["id", 3]]
=> [#<User:0x000055c0f638f740
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f6901cc8
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>]

NOT条件

where.notメソッドを使えば、条件にヒットしないものを検索することもできます。

> User.where.not(id: 1)
  User Load (1.9ms)  SELECT "users".* FROM "users" WHERE "users"."id" != $1  [["id", 1]]
=> [#<User:0x000055c0f6901d90
  id: 2,
  name: "Hanako Yamada",
  email: "yamada@sample.com",
  created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>,
 #<User:0x000055c0f6901cc8
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>]

AND条件

複数の条件にマッチするレコードを取得したい場合は、ハッシュの組み合わせを増やせばいいだけです。

> User.where(name: "Taro Tanaka", email: "tanaka@sample.com")
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."name" = $1 AND "users"."email" = $2  [["name", "Taro Tanaka"], ["email", "tanaka@sample.com"]]
=> [#<User:0x000055c0f35a3098
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>]

OR条件

複数の条件のうち、どれか一つでも当てはまるレコードを取得したい場合はorメソッドを使います。これはやや面倒(直感的でない)かもしれません。

> User.where(id: 1).or(User.where(email: "john@sample.com"))
 User Load (2.0ms)  SELECT "users".* FROM "users" WHERE ("users"."id" = $1 OR "users"."email" = $2)  [["id", 1], ["email", "john@sample.com"]]
=> [#<User:0x000055c0f5c56c08
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f5c56668
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>]

order

orderメソッドは並び順を指定してくれます。使い方は並び順の条件にしたいカラムを指定するだけで昇順で並び替えてくれます。

> User.order(:email)
  User Load (2.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."email" ASC
=> [#<User:0x000055c0f63223c0
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>,
 #<User:0x000055c0f6322258
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f6322028
  id: 2,
  name: "Hanako Yamada",
  email: "yamada@sample.com",
  created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>]

メアド昇順に並び変わってますね。

降順に並べる場合はdescを使います。(昇順も同じようにascを指定して表現することもできます)

> User.order(email: :desc)
  User Load (4.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
=> [#<User:0x000055c0f64a20d8
  id: 2,
  name: "Hanako Yamada",
  email: "yamada@sample.com",
  created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>,
 #<User:0x000055c0f64a1d40
  id: 1,
  name: "Taro Tanaka",
  email: "tanaka@sample.com",
  created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>,
 #<User:0x000055c0f64a1b88
  id: 3,
  name: "John Smith",
  email: "john@sample.com",
  created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
  updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>]

逆順になってる。

first

firstメソッドはprimary keyの順番で最初の1件のオブジェクトを取得するメソッドです。

> User.first
  User Load (1.9ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x000055c0f625f460
 id: 1,
 name: "Taro Tanaka",
 email: "tanaka@sample.com",
 created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>

結果は先ほどのfindメソッドと同じTaro Tanakaがヒットしていますが、SQL文に違いがあることがわかります。find_byWHEREで検索しているのに対して、firstORDERで検索してます。

また、orderメソッドと組み合わせることで、primary key以外の属性に対しても一番先頭のオブジェクトを取得することができます。こっちの方が便利な気がする。

> User.order(:email).first
  User Load (1.8ms)  SELECT "users".* FROM "users" ORDER BY "users"."email" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x000055c0f67357a0
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

last

なんとなくメソッド名からわかりますね。firstの逆、一番後ろのモデルオブジェクトを取得するメソッドです。

> User.last
  User Load (4.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x000055c0f69aa2b0
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

もちろんorderと組み合わせて使うことも可能!

> User.order(:email).last
  User Load (2.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."email" DESC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x000055c0f5f5ab70
 id: 2,
 name: "Hanako Yamada",
 email: "yamada@sample.com",
 created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
 password_digest: nil>

Update

データの更新の方法も大きく2つあります。どちらの場合もまずfindやfind_byを使って単一のオブジェクトを取得します。で属性のデータを変更したあとにsaveメソッドを使って更新するか、updateメソッドで更新するかです。

属性を更新してsave

一つ目の方法は取得したオブジェクトの属性の値を変更してCreateと同じようにsaveメソッドを使うことです。
Taro Tanakaさんのemailtaro@sample.comに変更してみましょう!

> user = User.find(1)
  User Load (2.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User:0x000055c0f67434b8
 id: 1,
 name: "Taro Tanaka",
 email: "tanaka@sample.com",
 created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:08:36 JST +09:00>

> user.email = "taro@sample.com"
=> "taro@sample.com"

> user.save
   (0.6ms)  BEGIN
  User Update (4.1ms)  UPDATE "users" SET "email" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["email", "taro@sample.com"], ["updated_at", "2020-01-23 20:05:27.588947"], ["id", 1]]
   (7.3ms)  COMMIT
=> true

> User.find(1)
  User Load (3.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User:0x000055c0f6893660
 id: 1,
 name: "Taro Tanaka",
 email: "taro@sample.com",
 created_at: Thu, 23 Jan 2020 17:08:36 JST +09:00,
 updated_at: Thu, 23 Jan 2020 20:05:27 JST +09:00>

email変わってますね。ついでにupdated_atも更新されてます!

updateメソッドを使う

もう一つの方法はupdateメソッドを使う方法です。
今度は、Hanako Yamadaさんのemailhanako@sample.comに変更してみます!

> user = User.find(2)
  User Load (2.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
=> #<User:0x000055c0f692fbc8
 id: 2,
 name: "Hanako Yamada",
 email: "yamada@sample.com",
 created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:10:31 JST +09:00>

> user.update(email: "hanako@sample.com")
   (0.6ms)  BEGIN
  User Update (3.3ms)  UPDATE "users" SET "email" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["email", "hanako@sample.com"], ["updated_at", "2020-01-23 20:09:03.502713"], ["id", 2]]
   (1.2ms)  COMMIT
=> true

> User.find(2)
  User Load (3.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
=> #<User:0x000055c0f6a42740
 id: 2,
 name: "Hanako Yamada",
 email: "hanako@sample.com",
 created_at: Thu, 23 Jan 2020 17:10:31 JST +09:00,
 updated_at: Thu, 23 Jan 2020 20:09:03 JST +09:00>

updateメソッドの場合は更新したい属性と値を括弧の中で指定します。
updateメソッドもcreateメソッドと同じで、データ保存に成功した場合はそのモデルオブジェクトが結果として返却されていますね。

Delete

データの削除にはdestroyメソッドを使います。今回はJohn Smithさんが退会した、みたいな感じでデータ削除してみましょう。

> user = User.find(3)
  User Load (0.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
=> #<User:0x000055c0f6af3040
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

> user.destroy
  (0.4ms)  BEGIN
  User Destroy (2.4ms)  DELETE FROM "users" WHERE "users"."id" = $1  [["id", 3]]
   (1.3ms)  COMMIT
=> #<User:0x000055c0f6af3040
 id: 3,
 name: "John Smith",
 email: "john@sample.com",
 created_at: Thu, 23 Jan 2020 17:11:03 JST +09:00,
 updated_at: Thu, 23 Jan 2020 17:11:03 JST +09:00>

> User.find(3)
  User Load (0.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=3
from /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/core.rb:177:in `find'

あっけないですね。
最終的にもともとJohn Smithさんに割り当てられていたID=3で検索してみたところ、ActiveRecord::RecordNotFoundの例外が発生していることからJohn Smithさんがちゃんと削除されていることがわかります。

後片付け

今日のデータをきれいにしておきましょう。今回もDBの再作成で。

> quit

quitコマンドでRails consoleから抜け出せます。

# exit

exitコンテナからぬけまして、

$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset

コンテナを停止して、rails db:migrate:resetを実行っと。

まとめ

今回は、Modelの作成と基本的なモデル(データ)の操作をやってみました。
ここは僕の中でRailsの使いやすいところだなーと思っているのですが、SQLを隠してくれているんですよね。モデルのメソッドって形でデータのCRUDできるのは本当に使いやすい。

次回はモデルにバリデーションをつけていこうと思います。今のままだとnameemailに値がなくても保存できちゃうんですよね。
あと、Userモデルにセキュアなパスワードの属性を追加していきます。単にパスワードをカラム追加してしまったら万が一データが盗まれた時に大変な個人情報流出です。Railsではセキュアなパスワードを扱うためにhas_secure_passwordメソッドが用意されているのでその使い方を紹介します。

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Reference

P.S. 間違っているところ、抜けているところ、説明の仕方を変えるとよりわかりやすくなるところなどありましたら、優しくアドバイスいただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Ajaxを用いて非同期でコメント機能の実装

実装すること

Ajaxを用いて非同期通信を行い、下記のコードを書いていきます。
①コメントの作成を非同期で行う。
②コメントの件数もコメント作成と同時に非同期で更新されるようにする。
③3件目以上のコメントは隠して「もっと見る」を押すと見れるようにする。
④コメントの削除も非同期で行えるようにする。

完成形

Image from Gyazo

Ajaxとは

✔︎同期通信

クライアントとサーバーが交互に処理を行い、同調して通信を行うこと。
→webブラウザがリクエストを送り、webサーバーが作成したHTMLファイルをレスポンスとして返し、webブラウザがそれを受け取って表示することでコンテンツの内容を変化させている。

・欠点
HTMLファイルを受け取ってから表示の処理を行うため、全体としてページの更新に時間がかかってしまう。
また、送信するデータも多くなりがちで、サーバーに負担がかかってしまう。

✔︎Ajax(非同期通信)

Webブラウザ上でJavascriptが直接Webサーバーと通信を行い、取得したデータを用いて表示するHTMLを更新する。データのやりとりにはXMLが用いられ、JavascriptはDOMを使ってXMLやHTMLを操作する。HTMLそのものをやりとりするのではなく、更新に必要なデータのみやりとりするため、送信するデータの量は同期通信の時よりも少なくなり、サーバーへの負担が抑えられる。

参考文献:「この1冊で全部わかる Web技術の基本」

ER図

User:Item = 1:N
Item:Comment = 1:N
User:Comment = 1:N
CommentテーブルがItemとUserの中間テーブルになります。
コメントER図.png

ページ設計

投稿詳細ページ(item/show.html.erb)でコメントができる。

モデルの作成

Userモデル、Itemモデルは作成した前提で進めていきます。
作成手順は下記リンク先で説明しております。
[非同期投稿と非同期いいねの実装]https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
それではCommentモデルを作成していきます。

$ rails g model Comment content:text user_id:integer item_id:integer
$ rails db:migrate

アソシエーションの確認

Userモデル

dependent: :destroyは、userが消えればitemもcommentも消えるようにするためです。

app/models/user.rb
class User < ApplicationRecord

 has_many :items, dependent: :destroy
 has_many :comments, dependent: :destroy

end

Itemモデル

app/models/item.rb
class Item < ApplicationRecord

  belongs_to :user
  has_many :comments, dependent: :destroy

end

Commentモデル

空欄で送信できないようバリデーションを掛けています。

app/models/comment.rb
class Comment < ApplicationRecord

  belongs_to :user
  belongs_to :item
  #バリデーション
  validates :content, presence: true

end

コントローラーの作成

$ rails g controller comments

ルーティングの作成

コメントがどの投稿へのものであるかを識別するために、ルーティングのURLに投稿のIDを含めます。(ネストする)
具体的には「/item/10/comment/」といったURLになります。10がitem_idです。

config/routes.rb
Rails.application.routes.draw do

  resources :users
  resources :items, only: [:index, :show, :new, :create] do
    resources :comments, only: [:create, :destroy]
  end

end

コントローラーの編集

items_controller.rb

order(created_at: :desc)で、コメントを作成順に取ってきています。

app/controllers/items_controller.rb
class ItemsController < ApplicationController

  def show
    @item = Item.find(params[:id])
    @comment = Comment.new
    #新着順で表示
    @comments = @item.comments.order(created_at: :desc)
  end

comments_controller.rb

buildを使うことで、@itemのidをitem_idに含んだ形でcommentインスタンスを作成します。
・保存がされると、render :indexによって「app/views/comments/index.js.erb」を探しにいきます。
form_withでフォームを送信した時は、デフォルトでjsファイルを探しにいく設定になっています。
htmlファイルを探しにいってほしい場合は、form_withの後にlocal: trueと記載する必要があります。
form_forでフォームを送信し、jsファイルを探しに行って欲しい場合はremote: trueと記載する必要があります。

app/controllers/comments_controller.rb
  class CommentsController < ApplicationController
    def create
      @item = Item.find(params[:item_id])
      #投稿に紐づいたコメントを作成
      @comment = @item.comments.build(comment_params)
      @comment.user_id = current_user.id
      @comment.save
      render :index
    end

    def destroy
      @comment = Comment.find(params[:id])
      @comment.destroy
      render :index
    end

    private
    def comment_params
      params.require(:comment).permit(:content, :item_id, :user_id)
    end
end

ビューの編集

items/show.html.erb

コメント一覧とコメント入力フォームはそれぞれパーシャルにしています。
id="comments_area" をターゲットにこのdiv内をAjaxで書き換えます。

app/views/items/show.html
<div class="row">
    <ul>
        <li class="comment-create">
            <h3 class="text-left title">レビュー</h3>
        </li>
        <li id="comments_area">
            <%= render partial: 'comments/index', locals: { comments: @comments } %>
        </li>
    </ul>
    <hr>
    <% if user_signed_in? %>
        <div class="comment-create">
            <h3 class="text-left">レビューを投稿する</h3>
            <%= render partial: 'comments/form', locals: { comment: @comment, item: @item } %>
        </div>
    <% end %>
</div>

comments/_index.html.erb

・each文のcomments.first(2)で最初の2件、comments.offset(2)で最初の2件以外を取ってきています。
・ 3件目以上は通常隠して、「もっと見る」を押すことで表示しています。これは Bootstrapのcollapseを使用しています。
参考: [Bootatrap4移行ガイド]https://cccabinet.jpn.org/bootstrap4/components/collapse
・コメントの削除は、remote:trueを付けることによって、コントローラのdestoryアクションから、index.js.erbを探しに行ってます。
また、(comment.item_id, comment.id)とすることで、投稿のidとコメントのidを渡しています。

app/views/comments/_index.html
<!-- コメント内容(2件) ------------------------------------------------------------------>
<%= comments.count %>件コメント
<h6 class="more" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">もっと見る....</h6>
<% comments.first(2).each do |comment| %>
  <% unless comment.id.nil? %>
    <li class="comment-container">
        <div class="comment-box">
            <div class="comment-avatar">
                <%= attachment_image_tag comment.user, :profile_image, fallback: "no_image.jpg", class:"comment-image", size: "40x40" %>
            </div>
            <div class="comment-text">
                <p><%= link_to "@#{comment.user.name}", users_user_path(comment.user.id) %></p>
                <div class="comment-entry">
                    <%= comment.content %>
                    <% if comment.user == current_user %>
                        <%= link_to item_comment_path(comment.item_id, comment.id), method: :delete, remote: true, class: "comment_destroy" do %>
                            <i class="fas fa-trash" style="color: black;"></i>
                        <% end %>
                    <% end %>
                </div>
                <span class="comment-date pull-right">
                      <%= comment.created_at.strftime('%Y/%m/%d %H:%M:%S') %>
                </span>
            </div>
        </div>
    </li>
  <% end %>
<% end %>
<!-- コメント内容(3件目以降) ------------------------------------------------------------------>
    <div class="collapse" id="collapseExample">
            <% comments.offset(2).each do |comment| %>
              <% unless comment.id.nil? %>
                <li class="comment-container">
                    <div class="comment-box">
                        <div class="comment-avatar">
                            <%= attachment_image_tag comment.user, :profile_image, fallback: "no_image.jpg", class:"comment-image", size: "40x40" %>
                        </div>
                        <div class="comment-text">
                            <p><%= link_to "@#{comment.user.name}", users_user_path(comment.user.id) %></p>
                            <div class="comment-entry">
                                <%= comment.content %>
                                <% if comment.user == current_user %>
                                    <%= link_to item_comment_path(comment.item_id, comment.id), method: :delete, remote: true, class: "comment_destroy" do %>
                                        <i class="fas fa-trash" style="color: black;"></i>
                                    <% end %>
                                <% end %>
                            </div>
                            <span class="comment-date pull-right">
                                  <%= comment.created_at.strftime('%Y/%m/%d %H:%M:%S') %>
                            </span>
                        </div>
                    </div>
                </li>
              <% end %>
            <% end %>
    </div>

comments/_form.html.erb

form_withで、modle: [item, comment]としています。itemとcommentはそれぞれ、投稿のビューで渡しているインスタンス変数です。投稿に紐づいたコメントを生成するため、ここでitem,commentのインスタンスを渡すことが必要になります。

app/views/comments/_form.html
<!-- コメント入力フォーム ------------------------------------------------------------>
<%= form_with(model: [item, comment], url: item_comments_path(@item) ) do |f| %>
    <%= f.text_area :content, class: "input-mysize" %>
    <%= f.submit "送信", class: "btn btn-outline-dark comment-submit float-right" %>
<% end %>

comments/index.js.erb

・items/show.html.erbの中でid="comments_area"の箇所を書き換える処理になります。
$("#comments_area")id = "comments_area"をターゲットとし、render 'index'で指定しているcomments/_index.html.erbの内容で書き換えています。
$("textarea").val('')でコメント送信後のコメント入力フォームを空にしています。

app/views/comments/index.js
$("#comments_area").html("<%= j(render 'index', { comments: @comment.item.comments }) %>")
$("textarea").val('')

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
Ajax関連でいいねとフォローの非同期も実装しています。

[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装
https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
[Rails]Ajaxを用いて非同期でフォロー機能の実装
https://qiita.com/yuto_1014/items/8d508b84fd0c2316ba01

参考

https://ysk-pro.hatenablog.com/entry/2018/02/10/101739

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

yarnが原因でdocker-compose up ができない ( Your Yarn packages are out of date!)

docker-compose up すると、下記のエラーが発生。解決していきます。

web_1  | => Booting Puma
web_1  | => Rails 6.0.2.1 application starting in development
web_1  | => Run `rails server --help` for more startup options
warning Integrity check: System parameters don't match
error Integrity check failed
error Found 1 errors.
web_1  |
web_1  |
web_1  | ========================================
web_1  |   Your Yarn packages are out of date!
web_1  |   Please run `yarn install --check-files` to update.
web_1  | ========================================
web_1  |
web_1  |
web_1  | To disable this check, please change `check_yarn_integrity`
web_1  | to `false` in your webpacker config file (config/webpacker.yml).
web_1  |
web_1  |
web_1  | yarn check v1.21.1
web_1  | info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.
web_1  |
web_1  |
web_1  | Exiting
myapp_web_1 exited with code 1

原因

ローカルでyarnを最新にしていたから整合性が取れなくなったのかな。と考えています。
yarnのパッケージに古いものが含まれているので、アップグレードしてね!といった様子。
なので、

yarn upgrade

これで直るか!と思いましたが、エラー文は変わらず。

結論 check_yarn_integrity: false に変更する

先程のエラー文をもう一度眺める。

web_1  | To disable this check, please change `check_yarn_integrity`
web_1  | to `false` in your webpacker config file (config/webpacker.yml).

下記の様に変更。

config/webpacker.yml
# Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules
check_yarn_integrity: false

check_yarn_integrity については、
webpacker の check_yarn_integrity オプションについて調べてみた
が参考になります。

再度、 docker-compose up を実行すると、無事起動しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker+RubyonRails でよく使うコマンドメモ

アプリを作成するときはDockerを利用するのですが、Railsコマンド打つとき最初よく分からなくて躓いてたんで自分用メモとして記します。

gemのインストール

docker-compose build --no-cache

Docker-compose downしてから行ってください。

dockerでrailsコマンドを打つ

docker-compose run --rm web rails

Dockerfileやdocker-compose.ymlの変更を反映、railsサーバーを再起動

docker-compose up --build

MySQL

docker-compose exec db mysql -u root -p

Mysqlは「database.yml」で指定したパスワードで中身を見ることができます。

まとめ

よくこの辺を使うので参考になればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 星評価の実装

rails 星評価を実装する手順

*players controllers で作業を行なっています。

まずやること

  • 3つの星の画像を保存する

    https://github.com/wbotelhos/raty/tree/master/lib/images

    上記のファイルの中の「satar」から始まる3つの画像をapp/assets/imagesにダウンロードする

  • jquaryファイルを保存する

    https://github.com/wbotelhos/raty/blob/master/lib/jquery.raty.js

    上記のファイルをapp/assets/javascript/jquery.raty.jsファイルを作成し、コピーする

  • app/assets/javascript/application.jsに

    「//=require jquary」と記入 *require_treeよりも必ず前に書く

  • Gem.fileに

    「gem 'jquary-rails'」と記入後bundle install

  • rate(float型)のカラムを作成 (rails g migration AddRateToテーブル名 rate:float)

手順1 routes.rbにsearchアクションを追加する

routes.rb
collection do
  get 'search'  
end

collection do => resourcesに含まれないアクションを追加するときに使用する

手順2 controllerでsearchアクションの定義づけをする

players.controller
def search
  @players = Player.search(params[:search])
end

手順3 rateカラムを保存するフォームを作成する

new.html
<input class="number" max="5.0" min="0" name="rate" step="0.5" type="number" placeholder="強さ(5段階評価)"/>

type="number" => タグ内で使用すると数値の入力欄が作成される

手順3 rateの数字に応じて星評価を表示する

show.html
<div id = "star-rate-<%= @player.id %>"></div>
<script>
  $('#star-rate-<%= @player.id %>').raty({
    size: 36,
    starOff:  '<%= asset_path('star-off.png') %>',
    starOn : '<%= asset_path('star-on.png') %>',
    starHalf: '<%= asset_path('star-half.png') %>',
    half: true,
    readOnly: true,
    score: <%= @player.rate %>,
  })
</script>
  • id => classと同じ役割。ただ、WEB1ページに1回しか使用できない。idクラスを呼ぶときは「#」を先頭につける
  • $ => jQuaryを呼び出す記号
  • ('#star-rate-<%= @player.id %>') => body要素内のstar-rate-<%= @player.id %>にアクセスする
  • .raty => ratyのプロバティを使用することができるようになる
  • asset_path => asset/imageの画像を表示する
  • readOnly: true, => 画面表示のみで変更できないようにする

以上の操作で私は星評価を作成することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Railsに保存した画像ファイルをVue.js側で表示するサンプルコード(Base64、Active Storage使用)

はじめに

Rails APIモード→Vue.jsでの画像データのやりとりをする方法を残します。(Base64、Active Storage使用)

今回の対象

:x: Vue.js→Rails(こちらの記事をご参照下さい。引用失礼します!)
:o: Rails→Vue.js ←ココ

環境

OS: macOS Catalina 10.15.3
Ruby: 2.6.5
Rails: 6.0.2.1
Vue: 2.6.10
axios: 0.19.0

前提:実施済とみなすこと

引用記事の例を使用します。

  • rails new
  • Active Storageのインストール
  • Postモデルの作成
  • Vue.jsのインストールと利用するための準備
  • eyecatchとして画像ファイルがPostモデルのインスタンスに添付されている
  • (今ココ)

1.【Rails】画像ファイルをBase64形式でエンコードするメソッドを定義する

base64_module.rb
  # 各モデルのレコードに添付された画像ファイルをBase64でエンコードする
  def encode_base64(image_file)
    image = Base64.encode64(image_file.download) # 画像ファイルをActive Storageでダウンロードし、エンコードする
    blob = ActiveStorage::Blob.find(image_file[:id]) # Blobを作成
    "data:#{blob[:content_type]};base64,#{image}" # Vue側でそのまま画像として読み込み出来るBase64文字列にして返す
  end

2.【Rails】Active Storageでアタッチした画像ファイルを読み込み

posts#showで投稿データを返すとします。

posts_controller.rb
  def show
    post = Post.find(params[:id]).as_json #JSON形式にしておく

    eyecatch = post.eyecatch #eyecatchは添付した画像ファイル

    if eyecatch.present?
      post['image'] = encode_base64(eyecatch) # 画像ファイルを1.で定義したメソッドでBase64エンコードし、renderするデータに追加する
    end

    render json: post
  end

3.【Rails】ルーティングを設定

Rails.application.routes.draw do
# 略
  get 'posts', to: 'posts#show'
# 略
end

4.【Vue.js】画像を取得し、表示するコンポーネントを作成

show.vue
<template>
  <div>
    <p>投稿表示フォーム</p>

      <!-- preventでsetPost()メソッドがページ遷移なく発火する -->
    <form v-on:submit.prevent="setPost()">
      <p>
        <label>Title</label>
        <input name="post.title" type="text" v-model="post.title"><br />
      </p>
      <p>
        <label>Body</label>
        <input name="post.body" type="text" v-model="post.body"><br />
      </p>

      <!-- post.idを指定して... -->
      <p>
        <label>IDを指定</label>
        <input name="post.id" type="text" v-model="post.id">
      </p>

      <!-- ここを押してデータ取得 -->
      <input type="submit" value="ここを押して投稿データ取得" >

      <!-- Base64形式であればimgタグでそのまま読み込みが可能 -->
      <img :src="post.image" alt="post.image">
    </form>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'sample',
  data() {
    return {
      post: {},
    }
  },
  methods: {
    setPost() {
      axios.get('/posts', {params: {id: this.post.id}}) //入力したidに応じてpostが返ってくる
      .then(response => {
        this.post = response.data 
      })
      .catch( error => {
        console.error(error)
      })
    }

  }
}
</script>

※実際は自分でidを指定することはないと思いますので、状況に応じて変更して頂ければと思います。

以上です!

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RedisベースのGo Background Jobライブラリー

 Asynq: RedisベースのGo Background Jobライブラリー

Ruby,Railsのサークルの中ではResqueやSidekiqがBackground-jobのライブラリーで人気ですが、Goのコミュニティーの中でこれといったライブラリーがあまり見つからなかったので自分でSidekiqのデザインを基にしてBackground Jobライブラリーを書いてみました(github.com/hibiken/asynq)。

GoやRedisに興味があってGithubでコントリビュートするプロジェクトを探していたら、是非!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseのログイン機能でユーザー名にバリデーションをかける。

・deviseを使用したログイン機能でのデフォルトバリデーションemail,passwordのみのため

~/models/user.rbにアクセス。

filename.rb
validates :nickname, presence: true, length: { maximum: 6 }

1.presenceは、指定された属性が空でないことを確認します。

2.length属性は値の長さを検証。多くのオプションがあり、長さ制限をさまざまな方法で指定可能。
:minimum - 属性はこの値より小さな値を取れない。
:maximum - 属性はこの値より大きな値を取れない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails/Laravel使いに送るドメインモデル~アクティブレコードの功罪~

みなさん、こんにちは!RailsやLaravel使ってますか? ActiveRecord(LaravelではEloquent)ってめっちゃ便利ですね。ただ便利ゆえにActiveRecord以外の存在を知らない人がいるので、メリット・デメリットをまとめてみました。最終的にはドメインモデル入門になっています。

最初にRailsやLaravelから入った人(つまり僕)にありがちなのですが、ActiveRecordがどのようなものか理解せずに実装するため、ActiveRecordなのにロジックがないことがあります。また、ActiveRecordパターン以外を知らないのでActiveRecordのメリット・デメリットを理解してません。そこでActiveRecordがどのようなものかを説明していきたいと思います。

ただ、一年ほどRailsのコードに触れていないので、もし書き方がおかしかったら容赦なく突っ込んでくださし。また個人の見解が多分に含まれているので、皆さんの思うところがあるかもしれません。その時は、ガンガン言ってください。

注意

Railsのよさは結合度の高さによる実装の速さです。RailsはActiveRecordを前提としています。後半に紹介するPOROsRepositoryはRailsWayから外れたものです。この記事はRailsにRepository層を設けることを勧めているわけではなく、このような考え方もあるよという記事です。

もしDDD前提の設計をしたいのであればHanamiというフレームワークがおすすめです。
https://magazine.rubyist.net/articles/0056/0056-hanami.html

ActiveRecordとは

マーチン・ファウラーという方が書いた「Pattern of Enterprise Application Architecture (PofEAA)」という本に、

データベーステーブルまたはビューの行をラップし、データベースアクセスをカプセル化してデータにドメインロジックを追加するオブジェクト

と書かれています。ここからActiveRecordの役割が3つあることがわかります。

  • データベースアクセス
  • テーブルの行に対応するデータの保持
  • ドメインロジックをもつ

データベースアクセスは

ruby
user = new User(params)
user.save

のようにモデル自体にsavecreateを持つことです。

テーブルの行に対応するデータの保持は

id name
1 ハト太郎
2 ハム助

とテーブルがある場合、

ruby
user = User.find(1)
puts(user.id)  #ハト太郎

のように行単位で属性を保持するインスタンスを生成できます。

ドメインロジックをもつは

例えば、20歳以下であれば料金が半額とする場合はコントローラーに

ruby
if user.age <= 20 then
# 料金半額
end

とするのではなく、モデルにロジックを追加して

ruby
class User < ApplicationRecord

  中略

  def isPriceHalf
    self.age <= 20
  end

end 
ruby
if user.isPriceHalf then
  # 料金半額
end

というふうにユーザーに関するロジック(ドメインロジックといいます)をユーザーモデルにカプセルしてしまうことです。重要なのは自身のデータを用いたインスタンスメソッドを実装することです。

歴史

そもそもアクティブレコードはドメインモデルの一つです。ドメインモデルはかんたんに説明するとオブジェクト指向にのっとり、データとそれに付随するロジックをクラスに閉じ込めたものになります。なぜそうするのが良いかというと、ロジックがデータと一緒にあることで、コードの重複が防げるからです。ここらへんは「現場で役立つシステム設計の原則(増田享)」という本に詳しく載っています。

データに付随するロジックをコントローラ層やサービス層に書くのは自由ですが、一応そういう考え方もあることを知っておいてください。

ActiveRecordのメリット・デメリット

メリット

  • テーブルと1対1にモデルが存在するので、テーブル設計が終わったあとすんなりと実装に入れる
  • データと永続化メソッドが1つのモデルにあるので、実装スピードが早い

デメリット

  • ツールありき。自ら実装するのは割と手間(RailsとLaravelは標準装備なのでこれはデメリットではない)。
  • テーブルと1対1にモデルが存在するので、モデルがテーブルに引っ張られる。例えばテーブルを変更するとモデルに影響を与えるので、テーブルとモデルの結合度が高い。
  • ツールを利用した場合、モデルの継承(extend)を利用する事が多く1つしかできない継承を消費してしまう(Rubyはmixinという機能があるのであまり問題にならないかもしれない)。

ちなみにPofEAAには次のように書かれています。

アクティブレコードの最大のメリットは、シンプルな構造である。アクティブレコードの構築は容易であり、また理解しやすい。最大の問題は、アクティブレコードが有効であるのが、アクティブレコードオブジェクトがデータベーステーブルと直接対応している(同一構造スキーム)場合だけという点である。

これはテーブルと1対1のモデル設計のメリット・デメリットですね。ほかにデメリットとして

ビジネスロジックが複雑な場合には、オブジェクトの直接的な関係、コレクション、継承などを使用したいとまず考えるだろう。しかし、これらの部品は簡単にはアクティブレコードにマッピングできず、また、断片的に追加すると状況はより複雑になる。以上の理由からデータマッパーの使用を考えるようになる。

と書かれています。オブジェクト指向とリレーショナル・データベースは同一のものではありません。例えばrubyでの配列

ruby
array = ["a", "b", "c']

をデータベースに保存するときにどのように保存すればよいでしょうか? リレーショナルデータベースはデータの横持ちが苦手なので、縦持ちにしてテーブルに保存するかもしれません。

また、CarクラスとCarクラスを継承するHybridCarクラスのデータを保存することを考えると、それぞれを保存するのはどうすればよいでしょうか?

ruby
class HybridCar < Car

  def doSomething 
  end

end 

おそらく、一般的にはtype属性を加えて継承を表現するかもしれません。様々な方法があるかもしれませんが、NoSQLとは違ってリレーショナルデータベースはこのような継承を表現するのが苦手です(苦手であってできないことはない)。アクティブレコードはリレーショナルデータベースのテーブルと密結合なので、テーブルが苦手な表現はアクティブレコードも苦手です(くどいができないことはない)。

Plain Old Ruby Objects(POROs)について

いままでActiveRecordしか使ったことのない方は、ぜひ他のモデルパターンを知ってほしいです。これから紹介するのはPOROsRepositoryです。もしデータベースとロジックの分離をしたいのであれば、こちらのパターンはおすすめです。

Plain Old Ruby Objectsは特になんのひねりもなくただのRubyのClassです。マーティンが単純なJavaのクラスをJavaBeansに対してPlain Old Java Objects (POJOs)と呼んだことにちなんでいます。

class Dog

  def initialize(name)
    @name = name 
  end

  def doSomething
    # do something
  end 
end

ただのRubyのClassなのでActiveRecordなどは継承していません。特定のツールに依存しないので、自分の自由に実装できます。外部API由来のモデルもDB由来のモデルもすべて同じように扱えます。継承を消費しないのでデザインパターンを適用しやすくなります。POROsはオブジェト指向を気持ちよく実装できます。

Repositoryパターンについて

RepositoryパターンはドメインモデルのIOを担当します。今回だと上記のPOROsと外部API通信やデータベースへの永続化を担当します。Repositoryという層をはさむ事によってモデルはデータベースを知る必要がありません。また、データベース以外に外部APIをモデルにマッピングすることもできます。背後にActiveRecordを使ってもQueryBuilderを使っても構いません。

注意: Repositoryパターンは本来インターフェイスを用いて実装することが多いです。今回はクラスとして実装しています。

ruby
class DogRepository

  def self.findDogById(id)
    # ActiveRecordもしくはQueryBuilderによって実装
  end

  def self.save(dog)
    # ActiveRecordもしくはQueryBuilderによって実装
  end

end
ruby
class DogController < ApplicationController

  def show
    @dog = DogRepository.findDogById(params.id)
  end

  def create

    dog = new Dog(params)
    # dog.save()ではない
    DogRepository.save(dog)

  end

end

Repositoryパターンを使うとモデルはActiveRecordではなく、モデル自身に永続化のメソッドがないので、dog.save()ではなく、DogRepository.save(dog)となっていることに注目してください。Repositoryパターンではモデルは自身のロジックに集中してデータベースへの永続化の処理はRepositoryが担当します。モデルは永続化については気にしなくて良いのです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 バリデーションをかける

目的

  • 空欄でのDB登録を避けるためのバリデーションの記載方法をまとめる

空での保存を避けるバリデーション

  • 下記の記載を当該のモデルファイルに記載する。

    class Post < ApplicationRecord
      validates :バリデーションをかけたいカラム名, {presence: true}
    end
    

空での保存を防ぎ、かつ総文字数を140文字以下にするバリデーション

  • 下記の記載を当該のモデルファイルに記載する。

    class Post < ApplicationRecord
        validates :content, {presence: true, length: {maximum: 140}}    
    end
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む