20200219のRubyに関する記事は13件です。

未経験から転職活動する際に、後悔したこと

未経験エンジニアが転職活動した際に、後悔しないようにする。

自分がいろいろ方向性が定まっておらず、後悔した点を多いので、記載しました。

まずは学習した言語で個人制作のAppを作成する。

Ruby on Railsを学習しているなら、まずはRailsでAppを作成する。
作成が完了できたら、Rubyの会社に応募していく。
まだ何も学習していない人は、PHP/Laravelから学習するといいと思います。なぜなら求人数がPHPの方が圧倒的に多いからです。

「DB設計は完璧なものを作ろう」

→これなかなか作業できない。やっちゃえ精神の方が学習スピード早いです。
難しい機能を作ろうとしている際は、いきなりDB設計完璧にしようとしても経験値が不足しているのだから、進みません。
複数回作り直してもいいのだから、基本的なDB設計をしたら作ってしまおう。完璧主義な人は陥りやすいかな。足りないカラムは後で足せばいい。

「デザインどうしよう???」

デザインに時間をかけてはダメ!!それよりも学習しなければいけないことが、たくさんある。
WEBデザイナーではないし、デザインに時間がけず、ダサいサイトでもいいから、とにかく機能を実装する。その後デザインを綺麗にすればいい。機能が揃ってから、興味を引くようなデザインにすればいい。WEBデザイナーになりたいのか、プログラマーになりたいのかどっち?

「どの言語で個人Appをつくろうかなあ??」

この時間無駄です。まずは自分ができる言語で作ればいいじゃん。
Rubyを学習した後にPHPで制作するか悩んでいた自分に言ってやりたい。

「このアイデア公開したくないなあ。」

自分の制作物を見せるべきか、悩むことがあると思います。
いいアイデアだとなおさらです。ですが、どうせ今の自分にはそれを世に広げる技術がないのだから、まずは作ってしまえばいい。作成ができたら、面接にも持っていけるし、投資家に出資してくれと相談もできる。それに起業するような人材は、すでに独立していると思うし、そんなアイデアパクらんでしょ。思った以上に人は行動しないから、大丈夫。ポートフォリオとしてサービスを展開していきましょう!それが誰よりもそのアイデア広げる道です。

「ポートフォリオサイトを作った方がいいかな」 → 後悔

個人制作物を十分に揃えていない状態で作成、割と後悔しました。
結局、個人制作物を出してと言われるので、ポートフォリオサイトはなくてもいいです。

個人制作物が増えてからじっくり作成するべきで、始めに作るのは時間の無駄。
制作物がないポートフォリオには価値がない(笑)
人物像だけ紹介されたも「で? 技術は??」ってなるわけです。
わざわざHTML、CSS、PHP、Laravelなどでゴリゴリ書いてポートフォリオサイトは作らなくてもいい。
他のポートフォリオになりそうなサービスを利用した方が、賢い

Githubをポートフォリオにすれば、よかった!!

まずは、下記のリンクを開いて欲しい。どんな機能があるのかわかりやすい。
これで十分にポートフォリオになる。
参考になるGithubアカウント

Githubはどれだけ学習しているのか、その日の色分けでわかる。
個人制作の機能説明に関しては、下記のリンクのように充実するといいでしょう。

Githubアカウントは間違いなくチェックされるので、まずはGithubのアカウントの充実度をあげればいい。
その後でポートフォリオサイト作れば、すぐに作れる。

Dockerより、Vue.jsをやればよかった。

Docker学習したいと思う気持ちがでるけど、Javascriptの方が重視している企業が多かった。
RubyとVue.jsを使って制作している企業もあるので、Vue.jsとReact.jsを学習するべき。

Vue.js

ドットインストール:Vue.js
js系は一部の機能だけ実装できたり、必要な分だけで済む。ミニアプリを作成してGithubにあげておけばアピールに繋がるはず。
Dockerは途中から導入できるので、ひとまず慣れている普段のローカル環境で個人Appを作成すればいい。

Dockerは途中から切り替えればOK

今制作した制作物を途中からDockerに切り替えることが可能。だったら、とりあえず個人制作物を作成完了してから、切り替えればいい。そうすれば、就職活動を始めるタイミングも早まるし、制作物も途中からDockerに切り替えることで学習規模も小さく抑えられる。Dockerの技術を深めるのであれば、本命の制作物ではなくて、どうなってもいいミニアプリでじっくり試した方がいい。

ドットインストール(Docker)
ドットインストール(試験運用中Docker)
途中からDockerに切り替える

新しい言語で個人制作する。(PHP)

Ruby on Railsをやったら、今度はPHPでAppを作成する。
DB設計に時間をかけすぎず、失敗しながら作成していく。
本当に大きな失敗をしたら、作り直せばいい。
とにかく作る回数、経験値が不足しているから、試作品をサクサク作ればいい。

「大作を作ろう」とは思わない

これは失敗するフラグです。機能を細かく分けて、一つの機能だけのAppを作ればいい。
- カレンダー機能だけ
- JSによるゲーム
- 簡単な投稿機能、いいね!機能

これらはミニアプリで学習してから、本命のAppに実装すればいい。
いきなり本命Appに実装しようとしない。細かく学べば、一日一機能学習できる

Udemyで学習して後悔。。。失敗しない教材はドットインストール

Udemyはオススメしない。プログラミング学習に特化したサイトではないので、使いづらい。
また、教材のスキルレベルが低い。Windowsでの制作が多いため、やりづらい。

PHPは下記から学習するのが望ましい。
ドットインストール:Laravel講座

正直、Udemyで学習を始めたのは大きな失敗だった。時間の無駄だったと後悔。
ドットインストールはPHPのカリキュラムが充実している。おそらく製作者もPHPを使っているのだろう。明らかにPHP/Laravelだけ充実度が違う。
PHPはドットインストールから学習した方が確実

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

【初学者のつまずきを記録】RailsでのMySQL接続:mysql.sock問題

はじめに

RailsでのMySQL接続手順について、備忘録的にまとめようと思います。
作業自体はごく基本的な内容かと思いますが、
初学者である僕が実際につまずいたことを重点的に記載しました。
僕と同じ初学者の方のご参考の一助となればうれしいです。
後半部分ではつまずいた原因について検討してみました。

※僕自身は「RailsのDBを(初めから| |後から)MySQLに変更する」を参考にしながら進めました。

前準備:MySQLを指定してRailsアプリを作成

Railsの仕様上、データベースを指定しない場合は自動的にSQLiteとなる。

$ rails new アプリ名 -d mysql

作成されたconfig/database.ymlを確認。

config/database.yml
default: &default
  adapter: mysql2
          ()

確かにMySQLとなっていることを確認。

MySQLとの接続

手順通り接続を試みる。が、エラー発生。

$ mysql -u root
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

mysql.sockを通じての接続ができなかったとのこと。

調べてみると、mysql.sockファイル自体が存在しないことが問題らしい。
確かに、tmpフォルダ内を確認してみても存在しない。下記実行で作成。

$ sudo touch /tmp/mysql.sock

その上で接続を試みても同じようなエラー発生。

$ mysql -u root             
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (38)

そもそも、mysql.sockファイルとは何だろう?
調べてみると、サーバーとクライアントの間で通信を行う際の仲介役的な役割らしい。
ここで、MySQLサーバーを起動していないことに気づきました。
というかエラー分にもしっかり MySQL serverとありますね。

MySQLサーバーを起動。

$ mysql.server start
Starting MySQL
.. ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxxx.pid).

PIDファイルがないとのお達し。
指定された場所に作成をする。

$ touch /usr/local/var/mysql/xxxx.pid

その上でサーバー起動を試みるも、、、

$ mysql.server start                            
Starting MySQL
.rm: /tmp/mysql.sock: Permission denied
2020-02-17T12:13:34.6NZ mysqld_safe Fatal error: Can't remove the socket file:
/tmp/mysql.sock.
Please remove the file manually
         (略)

別のエラーが発生。
mysql.sockを削除しろと??
ないと言うから作ったのに。

言われるがままに削除し、
どこか不本意な気持ちのまま再度サーバー起動。成功。

$ mysql.server start                            
Starting MySQL
. SUCCESS! 
$ mysql -u root                                 
Welcome to the MySQL monitor. 
      (略)
mysql> 

MySQLへの接続も成功したようだ。釈然としない。

成功した理由を考えてみた

調べてみると、mysql.sockはMySQLサーバー起動時に自動的に作成されるファイルであり、
手動で作成する類のものではないらしい。
つまり、一番始めに発生した下記エラーは、mysql.sockを作成していないことが問題ではなく、
Can't connect:つなげない状態」であったことが問題であったと思われる。

$ mysql -u root
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

よって、mysql.sockがない状態でも、
PIDファイルを作成してサーバー起動を成功させることによって(=ここでmysql.sockが自動作成され)
MySQLへの接続も成功した、、、という理解でいいのだろうか。

では手動作成のmysql.sockは何が問題であったのか?

調べてみるとどうやら手動で作成したmysql.sockは権限に問題があるようだ。
検証のために再度手動でmysql.sockを作成し、
自動作成(サーバー起動時)されたmysql.sockと$ ls -lの出力を比較してみた。

#手動
-rw-r--r--  1 root            wheel   0  2 17 22:56 mysql.sock

#自動作成
srwxrwxrwx  1 xxxx(ユーザー名)  wheel   0  2 17 22:45 mysql.sock

なるほど違う。

手動で作成された方はrootユーザーのみが書き込みの権限を持っている。
書き込みの権限がないとうまくいかないので、(参考:MySQL の設定上の考慮事項)
rootユーザー以外での実行ができない状態となっている??

ならばと手動作成した上でrootユーザー権限でサーバー起動を試みてみる。

$ sudo mysql.server start   
Starting MySQL
. ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxxx.pid).

だめかー。

では手動作成したものを自動作成と同じ権限に変えてみる。
これでダメな理由はないはず。

$ sudo chmod 777 mysql.sock  #権限を変更
$ sudo chown xxxx(ユーザー名) mysql.sock  #所有者も変更
$ mysql.server start                            
Starting MySQL
. SUCCESS! 

成功した!

手動でmysql.sockを作成した場合でも
権限周りを調整することによりサーバー起動ができることの確認がとれました。

まとめ

解決方法を色々とググりながら進めましたが、
「まずはじっくりとエラー分を解釈すること」の大切さを痛感しました。
またlinux等の知識が不足しており、後半の検証部分は気持ちとしてしこりが残っております。
(rootユーザー権限でサーバー起動できなかったあたり等)
今後の課題にするようにしたいと思います。

はじめての記事投稿でしたが、思考を整理するとても良い機会となりました。
誤り・認識違い、アドバイス等ご指摘いただけましたらとてもありがたいです。

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

サイドナビのリンクを開くと、そのナビだけ色が変わる。

サイドナビの色を変化させる。

サイドナビでナビをクリックすると、ナビの色が変化して、今開いているナビがわかるようにする。

処理の作成

application_helper.rb
module ApplicationHelper
  def active_class(link_path) # (link_path)にrake routeの_pathを記載する。
    "active" if request.fullpath == link_path
    # もしも開いたページのURL(request.fullpath)が、指定の(link_path)と同じ場合、"active"になる。
  end
end

classを付与させる

html.haml
%li{class: "#{active_class(users_path)}"}
// application_helper.rbで作成した処理により、activeが付与する。
// 指定したリンク以外では、activeは付与しない。
// この例の場合、users_pathのため、users#showのページを開くとナビの色が変化する

これで、完成

おまけにHTMLとSCSS

haml
%ul.list-group
  %li{class: "list-group-item #{active_class(user_path)}"}
    = link_to "", user_path, class: "nav-list__link"
    %p マイページ
  %li{class: "list-group-item #{active_class(new_product_path)}"}
    = link_to "", new_product_path, class: "nav-list__link"
    %p コンテンツ
scss
.list-group{
  width: 250px;
  &-item{
    position: relative;
    /* li全体をaタグにする。 */
    a{
      text-decoration: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
    &::after{
        /* 矢印 */
        position: absolute;
        top: 40%;
        right: 13px;
        content: '';
        width: 8px;
        height: 8px;
        border-top: 2px solid #ccc;
        border-right: 2px solid #ccc;
        -webkit-transform: rotate( -50%);
        -webkit-transform: rotate( -50%);
        transform: rotate(45deg);
        transition: all .2s;
      }
      &:hover{
        background: #fafafa;
        &::after{
          right: 9px;
          border-color: #333;
        }
      }
    /* 今開いているページのナビは、色を変化 */
    &.active{
      background: #EEEEEE;
      font-weight: bold;
      border-color: #3333;
      color: #212529;
      &::after{
        border-top: 2px solid #333333;
        border-right: 2px solid #333333;
      }
      .nav-list{
        &__link{
          text-decoration: none;
          font-weight: bold;
          color: #333333;
          font-size: 14px;
        }
      }
      &:hover{
        background: #EEEEEE;
      }
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フリマアプリ購入機能実装(pay.jp)購入編

前回の記事の続きです。

Payjp(Pay.jp)から既に顧客IDとカードIDを取得済みでの購入を想定しています。

前提条件

・devise/hamlが導入済みでログインができている
・payjpのアカウントが既に取得できていて、ユーザーとカードの登録が完了しており、cardテーブルに以下の情報が登録されている
user_id ... UserテーブルのID
customer_id ... payjpの顧客ID
card_id ... payjpのデフォルトカードID
顧客IDとデフォルトカードIDは以下の画面で顧客ごとに確認できます。
alt

1.コントローラーを作成しよう

コントローラとビュー(indexとdone)を作成するため、下記コマンドを実行します。

$ rails g controller buyers index done

コントローラーの中身を編集

app/controllers/buyers_controller.rb
class BuyersController < ApplicationController
  require 'payjp'#Payjpの読み込み
  before_action :set_card, :set_item

  def index
    if @card.blank?
      #登録された情報がない場合にカード登録画面に移動
      redirect_to new_card_path
    else
      Payjp.api_key = Rails.application.credentials[:PAYJP_PRIVATE_KEY]
      #保管した顧客IDでpayjpから情報取得
      customer = Payjp::Customer.retrieve(@card.customer_id) 
      #カード情報表示のためインスタンス変数に代入
      @default_card_information = customer.cards.retrieve(@card.card_id)
    end
  end

  def pay
    Payjp.api_key = Rails.application.credentials[:PAYJP_PRIVATE_KEY]
    Payjp::Charge.create(
      :amount => @item.price, #支払金額を引っ張ってくる
      :customer => @card.customer_id,  #顧客ID
      :currency => 'jpy',              #日本円
    )
    redirect_to done_item_buyers_path #完了画面に移動
  end

  def done
  end

  private

  def set_card
    @card = Card.find_by(user_id: current_user.id)
  end

  def set_item
    @item = Item.find(params[:item_id])
  end

end

set_card , set_itemの記述で、cardとitemの情報をもってきます
今回は、先ほど作成したindexから、form_tag内のaction: 'pay'で、controller内のpayを発動

2.購入画面と完了画面を作成しよう

購入画面

app/views/buyers/index.html.haml
    %h2.buy-content__title
        購入内容の確認
      .buy-content__item
        .buy-content__item__inner
          .buy-item-main
            .buy-item-image
              = image_tag "#{@item.images[0].url}", size: '64x64', class: 'buydetails-contet__image'
            .buy-item-detail
              .buy-item-name
                = @item.name
                %p.buy-price
                  = #{@item.price.to_s}"
                  %span.shipping-free (税込) 送料込み
      .buy-content__item
        %form.buy-form
          .buy-price-table
            .buy-price-table__left
              支払金額
            .buy-price-table__right
              = #{@item.price.to_s}"
      .buy-content__user-info
        .buy-content__user-info__inner
          %h3 支払方法
          .user-info-update
            = link_to "変更する", "#", calss:"update-btn"
          .user-info-text
          - if @default_card_information.blank?
            %br /
          - else
            = "**** **** **** " + "#{@default_card_information.last4}"
            %br
            - exp_month = @default_card_information.exp_month.to_s
            - exp_year = @default_card_information.exp_year.to_s.slice(2,3)
            = "有効期限 " + exp_month + " / " + exp_year
            %br
      .buy-content__user-info
        .buy-content__user-info__inner              
          %h3 配送先
          .user-info-update
            = link_to "変更する","#", calss:"update-btn"
          .user-info-text
            〒111-1111
            %br
            大阪府大阪市北区〇〇1-11
            %br
            山田太郎
            = form_tag(action: :pay, method: :post) do
              %button.buy-button{type:"submit"} 購入する

*exp_monthはカードの期限月、exp_yearは期限年、last4はカードの下4桁を取得
PAYJP カードオブジェクト

完了画面

app/views/purchase/done.html.haml
%h1.buy-content__attention
        %i.far.fa-clock
          発送をお待ちください
        %h2.buy-content__attention__title
          購入が完了しました
        .buy-content__item
          .buy-content__item__inner
            .buy-item-main
              .buy-item-image
                = image_tag "#{@item.images[0].url}", size: '64x64', class: 'buydetails-contet__image'
              .buy-item-detail
                .buy-item-name
                  = @item.name
                  %p.buy-price
                    %span 
                      = #{@item.price.to_s}"
                    %span
                      .shipping-free (税込) 送料込み
        .buy-content__item
          %form.buy-form
            .buy-price-table
              .buy-price-table__left
                支払金額
              .buy-price-table__right
                = #{@item.price.to_s}"
            .buy-content__user-info__submit
              = link_to "トップページへ戻る", root_path, class: 'buy-content__user-info__submit__button'

3.ルートを設定しよう

ルーティング設定ですが、商品詳細ページからitem_idを引き継ぎたかったので、下記のようにしました

config/routes.rb
resources :items do
    resources :buyers, only: [:index] do
      collection do
        get 'done', to: 'buyers#done'
        post 'pay', to: 'buyers#pay'
      end
    end
  end

購入の確認

購入が完了するとPayjpが以下の様に変わります

参考

https://qiita.com/takachan_coding/items/d21c0d2621368c9b0d9b
https://qiita.com/Ikuy_h/items/7232ba32e0b728ff77aa

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

ruby 2.7.0 CSV の liberal_parsing オプションについて調査してみた

概要

Ruby標準ライブラリのCSVの挙動についてのまとめ。
ruby 2.4.0 CSV の liberal_parsing オプションについて調査してみた のruby2.7.0バージョン。

csvの仕様

rubyの標準ライブラリのCSVパーサーではRFC4180準拠のパースが行われる。
こちらの仕様では、コンマの直後と次のコンマの直前に"をおくことで、コンマを含んだ文字列を一つのフィールドとして扱うことができる。

CSV.parse('a,"b,c",d')
=> [["a", "b,c", "d"]]

ダブルクオートを含む文字列を表すためには、もう一つのダブルクオートでエスケープする必要がある。

CSV.parse('a,"b""c",d')
=> [["a", "b\"c", "d"]]

しかし、RFC4180はなかなか厳密な仕様で例えば a,b"c,d のような文字列は不正なcsvになり、例外が発生する。

CSV.parse('a,b"c,d')      # CSV::MalformedCSVError (Illegal quoting in line 1.)

一方で上記のようなフォーマットは頻繁に出てきうるため、これを厳密にエラーにしていると取り扱いが難しい場合がある。そのために実用的なパースを行うためのliberal_parsingというオプションがruby2.4.0から導入されている。
上記の記事ではruby2.4を使ってliberal_parsingの挙動について調査を行っているが、ruby2.7で確認したところ挙動が変わっていたところも多くあったのでここでまとめておく。全般的にruby2.7の方が挙動が直感的になっている。

liberal_parsing のruby2.7での挙動

ダブルクォートに囲まれたフィールドの場合

正しくエスケープされたダブルクォートの場合

CSV.parse('a,"bb""b",c', liberal_parsing: true)
=> [["a", "bb\"b", "c"]]

文中にエスケープされていないダブルクォートがある場合

CSV.parse('a,"bb"b",c', liberal_parsing: true)
=> [["a", "\"bb\"b\"", "c"]]

ruby2.4では例外は発生していた。

ダブルクォートに囲まれていないフィールドの場合

途中にダブルクォートがある場合

SV.parse('a,bb"b,c', liberal_parsing: true)
=> [["a", "bb\"b", "c"]]

途中にエスケープされたダブルクォートがある場合

CSV.parse('a,bb""b,c', liberal_parsing: true)
=> [["a", "bb\"\"b", "c"]]

先頭にのみダブルクォートがある場合

CSV.parse('a,"bbb,c', liberal_parsing: true)
# CSV::MalformedCSVError (Unclosed quoted field in line 1.)

ruby2.4と同様に例外。

先頭と途中にダブルクォートがあって、ダブルクォートの合計が偶数の場合

CSV.parse('a,"bb"b,c', liberal_parsing: true)
=> [["a", "\"bb\"b", "c"]]

先頭と途中にダブルクォートがあって、ダブルクォートの合計が奇数の場合

CSV.parse('a,"b"b"b,c', liberal_parsing: true)
=> [["a", "\"b\"b\"b", "c"]]

ruby2.4では例外が発生していたが、ruby2.7ではパースできるようになった。

末尾と途中にダブルクォートがある場合

CSV.parse('a,bb"b",c', liberal_parsing: true)
=> [["a", "bb\"b\"", "c"]]

ruby2.4と同様にパースできる。

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

ActiveAdmin 画面遷移時にfilterのパラメーターをセットする

親要素から子要素に遷移する際, 親要素のidで絞りたい

これだけと何をいってるかわからないので

例)
Company : Shop = 1 : N のRelationの時,

Companyの画面から, Shopの一覧画面へ遷移する時,
そのままではデータが大きすぎるので,
最初からShopを外部キーのCompany_Idで絞っておきたい

  # カスタム アクション
  # 一度変数に入れないと, エスケープされずエラーとなる
  query = "q[company_id_equals]"
  action_item :my_button1, only: :show do
    link_to "店舗の一覧に遷移する", active_admin_shops_path({query => company.id})
  end

これだけで遷移と同時にfilterがかかります.
調べても載っておらず, 尚且つ需要があると思うので書きました.

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

フリマアプリ購入機能実装(pay.jp)

最終課題のチーム開発で担当した部分を自分の復習を兼ねて記録用に書きました。

商品の講入機能の実装

・クレジット登録(payjp)
  削除・再登録できる
・Payjpのコンソールからその売り上げが確認できる
・クレジット登録していないと購入できない
・購入すると商品状態が切り替わる(売り切れる)

PAY.JP クレジットカード機能

payjpとは?

PAY.JPを使うと、シンプルなAPIで
Webサービスなどにクレジットカード決済機能を簡単に導入できる。

payjpの仕組み

前提条件

hamlでの記載(gem 'haml-rails')
deviseが導入済みでログインができている
→Devise未導入や何もない状態からスタートする場合は、
『Devise導入の設定手順 ~haml使用/pay.jp導入の前準備~ (Rails)』を先に実施。

1.PAY.JPアカウントの作成

Payjpのサイトでアカウントを作成。

2.APIを確認しよう

ダッシュボードのAPIより確認ができます。
今回はテストモードでの実装なので、テスト秘密鍵とテスト公開鍵を使用。
alt

3.payjpのgemを設置しよう

下記をgemfileに記載しbundle installを実施.

gem 'payjp'

4.payjp.jsを読み込めるようにしよう

%script{src: "https://js.pay.jp/", type: "text/javascript"}を下記の通り追記します。

app/views/layouts/application.html.haml
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title payjptest
    %script{src: "https://js.pay.jp/", type: "text/javascript"}
    -# このscriptを記載
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

5.テーブルを作成しよう

下記コマンドでpayjpのデータを保管するテーブルを作成します。

rails g model Card user_id:integer customer_id:string card_id:string

テーブルのカラムの紐づけは下記の通りです。

user_id ... Userテーブルのid
customer_id ... payjpの顧客id
card_id ... payjpのデフォルトカードid

※デフォルトカードidはトークンとは違う

db/migrate/20200206000000_create_cards.rb
class CreateCards < ActiveRecord::Migration[5.2]
  def change
    create_table :cards do |t|
      t.integer :user_id, null: false
      t.string :customer_id, null: false
      t.string :card_id, null: false

      t.timestamps
    end
  end
end

マイグレーションを実施

$ rails db:migrate

※カード情報そのものを保存することは禁止されている
payjpに保管されている情報を顧客idカードidで呼び出すことで情報取得や支払いなどに対応

6.コントローラーを作成しよう

app/controllers/cards_controller.rb
class CardsController < ApplicationController

  def new
    card = Card.where(user_id: current_user.id)
    redirect_to card_path(current_user.id) if card.exists?
  end


  def pay #payjpとCardのデータベース作成
    Payjp.api_key = Rails.application.credentials[:PAYJP_PRIVATE_KEY]
    #保管した顧客IDでpayjpから情報取得
    if params['payjp-token'].blank?
      redirect_to new_card_path
    else
      customer = Payjp::Customer.create(
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      ) 
      @card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @card.save
        redirect_to card_path(current_user.id)
      else
        redirect_to pay_cards_path
      end
    end
  end

  def destroy #PayjpとCardデータベースを削除
    card = Card.find_by(user_id: current_user.id)
    if card.blank?
    else
      Payjp.api_key = Rails.application.credentials[:PAYJP_PRIVATE_KEY]
      customer = Payjp::Customer.retrieve(card.customer_id)
      customer.delete
      card.delete
    end
      redirect_to new_card_path
  end

  def show #Cardのデータpayjpに送り情報を取り出す
    card = Card.find_by(user_id: current_user.id)
    if card.blank?
      redirect_to new_card_path 
    else
      Payjp.api_key = Rails.application.credentials[:PAYJP_PRIVATE_KEY]
      customer = Payjp::Customer.retrieve(card.customer_id)
      @default_card_information = customer.cards.retrieve(card.card_id)
    end
  end
end

コントローラ内のRails.application.credentials[:PAYJP_PRIVATE_KEY]は環境変数でテスト秘密鍵を設定し読み込みます。
credential.ymlファイルにAPIキーを記載

credential.yml
PAYJP_PRIVATE_KEY = 'sk_test_000000000000000000000000'
PAYJP_KEY = 'pk_test_00000000000000000000000'

(自分が他にも参考にした記事、メモ)
※where:与えられた条件にマッチするレコードをすべて返す。
https://qiita.com/nakayuu07/items/3d5e2f8784b6f18186f2
※blank?:空のオブジェクト、またはnilのオブジェクトかどうかを判定するメソッド。
( empty? || nil? と同等)
空のオブジェクト、またはnilのオブジェクトの場合は true
値が存在する場合は false
※ActiveRecordで条件に一致するデータが存在するかどうかを調べたいときは、exists?を使う。
https://qiita.com/uw9623/items/851ac5f71e316834c0fa
※メタデータとは、本体であるデータに関する付帯情報が記載されたデータです。データのためのデータ

7.カードの登録画面を作成しよう

登録画面と確認兼削除画面の2つを作成

登録画面
app/view/cards/new.html.haml
            = form_tag(pay_cards_path, method: :post, id: 'charge-form',  name: "inputForm") do
              %label カード番号
              .require 必須
              = text_field_tag "number", "", class: "number", placeholder: "半角数字のみ" ,maxlength: "16", type: "text", id: "card_number"
              %ul.signup-card-list
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/visa.svg?238737266",width:"49px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/master-card.svg?238737266",width:"34px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/saison-card.svg?238737266",width:"30px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/jcb.svg?238737266",width:"32px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/american_express.svg?238737266",width:"21px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/dinersclub.svg?238737266",width:"32px",height:"20px"
                %li
                  = image_tag "https://www-mercari-jp.akamaized.net/assets/img/card/discover.svg?238737266",width:"32px",height:"20px"
              %br
              %label 有効期限
              .require 必須
              %select#exp_month{name: "exp_month", type: "text"}
                %option{value: ""} --
                %option{value: "1"}01
                %option{value: "2"}02
                %option{value: "3"}03
                %option{value: "4"}04
                %option{value: "5"}05
                %option{value: "6"}06
                %option{value: "7"}07
                %option{value: "8"}08
                %option{value: "9"}09
                %option{value: "10"}10
                %option{value: "11"}11
                %option{value: "12"}12
              %span 月/
              %select#exp_year{name: "exp_year", type: "text"}
                %option{value: ""} --
                %option{value: "2019"}19
                %option{value: "2020"}20
                %option{value: "2021"}21
                %option{value: "2022"}22
                %option{value: "2023"}23
                %option{value: "2024"}24
                %option{value: "2025"}25
                %option{value: "2026"}26
                %option{value: "2027"}27
                %option{value: "2028"}28
                %option{value: "2029"}29
              %span%br
              %br
              %label セキュリティコード
              .require 必須
              = text_field_tag "cvc", "", class: "cvc", placeholder: "カード背面3~4桁の番号", maxlength: "4", id: "cvc"
              %br
              #card_token
              %br
              = submit_tag "追加する", id: "token_submit"

※参考記事がこの書き方だったのでそのまま書きましたが
 他の記事も載せておきます
【Rails】date_selectタグの使い方メモ

(自分が他にも参考にした記事、メモ)
※text_field_tagの使い方
text_field_tagヘルパーは、Viewファイルのform_withヘルパー内に記載することで、文字列入力用のテキストボックスを実現することができるViewヘルパー

text_field_tag(テキストボックス名, 初期文字列, {オプション1, オプション2,,,})
「テキストボックス名」は、テキストフィールドのidとnameに割り当てられ、フォームから値を取得するときのキー
「初期文字列」は、テキストフィールドのデフォルト値で、指定された文字列が入力された状態で表示される
オプションには、以下の4種類に加え、各種HTML属性をハッシュ形式で設定することが可能
:disable 無効化(trueにすると、入力できなくなる)
:size     表示可能文字数
:maxlength  入力可能文字数
:placeholder フィールド内にデフォルト表示される文字列。ただし、値として設定されているわけではなく、フォーカスが当たると削除される

※append()の基本的な使い方
append()メソッドは、指定した要素内の最後に引数のコンテンツを追加するメソッドです。
コンテンツにはテキストの他、HTML要素やJQueryオブジェクトが指定できる。

確認兼削除画面
app/view/card/show.html.haml
  クレジットカード情報
          .form-content
            %br
            = "**** **** **** " + @default_card_information.last4
            %br
            - exp_month = @default_card_information.exp_month.to_s
            - exp_year = @default_card_information.exp_year.to_s.slice(2,3)
            = exp_month + " / " + exp_year
            = form_tag(card_path(current_user.id), method: :delete, id: 'charge-form',  name: "inputForm") do
              %input{ type: "hidden", name: "card_id", value: "" }
              %button.delete-btn 削除する

(自分が他にも参考にした記事、メモ)
sliceでカード年数の2、3番目を取得
※type属性をhiddenにしたときの大きな特徴は、「送信したいデータがブラウザに表示されない」ブラウザ上では見えないことを利用して、ユーザーからの命令がどのような種類なのかをサーバーに送信するときに判断させるために、hiddenを使うこともある

8.Payjpにデータを送りトークンを取得しよう

jQueryを使用するので、railsに未設定の場合は設定をしてください。
設定方法はこちら

pay.jpのサンプル

app/assets/javascripts/payjp.js
document.addEventListener(
  "DOMContentLoaded", e => {//DOM読み込みが完了したら実行
    if (document.getElementById("token_submit") != null) { //token_submitというidがnullの場合、下記コードを実行しない
      Payjp.setPublicKey("pk_test_31f3ec18c086406c969b76cb"); //ここに公開鍵を直書き
      let btn = document.getElementById("token_submit"); //IDがtoken_submitの場合に取得
      btn.addEventListener("click", e => { //ボタンが押されたときに作動
        e.preventDefault(); //ボタンを一旦無効
        let card = {//カード情報生成
          number: document.getElementById("card_number").value,
          cvc: document.getElementById("cvc").value,
          exp_month: document.getElementById("exp_month").value,
          exp_year: document.getElementById("exp_year").value
        }; //入力されたデータを取得
        Payjp.createToken(card, (status, response) => {//トークン生成
          if (status === 200) { //成功した場合
            $("#card_number").removeAttr("name");
            $("#cvc").removeAttr("name");
            $("#exp_month").removeAttr("name");
            $("#exp_year").removeAttr("name"); //データを自サーバにpostしないように削除
            $("#card_token").append(
              $('<input type="hidden" name="payjp-token">').val(response.id)
            ); //取得したトークンを送信できる状態
            document.inputForm.submit();
            alert("登録が完了しました"); 
          } else {
            alert("カード情報が正しくありません。"); 
          }
        });
      });
    }
  },
  false
);

(自分が他にも参考にした記事、メモ)
※ボタンを押したタイミングで PAY.JP のサーバにクレジットカード情報を送信し、結果としてトークンなどの情報を受け取っている
※removeさせることでカード情報をparamsの値として含まれないようにしています
※HTTP レスポンスステータスコードは、特定の HTTP リクエストが正常に完了したどうかを示します。レスポンスは 5 つのクラスに分類されています。
1. 情報レスポンス (100–199),
2. 成功レスポンス (200–299),
3. リダイレクト (300–399),
4. クライアントエラー (400–499),
5. サーバエラー (500–599)
-成功レスポンス200-
リクエストが成功したことを示します。成功が意味することは、 HTTP メソッドにより異なります。
* GET: リソースが読み込まれ、メッセージ本文で転送された。
* HEAD: メッセージ本文にエンティティヘッダーある。
* PUT または POST: 操作の結果を表すリソースがメッセージ本文で送信される。
* TRACE: メッセージ本文に、サーバーが受け取ったリクエストメッセージが含まれている。

9.ルートを作成しよう

config/routes.rb
resources :cards, only: [:new, :show, :destroy] do
    collection do
      post 'pay', to: 'cards#pay'
    end
  end

(自分が他にも参考にした記事、メモ)
リソースベースのルーティングでは「index」「show」「new」「edit」「create」「update」「destroy」の7つのアクションへのルーティング自動で設定されます。これに別のアクションを呼び出すためのルーティングを追加する為collectionを記述します。

10.カードを登録してみよう

http://localhost:3000/cards/new にアクセスして登録できるか確認。

その時、テストカードで登録するようにしてください。
それ以外を打ち込んだ場合はトークンが発行できずはねられてしまいます。

購入編

参考

https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

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

Rails6 のちょい足しな新機能を試す 121(PostgreSQL index_exists?編)

はじめに

Rails 6 に追加された新機能を試す第121段。 今回は、PostgreSQL index_exists? 編です。
Rails 6 (と Rails 5.2.4.1) では、 index_exists? が正しく動作しないバグが fix されています。

Ruby 2.6.5, Rails 6.0.2.1, Rails 5.2.4.1, Rails 5.2.3, PostgreSQL 12.0 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)

$ rails --version
Rails 6.0.2.1

今回は、 name の属性を持つ User モデルを作り、インデックスを追加して確認してみます。

Rails プロジェクトを作る

Rails プロジェクトを新たに作成します。

$ rails new rails_sandbox
$ cd rails_sandbox

User モデルを作る

name 属性を持つ User モデルを作ります。

$ bin/rails g model User name

インデックスを追加する

今回は2つのインデックスを作成します。 lower(name) を指定したインデックスと、単純に name カラムを指定したインデックスです。

bin/rails g migration add_index_lower_name_to_users
db/migrate/20200207213234_add_index_lower_name_to_users.rb
class AddIndexLowerNameToUsers < ActiveRecord::Migration[6.0]
  def change
    add_index :users, 'lower(name)', name: 'index_lower_name'
    add_index :users, 'name', name: 'index_name'
  end
end

マイグレーションを実行する

マイグレーションを実行します。

bin/rails db:create db:migrate

rails console で確認する

rails c を実行します。

$ bin/rails c
Running via Spring preloader in process 344
Loading development environment (Rails 6.0.2.1)

indexes で登録されているインデックスを調べてみましょう。
(わかりやすいように表示は折り返してます。)

irb(main):001:0> User.connection.indexes(:users)
=> [
#<ActiveRecord::ConnectionAdapters::IndexDefinition:0x00005617bc6ef580 
@table=:users, 
@name="index_lower_name", 
@unique=false, 
@columns="lower((name)::text)",  # <=  ここに注目
@lengths={}, 
@orders={}, 
@opclasses={}, 
@where=nil, 
@type=nil, 
@using=:btree, 
@comment=nil>, 
#<ActiveRecord::ConnectionAdapters::IndexDefinition:0x00005617bc8c3be0 
@table=:users, 
@name="index_name", 
@unique=false, 
@columns=["name"],  # <= ここに注目
@lengths={}, 
@orders={}, 
@opclasses={}, 
@where=nil, 
@type=nil, 
@using=:btree, 
@comment=nil>
]

index_lower_name に対応する @columns'lower((name)::text)' で String であるのに対して、 index_name に対応する @columns["name"] とArray になっていることに注意してください。

index_exists? を使って index_name が存在することを確認します。

irb(main):002:0> User.connection.index_exists?(:users, "name", name: :index_name)
=> true

今度は、 index_lower_name が存在することを確認します。

irb(main):003:0> User.connection.index_exists?(:users, "lower((name)::text)", name: :index_lower_name)
=> true

Rails 5 では

Rails 5.2.4.1 では、 Rails 6 と同じ動作ですが、 Rails 5.2.3 では、 index_lower_name の存在を確認したとき false を返します。

irb(main):001:0> User.connection.index_exists?(:users, "name", name: :index_name)
=> true
irb(main):002:0> User.connection.index_exists?(:users, "lower((name)::text)", name: :index_lower_name)
=> false

試したソース

https://github.com/suketa/rails_sandbox/tree/try121_index_exists_of_postgresql

参考情報

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

ActiveRecord::PendingMigrationErrorの解決方法

1.エラーメッセージの内容

下記のようなエラーの場合の解決方法を記載します
スクリーンショット 2020-02-19 7.45.15.png

2.エラーの原因

エラーの理由はmigrateし忘れてますよというものです。
考えられる理由は下記の3つです

1.単純にrails db:createのあとrails db:migrateし忘れている
2.すでにmigrationが完了している(up)状態でマイグレーションファイルを更新しrails db:migrateしてしまった
3.マイグレーションファイルの中に入れた外部参照キーが参照するテーブルがない状態でrails db:creatしてしまった(マイグレーションファイルの記述に誤りがある)

3.エラーの解決方法

1.単純にrails db:createのあとrails db:migrateし忘れている

マイグレーション実行の下記のコマンドを打ち込みましょう

$ rails db:migrate

2.すでにmigrationが完了している(up)状態でマイグレーションファイルを更新しrails db:migrateしてしまった

方法1 既存のテーブルを全削除し、再度マイグレーションの状態をupにする下記のコマンドを打ち込みましょう

$ rails db:migrate:reset

方法2 マイグレーションの最新のものだけ実行を取りやめて編集可能(down)にするコマンドrails db:rollbackののち、マイグレーションファイルの更新コマンドrails db:migrateを打ちましょう

$ rails db:rollback
$ rails db:migrate

※方法1と方法2の違いは処理速度(工程)の量の違いです。テーブルの数が多い場合は方法2としましょう。テーブルが数十程度なら方法1でも2でも大差はありません

※マイグレーションの状態を調べるコマンドは下記の通りです

$ rails db:migrate:status

3.マイグレーションファイルの中に入れた外部参照キーが参照するテーブルがない状態でrails db:creatしてしまった

①-1外部参照されるマイグレーションファイルを作成し、app/modelの中のファイルにアソシエーションを追記(または修正)する
①-2マイグレーションファイルから不必要な外部参照キーを消す

②上記①のどちらかの修正のあとに下記コマンドを打ち込みましょう

$ rails db:migrate:reset

4.すでにデータベースにレコードを入れてしまい、それを残したまま修正する方法(補足)

新たに追加したい項目のテーブルを作成し、本来追加したかったテーブルのidを外部キーとする方法をとりましょう。

参考(railsガイド): https://railsguides.jp/active_record_migrations.html#%E6%97%A2%E5%AD%98%E3%81%AE%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B

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

RubyのMysql2の各種情報(バージョン情報やオプション)を取得する方法

背景

Ruby on Rails 等で MySQLを利用している時、低レイヤーの根深い問題や、よくわからないエラーに遭遇したことはありますでしょうか?

RubyでMySQLを利用する際によく使われるライブラリである Mysql2 では、C拡張を利用して libmysqlclient とリンクし、データベースサーバ上の mysqld に対してアクセスしている都合上、複数の概念に対してのバージョン情報やオプションを扱う必要があります。

これらのバージョン情報は、問題の原因究明に重要であるため、取得方法を知っておくことで、効率的な作業が可能になります。自分が調べる時は勿論、他の人に手伝ってもらう時にも重要なヒントになります。

TL; DR

以下のようなデータを記録しておくと、調査が捗ります。

# Railsの場合の Mysql2::Client のオブジェクトの取得方法の例
client = ActiveRecord::Base.connection.raw_connection
# 自前の場合の Mysql2::Client のオブジェクトの作成方法の例
client = Mysql2::Client.new(host: 'localhost', username: 'root')

puts JSON.pretty_generate(
  'RUBY_DESCRIPTION' =>  RUBY_DESCRIPTION,            # Rubyの詳細
  'Mysql2::VERSION' =>  Mysql2::VERSION,              # gemのバージョン
  'Mysql2::Client#info' => client.info,               # libmysqlclient のバージョン
  'Mysql2::Client#server_info' => client.server_info, # 接続先の MySQL Server のバージョン
)

# 接続に関するオプション (おまけ)
puts JSON.pretty_generate(client.query_options.reject { |k, _v| k == :password })  # 他にも重要な情報はあるが、最低限、パスワードは取り除く

出力例

{
  "RUBY_DESCRIPTION": "ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18]",
  "Mysql2::VERSION": "0.4.10",
  "Mysql2::Client#info": {
    "id": 80016,
    "version": "8.0.16",
    "header_version": "8.0.16"
  },
  "Mysql2::Client#server_info": {
    "id": 50637,
    "version": "5.6.37"
  }
}

各種バージョン

Rubyの詳細情報

ruby -v の詳細が記述されます。

https://docs.ruby-lang.org/ja/latest/method/Object/c/RUBY_DESCRIPTION.html

gem のバージョン

gem のバージョンは、以下の Mysql2 自体のバージョン Mysql2::VERSION になります。

https://github.com/brianmario/mysql2

コードは以下のような形になっています。

https://github.com/brianmario/mysql2/blob/982dbdb1d521c668547cfd4ab927558c689b8bfd/lib/mysql2/version.rb#L2

module Mysql2
  VERSION = "0.5.3".freeze
end

libmysqlclient のバージョン

Mysql2 は、独自に MySQLサーバ との通信プロトコルを実装しているわけではなく、 MySQLの提供している、Cで書かれたクライアントライブラリを利用してアクセスしています。
この時のライブラリのバージョンによって、微妙な挙動の差異が発生する場合があるので、この情報も重要です。

Mysql2 では、以下のようにC拡張で Mysql2::Client#info が実装されています。

https://github.com/brianmario/mysql2/blob/f8560c551bf1999baf7df43290e8f89471e77af4/ext/mysql2/client.c#L948-L968

  version = rb_str_new2(mysql_get_client_info());

これが呼び出している mysql_get_client_info() が何を返すかは、MySQLのドキュメントに記述されています。

https://dev.mysql.com/doc/refman/5.6/en/mysql-get-client-info.html

Returns a string that represents the MySQL client library version (for example, "5.6.48").

ということで、クライアントライブラリ libmysqlclient のバージョンが返却されます。

接続先の MySQL Server のバージョン

問題が発生した時は、クライアントバージョンは勿論ですが、相手側のサーババージョンも気になるところです。

Mysql2 では、以下のようにC拡張で Mysql2::Client#server_info が実装されています。

https://github.com/brianmario/mysql2/blob/f8560c551bf1999baf7df43290e8f89471e77af4/ext/mysql2/client.c#L970-L994

  server_info = rb_str_new2(mysql_get_server_info(wrapper->client));

これが呼び出している mysql_get_server_info() が何を返すかは、MySQLのドキュメントに記述されています。

https://dev.mysql.com/doc/refman/5.6/en/mysql-get-server-info.html

Returns a string that represents the MySQL server version (for example, "5.6.48").

ということで、接続先の MySQL サーバのバージョンが返却されます。

接続に関するオプション

MySQLのサーバ側で設定されているオプションも勿論大切ですが、各クライアント(コネクション)毎に維持されているオプションも、細かい挙動を知る上では重要になってきます。

Mysql2 では、以下のようにインスタンス変数へのアクセッサーが生やされて、 Mysql2::Client#query_options があるので、それを利用する形が良いと思われます。

※ このアクセッサーにはパスワードを含む情報が格納されているので、情報共有の際には、それをしっかり取り除くことを留意しておいてください。

https://github.com/brianmario/mysql2/blob/98304cbbc0c9a964c833474a06f55c433ec26432/lib/mysql2/client.rb#L3

参考

https://github.com/brianmario/mysql2
https://dev.mysql.com/doc/refman/5.6/en/c-api-server-client-versions.html

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

Pathname#joinは単純に複数の文字列を連結しているだけではない、という話

はじめに

RubyのPathname#joinメソッドを使うと2つのパスを連結することができます。

pathname = Pathname.new('/pen/pineapple')
pathname.join('apple/pen').to_s
#=> "/pen/pineapple/apple/pen"

上の結果を見ると、Pathname#joinは2つのパスを/で連結してくれるメソッドのように見えます。

しかし、joinメソッドの引数が/で始まっていると、少し直感に反した動きになります。

pathname = Pathname.new('/pen/pineapple')
# "/"で始まるパスを指定すると、引数そのものを表すパスが戻り値になる
pathname.join('/apple/pen').to_s
#=> "/apple/pen"

ご覧のとおり、引数で渡した"/apple/pen"がそのまま戻り値になってしまいました。
(つまり、単純な文字列連結になっていない)

joinメソッドに複数の引数を渡したときも同様です。

pathname = Pathname.new('/pen')
# 複数の引数を渡す("/"で始まるパスなし)
pathname.join('pineapple', 'apple', 'pen').to_s
#=> "/pen/pineapple/apple/pen"

# 複数の引数を渡す(2つ目の引数が"/"で始まると、その手前のパスが消える)
pathname.join('pineapple', '/apple', 'pen').to_s
#=> "/apple/pen"

ところで、Pathname#joinと少し似たメソッドでFile.joinがあります。
File.joinはシンプルに複数のパスを連結してくれるので、人によっては「File.joinの方が自分の欲しいメソッドだ」と思うかもしれません。

# 以下のメソッド呼び出しはいずれも同じ結果が返る
# ("/"で始まる引数が2つ目以降に登場しても手前のパスが維持される)
File.join('/pen/pineapple', 'apple/pen')
#=> "/pen/pineapple/apple/pen"
File.join('/pen/pineapple', '/apple/pen')
#=> "/pen/pineapple/apple/pen"
File.join('/pen', 'pineapple', 'apple', 'pen')
#=> "/pen/pineapple/apple/pen"
File.join('/pen', 'pineapple', '/apple', 'pen')
#=> "/pen/pineapple/apple/pen"

では、Pathname#joinメソッドはなぜ、このように直感に反する動きをするのでしょうか?
この点が気になったので、以下のissueで僕の疑問点を質問してみました。

Bug #14891: Pathname#join has different behaviour to File.join - Ruby master - Ruby Issue Tracking System

そこで得られたzverok氏の回答が「なるほど」と思ったので、その内容を以下にまとめます。

Pathname#joinはシェルのcdコマンドのように振る舞う

zverok氏いわく、Pathname#joinはシェルのcdコマンドのように考えるのが良いそうです。
すなわち、

pathname = Pathname.new('/pen/pineapple')
pathname.join('apple/pen').to_s

が意味するところは、

$ cd /pen/pineapple
$ cd apple/pen
$ pwd
/pen/pineapple/apple/pen

に近い、というわけです。
(「同じ」ではなく、「近い」と書いたのは、cdコマンドとは異なり、Pathname#joinで指定するパスはpen.jpgのようなファイル名でも良いからです)

この理屈が頭に入っていれば、/で始まる引数を渡したときに、手前のパスが消える理由も納得がいきます。

# Pathname.new('/pen/pineapple').join('/apple/pen') のイメージ
$ cd /pen/pineapple
$ cd /apple/pen
$ pwd
/apple/pen

# Pathname.new('/pen').join('pineapple', '/apple', 'pen') のイメージ
$ cd /pen
$ cd pineapple
$ cd /apple
$ cd pen
$ pwd
/apple/pen

cdコマンドのように"."や".."も渡せる

cdコマンドに近い」ということがわかれば、Pathname#joinメソッドに...が渡せることを知っても驚きは小さいかもしれません。

pathname = Pathname.new('/pen/pineapple')
# 現在のパスのまま
pathname.join('.').to_s
#=> "/pen/pineapple"

# 1つ上のパスへ
pathname.join('..').to_s
#=> "/pen"

# 1つ上のパスに上がり、そこからappleというパスを指定
pathname.join('../apple').to_s
#=> "/pen/apple"

このように、Pathname#joinは単純に複数のパス文字列を/で連結するのではなく、あたかもシェル上のパスを扱うかのように振る舞います。
単純な文字列連結だと思っていると、/で始まる引数が渡されたときの挙動が不自然に思えてしまうので注意してください。(って、かつての僕がそうだったんですが?)

Pathname#joinを使うと便利なユースケース(?)

zverok氏はPathname#joinを使うと便利なユースケースについても言及してくれました。
たとえば、設定によって「特定のディレクトリ以下を相対パスで指定したい場合」と、「特定のディレクトリに関係なく、絶対パスで指定したい場合」の2パターンがあるときに、Pathname#joinを使えば条件分岐なしでこの挙動を実現できます。

zverok氏が示してくれたコード例とは異なりますが、説明用にサンプルコードを書くとしたらこんな感じです。
(注:これはあくまで説明用のサンプルコードです。実際にこんなメソッドがあるとかなり危険です?)

require 'pathname'
require 'fileutils'

# 指定されたパスでlsコマンドの実行結果を表示するメソッド
def ls(path)
  # 基準となるパスを/varとする
  base_path = Pathname.new('/var')

  # 引数pathの値に応じて、/var内、/var外のどちらのディレクトリも指定可能
  target_path = base_path.join(path)

  # 対象となるパスでlsコマンドを実行
  puts system("ls #{target_path}"
end

# /var/logが対象になる(相対パス指定)
ls('log')

# /binが対象になる(絶対パス指定)
ls('/bin')

実際にこういうコードが必要になる機会がどれくらい頻繁にあるのかはわかりませんが、たしかにPathname#joinならではの使い方かもしれません。

まとめ

というわけで、この記事ではPathname#joinメソッドの挙動を正しく理解するための考え方を説明してみました。

ちょっと余談になりますが、Pathname#joinがややこしいのは、File#joinと同じメソッド名で、なおかつPathnameFileという、概念的にもよく似たクラスなのに挙動が異なる点かもしれません。
使い方によっては両者はほぼ同じようにパスを連結するので、そうした点がPathname#joinの仕様を勘違いしてしまう原因になっている気がします。
もしかすると、join以外の名前(たとえばPathname#mergeとか)を与えていたら、「単純に連結するだけじゃないのかも?」と予想しやすかったかもしれません。

いずれにせよ、「Pathname#joinは単純に文字列を連結するだけではない」ということをこの記事を読んで理解してもらえれば幸いです。

あわせて読みたい

Rails.rootメソッドが返すのもPathnameオブジェクトです。
RailsプログラマがPathnameオブジェクトを一番よく使うのはこのユースケースかもしれません。

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

画像アップロード機能実装について

目的

作成したアプリケーションに画像アップロード機能を実装するための手順や
考え方などを備忘録的に残していきます。

画像アップロードとは

アップロードとはファイルやデータをPCやスマホからインターネット上のサーバーへ転送することを指します。

画像アップロードの仕組み

  • ファイル選択ボタンを押して、画像を選択
  • 送信ボタンをおす
  • 画像ファイルを画像パスがリクエストして送信される
  • 画像ファイルはアクセス先コンピューター内に、画像のパスはDBないにあるテーブルに保存される (画像の保存先には外部のストレージを使うことが多い。AWSサービスのS3など..)

実装の流れ

ライブラリのインストール
Gemfile
gem 'carrierwave'
gem 'mini_magick'
bundle install
アプリの雛形を作成

scaffoldで必要なモデルやコントローラーなど一式を作成します。
モデル名はFeedとし、画像を保存するためにimageカラムを作成します。

$ rails g scaffold feed image:text
$ rails db:migrate
アップローダファイルを作成

アップローダファイルとは アップロードに関する設定 をするためのファイルです。
アップロードに関する設定とは以下が挙げられます。

  • 外部ストレージを連携するかどうか
  • 保存形式
  • 画像ファイルの保存先の設定
  • 画像サイズの調整
$ rails g uploader Image
app/uploader/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
   # MiniMagickをincludeすると画像サイズ調整ができるようになる
  storage :file # 保存形式の設定。他に使うものとしてはfog形式などがある。
  process :resize_to_limit => [50, 50] # 画像サイズの調整
  # 画像ファイルの保存先の設定
  # 保存先を指定するには `store_dir` というメソッドに定義します。
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
end

保存形式を指定しているのが storage :file という記述です。
file以外の保存形式として クラウドのストレージ が挙げられます。
クラウドのストレージとして、Amazonが提供している S3 というサービスが有名です。file形式は画像が実体として存在しますが、クラウドのストレージにおける画像は実体が存在しません。この場合は専用のgemを利用して storage :fog という記述をします。

最後に該当のモデルにカラムとアップローダを指定します。
Feedモデル内に以下を記述

app/model/feed.rb
~省略~
#アップローダ定義
 mount_uploader :image, ImageUploader
~省略~

上記の記述をすることで、feedsテーブルの中にあるimageカラムにimageUploaderという名前のアップローダ機能を追加することができます。

まとめ

アップローダ定義の意味を総括すると以下のようになります。

  • feedモデルが関係しているのでfeedsテーブルが関係している
  • mount_uploaderは、画像アップロードの宣言をしている
  • :imageはfeedsテーブル内の画像パスが入っているカラム名を指している
  • ImageUploaderは、ImageUploaderファイル内の設定を元にアップロードすることを意味している
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの配列で使えるメソッド

Railsアプリで配列データを分けて出力しようとした時に色々試したので記録しとこうと思います。

students = ["Tom","Mike"],["Sam","Eric"],["Billy"],["Jorge","Nancy","Rick"]

適当に配列を用意しました。

flatten

入力
p students.flatten
出力
["Tom", "Mike", "Sam", "Eric", "Billy", "Jorge", "Nancy", "Rick"]

配列がくっつきます。

join

入力
p students.join
出力
"TomMikeSamEricBillyJorgeNancyRick"

これもくっつきます。

flattenのクラスはArray、joinの場合はstringです。

ただ作ってたアプリでは複数の配列からランダムに一つずつ出力したかったため、これらでは上手くいかなくて(なぜかflattenでくっつかなかった)

sample

入力
p students.sample(1)
出力
[["Jorge", "Nancy", "Rick"]]

sampleは配列の要素を1個(引数を指定した場合は自身の要素数を越えない範囲で n 個) ランダムに選んで返します。
この場合はランダムに配列を1つ出力しています。

入力
students.each do |student|
  p student.sample(1)
end
出力
["Mike"]
["Sam"]
["Billy"]
["Jorge"]

この場合は一度ループ処理をしているので各配列から一つずつ要素が出力されています。
これを応用してアプリの機能が実装できました。

配列面白いですよね。

参考

Ruby 2.7.0 リファレンスマニュアル

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