20201108のRailsに関する記事は29件です。

【AWS】EC2インスタンス(ターゲットグループ)のヘルスチェックにどうしても失敗する。。

現在個人開発しているRailsアプリがやっと完成し、AWSへのデプロイ作業にチャレンジしました。(ECR × ECS × CircleCIを利用し、gitからmasterブランチへpushするとCIがビルドされ、テストが通ると自動デプロイされるといった感じの構成です)

その際、EC2インスタンスへのヘルスチェックが通らずに困り果ててしまったので、ついにプログラミング独学勢?の切り札であるMENTAを利用しました。

そしたら1~2週間程悩んでいた問題が開始わずか10分ほどで解決してしまい、あっけなく終わってしまいました。。調べてもこの解決方法はなかなか見当たらなかった(多分僕の探し方の問題)ので、僕と同じように膨大な時間を無駄にしないためにも、ここに備忘録として残そうと思います!

今回相談させていただいたメンタ―さん
ルビコン@クラウド(AWS)エンジニアさん
ルビコンさんのTwitter

単発の利用だったにもかかわらず、本題とは関係ない質問にも親切丁寧に教えてくださりました。(しかも15分ぐらいお時間サービスしていただきました。)
この記事でもどうにもならない場合、是非一度相談してみてください!僕もまた困ったら是非利用したいと思える方でした :smile:

そもそもヘルスチェックとは?

ヘルスチェックは、特定のサーバー上のサービスに、作業を正常に実行できるかどうかを確認する方法です。 ロードバランサーは、各サーバーにこの質問を定期的に行い、トラフィックを転送しても安全なサーバーを判断します。

Amazonの公式ページより抜粋

イメージ的には、定期的にロードバランサ―からターゲットグループ(EC2インスタンス)にリクエストを送り返ってきたHTTPステータスを確認しているといった感じです。

このヘルスチェックが失敗してしまうせいでタスクが強制終了してしまい、また新しいタスクが起動するといったことを20分毎ぐらいのペースで繰り返していました。(health checks failed with these codes: [301]というエラーが出てました)
そのせいで、アクセスするタイミングによってページが表示されたり、50xエラーが返ってきたりしているといった状況でした。

解決方法

結論から言いますと、ターゲットグループの設定でHTTPステータスコードの301を許可したら治りました。

上記にも述べました通り、health checks failed with these codes: [301]というのはページがリダイレクトされたということを意味します。なぜリダイレクトされるかというと、Railsアプリの設定で以下のような設定をしていたためです。

production.rb
config.force_ssl = true

SSL化をしていたので、httpからhttpsにリダイレクトさせる設定をしていました。
ですので、ヘルスチェックを行ったときにHTTPステータスコードで301が返ってきて当然です。

しかし、ターゲットグループの設定では200しか許可されていないので、unhealthyとなってしまいタスクが終了してしまうのです。

設定方法

まず、サービスからEC2を選択し、ターゲットグループをクリックします。すると以下のような画面が出てくるので、そこにあるGroup detailsタブのEditをクリックします。
スクリーンショット 2020-11-08 222221.png

次に赤丸の部分をクリックします。
スクリーンショット 2020-11-08 222406.png

Success codesに200と書いてあると思いますので、ここに301を追加して許可します。(カンマで区切って記述してください!)
記述したら変更を保存して終了です。
スクリーンショット 2020-11-08 222449.png

これで数分すればhealthyに変わるはずです!(ALBやセキュリティグループなどの設定が正しくできていれば)

スクリーンショット 2020-11-08 222057.png

これでもヘルスチェックに失敗する場合はセキュリティグループやALBの設定、ヘルスチェックの猶予時間などの設定に問題がある能性がありますので、見直してみてください!

それでも解決しない場合、是非ルビコン@クラウド(AWS)エンジニアさんに相談してみてください :thumbsup:

最後まで読んでいただきありがとうございます!

日々学んだことをアウトプットしております!!ご指摘などございましたらコメントいただけますと幸いです。

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

Leet文字列(ruby編)

Leet文字列(ruby編)

先に言っておくと、期待通りの出力ができなかったので分かる方教えて頂けると嬉しいです?

問題

Leet ではいくつかのアルファベットをよく似た形の他の文字に置き換えて表記します。 Leet の置き換え規則はたくさんありますが、ここでは次の置き換え規則のみを考えましょう。
置き換え前 置き換え後
A 8
E 3
G 6
I 1
O 0
S 5
Z 2

文字列が入力されるので、これを Leet に変換して出力するプログラムを書いてください。

入力される値

入力は以下のフォーマットで与えられます。
i
i は Leet に変換する前の文字列を表します。

期待する出力

i を Leet に変換した文字列を1行に出力してください。

入力例1

APPLE

出力例1

8PPL3

私の答え(誤った答えです)

ruby
array = {
"A" => "8",
"E" => "3",
"G" => "6",
"I" => "1",
"O" => "0",
"S" => "5",
"Z" => "2"
}
str = gets.gsub(/[A-Z]/, array)
print str

悩んだポイント

今回のポイントは「アルファベットが当てはまったら該当のアルファベットのみ数字に置換する」という事でした。ですが、gsub(/[A-Z]/)でこのままのコードでは出力時に83のみ出力されてしまいます。(おそらく条件分岐でelseを用いて当てはまらなった部分を出力できていないから。)8PPL3と出力する際の正規表現なのかそもそものgsubメソッド以外に適したメソッドがあるのか分かりませんでした。ちなみに下記に条件分岐での手段も試してみましたがうまくいきませんでした。

str = gets.chomp.split(" ")
str.each { |s|
case s
when "A"
  print "8"
when "E"
  print "3"



(省略)
else
  print s
}

この問題の8PPL3という出力結果にする方法は様々あると思いますが当てはまったら部分的に置換するという方法がこれ以上できませんでした。もし分かる方、お優しい方がいらっしゃいましたらご教授頂けますと幸いです。

以上

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

ターミナルでRailsコマンドを確認する方法

はじめに

ターミナルのコマンドで、複数形なのか単数形なのか、忘れてしまうことがあり、簡単に知ることができないかと思い、調べてみた。

ターミナルでコマンドを確認

% rails g --help

と実行すると、rails gに続けられるコマンドが一覧となって表示される。
自分の導入しているGemに限られるが、それぞれのGemで使えるコマンドが表示される。
gdに変えて、deleteのコマンドも見ることができる。
コマンドを忘れていしまった時に使える!

最後に

完全に余談だが、

% cal

を実行すると、カレンダーが表示される。

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

間違ってモデルを作ってしまった時の対処法

結論

下記のようにモデルのみを指定して削除します。

ターミナル
rails destroy model [モデル名]

モデルを削除するときはカラムを指定する必要はないのでこのようなコマンドになります。

ただ、rails db:migrateをしてしまっていた場合は、テーブルを作成してしまっているので念のためにテーブルの削除をした方がいいと思います。

テーブルの削除方法

マイグレーションファイルを編集する

create_table :commentsの部分をdrop_table :commentsに変更する。

class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.string :content
    end
  end

最後にターミナルで以下のコマンドを打てばデータベールからテーブルが削除されます。

rails db:migrate

参考記事

[Rails]モデル削除コマンド
Railsでテーブルの削除

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

RSpecとFactoryBotを使ったインスタンスの作成

はじめに

先日こんなツイートをしました。


こちらがその書籍です。単にRSpecの使い方を説明しているだけでなく、なぜテストをするのか、テストの原則、等RSpecにとらわれない考え方というのが非常に参考になりました。
https://leanpub.com/everydayrailsrspec-jp
こちらの書籍を参考にしながら以下のような自分なりには納得できるモデルのテストをパスさせることができたので学んだことをアウトプットしていきたいと思います。その前になぜテストをするのか、何をテストするのか、ということもまとめたのでこちらの記事もご覧ください。
https://qiita.com/shuntagami23/items/c4d6bd2ad282f670156d
9E853AAF-2428-4AA1-8CC8-A9496543DD35.jpeg

1 インスタンスの生成(基本)

UserモデルのテストをするにはUserのインスタンスを生成する必要があります。例えばname, email, passwordのカラムを持ったUserモデルのインスタンスは以下のように生成するのが一般的です。

spec/models/user_spec.rb
equire 'rails_helper'

RSpec.describe User, type: :model do
  before do
    @user = User.create (
      name: 'test',
      email: 'sample@exmple.com',
      password: '123456'
    )
  end
 # ここからexampleを書いていく
end

before do〜endの中でインスタンスを生成しておくことで各exampleが実行される前に毎回@userを生成します。gemのFactoryBotを使うとこのインスタンスは別の場所に置いておくことができます。以下のように書きます。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'TestUser' }
    sequence(:email) { |n| "test#{n}@example.com" }
    password { 'password' }
  end
end

ここで生成したインスタンスは次のようにして呼び出します。

spec/models/user_spec.rb
require 'rails_helper'

describe User do
  before do
    @user = create(:user)
  end
  it '有効なファクトリを持つこと' do
    expect(@user).to be_valid
  end
end

たったこれだけだとあまり利点を感じられないですが、デモデータが多くなったときや関連するモデルのインスタンスを生成する際に非常に役に立ちます。

2 letとlet!について

上記の呼び出し方の他にletを使う方法があります。以下のようになります。

spec/models/user_spec.rb
require 'rails_helper'

describe User do
  let(:user) { create(:user) }
  it '有効なファクトリを持つこと' do
    expect(user).to be_valid
  end
end

letを使う方法だとbefore do〜endで毎回@userを作らなくていいので簡潔に書けますね!また、beforeを使った時のように、exampleが読み込まれる前に@userを生成しておきたい場合はletをlet!に変えるだけでできます。ただし、letはインスタンスの呼び出しにしか使えないのでインスタンスを呼び出すときはletとlet!を使いそれ以外の処理をするときにbeforeを使うというように使い分けるといいと思います。

3 関連するモデルのインスタンスの生成

アソシエーション先のインスタンスを生成したい時もあります。例えば、「userを削除すると、関連するpostも削除されること」を確かめるときです。

spec/models/user_spec.rb
it 'ユーザーを削除すると、関連するコメントも削除されること' do
 post = user.create (
    text: 'test'
  )
  expect { user.destroy }.to change { Post.count }.by(-1)
end

この書き方でも問題はないですがtraitとcreate_listというメソッドを使ってリファクタリングができます。先にコードを見せます。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'TestUser' }
    sequence(:email) { |n| "test#{n}@example.com" }
    password { 'password' }
  end
 trait :with_posts do
    after(:create) do |user|
      create_list(:post, 1, user: user)
    end
  end
end
spec/models/user_spec.rb
it 'ユーザーを削除すると、関連する投稿も削除されること' do
   user = create(:user, :with_posts)
   expect { user.destroy }.to change { Post.count }.by(-1)
end

① trait
これはクラスの継承を表します。これを使うことによってuserのname, email, passwordを継承してくれるので、再定義しなくてよいということになります。クラスを継承しつつuserにpostを持たせます。この処理をするのが次のcreate_listです。

② create_list
これは複数のインスタンスを生成するメソッドです。第一引数に元となるファクトリ、第二引数にその個数、それ以降にカラムに入る値を設定します。ただし、以下のようにpostインスタンスをFactoryBot内で定義してあり、アソシエーションが組まれていることが条件です。

spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    text { 'test' }
    association :user
  end
end

①、②をまとめると、user = create(:user, :with_posts)とすることでpostを1つもったuserインスタンスを生成できるということです。このuserをdestroyしたときにPost.countが1減っているか、ということをテストしています。

最後に

今回は書籍を使って学習したうちのごく一部をまとめただけになりましたが、文章を書くのに慣れていないこともあり非常に大変だなあと感じています。拙い文章で申し訳ないです。。

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

net/httpを利用した外部API連携クラスの作成手順

アプリケーション開発をするにあたり、外部サービスとAPI連携を行う場合があります。

RubyではHTTP通信を行う標準ライブラリとしてnet/httpが用意されています。
今回はnet/httpを利用して、API経由で外部サービスのデータを取得する方法について紹介します。

今回作成するもの

今回は外部サービスの例としてQiitaを利用します。

ゴールは以下の通りです。

  • QiitaClientAPIというQiita APIと連携するクラスを作成する
  • get_itemsというクラスメソッドを利用することでQiitaの記事一覧が取得できるようにする
  • API連携でエラーが発生した場合は、エラーレスポンスの内容を例外として出力する
  • localhost:3000/qiita_itemsを実行するとQiitaClientAPI.get_itemsの結果が取得できる

なお、今回紹介するサンプルはrails 6.0.3.4のAPIモードで作成しています。

最終的な成果物

今回は手順を追いながらソースコードの紹介をしていきます。
最終的なアウトプットは以下のようになります。

ソースコード

config/routes.rb
Rails.application.routes.draw do
  resources :qiita_items, only: %i(index)
end
app/controllers/qiita_items_controller.rb
class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items

    # レスポンスを簡略化するため、titleプロパティのみ返すようにしている
    render json: response_json.map {|item| item.slice('title') }
  end
end
lib/qiita_api_client.rb
class QiitaApiClient
  class HTTPError < StandardError
    def initialize(response)
      super "code=#{response.code} body=#{response.body}"
    end
  end

  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    http_client.request(request)
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise QiitaApiClient::HTTPError.new(response)
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

実行結果

API連携が成功した場合は以下のような結果になります。

$ curl 'http://localhost:3000/qiita_items'

[{"title":"highlight.jsを動的に使ってみた - CodePen"},{"title":"飛び飛びセル順次コピペ"},{"title":"テキスト入力中の点滅するカーソルに好きなCSSを当てる方法"},{"title":"jQueryいろいろ(wrapAll, MutationObserverなど)"},{"title":"高機能なSQL開発ツール「A5:SQL Mk-2」をUbuntuで使う"},{"title":"Amazon Aurora カスタムエンドポイントの検証と考察"},...
()
...
]

API連携が失敗した場合は以下のような結果になります。

### 不正なトークンが利用されている場合
$ curl 'http://localhost:3000/qiita_items'

QiitaApiClient::HTTPError (code=401 body={"message":"Unauthorized","type":"unauthorized"})

下準備

実装をするにあたり、Qiita APIと連携するための準備をします。

アクセストークンの取得

Qiita APIの認証認可に必要なアクセストークンを取得します。

アクセストークンはユーザの管理画面で取得できます。

なおQiita APIのGETリクエストではアクセストークンは不要なため1、今回紹介する記事一覧取得API(/api/v2/items)のみを実装したい場合はこの作業は不要です。

アクセストークンをRailsアプリケーションに登録する

今回はRails 6のcredentialsにトークンを保存しました。

### config/credentials/development.yml.encの編集
$ export EDITOR="vim"
$ rails credentials:edit -e development
→ このタイミングでconfig/credentials/development.keyとconfig/credentials/development.yml.encが作成される
config/credentials/development.yml.enc
qiita:
  # 取得したトークンをセットする
  token: xxxxxxx
$ rails c

### 取得したトークンが表示されればOK
> Rails.application.credentials.qiita[:token]
=> 'xxxxxxx'

ルーティングの追加

検証で利用するlocalhost:3000/qiita_itemsのエンドポイントを作成します。

config/routes.rb
Rails.application.routes.draw do
  resources :qiita_items, only: %i(index)
end

lib配下のクラスを読み込むようにする

今回はQiita APIと連携するクラスをlib配下に作成します。
lib配下のクラスが読み込まれるようにするため以下のように修正します。

config/application.rb
module RailsApiClient
  class Application < Rails::Application
    # 以下を追加
    config.paths.add 'lib', eager_load: true
  end
end

net/httpを利用した外部APIとの連携方法

ここからは順を追って実装について紹介していきます。

シンプルな方法

Qiita APIと連携するクラスを作成せず、ロジック2を直接記述するパターンです。

app/controllers/qiita_items_controller.rb
class QiitaItemsController < ApplicationController
  def index
    uri = URI.parse('https://qiita.com')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{Rails.application.credentials.qiita[:token]}"
    )
    response = http.request(request)
    response_json = JSON.parse(response.body)

    # レスポンスを簡略化するため、titleプロパティのみ返すようにしている
    render json: response_json.map {|item| item.slice('title') }
  end
end

ソースコードを見ればロジックはわかると思いますが、net/httpを利用したGETリクエストの流れについて改めてまとめると以下のようになります。

  1. Net::HTTP.newでHTTPのクライアントのオブジェクトを作成
  2. Net::HTTP::Get.newでGETリクエストのオブジェクトを作成
  3. HTTPクライアントを利用してGETのリクエスト

Qiita APIと連携する専用クラスを作成する

QiitaApiClient.get_itemsを呼ぶことでデータが取得できるようにします。
lib配下にQiitaApiClientクラスを作成し、API連携のロジックを移行します。

lib/qiita_api_client.rb
class QiitaApiClient
  class << self

    QIITA_HOST = 'https://qiita.com'

    def get_items
      uri = URI.parse(QIITA_HOST)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true # SSLを有効化する
      request = Net::HTTP::Get.new(
        '/api/v2/items', # 記事一覧を取得するエンドポイント
        'Authorization' => "Bearer #{Rails.application.credentials.qiita[:token]}" # Bearer認証
      )
      response = http.request(request)
      JSON.parse(response.body)
    end
  end
end

API連携のロジックをQiitaApiClientに移行したので呼び出す側は以下のようになります。

app/controllers/qiita_items_controller.rb
class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

リファクタ: Net::HTTPオブジェクトの作成を共通化する

このままでも問題ないのですが、クラスメソッド(今回でいうget_items)でNet::HTTP.newを実行しているため、クラスメソッドを追加するたびにNet::HTTP.newも追加されてソースコードが少し冗長になります。

そこで、Net::HTTP.newを実行するインスタンスメソッドを作成します。

lib/qiita_api_client.rb
class QiitaApiClient
  def initialize
    # トークンはインスタンス変数として呼び出せるようにする
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    # self.http_clientを呼び出す
    response = http_client.request(request)
    JSON.parse(response.body)
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

QiitaApiClientを呼び出す側は以下のようになります。

app/controllers/qiita_items_controller.rb
class QiitaItemsController < ApplicationController
  def index
    qiita_client = QiitaApiClient.new
    response_json = qiita_client.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

リファクタ: QiitaApiClientインスタンスを呼び出し側で作成しなくて済むようにする

インスタンスを作成しなくてもQiitaApiClient.get_itemsを実行するだけでAPI連携できるようにリファクタリングした結果は以下のとおりです。

lib/qiita_api_client.rb
class QiitaApiClient
  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    response = http_client.request(request)
    JSON.parse(response.body)
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

これで、以下のコードでQiita APIからデータを取得できるようになりました。

app/controllers/qiita_items_controller.rb
class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

例外処理の追加

Qiita APIのレスポンスがエラーだった場合、APIのエラーの内容がわかるよう例外処理を追加します。
今回はエラー時のレスポンスの内容とエラーコードを例外のメッセージに追加しました。

lib/qiita_api_client.rb
class QiitaApiClient
  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise "code= #{response.code}, body = #{response.body}"
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

たとえば、不正なトークンをセットしてリクエストを送った場合、以下のような例外が発生します。

$ curl 'http://localhost:3000/qiita_items'

RuntimeError (code= 401, body = {"message":"Unauthorized","type":"unauthorized"}):

カスタム例外を作成する

カスタム例外には例外の発生場所がわかりやすくなるというメリットがあります。カスタム例外の作成については賛否両論ありますが一応紹介しておきます。

今回はStandardErrorを継承したHTTPErrorというカスタム例外を作成しました。

lib/qiita_api_client.rb
class QiitaApiClient
  class HTTPError < StandardError
    def initialize(response)
      super "code=#{response.code} body=#{response.body}"
    end
  end

  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    http_client.request(request)
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise QiitaApiClient::HTTPError.new(response)
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

これにより、カスタム例外のクラスで例外処理がされるようになりました。

$ curl 'http://localhost:3000/qiita_items'

QiitaApiClient::HTTPError (code=401 body={"message":"Unauthorized","type":"unauthorized"}):

まとめ

以上でnet/httpを利用した外部API連携の方法の紹介を終わります。

外部APIと連携する専用のクラスを作成することでQiitaApiClient.get_itemsのような形で外部サービスのデータを取得できます。
今回はシンプルなGETメソッドのみを実装しましたが、同様の手順でクエリやリクエストボディのついたメソッドの実装もできます。

今回のサンプルはあくまで一例ですので、よりよい方法があれば教えていただけるとありがたいです。

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

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

【Rails】Travis CIを導入してdb:createで詰まったこと

はじめに

自動テストツールTravis CIを導入するときにつまづいたことを書きます。

環境

・Ruby 2.6.5
・Rails 6.0.3.2
・Mysql Ver 14.14 Distrib 5.6.47

問題点

以下のような.travis.ymlに記述しているdb:createがどうしても通らないという問題が発生しました。

script:
  - bundle exec rake db:create RAILS_ENV=test
  - bundle exec rake db:migrate RAILS_ENV=test
  - bundle exec rspec

Travis CIの画面ではこのようなエラーメッセージがでました。

DBcreate.png

Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

いろいろ調べてみるとソケットファイルが無いということがわかりました。
database.ymlの記述のソケットファイルへのパスが誤っているということです。
今回使用したDBは MySQLです。

test:
  <<: *default
  database: (myapp)_test
  adapter: mysql2
  encoding: utf8
  username: root
  password:
  socket: (ここの記述)

解決策

でもローカルではないTravis CIのなかのファイル構成はどうしてもわからなかったので自力で確認することにしました。

まずはソケットファイルの場所を調べるコマンドは以下です。

$ mysql_config --socket

ローカル環境ではこれでわかるので、もしかしたら.travis.ymlscript:に記述すれば「Travis CI上のmysqld.sockの場所がわかるかもと思い、この記述をプラスしました。(正しいやり方かはわかりませんけど・・・)

script:
  - mysql_config --socket  (←ソケットファイルを確認する)
  - bundle exec rake db:create RAILS_ENV=test
  - bundle exec rake db:migrate RAILS_ENV=test
  - bundle exec rspec

すると、

MySQL.soke.png

なんと値が返ってきました!515行目の記述です!

/var/run/mysql/mysqld.sock

これをdatabase.ymlに記述すれば、無事にdb:createが通りました!

ローカルの時とTravisCIの時の使い分け

このままだと逆にローカルでのテストがうまくいかないのでTravisCIでのテストの時には上記でしらべたソケットを参照するようにします。

TravisCI専用としてdatabase_travis.ymlを作成し以下のような記述。

test:
  <<: *default
  database: (myapp)_test
  adapter: mysql2
  encoding: utf8
  username: root
  password:
  socket: /var/run/mysqld/mysqld.sock

そして.travis.ymlに以下を追記

before_script:
  - "cp config/database_travis.yml config/database.yml"

この時だけ書き換えるようにしたらローカルでもTravisCIでもテストが動きました。
データベースのエラーはなかなか原因にたどり着けなく苦労しました。

buildpass.png

無事にこのバッジをReadmeにつけることができました!!

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

【Ruby/Rails】Rails6でのjQuery導入方法

はじめに

やりたいこと

  • いいね機能のAjax対応を実装したい
  • そのためにjQueryを導入したい

動作環境

ruby 2.6.5 / Rails 6.0.3.4

やってみて

Rails6から標準装備されているWebpackerを利用することで簡単に実装できました。
多くの記事がRails5までの情報でとっっっても遠回りしたので、6以降の方は僕のように無駄な時間を使わないでいただけたら…

参考URL

Railsガイド > Rails で JavaScript を使用する

手順

  • jQueryのインストール
  • Webpackの設定
  • application.jsの設定
  • 動作確認

 jQueryのインストール

そもそもjQueryとは

JavaScriptをより簡単に記述するためのライブラリのひとつ。

したがって、jQuery(ライブラリ)を使うのであればJavaScript(元のプログラミング言語)にコンパイル(翻訳)する必要があります。
コンパイルする方法はいろいろありますが、初学者故、とりあえず簡単なWebpackerを使用していこう!というのが本記事の主旨であります。

ではまず、jQueryのインストールを行います。

ターミナル
% yarn add jquery

Rails5以前の導入方法ではjquery-railsというGemをインストールするのが基本線のようですが、Webpackerで管理する際はyarnコマンドを使用してインストールします。

yarnとは

JavaScriptのパッケージマネージャー。Node.jsで動作するパッケージを管理する。

Node.jsとは

本来フロントサイド開発用の言語であるJavaScriptをサーバーサイドで使うための「環境」のこと
Node.jsのおかげで簡単にAjax対応ができたりする。

つまり、Webpackerという翻訳機にjQueryという言語をyarnというサーバー用の説明書で登録した、という感じでしょうか(違ってたらごめんなさい)

Webpackの設定

Webpackの設定ファイルでjQueryを管理下として認定します。

config/webpack/environment.js
const { environment } = require('@rails/webpacker')
// 以下追記
const webpack = require('webpack')
environment.plugins.prepend('Provide',
    new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
    })
)
// ここまで
module.exports = environment

application.jsの設定

application.jsでjQueryを呼び出せるようにします。

javascript/packs/application.js
//中略

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
// 追記
require('jquery')

//中略

以上で導入は完了です。

動作確認

jQueryが問題なく動作しているか確認します。
好きなページに次のコードを転記します。

好きなページ.html.erb
<p>テスト</p>
<script type="text/javascript">
  $(document).ready(function() {
    $("p").text("成功!!");
  });
</script>

p要素のテキストに”成功!!”を代入するようになっています。

(成功例)
スクリーンショット 2020-11-08 18.55.05.png
このように「テスト」ではなく「成功!!」と表示されていれば動作確認は完了です。

おわりに

いろいろな記事を調べてgemを導入したりしていたのですが、Webpackerを使えば簡単に実装できました。

特に今回、JavaScriptとjQueryを調べていく中で、Node.jsのことを表面的にでも理解できたのはよかったかなと思います。

参考にさせていただいた記事
エンジニアの入り口 > 初心者向け!3分で理解するNode.jsとは何か?


Ruby / Rails 初学者向けの記事を書いています。
今後も週3〜4記事ペースで更新していきたいと思いますので、初学者のみなさん、ぜひフォローお願いします!!

最後までお読みいただきありがとうございました!

✔︎

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

【ActiveAdmin】デフォルトのcreateとupdateの処理をカスタマイズしたい

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

【ActiveAdmin】select_boxで表示するcollectionのscopeを指定したい

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

deviseで使えるヘルパーメソッド

メソッド 用途
before_action :authenticate_user! コントローラに設定し、ユーザーのみアクセスを許可する。
user_signed_in? ユーザーがログインしているかどうか判定を行う。ユーザーがログインしていればtrueを、ログアウト状態であればfalseを返します。
current_user 現在ログインしているユーザー取得する
user_session ユーザーのセッション情報にアクセスする

before_action :authenticate_user!

ログイン状態によって表示するページを切り替えるdeviseのメソッド。

class SampleController < ApplicationController
  before_action :authenticate_user!, only: [:show]

  def index
  end

  def show
  end
end

onlyオプションを使うと、showアクションはログイン済みユーザーのみアクセス可能とし、indexアクションはログインしていなくてもアクセスできるようになります。

user_signed_in?

<% if user_signed_in? %>
  <div class="user_nav grid-6">
    <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <%= link_to "投稿する", new_tweet_path, class: "post" %>
  </div>
<% else %>
    <div class="grid-6">
      <%= link_to "ログイン", new_user_session_path, class: "post" %>
      <%= link_to "新規登録", new_user_registration_path, class: "post" %>
    </div>
<% end %>

サインインしていれば"ログアウト"と"投稿する"のページに、サインインしていなければ"ログイン"と"新規登録"を表示といった記述も可能。

deviseの設定がまだ不慣れなため、備忘録として

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

Rails, RSpecの導入手順

環境

rails 6.0.3.4
ruby 2.6.5

流れ

・モデルの単体テストの目的、何をテストするのか
・RSpec導入準備(必要なgemのインストール)
このような流れで説明していきます。

1 テストの目的

結論、コードの保守性を保つためです。当たり前ですがリリースしたアプリケーションにバグが生じるのは望ましくありません。バグが生じないことを手作業で確かめるの思わぬ見落としがあるかもしれない、ということで信頼できるテストを書いておけばコードに変更があった際でも「テストが通ったからok」と手作業で挙動を確かめる手間を省けるということです。

2 何をテストするか

① バリデーション
データベースに値を保存する際には「空の値を保存しない」、「文字数は何文字以内」、「数字のみ保存する」、「大文字は小文字に変換する」などのルールを定義します。規則性のない値が保存されてしまうことがデータベース設計上望ましくないからです。rails側でこのルールを定義する方法がモデルのバリデーションです。そのため、モデルのテストではバリデーションが正しく動作しているかをチェックします。

② メソッド
モデルにはそのモデルの振る舞いを表すメソッドを自分で定義することができます。これも手動でテストするのは大変なのでテストに組み込みます。

③ その他
モデルの役割として「アソシエーションの定義」があります。アソシエーション自体はrails側で定義されているものなので特段テストしなくていいそうですが、あるモデルのデータを削除した時に関連するモデルが削除されているかということもテストに含めます。

3 導入編

まず、RSpecの設定からです。以下のコマンドを実行します。

% bin/rails generate rspec:install

するとジェネレータが以下のようにrspecの設定ファイルと保存フォルダを生成してくれます。

Running via Spring preloader in process 28211
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb

.rspec ファイルを開き、以下のように変更してください。これによりテストの実行結果の表示をきれいに出力できます。

.rspec
--require spec_helper
--format documentation

次にgemのインストールです。

Gemfile
group :development, :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails', '~> 4.0.0'
  # 以下省略
end

group :development do
  gem 'spring-commands-rspec'
  # 以下省略
end

最後の'spring-commands-rspec'はRSpecテストランナーのためのbinstubです。これによりアプリケーションの起動時間を早くするspringの恩恵を受けられます。Springを使いたくない場合は無視してください。最後にもう一つ設定する項目があります。rails gコマンドを実行した際にRSec用のスペックファイルも一緒に作ってもらうようRailsを設定しましょう。また、不要なファイルを生成しないようにします。
config/application.rbを開き、以下のように編集します。

config/application.rb
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.

    config.generators do |g|
      g.test_framework :rspec,
                       view_specs: false,
                       helper_specs: false,
                       routing_specs: false
    end
 end

これでRSpecを導入するための設定が完了しました!

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

deviseでストロングパラメーターを使えるようにする

個人的に苦手なdeviseを使用する際に記述するメソッドなど。備忘録として

devise_parameter_sanitizerメソッド

private
def configure_permitted_parameters 
  # deviseのUserモデルにパラメーターを許可
  devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end

ストロングパラメーターをコントローラーに記述するが、deviseの処理を行うコントローラーはGem内に記述されているため、編集することができない。また、deviseでログイン機能を実装した場合は、paramsの他に、paramsとは異なる形のパラメーターも受け取っています。deviseのコントローラーにストロングパラメーターを反映する方法と、devise特有のパラメーターを取得する方法が、必要になります。

devise_parameter_sanitizerメソッド

deviseのUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できます。
このメソッドとpermitメソッドを組み合わせることにより、deviseに定義されているストロングパラメーターに対し、自分で新しく追加したカラムも指定して含めることができます。

private
def configure_permitted_parameters  # メソッド名は慣習
  # deviseのUserモデルにパラメーターを許可
  devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end

deviseのpermitは、第一引数にdeviseの処理名、第二引数にkeysというキーに対し、配列でキーを指定することで、許可するパラメーターを追加します。

deviseの処理名

メソッド 用途
:sign_in ログインの処理を行うとき
:sign_up 新規登録の処理を行うとき
:account_update アカウント情報更新の処理を行うとき

第一引数で指定した処理に対して、第二引数のkeysで指定された名前と同じキーを持つパラメーターの取得を許可します。ビューに記述した各フォーム部品のname属性値が、フォームから送信されるパラメーターのキーである。
deviseにストロングパラメーターを追加するコードは、deviseのコントローラーが編集できないため、⇩こんな感じでapplication_controllerに記述。

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end

"if: :devise_controller?"はもしdeviseに関するコントローラーの処理であれば、
そのときだけconfigure_permitted_parametersメソッドを実行しまうよ。という記述。
他のtweetsコントローラーなどでは処理は読み込まれても、実行まではされません。

以上備忘録。

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

備忘録:Railsによるコメント機能実装

はじめに

Rubu on Railsでオリジナルアプリを作成しています。そのアプリにコメント機能を実装しました。そのため忘れないように書き記します。

手順

・commentモデル、テーブルの作成
・commentsコントローラーとルーティングの設定
・各コントローラーのアクション設定

commentモデル、テーブルの作成

はじめに下記のようにターミナルでcommentモデルを作成します。

rails g model comment

その後マイグレーションファイルに、textカラムを追加します(このtextカラムにはコメントが保存されます)。

2020*********_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.text :text, null: false
      t.references :user, foreign_key: true
      t.references :desk, foreign_key: true
      t.timestamps
    end
  end
end

userとdeskを外部キー設定します。このコメントは誰が、どのdesk(画像)にしたものか管理するためです。同様にcommentモデルにもバリデーションの他、アソシエーションを設定する必要があります。

desk.rb
  validates :text, presence: true
  belongs_to :user
  belongs_to :desk

userモデル、deskモデルにもアソシエーションを追加します。なお、今回のバリデーションはコメントが空だと保存できないような設定です。

commentsコントローラーとルーティングの設定

ターミナルでコメントのコントローラーを作成します。この時、コントローラー名は複数形にする(いつもどっちだっけ?って悩んでます?)。

rails g controller comments

お次は作成したcommentsコントローラーにcreateアクションを設定します。このとき、中身は空で一旦OKです。

comments_controller.rb
def create
end

最後にルーティングを。コメントはdesk(画像)と紐付けします。なのでネスト(入れ子構造)にします。

route.rb
resources :desks do
    resources :comments, only: :create
end 

くどくなりますが、どの画像に対するコメントなのかをパスから判断できるようにすることが重要です。

各コントローラーのアクション設定

今回はdesks(画像)コントローラーのshowアクションに対するレスポンスページでコメント保存、表示します(元々このページは、投稿された画像の詳細ページです)。なので、まずはdeskコントローラーのshowアクションに@commentのインスタンス変数を定義します。

desks.controller.rb
def show
    @desk = Desk.find(params[:id])
    @comment = Comment.new
    @comments = @desk.comments
end

なお画像に付けられたコメントを一覧で表示するため。@commentsでコメントを取得しています。

今度はcommentsコントローラーへの設定です。こちらではストロングパラメータを設定します。requireにモデル名、permitに保存カラムを設定します。アソシエーション関係にあるものはmergeに記述します。なおdevise Gemがインストールされているので、current_userメソッドを使用しています。これによりコメントが誰が(user_id)、何に(desk_id)にされたものか管理できます。

comments.controller.rb
def create
    @comment = Comment.new(comment_params)
    if @comment.save
      redirect_to desk_path(@comment.desk)
    else
      @desk = @comment.desk
      @comments = @desk.comments
      render "desks/show"
    end
end
  private

  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, desk_id: params[:desk_id])
  end

以上

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

Railsでいいね機能のAjax処理を実装してみた

はじめに

Railsでいいね機能を実装していたのですが、この機能を使うと下記のような問題が発生していました。

  • ページ全体をレンダリングし直すため、無駄な処理を伴ってしまう。
  • ページ遷移が伴ってしまうため、ページ下部でlikeやコメントをしてもページの一番上に遷移してしまい、ユーザービリティを下げている。

そのため、Ajax機能追加し、ページ遷移を伴わないようにしてユーザービリティをあげようと思いました。

最初考えた実装方法は、JavaScript・jQueryを使って、Ajax処理を発火していく方法がベストなのかなと思ったのですが、調べていくと、Railsには、link_toなどのメソッドにremote: trueを引数で付けるとAjax処理を使えるようになるとのことでした。最初考えてた実装方法と違って、簡潔だったので、さっそくその方法で実装してみました。

前提

  • 投稿機能・いいね機能は既に実装済み。
  • 実装しているいいね機能は、いいねボタンを押下すると、ページ遷移が伴うタイプのもの。
  • 上記に対して、Ajax処理で、非同期通信で実装を行うものとします。この記事はその実装で行った手順の紹介です。

環境・バージョン

  • Ruby 2.6.3
  • Rails 6.0.2.1
  • jQuery 3.5.1

実装

まず、app/views/posts/show.html.erbにいいねボタンapp/views/likes/_like.html.erbrenderさせます。

app/views/posts/show.html.erb
# ・・・省略

<div class="post-like mr-auto">
  <%= render partial: 'likes/like', locals: { post: @post, like: @like } %>
</div>

# ・・・省略

いいね機能のdeletepostの引数にremote: trueを追加してAjax機能を追加します。

app/views/likes/_like.html.erb
<% if current_user.already_liked?(post) %>
  <%= link_to 'Like', post_like_path(post, like), method: :delete, class:'post-like__cancel', remote: true %>
<% else %>
  <%= link_to 'Like', post_likes_path(post), method: :post, class:'post-like__enable', remote: true %>
<% end %>

追加することで、それぞれのリンク押下後に下記の処理を実行することができようになります。

  • deleteメソッド→app/views/likes/destroy.js.erbのファイルを実行
  • postメソッド→app/views/likes/create.js.erbのファイルを実行

次に、js.erbファイルは、下記のコードのように@post@likeなどのControllerのメソッドで定義されたインスタンス変数を使うことができます。これは便利ですね。

app/views/likes/destroy.js.erb
$('.post-like').html('<%= j(render partial: 'likes/like', locals: { post: @post, like: @like }) %>');

app/views/likes/create.js.erb
$('.post-like').html('<%= j(render partial: 'likes/like', locals: { post: @post, like: @like }) %>');

できたもの

できたものは下記です。
ページ遷移せずに動いていることを確認できました。

like-ajax.gif

さいごに

他にも応用して、フォロー機能やコメントも実装できました。
今後は、検索機能も作っていくので、Ajaxも使っていこうと思います。

参考記事

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付)

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

評価の平均値を星で表示する

各店舗の評価の平均値を一覧表示ページで表示する方法を記述します!
イメージはこんな感じです。
1b1a25ccb85f56b8e0a1b46837bd6cd6.png

こちらの実装は
Farstep【プログラミング講座】さんの【Ruby on Rails】星レビュー機能を作ろう(PART1)How To Create a Star Rating with Ruby on Railsを参考にしています。

使用環境

ruby 2.6.5
Ruby on Rails 6.0.3.3
一部、余白の指定でBootstrap4を使用しています。

実装内容

モデルの実装

model
class Laundry < ApplicationRecord
  def avg_score
    unless self.comments.empty?
      comments.average(:rate_id).round(1)
    else
      0.0
    end
  end

 def avg_score_percentage
   unless self.comments.empty?
     comments.average(:rate_id).round(1).to_f*100/5
   else
     0.0
   end
 end
end

はじめにコメントが空かどうかで場合分けをして存在している場合は平均値の計算を行います。
averageメソッドを使用してカラム名の平均値を出します。
round(1)とすることで小数点第2位に対して四捨五入をします。

avg_score_percentageメソッドではパーセンテージの計算をしてビューに渡します。

ビューの実装

コードは以下の通りです。

html
<div class="average-score mb-3">
 <div class="star-rating ml-2">
   <div class="star-rating-front" style="width: <%= laundry.avg_score_percentage %>%">★★★★★</div>
   <div class="star-rating-back">★★★★★</div>
 </div>
 <div class="average-score-display">
   (<%= laundry.avg_score %>点)
 </div>
</div>
css
.average-score {
  display: flex; // ★と点数を横並びにする
  justify-content: center;
}

.star-rating {
  position: relative;
  width: 5em;
  height: 1em;
  font-size: 17px;

}
.star-rating-front {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden; // 指定幅からはみ出した部分を隠す。
  white-space: nowrap; // widthが足りない時に、折り返さないようにする。
  color: #ffcc33;
  height: 25px;
}
.star-rating-back {
  color: #ccc;
}

2つの5つ星(★★★★★)を用意してその2つを重ね合わせることで実装を行います。
.star-rating-front・・・黄色い星の部分、上に重ねて"width: <%= laundry.avg_score_percentage %>%とすることで幅を指定する。
.star-rating-back・・・灰色の星の部分、下におくことで黄色くならなかった部分を埋める。

ポイントとして以下の2点だと思います。
1.overflow: hidden;を指定。
overflow: hidden;を指定することで指定幅からはみ出した部分を隠して後ろの灰色の星を表示させます。
ここを指定しないと、せっかく幅を指定しても幅からはみ出した部分が隠れていないため必ず星5つの評価になってしまいます。
overflow: hidden;なし
2609974bbcd16eb7c656c6b0494fe206.png

2.white-space: nowrap;とすることでwidthが足りない時に、折り返さないようにしてくれます。
これを指定することで小数点以下の点数も星に反映されます。
white-space: nowrap;あり
44bdcca4dc3ecd2c6133f7dd72db666b.png

white-space: nowrap;なし
ea00e4658620d7173e5f742fd6b5a946.png

これで評価の平均値を星で表示することができました!

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

stripeを使いショッピングサイトを作ろう!(購入編)

プチ宣伝

https://www.code-sell.net/
コードを販売できるサービスを作りました!いらないコードがある方はぜひ使ってく見てください。ちなみにこの記事で説明するstripeを使用しています。

初めに

こんにちは!今回はrailsとstripeを使い簡単?にECサイトを作っていきます。少し前自分もrailsとstripeで コードを販売するサイトを作ったのですがstripeの情報が少なすぎて非常に苦労しました。正確には情報自体はたくさんあるのですが実践的な情報が少なくただただ公式ドキュメントのようにコードを並べちょこっと説明するみたいなものばっかりでした。今回は実際にサイトを作っていきます。

使う技術・作るもの

noteみたいな記事を購入できるもの

rails
ruby
stripe

stripeってそもそもなに?

決済システムです。payjpとくらべ手数料が安かったり送金機能があったりします。

全体像

https___stripe.com_img_docs_connect_overview.png

Customer(顧客): 購入者でありお金を支払う方。上の図で緑。
Platform(プラットフォーム): 今から作るECサイト。サービスを提供するところ。
Connected accounts: プラットフォームを利用してサービスを提供し、入金を受ける方・販売者(子アカウントとも呼ばれます)。上の図のピンク。

アカウントのタイプ

stripeにはStandardとCustomという子アカウント(販売者)のタイプがあります。特徴・登録方法が違うので目を通りておきましょう。

Standard

開発コスト(手間):簡単
ユーザー視点:微妙
何かあった時の責任:販売者(子アカウント)
おすすめ度:中

これは実装するのが非常に簡単なタイプです。あとで書きますが登録フォームやシステムはほとんどstripe側がやってくれます。僕たちはその登録フォームのリンクを張り付けちょこっとコピペでコントローラーに書くだけです。なにかあった時(マイナス残高など)も僕たちではなく利用しているユーザー側の責任となります。ただこの方法だとユーザーにstripeを使っていることがしっかり伝わってしまいます。登録フォームは完全にstripeが作っているしStripe の管理画面(ダッシュボード)へ、販売者がアクセスできるようになります。デザインも変更できません。

Custom

開発コスト(手間):難
ユーザー視点:いい
何かあった時の責任:プラットフォーム(開発者)
おすすめ度:高

これは実装するのが難しいタイプです。登録フォームもstripeに送信するシステムもダッシュボードも自分で作ります。何かあった時も自分の責任です。ただ登録から管理画面まですべて自分のサイトで完結します。デザインももちろん自由です。

準備編

ながなが説明してきましたがとりあえず細かいことは作って覚えましょう。
今回作るのは単発の購入のサイトです。定期支払などもできますがそれは別の記事でやっていたので...。

登録してAPIキーをもらう
新規登録
APIキーを取得する画面

gemをインストール

gemfile
gem "stripe"
gem 'dotenv-rails'

bundleを忘れずにー。
そしたら.envというファイルをアプリフォルダの直下に作り

PUBLISHABLE_KEY="pk_test_xxx"
SECRET_KEY="sk_test_xxx"
CLIENT_ID="ca_xxx"

と記述してください。
CLIENT_IDはこちらから取得

config/initializers/stripe.rb
Rails.configuration.stripe = {
  publishable_key: ENV["PUBLISHABLE_KEY"],
  secret_key: ENV["SECRET_KEY"],
}
Stripe.api_key = Rails.configuration.stripe[:secret_key]

アプリケーションを作っていく

最初から作るの手間なのでscaffoldにします。最初にも書きましたが今回はnoteのような記事を購入できるサービスを作ります。本当は画像アップロード機能とかもあるほうがいいですが今回はあくまでstripeが中心なのでアプリの機能は最小限にします。

rails g scaffold post title:string content:text price:integer

content...内容、商品
price...値段

rails db:migrate

これでscaffoldができたと思います。

購入機能

購入機能は意外に簡単です。とりあえず最初に作ってしまいましょう。

routes.rb
post "posts/:id/charge", to: "charge#create", as: "charge"

views

↓erbバージョン

show.html.erb
<%= form_tag charge_path(@post) do %>
  <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="#{ENV["PUBLISHABLE_KEY"]}"
    data-amount="<%= @post.price %>"
    data-currency="jpy"
    data-description="クレジット決済"
    data-name=<%= "#{@post.title}を購入" %>
    data-email=<%= "#{current_user.email}" %>
    data-label="購入する"
    data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
    data-locale="auto"
    data-allow-remember-me="false">
  </script>
<% end %>

↓slimバージョン

show.html.slim
= form_tag charge_path(@post) do
  script.stripe-button data-amount="#{@post.price}\
  " data-currency="jpy" data-description="クレジット決済\
  " data-key="#{ENV["PUBLISHABLE_KEY"]}" data-locale="auto" data-name="#{@post.title}を購入\
  " data-email="#{current_user.email}" data-label="購入する\
  " data-allow-remember-me="false" src="https://checkout.stripe.com/checkout.js"

viewsをかいたら
charges_controller.rb
というコントローラーを作ってください。

charges_controller.rb
class ChargesController < ApplicationController
  def create
    @post = Post.find(params[:id])
    customer = Stripe::Customer.create({
      email: params[:stripeEmail],
      source: params[:stripeToken],
    })
    charge = Stripe::Charge.create({
      customer: customer.id,
      amount: @post.price,
      description: "商品ID:#{@post.id} 商品名:#{@post.title}",
      currency: "jpy",
    })
  rescue Stripe::CardError => e
    flash[:error] = e.message
    redirect_to new_charge_path
  end
end

簡単に説明すると4行目でcustomer(顧客)を作っています。chargeは支払い情報をつくっています。
customerはそのまま
amountは商品の値段を設定
descriptionで商品の情報を設定(内容はなんでもいい)
currencyで扱う通貨を設定(USDやJPYなど)

これで購入ができると思います。
テストするときのカード番号は
4242 4242 4242 4242
です。
cvcはなんでもいいです。
カードの期限は今後であればいつでもいいです。
ほかにもいくつかあります。
テストカード一覧

終わりに

今回はここまでにします。
次回はdeviseを導入してマイページを作りスタンダードアカウントやカスタムアカウントの作り方を説明していこうと思います。
大変なので結構先になるかもしれません。

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

第1回 railsでショッピングサイトを作ろう!(購入編)

プチ宣伝

https://www.code-sell.net/
コードを販売できるサービスを作りました!いらないコードがある方はぜひ使ってく見てください。ちなみにこの記事で説明するstripeを使用しています。

初めに

こんにちは!今回はrailsとstripeを使い簡単?にECサイトを作っていきます。少し前自分もrailsとstripeで コードを販売するサイトを作ったのですがstripeの情報が少なすぎて非常に苦労しました。正確には情報自体はたくさんあるのですが実践的な情報が少なくただただ公式ドキュメントのようにコードを並べちょこっと説明するみたいなものばっかりでした。今回は実際にサイトを作っていきます。

使う技術・作るもの

noteみたいな記事を購入できるもの

rails
ruby
stripe

stripeってそもそもなに?

決済システムです。payjpとくらべ手数料が安かったり送金機能があったりします。

全体像

https___stripe.com_img_docs_connect_overview.png

Customer(顧客): 購入者でありお金を支払う方。上の図で緑。
Platform(プラットフォーム): 今から作るECサイト。サービスを提供するところ。
Connected accounts: プラットフォームを利用してサービスを提供し、入金を受ける方・販売者(子アカウントとも呼ばれます)。上の図のピンク。

アカウントのタイプ

stripeにはStandardとCustomという子アカウント(販売者)のタイプがあります。特徴・登録方法が違うので目を通りておきましょう。

Standard

開発コスト(手間):簡単
ユーザー視点:微妙
何かあった時の責任:販売者(子アカウント)
おすすめ度:中

これは実装するのが非常に簡単なタイプです。あとで書きますが登録フォームやシステムはほとんどstripe側がやってくれます。僕たちはその登録フォームのリンクを張り付けちょこっとコピペでコントローラーに書くだけです。なにかあった時(マイナス残高など)も僕たちではなく利用しているユーザー側の責任となります。ただこの方法だとユーザーにstripeを使っていることがしっかり伝わってしまいます。登録フォームは完全にstripeが作っているしStripe の管理画面(ダッシュボード)へ、販売者がアクセスできるようになります。デザインも変更できません。

Custom

開発コスト(手間):難
ユーザー視点:いい
何かあった時の責任:プラットフォーム(開発者)
おすすめ度:高

これは実装するのが難しいタイプです。登録フォームもstripeに送信するシステムもダッシュボードも自分で作ります。何かあった時も自分の責任です。ただ登録から管理画面まですべて自分のサイトで完結します。デザインももちろん自由です。

準備編

ながなが説明してきましたがとりあえず細かいことは作って覚えましょう。
今回作るのは単発の購入のサイトです。定期支払などもできますがそれは別の記事でやっていたので...。

登録してAPIキーをもらう
新規登録
APIキーを取得する画面

gemをインストール

gemfile
gem "stripe"
gem 'dotenv-rails'

bundleを忘れずにー。
そしたら.envというファイルをアプリフォルダの直下に作り

PUBLISHABLE_KEY="pk_test_xxx"
SECRET_KEY="sk_test_xxx"
CLIENT_ID="ca_xxx"

と記述してください。
CLIENT_IDはこちらから取得

config/initializers/stripe.rb
Rails.configuration.stripe = {
  publishable_key: ENV["PUBLISHABLE_KEY"],
  secret_key: ENV["SECRET_KEY"],
}
Stripe.api_key = Rails.configuration.stripe[:secret_key]

アプリケーションを作っていく

最初から作るの手間なのでscaffoldにします。最初にも書きましたが今回はnoteのような記事を購入できるサービスを作ります。本当は画像アップロード機能とかもあるほうがいいですが今回はあくまでstripeが中心なのでアプリの機能は最小限にします。

rails g scaffold post title:string content:text price:integer

content...内容、商品
price...値段

rails db:migrate

これでscaffoldができたと思います。

購入機能

購入機能は意外に簡単です。とりあえず最初に作ってしまいましょう。

routes.rb
post "posts/:id/charge", to: "charge#create", as: "charge"

views

↓erbバージョン

show.html.erb
<%= form_tag charge_path(@post) do %>
  <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="#{ENV["PUBLISHABLE_KEY"]}"
    data-amount="<%= @post.price %>"
    data-currency="jpy"
    data-description="クレジット決済"
    data-name=<%= "#{@post.title}を購入" %>
    data-email=<%= "#{current_user.email}" %>
    data-label="購入する"
    data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
    data-locale="auto"
    data-allow-remember-me="false">
  </script>
<% end %>

↓slimバージョン

show.html.slim
= form_tag charge_path(@post) do
  script.stripe-button data-amount="#{@post.price}\
  " data-currency="jpy" data-description="クレジット決済\
  " data-key="#{ENV["PUBLISHABLE_KEY"]}" data-locale="auto" data-name="#{@post.title}を購入\
  " data-email="#{current_user.email}" data-label="購入する\
  " data-allow-remember-me="false" src="https://checkout.stripe.com/checkout.js"

viewsをかいたら
charges_controller.rb
というコントローラーを作ってください。

charges_controller.rb
class ChargesController < ApplicationController
  def create
    @post = Post.find(params[:id])
    customer = Stripe::Customer.create({
      email: params[:stripeEmail],
      source: params[:stripeToken],
    })
    charge = Stripe::Charge.create({
      customer: customer.id,
      amount: @post.price,
      description: "商品ID:#{@post.id} 商品名:#{@post.title}",
      currency: "jpy",
    })
  rescue Stripe::CardError => e
    flash[:error] = e.message
    redirect_to new_charge_path
  end
end

簡単に説明すると4行目でcustomer(顧客)を作っています。chargeは支払い情報をつくっています。
customerはそのまま
amountは商品の値段を設定
descriptionで商品の情報を設定(内容はなんでもいい)
currencyで扱う通貨を設定(USDやJPYなど)

これで購入ができると思います。
テストするときのカード番号は
4242 4242 4242 4242
です。
cvcはなんでもいいです。
カードの期限は今後であればいつでもいいです。
ほかにもいくつかあります。
テストカード一覧

終わりに

今回はここまでにします。
次回はdeviseを導入してマイページを作りスタンダードアカウントやカスタムアカウントの作り方を説明していこうと思います。
大変なので結構先になるかもしれません。

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

railsでショッピングサイトを作ろう!(購入編)

プチ宣伝

https://www.code-sell.net/
コードを販売できるサービスを作りました!いらないコードがある方はぜひ使ってく見てください。ちなみにこの記事で説明するstripeを使用しています。

初めに

こんにちは!今回はrailsとstripeを使い簡単?にECサイトを作っていきます。少し前自分もrailsとstripeで コードを販売するサイトを作ったのですがstripeの情報が少なすぎて非常に苦労しました。正確には情報自体はたくさんあるのですが実践的な情報が少なくただただ公式ドキュメントのようにコードを並べちょこっと説明するみたいなものばっかりでした。今回は実際にサイトを作っていきます。

使う技術・作るもの

noteみたいな記事を購入できるもの

rails
ruby
stripe

stripeってそもそもなに?

決済システムです。payjpとくらべ手数料が安かったり送金機能があったりします。

全体像

https___stripe.com_img_docs_connect_overview.png

Customer(顧客): 購入者でありお金を支払う方。上の図で緑。
Platform(プラットフォーム): 今から作るECサイト。サービスを提供するところ。
Connected accounts: プラットフォームを利用してサービスを提供し、入金を受ける方・販売者(子アカウントとも呼ばれます)。上の図のピンク。

アカウントのタイプ

stripeにはStandardとCustomという子アカウント(販売者)のタイプがあります。特徴・登録方法が違うので目を通りておきましょう。

Standard

開発コスト(手間):簡単
ユーザー視点:微妙
何かあった時の責任:販売者(子アカウント)
おすすめ度:中

これは実装するのが非常に簡単なタイプです。あとで書きますが登録フォームやシステムはほとんどstripe側がやってくれます。僕たちはその登録フォームのリンクを張り付けちょこっとコピペでコントローラーに書くだけです。なにかあった時(マイナス残高など)も僕たちではなく利用しているユーザー側の責任となります。ただこの方法だとユーザーにstripeを使っていることがしっかり伝わってしまいます。登録フォームは完全にstripeが作っているしStripe の管理画面(ダッシュボード)へ、販売者がアクセスできるようになります。デザインも変更できません。

Custom

開発コスト(手間):難
ユーザー視点:いい
何かあった時の責任:プラットフォーム(開発者)
おすすめ度:高

これは実装するのが難しいタイプです。登録フォームもstripeに送信するシステムもダッシュボードも自分で作ります。何かあった時も自分の責任です。ただ登録から管理画面まですべて自分のサイトで完結します。デザインももちろん自由です。

準備編

ながなが説明してきましたがとりあえず細かいことは作って覚えましょう。
今回作るのは単発の購入のサイトです。定期支払などもできますがそれは別の記事でやっていたので...。

登録してAPIキーをもらう
新規登録
APIキーを取得する画面

gemをインストール

gemfile
gem "stripe"
gem 'dotenv-rails'

bundleを忘れずにー。
そしたら.envというファイルをアプリフォルダの直下に作り

PUBLISHABLE_KEY="pk_test_xxx"
SECRET_KEY="sk_test_xxx"
CLIENT_ID="ca_xxx"

と記述してください。
CLIENT_IDはこちらから取得

config/initializers/stripe.rb
Rails.configuration.stripe = {
  publishable_key: ENV["PUBLISHABLE_KEY"],
  secret_key: ENV["SECRET_KEY"],
}
Stripe.api_key = Rails.configuration.stripe[:secret_key]

アプリケーションを作っていく

最初から作るの手間なのでscaffoldにします。最初にも書きましたが今回はnoteのような記事を購入できるサービスを作ります。本当は画像アップロード機能とかもあるほうがいいですが今回はあくまでstripeが中心なのでアプリの機能は最小限にします。

rails g scaffold post title:string content:text price:integer

content...内容、商品
price...値段

rails db:migrate

これでscaffoldができたと思います。

購入機能

購入機能は意外に簡単です。とりあえず最初に作ってしまいましょう。

routes.rb
post "posts/:id/charge", to: "charge#create", as: "charge"

views

↓erbバージョン

show.html.erb
<%= form_tag charge_path(@post) do %>
  <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="#{ENV["PUBLISHABLE_KEY"]}"
    data-amount="<%= @post.price %>"
    data-currency="jpy"
    data-description="クレジット決済"
    data-name=<%= "#{@post.title}を購入" %>
    data-email=<%= "#{current_user.email}" %>
    data-label="購入する"
    data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
    data-locale="auto"
    data-allow-remember-me="false">
  </script>
<% end %>

↓slimバージョン

show.html.slim
= form_tag charge_path(@post) do
  script.stripe-button data-amount="#{@post.price}\
  " data-currency="jpy" data-description="クレジット決済\
  " data-key="#{ENV["PUBLISHABLE_KEY"]}" data-locale="auto" data-name="#{@post.title}を購入\
  " data-email="#{current_user.email}" data-label="購入する\
  " data-allow-remember-me="false" src="https://checkout.stripe.com/checkout.js"

viewsをかいたら
charges_controller.rb
というコントローラーを作ってください。

charges_controller.rb
class ChargesController < ApplicationController
  def create
    @post = Post.find(params[:id])
    customer = Stripe::Customer.create({
      email: params[:stripeEmail],
      source: params[:stripeToken],
    })
    charge = Stripe::Charge.create({
      customer: customer.id,
      amount: @post.price,
      description: "商品ID:#{@post.id} 商品名:#{@post.title}",
      currency: "jpy",
    })
  rescue Stripe::CardError => e
    flash[:error] = e.message
    redirect_to new_charge_path
  end
end

簡単に説明すると4行目でcustomer(顧客)を作っています。chargeは支払い情報をつくっています。
customerはそのまま
amountは商品の値段を設定
descriptionで商品の情報を設定(内容はなんでもいい)
currencyで扱う通貨を設定(USDやJPYなど)

これで購入ができると思います。
テストするときのカード番号は
4242 4242 4242 4242
です。
cvcはなんでもいいです。
カードの期限は今後であればいつでもいいです。
ほかにもいくつかあります。
テストカード一覧

終わりに

今回はここまでにします。
次回はdeviseを導入してマイページを作りスタンダードアカウントやカスタムアカウントの作り方を説明していこうと思います。
大変なので結構先になるかもしれません。

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

Railsで RoutingError がFATALレベルでログに出てくることへ対処(したかった)

概要

RoutingErrorFATALレベルでログ出力される点が困っている。FATALは運用時に「即時対処が必要なもの」と決めていて夜間でもアラートが鳴るようにしているのだが、 RoutingError はクライアントが不正なリクエストをすれば簡単に起こせるので、そもそもログに出されなくていい。(404はwebサーバーのアクセスログでもわかる)

何か設定を間違えているだけかと思っていたが、Railsガイド通りに新規作成したばかりのrailsアプリでも同じことが起きたため、railsが何をしているのかソースコードを色々と調べることになった。結局適切なオプションは見つけられず、railsの MiddlewareStack に手を加えるという形になってしまった。(この辺の仕組みに目を通せたのは収穫。)

環境: Rails 6.0.0 / Ruby 2.5.3 / Ubuntu 20.04

ログのサンプル

リクエストを受けた際のrailsのログを以下に示す。

ルーティングあり
I, [2020-11-07T23:01:42.988933 #12481]  INFO -- : Started GET "/" for 127.0.0.1 at 2020-11-07 23:01:42 +0900
I, [2020-11-07T23:01:42.992722 #12481]  INFO -- : Processing by MiniController#index as HTML
I, [2020-11-07T23:01:43.002614 #12481]  INFO -- :   Rendering text template
I, [2020-11-07T23:01:43.005314 #12481]  INFO -- :   Rendered text template (Duration: 0.0ms | Allocations: 4)
I, [2020-11-07T23:01:43.005728 #12481]  INFO -- : Completed 200 OK in 9ms (Views: 8.9ms | Allocations: 1395)
ルーティングなし
I, [2020-11-07T23:03:08.801059 #12481]  INFO -- : Started GET "/illegal" for 127.0.0.1 at 2020-11-07 23:03:08 +0900
F, [2020-11-07T23:03:08.801822 #12481] FATAL -- :   
ActionController::RoutingError (No route matches [GET] "/illegal"):

actionpack (6.0.0) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
actionpack (6.0.0) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
(省略)
/opt/rbenv/versions/2.5.3/lib/ruby/2.5.0/webrick/server.rb:307:in `block in start_thread'

ちなみにrails5ではFATALログが4つに分割されていた(空行・例外・空行・バックトレース)。rails6ではこの通りひとまとめになったので、監視側でログを無視するのは楽になった。

ログが出るまでの仕組み

そもそも RoutingError はどう発生し、どこでログに記録しているのだろうと疑問に思った。

ミドルウェアの入れ子

ログのスタックトレースを頼りにgemをgrepしていたところ、 Rails::Application::DefaultMiddlewareStack の中で一連のアプリを登録していた。 ※railsの設定によって個数は変わる

ミドルウェア
::ActionDispatch::HostAuthorization
::ActionDispatch::SSL
::Rack::Sendfile
::ActionDispatch::Static
::Rack::Cache
::Rack::Lock
::ActionDispatch::Executor
::Rack::Runtime
::Rack::MethodOverride
::ActionDispatch::RequestId
::ActionDispatch::RemoteIp
::Rails::Rack::Logger
::ActionDispatch::ShowExceptions
::ActionDispatch::DebugExceptions
::ActionDispatch::ActionableExceptions
::ActionDispatch::Reloader
::ActionDispatch::Callbacks
::ActionDispatch::Cookies
config.session_store
::ActionDispatch::Flash
::ActionDispatch::ContentSecurityPolicy::Middleware
::Rack::Head
::Rack::ConditionalGet
::Rack::ETag
::Rack::TempfileReaper

これらのクラスには #call メソッドがあり、上側のクラスはひとつ下のクラスにリクエストを渡してレスポンスを受け取るという入れ子構造になっている。もちろんただ #call を呼ぶだけでなく、前後にリクエスト・レスポンスを編集したり、例外の送出や捕捉なども必要に応じてしていて、それぞれが自分の役割を果たしている。

※入れ子の順序は MiddlewareStack が管理するので、各クラスは自分が誰を呼び出すか知っている必要は無い。

DebugExceptions

肝心のこのアプリが何をしているのか、中身を読んだ。

https://github.com/rails/rails/blob/v6.0.0/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L30

ActionDispatch::DebugExceptions#call
    def call(env)
      request = ActionDispatch::Request.new env
      _, headers, body = response = @app.call(env)

      if headers["X-Cascade"] == "pass"
        body.close if body.respond_to?(:close)
        raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
      end

      response
    rescue Exception => exception
      invoke_interceptors(request, exception)
      raise exception unless request.show_exceptions?
      render_exception(request, exception)
    end
  1. すぐ次のアプリにリクエストを渡してレスポンスを受け取る
  2. レスポンスヘッダーに X-Cascade: pass が設定されていたら RoutingError を送出する
  3. 処理中に例外が起きていれば捕捉し、必要に応じて処理をする
    • interceptorが登録されていれば実行する(rails6で追加)
    • 例外をログに記録する
    • 例外の詳細についてのレスポンスを作成する ※development環境でよく出てくるやつ
    • レスポンスを作成しないなら例外を再送出する(前段のアプリに任せる)

この「例外をログに記録する」処理を回避でき、他の処理にも悪影響が出ない条件分岐があればいい。(なお、ログ出力は fatal でハードコードされていて、レベルを下げることはできない)

対処(の試行錯誤)

案1. DebugExceptions を外す

「クラス名がdebugだし、無くても動くだろう」と思って試した。railsの設定時に、登録したミドルウェアを抜くことができる。

config/environments/<env>.rb などに追加
config.middleware.delete ::ActionDispatch::DebugExceptions

するとFATALログは消えたものの、レスポンスボディが Not Found だけになってしまい、404ページが表示されなくなった

クライアント側
$ curl -i http://localhost:3000/illegal
HTTP/1.1 404 Not Found 
X-Cascade: pass
Cache-Control: no-cache
X-Request-Id: fefa1568-57dd-45be-bc31-f91f17a5c916
X-Runtime: 0.005418
Server: WEBrick/1.4.2 (Ruby/2.5.3/2018-10-18)
Date: Sat, 07 Nov 2020 14:32:30 GMT
Content-Length: 9
Connection: Keep-Alive

Not Found

DebugExceptions を外すということは RoutingError が出なくなるということであり、前段のアプリ群が元と同じ例外処理をしなくなってしまう名前に反してかなり重要なミドルウェア?

というわけで却下。

案2. show_exceptions オプション

DebugExceptions の中でログ出力せずに例外を投げてくれればいいだろう」ということで、コード中の request.show_exceptions? が偽になるようにする。これはrailsの設定項目に存在する。

config/environments/<env>.rb などに追加
config.action_dispatch.show_exceptions = false

やってみたら500エラーになってしまった。ログもrailsは問題ないが、webサーバー(webrick, pumaなど)のほうでエラーを吐き出している。

illegal-500.png

ログ
I, [2020-11-07T23:34:09.728873 #13864]  INFO -- : Started GET "/illegal" for 127.0.0.1 at 2020-11-07 23:34:09 +0900
[2020-11-07 23:34:09] ERROR ActionController::RoutingError: No route matches [GET] "/illegal"
    /.../actionpack-6.0.0/lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
    /.../actionpack-6.0.0/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
(省略)
    /opt/rbenv/versions/2.5.3/lib/ruby/2.5.0/webrick/server.rb:307:in `block in start_thread'

というのも、これは名前通り前段の ShowExceptions を無効化するもので、 /public/404.html などエラーレスポンスが用意されなくなる。さらに、元々はここで RoutingError を捕捉していたのに再送出してしまうようになり、前段のアプリ群の処理が変わってしまう。

  • ActionDispatch::ShowExceptions rescues any exception returned by the application and renders nice exception pages if the request is local or if config.consider_all_requests_local is set to true. If config.action_dispatch.show_exceptions is set to false, exceptions will be raised regardless.

https://guides.rubyonrails.org/configuring.html#configuring-middleware より引用、一部強調

というわけで却下。

案3. DebugExceptions にモンキーパッチ

確実に成功する方法を試しておく。問題のFATALログの出力箇所はひとつのメソッドに纏められているので、それを何もしないように上書きしてしまえばいい。

config/environments/<env>.rb などに追加
class ::ActionDispatch::DebugExceptions
  def log_error(*); end
end

成功例は省略する。問題としては、

  • RoutingError 以外の例外もログ出力されなくなってしまう
  • ライブラリのコード変更に弱い(しかも対象がprivateメソッド)
  • モンキーパッチということで行儀が悪い

案4. DebugExceptions の代替品を自作

案3よりは正攻法であり、柔軟性があり、その分だけ面倒でもある。

my_debug_exceptions.rb
# RoutingError の送出部分だけをコピーした
class MyDebugExceptions
  # 第1引数 app は必須、追加の引数は自由(今回は無視)
  def initialize(app, *)
    @app = app
  end

  # 引数は env
  def call(env)
    request = ActionDispatch::Request.new env
    _, headers, body = response = @app.call(env)

    if headers["X-Cascade"] == "pass"
      body.close if body.respond_to?(:close)
      raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
    end

    response
  rescue ActionController::RoutingError => routing_error
    # RoutingError 専用の処理、今回はひとまず無し
    raise routing_error
  rescue Exception => exception
    # RoutingError 以外の処理、今回はひとまず無し
    raise exception
  end
end

作成したアプリを元のものと差し替える。これはrailsの設定で可能。

config/environments/<env>.rb などに追加
# app 以外の引数はここで指定できる
config.middleware.swap ActionDispatch::DebugExceptions,
                       MyDebugExceptions, "arg1", "arg2"

結局…

「本当にこんな方法しか無いの?」という気持ちになった。リリースするものに組み込むのは躊躇する。一応もう少し調べ続けてみようと思う。

先に述べた通り、rails6では問題のFATALログがひとまとめになったので、railsには標準的な動作をさせておいて監視側で無視したほうがわかりやすい気もする。

付録

実験に使用したrailsアプリ

実験ではほとんどの設定を削ぎ落とすため、過去に作成した20行程度のものをベースにした。以下にコード全文を載せる。

クリックして展開
Gemfile
source "https://rubygems.org"
gem "railties", "6.0.0"
bin/rails
APP_PATH = File.expand_path('../config/application', __dir__)
require 'rails/commands'
config.ru
require_relative 'config/environment'
run Rails.application
config/environment.rb
require_relative 'application'
Rails.application.initialize!
config/application.rb
require 'action_controller/railtie'

class MiniApp < Rails::Application
  config.logger = ::Logger.new(STDOUT)  # ログをターミナル上で見れるように
end
config/routes.rb
Rails.application.routes.draw do
  root to: 'mini#index'
end
app/controllers/mini_controller.rb
class MiniController < ActionController::Base
  def index
    render plain: "Hello, world!\n"
  end
end

エラーページの静的ファイル public/404.html はお好みで。

(コードはここまで)


development環境だと設定が追加されることがあるので、production環境で実行する。 SECRET_KEY_BASE を指定する必要があるが、適当でいい。

実行方法
$ bundle install --path vendor/bundle  # 検索やコード改変しやすいように

$ bundle exec rails routes  # ルーティング確認

$ RAILS_ENV=production SECRET_KEY_BASE=_ bundle exec rails server

あとは curl やブラウザで http://localhost:3000/ にアクセスすれば試せる。

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

【Rails】DM(チャット)機能 + Ajax 実装!

まえがき

以前スクールの課題でDM機能を実装した際に、色々なサイトを参考にして作成したのですが、かなり複雑で無駄な記述が多くなってしまいました。無駄な部分を省きながらoutputしていきたいと思い投稿しました。おかしな点はご指摘お願いします、、!

実装環境

ruby '2.6.3'
gem 'rails', '~> 5.2.4', '>= 5.2.4.3'
gem 'jquery-rails'
gem 'devise'(DM機能実装過程でヘルパーメソッドのcurrent_userを使用しています)
gem 'bootstrap-sass', '~> 3.3.6'(無くても可)

userモデルは事前に作成して、他ユーザーのshowページを閲覧できるようにしてある前提で進めていきます。

実装していく内容

usersのshowページにリンクを作成し、DMページ(chatsのshow)へ遷移して実際にトークが行えるように実装していきます。
model名やページ内に表示したい文字列等は適当に置き換えてください。

実装後のイメージ

image.png

※スタイルはかなり適当なのでご自身で整えてください

実装していきましょう!!

①モデルとカラムの作成

rails g model room
rails g model user_room user_id:integer room_id:integer
rails g model chat user_id:integer room_id:integer message:string
rails db:migrate

②コントローラーの作成

rails g controller chats show

解説(クリックしてください)
showアクションも同時に作成してしまいます。

③モデル同士のアソシエーション

user.rb
 has_many :user_rooms
 has_many :chats
 has_many :rooms, through: :user_rooms
room.rb
 has_many :chats
 has_many :user_rooms
user_room.rb
 belongs_to :user
 belongs_to :room
chat.rb
 belongs_to :user
 belongs_to :room

throughについて上手く説明できる自信がないので下記のQiita記事を参考にしてみてください☟
【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】?

④ルーティングの作成

routes.rb
  get 'chat/:id' => 'chats#show', as: 'chat'
  resources :chats, only: [:create]

⑤users_controllerへ記述

users_controller.rb
def show
  @user = User.find(params[:id])
end
#余計なコードは省いて記述しています。

⑥usersのshowにリンク作成

users/show
<% if current_user != @user %>
  <%= link_to 'チャットする', chat_path(@user.id) %>
<% end %> 

解説(クリックしてください)
current_userはDeviseのヘルパーメソッドです、ここではログインユーザーと@userが一致しない場合のみリンクを表示する記述しています。
pathの内容は一応rails routesで確認してから記述してください。
現時点でリンクをクリックすると作成してあるshowページに遷移するはずです。

⑦chatsコントローラーに記述

chats_controller.rb
 def show
    @user = User.find(params[:id])
    #ログインしているユーザーのidが入ったroom_idのみを配列で取得(該当するroom_idが複数でも全て取得)
    rooms = current_user.user_rooms.pluck(:room_id)
    #user_idが@user 且つ room_idが上で取得したrooms配列の中にある数値のもののみ取得(1個または0個のはずです)
    user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)

    if user_rooms.nil? #上記で取得できなかった場合の処理
      #新しいroomを作成して保存
      @room = Room.new
      @room.save
      #@room.idと@user.idをUserRoomのカラムに配列で保存
      UserRoom.create(user_id: @user.id, room_id: @room.id)
      #@room.idとcurrent_user.idをUserRoomのカラムに配列で保存
      UserRoom.create(user_id: current_user.id, room_id: @room.id)
    else
      #取得している場合は、user_roomsに紐づいているroomテーブルのレコードを@roomに代入
      @room = user_rooms.room
    end
    #if文の中で定義した@roomに紐づくchatsテーブルのレコードを代入
    @chats = @room.chats
    #@room.idを代入したChat.newを用意しておく(message送信時のform用)←筆者の表現が合っているか分かりません、、
    @chat = Chat.new(room_id: @room.id)
  end

  def create
    @chat = current_user.chats.new(chat_params)
    @chat.save
  end

  private
  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end

⑧Chatsのshowに記述

chats/show.html.erb
<div class="container">
    <div class="row">
        <div class="col-xs-6">
            <h2 id="room" data-room="<%= @room.id %>" data-user="<%= current_user.id %>"><%= @user.name %> さんとのチャット</h2>

            <table class="message table">
              <thead>
                <tr>
                  <th style="text-align: left; font-size: 20px;"><%= current_user.name %></th>
                  <th style="text-align: right; font-size: 20px;"><%= @user.name %></th>
                </tr>
              </thead>
              <% @chats.each do |chat| %>
                <% if chat.user_id == current_user.id %>
                <tbody>
                  <tr>
                    <th>
                      <p style="text-align: left;"><%= chat.message %></p>
                    </th>
                    <th></th>
                  </tr>
                </tbody>
                <% else %>
                <tbody>
                  <tr>
                    <th></th>
                    <th>
                      <p style="text-align: right;"><%= chat.message %></p>
                    </th>
                  </tr>
                </tbody>
                <% end %>
              <% end %>
            </table>

            <%= form_with model: @chat do |f| %>
              <%= f.text_field :message %>
              <%= f.hidden_field :room_id %>
            <% end %>
        </div>
    </div>
</div>

※bootstrapで少しだけそれっぽい見た目にしているのでinstallしてない方は、必要のないdivタグやclass指定は省いて必要な部分のみの記述にしてください。

解説(クリックしてください)
form_withの記述でlocal: trueを記述していない点に注意してください。js形式のリクエストを送信する必要があるので何も記述しなくても問題ありません、<%= form_with model: @chat, remote: true do |f| %>と記述するのと同義になります。
destroyアクションなど追加して非同期にする際は、そちらにremote: trueと記述する必要があります。

⑨jsファイル作成(あと少しです!)

app/views/chats直下ににcreate.js.erbを作成して、以下の内容を記述します。

chats/create.js.erb
$('.message').append("<p style='text-align: left;'><%= @chat.message %></p>");
$('input[type=text]').val("")

解説(クリックしてください).meesageは指定してあるclass名です。
.append("

<%= @chat.message %>

");
で部分的な更新をしています。2行目の記述で更新時に入力フォームの中身を空にする処理の記述をしています。

・筆者は.appendについて上手く説明できないので以下の記事を参考にしてみてください☟
jQueryのappendメソッド

以上で実装が出来たはずです。
おかしな記述や表現がある場合は指摘をおねがいします。
良ければこちらの記事も参考にしてみてください☟
Gemなし 複数検索機能の実装 に関する筆者の記事

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

【Rails(5.0)】 japanMapリンク使用時 js.erbファイル内のパラメータ書き方

初投稿になります。プログラミング始めて4ヶ月の初心者です。よろしくお願いします。

ポートフォリオ作成の際にjapanMapを使用して8地方ごとにイベントを検索できるようにしました。

その際、地方ごとのリンクに任意のパラメータを渡すのに少し苦労したので、自分で確認用として投稿しています。

今回は application.jsファイルにjapanMapのプログラムを記述しました。

以下が自分の書いたコードになります。

application.js
//検索ページ日本地図
$(function(){
  //8地方でリンク作成
  var areaLinks = {
    1:"/user/index?sort=hokkaido",
    2:"/user/index?sort=tohoku",
    3:"/user/index?sort=kanto",
    4:"/user/index?sort=chubu",
    5:"/user/index?sort=kinki",
    6:"/user/index?sort=chugoku_shikoku",
    7:"/user/index?sort=kyusyu_okinawa",
  };

  //8地方エリア指定
  var areas = [
    {code : 1, name: "北海道", color: "#ab86c4", hoverColor: "#dfcceb", prefectures: [1]},
    {code : 2, name: "東北",   color: "#6d93d1", hoverColor: "#91b0e3", prefectures: [2,3,4,5,6,7]},
    {code : 3, name: "関東",   color: "#f5a164", hoverColor: "#f5c09a", prefectures: [8,9,10,11,12,13,14]},
    {code : 4, name: "中部",   color: "#77e077", hoverColor: "#adedad", prefectures: [15,16,17,18,19,20,21,22,23]},
    {code : 5, name: "近畿",   color: "#ffe966", hoverColor: "#fff2a3", prefectures: [24,25,26,27,28,29,30]},
    {code : 6, name: "中国・四国",   color: "#e68ccc", hoverColor: "#f0b9e0", prefectures: [31,32,33,34,35,36,37,38,39]},
    {code : 7, name: "九州・沖縄",   color: "#de6474", hoverColor: "#f29da9", prefectures: [40,41,42,43,44,45,46,47]},
  ];

  //地図表示設定
  $("#map-container").japanMap({
    width: 600,
    areas  : areas,
    selection : "area",
    borderLineWidth: 0.25,
    drawsBoxLine : false,
    movesIslands : true,
    showsAreaName : true,
    font : "MS Mincho",
    fontSize : 13,
    fontColor :"#777",
    fontShadowColor : "white",
    onSelect : function(data){
    location.href = areaLinks[data.area.code];
  };
});

上記のこの部分がリンクになります。

var areaLinks = {
    1:"/user/index?sort=hokkaido",
    2:"/user/index?sort=tohoku",
    3:"/user/index?sort=kanto",
    4:"/user/index?sort=chubu",
    5:"/user/index?sort=kinki",
    6:"/user/index?sort=chugoku_shikoku",
    7:"/user/index?sort=kyusyu_okinawa",
};

html.erbファイルでは下記のように記述するところを

○○.html.erb
<%= link_to '◯◯', ○◯_path(:sort => 'hokkaido') %>

js.erbファイルでのlink_to の書き方がわからなかった為、

○○.js.erb
1:"/user/index?sort=hokkaido"

のように記述しました。こうすることで任意のparams[:sort]のパラメータを渡すことができました。
[?sort=hokkaido] の部分がパラメーターになります。

○○_controller.rb
def index
    if params[:sort] == 'hokkaido'
        @events = Event.where(prefecture_code: "北海道")
        @events = @events.page(params[:page]).per(6).order("id DESC")

上記がコントローラーの一部になります。
[if params[:sort] == 'hokkaido']の記述でパラメーターを区別し表示するイベントを変更しています。

初めての投稿で見にくいところ間違いなどあるかもしれませんが最後まで見ていただきありがとうございました。

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

Rails Tutorial 拡張機能のメッセージ機能を作ってみた(その1):モデルを作成

Rails Tutorialの第14章にある、拡張機能を作る件の続きです。

前回までで返信機能ができました。機能追加の2つ目、メッセージ機能を作ります。

機能の要件を調査

チュートリアルには

Twitterでは、ダイレクトメッセージを行える機能がサポートされています。この機能をサンプルアプリケーションに実装してみましょう

とあるので、Twitterの機能を確認します。

(ヒント: Messageモデルと、新規マイクロポストにマッチする正規表現が必要になるでしょう)。

MessageモデルとはRailsの機能なのか?ネットで調べることにします。

まずはTwitterの機能確認です。

DMを送信できる条件で、相手が自分をフォローしている必要があります。
この機能は後で作ることにします。

既読を表示する機能もありますが、この機能は諦めます。

DMを受け取った際にプッシュ通知/SMS通知(ショートメール通知)/メール通知のいずれかが届くように設定できます。
この機能も諦めます。

また、特定のアカウントからのDMをミュートすることで、通知を受け取らないようにすることも可能です。
この機能も諦めます。

特定のアカウントからのDMを拒否する機能もあります。相手をブロックできます。この機能も諦めます。

受け取ったDMを削除することはできます。この機能は作ることにします。
送信者が送信後のDMを削除することはできません。

DMとpostは違う画面が作られています。

文字数の最大数は違うのか、調べたところ2015年7月までは140文字それ以降は1万文字でした。
今回は140文字にします。

送信先に複数を使えるか調べたところ、グループを作成して複数のアカウントと会話する機能がありました。2015年1月に出来たとのことなので、この機能は諦めます。

Twitterの機能が分かり、今回作る機能もイメージできました。

MessageモデルとはRailsの機能なのか調べましたが、それらしい記事は見つかりませんでした。チャット機能を自分で作る記事や、リアルタイムチャットを作る記事は見つかりましたが関係なさそうです。

機能のまとめ

1.メッセージを送ることができる。
2.送信先は複数ではなく1人だけ。
3.文字数は140文字まで。
4.送信できるか制限はせず、誰にでも送信できる。この制限機能は後で作ることとする。
5.既読の表示機能は作らない。
6.DMを受信時の通知機能は作らない。
7.受信したDMを削除することができる。
8.送信者は送信したDMを削除することができない

モデルの設計

モデルの仕様を作ります。
tutorialのMicropostを作るところを読み直します。13章では最初に13.1でモデルを作っています。
同様にDMのモデルを作ることにします。

列名 属性
id integer
content text
sender_id integer
receiver_id integer
created_at datetime
updated_at datetime

図 DMモデル

ここでreceiverもUserモデルと関係があります。この関係はfollowのmodelを作ったときと同じではと考え、tutorialを読み直します。

user削除時の仕様を検討

relationでは、userを削除するとrelationも削除されます。DMでは削除しない仕様に決めました。この点は違います。

relationでは、unfollowするとrelationも削除されます。DMではDMを受信者が削除しても、そのDMが消えるわけではなく、送信者の画面からは消えません。受信者の画面に表示されなくなるだけです。これはどういうことか考えてみます。DMの削除とは、なかったことにするのではない、送った事実は残るわけです。メールと同じで、受信者が受信済みのメールを削除したときに、送信者の送信済みのメールは削除されないです。Twitterで試してみましたが、確かでした。

メールのように、DMが入ったバケツを考えます。送信者と受信者が別々のバケツを持つモデルです。DMを送ると、送信者のバケツに1通、受信者のバケツに1通を入れます。バケツ2個に全く同じメッセージを入れるというのが重複感があると感じました。

なのでバケツは1個にして、受信者が削除したことが分かるように、削除フラグを追加することにします。

trueかfalseを入れる属性をネットで調べ、booleanがあることと、必ずデフォルト値を入れるべきと分かりました。理由はrubyではnilとfalseが同じ扱いになるためです。
https://qiita.com/jnchito/items/a342b64cd998e5c4ef3d

変更後のモデルです。

列名 属性
id integer
content text
sender_id integer
receiver_id integer
deleted boolean
created_at datetime
updated_at datetime

図 DMモデル

DMモデルの開発

トピックブランチを作ります。

ubuntu:~/environment/sample_app (master) $ git checkout -b create-dm

dmモデルを生成します。

ubuntu:~/environment/sample_app (create-dm) $ rails generate model dm content:text user:references
      create    db/migrate/20201102003220_create_dms.rb
      create    app/models/dm.rb
      create      test/models/dm_test.rb
      create      test/fixtures/dms.ymlrails generate mode dm content:text user:references

マイグレーションを変更します。
インデックスは、senderを指定して時系列で取り出す場合と、receiverを指定して時系列で取り出す場合の2通りが考えられたので作りました。
deletedフラグのnullの扱いは先ほどのネットの記事を参考にしました。

db/migrate/20201102003220_create_dms.rb
class CreateDms < ActiveRecord::Migration[5.1]
  def change
    create_table :dms do |t|
      t.text :content
      t.integer :sender_id
      t.integer :reciever_id
      t.boolean :deleted, default: false, null: false

      t.timestamps
    end
    add_index :dms, [:sender_id, :created_at]
    add_index :dms, [:receiver_id, :created_at]
  end
end

データベースを更新します。

ubuntu:~/environment/sample_app (create-dm) $ rails db:migrate

UserとDMの関連づけをします。
tutorialの14.1.2 「User/Relationshipの関連付け」を読みます。

app/models/user.rb
class User < ApplicationRecord
  has_many      :microposts, dependent: :destroy
  has_many      :active_relationships,class_name:  "Relationship",
                                      foreign_key: "follower_id",
                                      dependent:    :destroy
  has_many      :passive_relationships, class_name: "Relationship",
                                        foreign_key: "followed_id",
                                        dependent:  :destroy
  has_many      :following, through: :active_relationships, source: :followed                    
  has_many      :followers, through: :passive_relationships, source: :follower
  has_many      :sent_dms,class_name: "Dm",
                          foreign_key: "sender_id"
  has_many      :received_dms,class_name: "Dm",
                              foreign_key: "receiver_id"
app/models/dm.rb
class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
end

関連を整理するために、図を書きます。

id name
1 Michael
2 Archer

userモデル

has_many

sender_id receiver_id content
1 2 ...
1 3 ...

dmモデル

has_many

id name
2 Archer
3 ...

userモデル
図 UserとDMの関連

使えるようになるメソッド

使えるようになるメソッドは以下のとおりです。

メソッド 用途
user.sent_dms Userが送ったDMの集合を返す
sent_dms.sender senderを返す
sent_dms.receiver receiverを返す
user.sent_dms.create(receiver_id: other_user.id) userと紐づけてDMを作
user.sent_dms.create!(receiver_id: other_user.id) userと紐づけてDMを作る(失敗時にエラーを出力)
user.sent_dms.build(receiver_id: other_user.id) userと紐づた新しいDMオブジェクトを返す
user.sent_dms.find_by(id:1) userと紐づいていて、idが1のDMを返す

コンソールで試してみます。dm1を作ります。

>> user1 = User.first
>> user2 = User.second
>> dm1 = user1.sent_dms.create(receiver_id: user2.id, content: "hoge dm1")

sender,receiverのUserオブジェクトが返されました。

>> dm1.sender
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-10-26 01:37:04", updated_at: "2020-10-26 01:37:04", password_digest: "$2a$10$2TZtcwmSTCfl9Bigz2nYGO8U1YA8ksfNXUr2O/fSGOY...", remember_digest: nil, admin: true, activation_digest: "$2a$10$EaQUKa6hfGEHosjnICR4VuYMxfOxunTOsPGQYUimNLn...", activated: true, activated_at: "2020-10-26 01:37:03", reset_digest: nil, reset_sent_at: nil, unique_name: "Example">
>> dm1.receiver
=> #<User id: 2, name: "Van Zemlak", email: "example-1@railstutorial.org", created_at: "2020-10-26 01:37:04", updated_at: "2020-10-26 01:37:04", password_digest: "$2a$10$H22BJeNVA3hYdEw/a5RArekRy73q/0AtvidwRiVpoUK...", remember_digest: nil, admin: false, activation_digest: "$2a$10$xm7AJE4Q3fzq3gi5tmVnyeld8wahxMHN/dE2Sn2jSUW...", activated: true, activated_at: "2020-10-26 01:37:04", reset_digest: nil, reset_sent_at: nil, unique_name: "Craig1">

userのDMのリストを検索、dmをidで検索します。

>> user1.sent_dms
  Dm Load (0.2ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."sender_id" = ? LIMIT ?  [["sender_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

>> user1.sent_dms.find_by(receiver_id: 2)
  Dm Load (0.4ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."sender_id" = ? AND "dms"."receiver_id" = ? LIMIT ?  [["sender_id", 1], ["receiver_id", 2], ["LIMIT", 1]]
=> #<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">

自分がrecieverのDMのリストが必要です。followedが同じ構造なので、tutorialを読みます。メソッドはありそうなのでコンソールで試します。

>> user2.received_dms
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."receiver_id" = ? LIMIT ?  [["receiver_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

モデルはできましたが、要件に必要なメソッドが揃ったのかがあいまいと感じました。

Twitterの画面をもう一度見直します。DMの画面は親子の構造になっており、親の画面は過去にやり取りしたことがあるユーザーのリストです。ユーザーを選ぶとDMのやり取りが一覧化された画面です。

必要なメソッドとして、過去にやり取りしたことがあるユーザーのリストを返すメソッドです。
ネットでfindを検索したところ、whereでOR条件が使えると分かりました。
https://qiita.com/nakayuu07/items/3d5e2f8784b6f18186f2
コンソールで試します。

>> Dm.where(sender_id: 1).or(Dm.where(receiver_id: 1))                                                                                         
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE ("dms"."sender_id" = ? OR "dms"."receiver_id" = ?) LIMIT ?  [["sender_id", 1], ["receiver_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>
>> Dm.where(sender_id: 2).or(Dm.where(receiver_id: 2))                                                                                        
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE ("dms"."sender_id" = ? OR "dms"."receiver_id" = ?) LIMIT ?  [["sender_id", 2], ["receiver_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

この検索では親子の画面の機能まではできません。どの相手のDMも全て混ざっています。
ここで、仕様を深堀りし切れていないことが分かりました。
DMの相手ごとに画面を作る機能は、後で作れれば作ることとして進めることにします。

モデルのテスト

モデルのテストを作ります。
DMのバリデーションをMicropostを参考に作ります。tutorialの13.1.2 「Micropostのバリデーション」を読みます。
fixtureのDMのファイルの中はサンプルなので削除します。

test/models/dm_test.rb
class DmTest < ActiveSupport::TestCase

  def setup
    @sender   = users(:michael)
    @receiver = users(:archer)
    @dm = Dm.new(content: "hogehoge1", sender_id: @sender.id, receiver_id: @receiver.id)
  end

  test "should be valid" do
    assert @dm.valid?
  end

  test "sender should be present" do
    @dm.sender_id = nil
    assert_not @dm.valid?
  end

  test "receiver should be present" do
    @dm.receiver_id = nil
    assert_not @dm.valid?
  end

  test "contentr should be present" do
    @dm.content = nil
    assert_not @dm.valid?
  end

  test "contentr should be at most 140 characters" do
    @dm.content = "a" * 141
    assert_not @dm.valid?
  end

end

micropostと同じ用にバリデーションを追加します。テストがGREENになりました。

app/models/dm.rb
class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
  validates :content, presence: true, length: { maximum: 140 }
end

DMを作るときのメソッドを慣習的に正しいやりかたに変更します。

test/models/dm_test.rb
  def setup

    @dm = @sender.sent_dms.build(content: "hogehoge1", receiver_id: @receiver.id)
  end

DMを新しい順に返すようにします。13.1.4 「マイクロポストを改良する」を読みます。
先にテストを書きます。

test/models/dm_test.rb
class DmTest < ActiveSupport::TestCase
...
  test "order should be most recent first" do
    assert_equal sent_dms(:most_recent), Dm.first
  end
end

fixtureに親子関係のデータを作ります。
https://qiita.com/seimiyajun/items/ffefdfc74b9fce76a538
を参考にしました。

test/fixtures/dms.yml
morning:
  content: "Good morning!"
  sender:   michael
  receiver: archer
  created_at:  <%= 10.minutes.ago %>

created_atの順に並ぶように設定します。テストがGREENになりました。

app/models/dm.rb
class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
  default_scope -> { order(created_at: :desc) }
  validates :content, presence: true, length: { maximum: 140 }
end

:destroyを追加するか考えます。
ユーザーが削除されたときには過去のDMは残る仕様に決めていました。
そのテストを追加します。リスト13.20を参考にします。

test/models/user_test.rb GREEN
test "associated dms should not be destroyed" do
    @user.save
    @user.sent_dms.create!(content: "Lorem ipsum", receiver_id: users(:archer).id)
    assert_no_difference 'Dm.count' do
      @user.destroy
    end
  end
end

ここまででモデルはできました。

所要時間

10/31から11/6までの8.5時間です。

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

Rubyでの正規表現における行頭・行末の扱い

Railsでモデルのバリデーションを正規表現で、チェックしようとしたところエラーが起こったので備忘録としてメモしておきます。

環境

Ruby 2.5.1
Rails 5.2

エラーになった実装

user.rb
class User < ApplicationRecord
  validates :age, format: { with: /^[0-9]+$/, message: "は数値のみ入力可能です。"}
end

エラーの内容

The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \A and \z, or forgot to add the :multiline => true option?

エラーの内容としては、^,$はセキュリティのリスクがあるため、\A,\zを使えと言われているようです。
^A
$z
上記の方法ではない場合、:multiline => trueオプションをつけることで脆弱性を含むコードでも、あえてエラーを発生させないとい方法もあります。

調べたところによると、Rails4以降ではセキュリティ対策として、正規表現が厳しくなったようです。

Rubyでは特定の頭と末尾を指定したマッチを行いたい場合は以下のように実装するのが良さそうです。
:multiline => trueオプションをつける方法もありますが、バリデーション処理の場合などは、特別な理由がない限りは、指定しないほうがいいのかなと感じました。

user.rb
class User < ApplicationRecord
  validates :age, format: { with: /\A[0-9]+\z/, message: "は数値のみ入力可能です。"}
end

一応Rubyの公式リファレンスでも以下のように定義されています。

^ 行頭にマッチします。行頭とは、文字列の先頭もしくは改行の次を 意味します。
$ 行末にマッチします。 行末とは文字列の末尾もしくは改行の手前を意味します。
\A 文字列の先頭にマッチします。
\z 文字列の末尾にマッチします。

参考文献

正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう
Rails4では正規表現が厳しくなった。
Ruby 2.7.0 リファレンスマニュアル 正規表現

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

便利❣️Rails開発がスムーズになる機能!

現在私はdocker環境下でRuby on Railsで自社サービスの開発を行っており、
dockerを使用した方ならご理解頂けると思うのですが、、、



_人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人_
> ?docker関連のコマンド反応がめっっっっっっっっっっっちゃ遅い!!!? <
 ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄



特に以下のケースを確認する時に時間がかかるとイライラします。。



  • コントローラに渡すパラメーターの中身を確認したい。

  • 呼び出しているActive Recordの種類。

  • render元のファイル。



とまあいろいろあるのですが、とにかく以下から始まるコードはとにかく遅いです。
(この前migrationファイルを作成するのに約10分かかりました。。)

$ docker-compose run ~



そんな私と同じような環境で開発している、Rails初心者の方へ朗報です!
Rails panellというchromeの拡張機能はご存知でしょうか??



結論から言うとめちゃ便利です。
デバッグ関連でpryとか使用している人もこれは入れて欲しい。
以下概要です!



スクリーンショット 2020-11-08 11.16.24.png
Chrome store link
https://chrome.google.com/webstore/detail/railspanel/gjpfobpafnhjhbajcjgccbbdofdckggg



これを拡張機能に追加すると、検証ツールの一番右側に追加されて、以下のことを簡単に確認できます!
(上記画像参照。)

  • Breakdown: 処理時間の内訳(ActiveRecord, Rendering, Other)

  • Params: コントローラから参照できるparamsの内容

  • ActiveRecord: そのリクエストを処理する際に発行したSQLと処理時間

  • Rendering: ビューテンプレートごとの描画時間

(参考元:https://chopschips.net/blog/2015/03/06/rails-panel/)


なのでいちいちコンソールを立ち上げたり、pryで止めたりせずとも簡単に確認できちゃいます!
これでデバッグに係る時間を少しでも削減できますね♫


今回初投稿となり拙い文章で間違っている箇所もあるかと思いますが、
もしここまで読んでくれた方がいたらとても嬉しいです!


もっと便利な拡張機能あるよとかあれば、是非コメントで教えてください?‍♂️
以上初心者エンジニアでした!

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

【Rails/AWS】RDSのMySQLに繋がらないエラーの考えられる原因(database.yml)

はじめに

本記事は、RailsアプリをAWSにデプロイした際に発生する可能性のある
MySQLに繋がらないという事象に対する原因例を紹介します。
原因はかなり初歩的な原因でしたが、筆者はこのエラー原因が特定できずとても苦労したため、
今後同じエラーに遭遇した方の助けになれば幸いです。

開発環境

  • Ruby 2.5.1
  • Rails 5.2.4.4
  • AWS(EC2, RDS)
  • MySQL(RDS) 5.6.48

前提条件

  • RailsアプリをEC2のWebサーバー上にgitクローン済み。
  • 基本的な設定は完了済みで、rake db:create RAILS_ENV=productionのコマンドを実行する手前の状態。
  • RDSのDBインスタンスを作成済み。
  • Railsのdatabase.ymlは下記の内容です。
database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock

production:
  <<: *default
  database: データベース名
  host: <%= ENV['DATABASE_HOST_PRODUCTION'] %>
  username: <%= ENV['DATABASE_USER_NAME_PRODUCTION'] %>
  password: <%= ENV['DATABASE_PASSWORD_PRODUCTION'] %>

エラー内容

以下のコマンドだとRDSのMySQLに繋がったのですが・・・・

[ec2-user@ip-10-0-1-10 アプリのディレクトリ]$ mysql -h RDSのエンドポイント -u root -P 3306 -p

以下のコマンドだと繋がらず、MySQLに繋がらないというエラーが出ました。

[ec2-user@ip-10-0-1-10 アプリのディレクトリ]$ rake db:create RAILS_ENV=production
Can't connect to MySQL server on '10.0.1.10' (111)
Couldn't create 'データベース名' database. Please check your configuration.
rake aborted!
Mysql2::Error::ConnectionError: Can't connect to MySQL server on '10.0.1.10' (111)

Tasks: TOP => db:create
(See full trace by running task with --trace)

原因/解決策

当初はホスト名の設定ができていなかったので、エンドポイントを環境変数に入れて修正したのですが、
mysqlコマンドだと繋がるのにrakeコマンドだと繋がらないという謎の現象が発生しました。
いろいろ調べたところ、MySQLのパスワードにパスワードに"#"が入っていることが原因でした。
YAMLファイルはコメントアウトの記法が#ということで、パスワードに"#"が入っているとコメントされてしまいます。
パスワードを変更したら無事rakeコマンドが通りました。
みなさんお気を付けください。

まとめ

MySQLのパスワードに"#'を含めるのは良くない。

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

【RSpec】Shoulda-Matchers で独自に設定したエラーメッセージをテストする方法

shoulda-matchersを使ってカスタムメッセージのバリデーションテストをするとき、書き方がわからなくて少し詰まったのでここに共有させて頂きます。

shoulda-matchersとは何ぞや?

shoulda-matchersというのは、通常のRSpecで書くと長くなるテストが簡潔にかけるようになるgemです。

例えば以下のようなバリデーションを持ったモデルがあったとします。

user.rb
class User < ApplicationRecord
  validates :nickname, presence: true, length: { maximum: 30 }
end

このテストが以下のように一行でかけます。

spec/models/user_spec.rb
it { is_expected.to validate_length_of(:nickname).is_at_most(30) }

なかなかに便利ですね。

独自に設定したエラーメッセージをテストしたい場合

例えば以下のような、独自のエラーメッセージを持ったバリデーションを設定したとします。

user.rb
validates :email, presence: { message: 'が入っていません' }

バリデーションと同時に、独自のエラーメッセージが表示されているかということもチェックしたい場合

spec/models/user_spec.rb
it { should validate_presence_of(:email). with_message('が入っていません') }

このように書くことで独自のエラーメッセージとバリデーションを同時にチェックすることができます。

他にも使い方が色々あるので、気になる方はこちらを一読してみてください。
導入方法も書いてあります。
Shoulda-Matchers READ ME

最後まで読んでいただきありがとうございます!

日々学んだことをアプトプットしてます!なにかご指摘などあればコメントいただけますと嬉しいです!

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

ruby小文字を大文字にする

Rubyで小文字を大文字にする

半角アルファベットの小文字で構成された長さ t の文字列 y が与えられます。

文字列 y を大文字に変換して出力して下さい。

入力される値

入力は以下のフォーマットで与えられます。

y

期待する出力

y を大文字に変換した文字列を出力してください。

入力例1

qiita

出力例1

QIITA

私の答え

y = gets
puts y.upcase

今回のポイント

1行目のgetsで文字列を取得しています

2行目で取得した文字列yをupcaseメソッドで出力しています

upcaseメソッドは小文字→大文字にするメソッドです。ちなみにその逆はdowncaseメソッドです。

y = "AAAAA"
puts y.downcase
出力結果
▶︎ AAAAA

となります。

他にも小文字のみや大文字のみを逆にしてくれるswapcaseメソッドや先頭の大文字を小文字に変換してくれるcapitalizeメソッドがあります。

以上!

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

Rails Asset Pipeline 基礎のキ

Asset Pipelineとは

JavaScriptやCSSのアセットを最小化 (minify: スペースや改行を詰めるなど) または圧縮して連結するためのフレームワーク
参照: Railsガイド -AssetPipelineについて-
https://railsguides.jp/asset_pipeline.html

この記事は、AssetPipelineの概要を説明しておくことで、RailsガイドのAssetPipeline記事の理解度を深めるためのものです。

そもそもcoffeescript、sass、erbなどはプログラムを書く・読む人側にとって生のjs/cssよりも簡単に書けるので嬉しいが、実行するマシン側にとっては読み取りづらいもの。これらcoffeescript, sassなどは高級言語と呼ばれるが、これらを .js.coffee→.jsに、 .css.sass→.css に自動でコンパイルとminify(縮小)してくれるのが、Asset Pipelineの役目。

ただし、コンパイルは、開発(development)環境のみで行われる。開発環境では、debuggerなどでデバッグをする以上、出来る限り、開発中のjs/cssを保持していた方がプログラムを書く・読む人側にとっては助かるからである。

ここでのコンパイルの定義は、プリコンパイルを含まないものとします。後述。

では、本番環境では何が行われているか?
本番環境では

1: プリコンパイルファイル群の存在
2: 1のファイル群を1枚のjsファイル、1枚のcssファイルに統合
3: 圧縮
4: ダイジェスト付与

を行う。この最初に出てきたプリコンパイルファイルについてだが、そもそも本番環境でもコンパイルを行ってしまうと、アプリケーションの実行速度に大きく影響されてしまう。
そのため、予めコンパイルされたファイル群を持たせておく(プリコンパイルする)ことで本番環境での実行速度を保とう、ということだ。

開発環境と、本番環境の流れを図で表すと、以下のようになる。左上から始まり、開発(development)環境は途中で出力されるが、本番環境は最後の右下に到達する。
スクリーンショット 2020-11-07 23.45.58.png

上記の内容がRailsガイドの理解に深まることを願う。
https://railsguides.jp/asset_pipeline.html

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