20200116のRubyに関する記事は17件です。

ログイン機能の実装(ウィザード形式にするために)

1.deviseの導入及びユーザーのデフォルトでのログインが可能になるまで

以下の手順で導入

GEMFILE.
gem 'devise'
ターミナル.
rails g devise:install

任意のコントローラー(***)を用意して、ルーティングの設定をします。
この場合は、トップページに当たる部分のルーティングの設定をしています。

config/routes.rb
Rails.application.routes.draw do
  root to: "***#index"
end
ターミナル.
rails g controller ***

今回はindexアクションにおいて、特にモデルとのやり取りなどは行わないので、コントローラ内の記述はしません。(任意でトップページのビューは作成したものを使用)

次いでdeviseにおけるデフォルトのログイン機能を実装します。

ターミナル.
rails g devise user

今回は、deviseのデフォルトで用意されているemailとpasswordを最初のビュー場で登録させてから、次のページにてuserのprofileを登録するようにしたいと思います。
その為、そのまま下記のコマンドを実行していきます。

ターミナル.
rails db:migrate

※ただし、今回とは異なりuserモデルの方に名前等を追加したい場合には、下記のコマンド実行前にマイグレーションファイルの編集をしてカラムの追加の必要性が発生します。
その場合には下記のようにコントローラー及びモデルのバリデーションの記述を一部追加する必要性がある

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:追加カラム名, :追加カラム名])
  end
end
app/models/user.rb
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :追加カラム名, :追加カラム名 ,presence: true
end

上記を任意の状況に応じて実行後、追加したカラムを入力できるように、新規登録画面のビューを編集する必要があります。デフォルトではdeviseのビューファイルは隠れているので、以下のコマンドを実行します。

ターミナル.
rails g devise:views
app/views/devise/registrations/new.html.erb
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :追加カラム名 %><br />
    <%= f.text_field :追加カラム名 %>
  </div>

  <div class="field">
    <%= f.label :追加カラム名 %><br />
    <%= f.number_field :追加カラム名 %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

<%# 以下省略 %>

※追加するカラムがない場合にはrails db:migrateを実行後にここまでの操作を省略して、以下から続きを実行
任意のビューに下記の記述を追加してログインしていない場合には新規登録またはログインを実行させる画面に移行させる記述を追記していきます。

app/views/***/index.html.erb
<% if user_signed_in?%>
  <h2>ログインしています</h2>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% else %>
  <h2>ログインしていません</h2>
  <%= link_to "新規登録", new_user_registration_path %>
  <%= link_to "ログイン", new_user_session_path %>
<% end %>

2.ウィザード形式でのユーザーの新規登録を実行出来るようにする

今回やっていくこととしては、ユーザー登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。そして次のプロフィール情報を登録する画面で名前や年齢、性別を入力させ、最後のステップでsessionに保持していたユーザー情報と、それに関連するプロフィール情報をテーブルに保存する流れで行なっていきます。
まず、まだ作成していないプロフィールモデルの作成を実行します。

ターミナル.
rails g model profile

次いで、Userモデルとのリレーションのために、外部キーとしてuser_idが入るようにして、コマンドを実行します。

db/migrate/20XXXXX.rb
class CreateProfiles < ActiveRecord::Migration[5.2]
  def change
    create_table :profiles do |t|
      t.string :name, null: false
      t.integer :age, null: false
      t.string :gender, null: false
      t.references :user
      t.timestamps
    end
  end
end
ターミナル.
rails db:migrate

マイグレートを実行後、テーブルやカラム作成されているか確認して、モデルのバリデーションおよびアソシエーションを設定します。
Userモデルに対してoptional: trueを設けています。optional: trueは外部キーがnullであることを許可するオプションです。
同様にUserモデルについてもアソシエーションを設定します。

app/models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user, optional: true
  validates :name, :age, :gender ,presence: true
end
app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_one :profile
end

次いでdeviseのコントローラを作成し、編集できる形にします。

ターミナル.
rails g devise:controllers users

現状rake routesを行うとdevise管理化のコントローラーが呼ばれてしまっているということが確認できます。
そこで以下のようにroutes.rbを編集して、どのコントローラを参照するのか明示してあげます。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
  }
  root to: "***#index"
end

編集後に再度rake routesをすると、参照するコントローラが変更されていることが確認できます。(今回はユーザ新規登録に必要なregistrationsコントローラのみに適用)

そしてここからの流れを再度確認すると
●userモデルのインスタンスの作成
●バリデーションチェックしたユーザー情報をsessionに保持してprofileモデルのインスタンスの生成
●バリデーションチェックしたバリデーションチェックしたプロフィール情報とsessionで保持していたユーザー情報を保存
という流れで実装していく必要があります。

userモデルのインスタンスの作成

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
# 省略
  def new
    @user = User.new
  end
# 省略
end

newアクションに対応するフォーム形成

app/views/devise/registrations/new.html.erb
<h2>ユーザー情報登録</h2>

<%= form_for(@user, url: user_registration_path) do |f| %>
  <%= render "devise/shared/error_messages", resource: @user %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Next" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

上記で元のところからの変更点は、resourceという文字列部分(このdeviseのログイン機能で実装されるモデルが入るresourceとは、仮引数のようなもので実装する状況に応じたモデル名に変更する必要がありそうです)やボタンをnextにするなど細かい部分を修正しています。
変更後にビューが反映されているかを確認します。
確認後はユーザー登録フォーム上で"Next"をクリックすると、次の情報を登録するページに遷移しますが、その前にcreateアクション内で追記する必要があるので下記のように編集を実施します。

createアクションの編集

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]

# 省略

   def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @profile = @user.build_profile
    render :new_profiles
  end

# 省略

  protected

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  end

end

上記の編集によってどのようになったかを項目ごとに確認していきます。

1ページ目で入力した情報のバリデーションチェック

まず、Userモデルのインスタンスを生成し、最初の画面から送られてきたパラメータをインスタンス変数@userに代入します。そのインスタンス変数に対してvalid?メソッドを適用することで送られてきたパラメータが指定されたバリデーションに違反しないかどうかチェックしています。falseになった場合は、エラーメッセージとともにnewアクションへrenderさせます。

1ページで入力した情報をsessionに保持させること

次いで入力した情報をsessionに保持させます。
今回のようにウィザード形式にする場合には最後のページまで遷移した後に保存するというようにするために、今回はsessionという機能を用います。
バリデーションチェックが完了したら、session["devise.regist_data"]に値を代入します。この時、sessionにハッシュオブジェクトの形で情報を保持させるために、attributesメソッドを用いてデータを整形しています。また、paramsの中にはパスワードの情報は含まれていますが、attributesメソッドでデータ整形をした際にパスワードの情報は含まれていません。そこで、パスワードを再度sessionに代入する必要があります。

次画面にてプロフィール情報登録で使用するインスタンスを生成、当該ページへ遷移すること

次画面では、このユーザーモデルに紐づくプロフィール情報を入力させるため、該当するインスタンスを生成しておく必要があります。そのために、build_profileで今回生成したインスタンス@userに紐づくProfileモデルのインスタンスを生成します。ここで生成したProfileモデルのインスタンスは、@profileというインスタンス変数に代入します。そして、プロフィール情を登録させるページを表示するnew_profileアクションのビューへrenderしています。

次にnew_profileアクションとcreate_profileアクションのルーティングを設定します。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  devise_scope :user do
    get 'profiles', to: 'users/registrations#new_profile'
    post 'profiles', to: 'users/registrations#create_profile'
  end
  root to: "***#index"
end
app/views/devise/registrations/new_profile.html.erb
<h2>プロフィール情報登録</h2>

<%= form_for @profile do |f| %>
  <%= render "devise/shared/error_messages", resource: @profile %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :age %><br />
    <%= f.text_field :age %>
  </div>

  <div class="field">
    <%= f.label :gender %><br />
    <%= f.text_field :gender %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

ルーティング先のビューも下記のように作成して、画面遷移の確認をします。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  devise_scope :user do
    get 'profiles', to: 'users/registrations#new_profile'
    post 'profiles', to: 'users/registrations#create_profile'
  end
  root to: "***#index"
end

最後にcreate_profileアクションで、ユーザー情報とプロフィール情報全てをテーブルに保存するように実装します。

create_profileアクションの再編集
app/controllers/users/registrations_controller.rb
#省略

  def create_profile
    @user = User.new(session["devise.regist_data"]["user"])
    @profile = Profile.new(profile_params)
    unless @profile.valid?
      flash.now[:alert] = @profile.errors.full_messages
      render :new_profile and return
    end
    @user.build_profile(@profile.attributes)
    @user.save
    sign_in(:user, @user)
  end

#省略

  protected

  def profile_params
    params.require(:profile).permit(:name, :age, :gender)
  end

上記の編集によってどのようになったかを項目ごとに確認していきます。

2ページ目で入力したプロフィール情報のバリデーションチェック

最初の入力画面でのcreateアクションと同様に、valid?メソッドを用いて、バリデーションチェックを行います。

バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること

build_profileを用いて送られてきたparamsを、保持していたsessionが含まれる@userに代入します。そしてsaveメソッドを用いてテーブルに保存します。

ログインをすること

ユーザーの新規登録ができても、ログインができているわけではありません。それをsign_inメソッドを利用してログイン作業を行いましょう。
上記が完了したらprofile_createに対応するビューを作成します。

app/views/devise/registrations/create_profile.html.erb
<h2>登録が完了しました</h2>
<%= link_to "トップへ戻る", root_path%>
sessionを削除すること

このようにすることでユーザー登録画面をウィザード形式にすることが可能となります。
次の記事ではSNS認証をこちらに追加していきたいと思います。

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

【RSpec】同じパターンのテストシナリオをJSONで定義して外出しする

解決できるケース

以下のようなケースに適用できると思います。
あくまで飛び道具的な手法なので、常用は良いと思いません。

  • 同じ機能を複数のパラメータパターンで実行して結果を評価する場合、実装を軽量化できる。
  • JSONファイルにパラメータ部分のみを外出しするため、RSpecを書けない人でもテストケースのメンテナンスが可能

経緯

ある案件で給与計算モジュールを実装した際に、深夜・残業・休日の手当や、勤務時間に関するモジュールなどを実装し、それぞれテストケースを作りました。
ただ、それだと心もとないため、給与計算モジュール群の統合テストケースを実装することとなりました。

例えば、
「時給¥1000で10:00~15:00勤務」「時給¥1001円で18:00~24:00勤務」・・・など
一定のパラメータに複数パターンを入力して結果を評価するテストケースが必要になりました。
そこで、ケースはJSONファイルに書き出しておき、テストの実装はループでやってみようということで、以下のようなRSpecを作成しました。

実装

contextをtest_caseの数分ループすることで、同じようなテストケースをずらずら書き並べる必要がなくなっています。
※内容は例なので簡略化しています。

Rspec

  # 給与計算パターンの検証
  describe 'calculate salary' do

    # テストケースJSONファイルをロード
    test_cases = JSON.parse(IO.read(Rails.root.join("spec/json/salary_calculator_test_case.json")), symbolize_names: true)

    # テストケースの数分、テストを実行する
    test_cases.each do |test_case|

      # [:execution]がfalseのテストケースは無視する
      next unless test_case[:execution]

      # [:pattern]に設定してあるケース名を引用
      context "case [#{test_case[:pattern]}]" do

        # 勤務時間
        let(:worktimes) { test_case.key?(:worktimes) ? test_case[:worktimes] : [] }
        # 休憩時間
        let(:resttimes) { test_case.key?(:resttimes) ? test_case[:resttimes] : [] }
        # 給与
        let(:hourly_wage) { test_case.key?(:hourly_wage) ? test_case[:hourly_wage] : 1000 }

        # 計算処理の実行結果を評価対象とする
        subject { execute(worktimes, resttimes, amount)}

        # テスト結果の評価
        it 'calculate correctly ' do
            expect(subject).to eq test_case[:expect]
        end
      end
    end
  end

テストケースJSON

[
    {
        "pattern": "work 10:00-15:00/rest 12:00-13:00/hourly_wage ¥1000",
        "worktimes": [
            { "start": "2020-01-06 10:00:00", "end": "2020-01-06 15:00:00" }
        ],
        "resttimes": [
            { "start": "2020-01-06 12:00:00", "end": "2020-01-06 13:00:00" }
        ],
        "hourly_wage": 1000,
        "expect": 4000,
        "execution": true,
        "comment": "通常"
    },
    {
        "pattern": "work 18:00-24:00/rest 20:00-21:00/hourly_wage ¥1001",
        "worktimes": [
            { "start": "2020-01-06 18:00:00", "end": "2020-01-07 00:00:00" }
        ],
        "resttimes": [
            { "start": "2020-01-06 20:00:00", "end": "2020-01-06 21:00:00" }
        ],
        "hourly_wage": 1001,
        "expect": 5501
        "execution": true,
        "comment": "深夜手当あり"
    },
    {
        ...
    }
    ...
]

JSONのテストケース中には、以下の項目を含んでおくと便利でした。

  • "execution": テストケースを実行するかどうか。1ケースだけテストしたい時に他を全てfalseにして無駄な時間を食わずに済む。
  • "comment": テストでは使用しないが、メンテナンス時にわかりやすくなる。テストケースを定義した資料に記載されているケース名を転記して、わかりやすくしたり。

あとがき

ご参考になれば幸いです。

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

macOSをCatalinaにしたらRubyとRailsが消えた時【zsh: /usr/local/bin/rails: bad interpreter: System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby: no such file or directory】の対処

Catalinaにした後Rubymineのプラグインを挿入後の再起動でRailsが消えた

エディターでRubymineを使用しているのですが、プラグインを挿入し再起動、%rails sなどのコマンドをしても、『Rails is not currently installed on this system. To get the latest version, simply type: $ sudo gem install rails』とエラーが発生。

rbenvのパスの設定が変わったと想定

ここら辺の環境設定ではプログラミング初心者はとても頭を悩ます分野。
プログラミングスクールのDive Into Codeに通っていた頃、メンターの皆様によく教わりました。
「bash~が〜」とか「システムでのRubyとローカルは違う〜」「rbenvのpathは〜」
色々教わりましたが、Rubyの構文やRailsの仕組み、そもそもブラウザの仕組みやHTMLやCSSを働きながら多方面の分野を勉強する為、環境構築はとても苦手でした。

極論:教授して頂いたコードをコピペ

ここでは一応腹落ちするまで教えて頂きましたが、30分ググったりして悩んだ場合は素直に教えてもらい先に進んだ方が良い。ただ自分のこの調べる力は、後々のエンジニアとしての『再現性』を豊にするの実感。
エンジニアの8~9割りはエラーやバグの解決に時間を要する為、プログラミング初心者は解決できなくともそこで挫折しないで良い経験だとして、力として欲しい。

「~/.zshrc」ファイルにrbenvのpath設定をしてあげる

話題を本題に戻します。
%which ruby をコマンドした場合、
/usr/bin/ruby の表示。
このままだと、いくら%rails sなどのRailsコマンドをしても、
【zsh: /usr/local/bin/rails: bad interpreter: /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby: no such file or directory
Rails is not currently installed on this system. To get the latest version, simply type:】のエラー表示が出る。
その後には、【 $ sudo gem install rails】とアドバイスされる。

結果論: $ sudo gem install railsのコマンドは要らない

元々railsのgemはインストールされている中で、pathが違った為のエラー。
なので、pathを通す為にコマンドしてあげれば良い。

%which rubyで【/Users/ユーザー名/.rbenv/shims/ruby】になるまで

https://teratail.com/questions/218281 このサイトを参考にしました。
ただここでは一時的な解決にしかならない為、エディターを再起動したり、違うターミナル開けた場合、同じエラーになりました。

対処法の結論

%echo 'export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"' >> ~/.zshenv
%echo 'eval "$(rbenv init -)"' >> ~/.zshenv
%echo 'source $HOME/.zshenv' >> ~/.zshrc
exec $SHELL
source $HOME/.zshenv

%Which ruby
/Users/ユーザー名/.rbenv/shims/ruby

%which rails
/Users/ユーザー名/.rbenv/shims/rails

もうOSのアップデートの度に、びっくりしなくて済みます。

参考にしたサイト

https://medium.com/@petehanner/getting-rails-to-work-with-catalina-zsh-84146e1d2099
備忘録としてコマンドだけをツイートしたのも残しておきます。
https://twitter.com/ARTS_papa/status/1217754991819030529

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

Homebrew + rbenv + Bundler + dockerで開発環境をマネージドに構築する(番外編-Ruby on Railsアプリ)

後編〜からの続き

前提

  • 便宜上、既存のワーキングコピーがあります
  • ただし、 Dockerfiledocker-compose.yml は後編からガラッと変わります

Docker手順

Dockerfileのコマンド

コマンド 説明
FROM 使用するイメージとバージョン
RUN コマンドの実行。railsに必要な必要なnodejsとpostgeqsqlをインストールしている
WORKDIR そのままの意味。作業ディレクトリを設定します。
ADD ローカルのファイルをコンテナへコピーする(昔のCOPYコマンドになります)
ENTRYPOINT 一番最初に実行するコマンド(ここではentrypoint.shを参照)
EXPOSE コンテナがリッスンするport番号
CMD イメージ内部のソフトウェア実行(つまりRailsのことですね)
1. Dockerfileを作成する
Dockerfile
FROM ruby:2.6.5

# Setting environment
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo
ENV APP_HOME /var/www/capistrano_sample_app_v1

# Install libraries
RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs \           
                       vim \
                       default-mysql-client
RUN gem install bundler -v '2.1.4'

# Create app home
RUN mkdir -p $APP_HOME

WORKDIR $APP_HOME

# Copy Gemfile from origin
ADD Gemfile $APP_HOME/Gemfile

RUN bundle _2.1.4_ install --path vendor/bundle

ADD . $APP_HOME
2. docker-composeを作成する
docker-compose.yml
version: '3'
services:
  mysql:
    # https://dev.mysql.com/doc/relnotes/mysql/5.7/en/
    image: mysql:5.7
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      MYSQL_DATABASE: capistrano_sample
      MYSQL_USER: developer
      MYSQL_PASSWORD: %masking%
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    command: mysqld --innodb_file_per_table=1 --innodb_file_format=barracuda --innodb_large_prefix=1
  redis:
    # https://github.com/RedisLabs/docker-library-redis
    image: redis:3.2-alpine
  # memcached:
    # https://github.com/autopilotpattern/memcached/releases
    # image: memcached:1.4-alpine
  app:
    build:
      context: .
      dockerfile: "Dockerfile"
    tty: true
    stdin_open: true
    ports:
      - "8080:8080"
    # environment:
      # RAILS_LOG_TO_STDOUT: "true"
      # STACKDRIVER_LOGGING_MODE: "agent"
    # command: /bin/sh -c "rm -f /capistrano_sample_app_v1/tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"
    volumes:
      # ホストのカレントディレクトリをコンテナの/appにマウント
      - "./:/app"
    links:
      - mysql
      - redis
      # - memcached
3.docker build
Terminal
$ docker-compose up --build
Creating network "capistrano_sample_app_v1_default" with the default driver
Pulling mysql (mysql:5.7)...
5.7: Pulling from library/mysql
804555ee0376: Pull complete
c53bab458734: Pull complete
ca9d72777f90: Pull complete
2d7aad6cb96e: Pull complete
8d6ca35c7908: Pull complete
6ddae009e760: Pull complete
327ae67bbe7b: Pull complete
9e05241b7707: Pull complete
e822978df8f0: Pull complete
14ca71ed53be: Pull complete
026afe6fd35e: Pull complete
Digest: sha256:2ca675966612f34b4036bbcfa68cb049c03e34b561fba0f88954b03931823d29
Status: Downloaded newer image for mysql:5.7
Pulling redis (redis:3.2-alpine)...
3.2-alpine: Pulling from library/redis
4fe2ade4980c: Pull complete
fb758dc2e038: Pull complete
989f7b0c858b: Pull complete
42b4b9f869ad: Pull complete
17e06138ef20: Pull complete
c0ecd66db81e: Pull complete
Digest: sha256:e9083e10f5f81d350a3f687d582aefd06e114890b03e7f08a447fa1a1f66d967
Status: Downloaded newer image for redis:3.2-alpine
Building app
Step 1/11 : FROM ruby:2.6.5
 ---> a161c3e3dda8
Step 2/11 : ENV LANG C.UTF-8
 ---> Using cache
 ---> 5e1f7a284c55
Step 3/11 : ENV TZ Asia/Tokyo
 ---> Using cache
 ---> 36064309c74a
Step 4/11 : ENV APP_HOME /var/www/capistrano_sample_app_v1
 ---> Using cache
 ---> b6ad93523f4f
Step 5/11 : RUN apt-get update -qq &&     apt-get install -y build-essential                        libpq-dev                        nodejs                        vim                        default-mysql-client
 ---> Using cache
 ---> 27f7958c4205
Step 6/11 : RUN gem install bundler -v '2.1.4'
 ---> Using cache
 ---> 2152e68a959d
Step 7/11 : RUN mkdir -p $APP_HOME
 ---> Using cache
 ---> 34bf7f30bbcb
Step 8/11 : WORKDIR $APP_HOME
 ---> Using cache
 ---> 1be2dad77d77
Step 9/11 : ADD Gemfile $APP_HOME/Gemfile
 ---> Using cache
 ---> f2549fe43d36
Step 10/11 : RUN bundle _2.1.4_ install --path vendor/bundle
 ---> Using cache
 ---> 55ba3d1dbf0c
Step 11/11 : ADD . $APP_HOME
 ---> 4d459e27e55e
Successfully built 4d459e27e55e
Successfully tagged capistrano_sample_app_v1_app:latest
Creating capistrano_sample_app_v1_mysql_1 ... done
Creating capistrano_sample_app_v1_redis_1 ... done
Creating capistrano_sample_app_v1_app_1   ... done
Attaching to capistrano_sample_app_v1_redis_1, capistrano_sample_app_v1_mysql_1, capistrano_sample_app_v1_app_1
mysql_1  | 2020-01-16 09:55:41+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.29-1debian9 started.
redis_1  | 1:C 16 Jan 09:55:40.955 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
mysql_1  | 2020-01-16 09:55:45+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
redis_1  |                 _._                                                  
redis_1  |            _.-``__ ''-._                                             
redis_1  |       _.-``    `.  `_.  ''-._           Redis 3.2.12 (00000000/0) 64 bit
redis_1  |   .-`` .-```.  ```\/    _.,_ ''-._                                   
redis_1  |  (    '      ,       .-`  | `,    )     Running in standalone mode
redis_1  |  |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
redis_1  |  |    `-._   `._    /     _.-'    |     PID: 1
redis_1  |   `-._    `-._  `-./  _.-'    _.-'                                   
redis_1  |  |`-._`-._    `-.__.-'    _.-'_.-'|                                  
redis_1  |  |    `-._`-._        _.-'_.-'    |           http://redis.io        
redis_1  |   `-._    `-._`-.__.-'_.-'    _.-'                                   
redis_1  |  |`-._`-._    `-.__.-'    _.-'_.-'|                                  
redis_1  |  |    `-._`-._        _.-'_.-'    |                                  
redis_1  |   `-._    `-._`-.__.-'_.-'    _.-'                                   
redis_1  |       `-._    `-.__.-'    _.-'                                       
redis_1  |           `-._        _.-'                                           
redis_1  |               `-.__.-'                                               
redis_1  | 
redis_1  | 1:M 16 Jan 09:55:40.961 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1  | 1:M 16 Jan 09:55:40.961 # Server started, Redis version 3.2.12
redis_1  | 1:M 16 Jan 09:55:40.961 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis_1  | 1:M 16 Jan 09:55:40.961 * The server is now ready to accept connections on port 6379
mysql_1  | 2020-01-16 09:55:45+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.29-1debian9 started.
mysql_1  | 2020-01-16 09:55:45+00:00 [Note] [Entrypoint]: Initializing database files
mysql_1  | 2020-01-16T09:55:45.597324Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
mysql_1  | 2020-01-16T09:55:45.635369Z 0 [Warning] InnoDB: Using innodb_file_format is deprecated and the parameter may be removed in future releases. See http://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html
mysql_1  | 2020-01-16T09:55:48.077398Z 0 [Warning] InnoDB: New log files created, LSN=45790
mysql_1  | 2020-01-16T09:55:48.646754Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
mysql_1  | 2020-01-16T09:55:48.952749Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 5f8348e1-3846-11ea-8ab5-0242ac130003.
mysql_1  | 2020-01-16T09:55:49.020134Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
mysql_1  | 2020-01-16T09:55:51.294446Z 0 [Warning] CA certificate ca.pem is self signed.
app_1    | from /var/www/capistrano_sample_app_v1/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap.rb:22:in `setup': The 'disable_trace' method is not allowed with this Ruby version. current: 2.6.5, allowed version: < 2.5.0
mysql_1  | 2020-01-16T09:55:51.879252Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
app_1    | => Booting Puma
app_1    | => Rails 5.2.4.1 application starting in development 
app_1    | => Run `rails server -h` for more startup options
app_1    | Puma starting in single mode...
app_1    | * Version 3.12.2 (ruby 2.6.5-p114), codename: Llamas in Pajamas
app_1    | * Min threads: 5, max threads: 5
app_1    | * Environment: development
app_1    | * Listening on tcp://0.0.0.0:8080
app_1    | Use Ctrl-C to stop
mysql_1  | 2020-01-16 09:55:58+00:00 [Note] [Entrypoint]: Database files initialized
mysql_1  | 2020-01-16 09:55:58+00:00 [Note] [Entrypoint]: Starting temporary server
mysql_1  | 2020-01-16 09:55:58+00:00 [Note] [Entrypoint]: Waiting for server startup
mysql_1  | 2020-01-16T09:55:58.626438Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
mysql_1  | 2020-01-16T09:55:58.627939Z 0 [Note] mysqld (mysqld 5.7.29) starting as process 80 ...
mysql_1  | 2020-01-16T09:55:58.631949Z 0 [Warning] InnoDB: Using innodb_file_format is deprecated and the parameter may be removed in future releases. See http://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html
mysql_1  | 2020-01-16T09:55:58.632065Z 0 [Note] InnoDB: PUNCH HOLE support available
mysql_1  | 2020-01-16T09:55:58.632232Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
mysql_1  | 2020-01-16T09:55:58.632267Z 0 [Note] InnoDB: Uses event mutexes
mysql_1  | 2020-01-16T09:55:58.632275Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier
mysql_1  | 2020-01-16T09:55:58.632281Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11
mysql_1  | 2020-01-16T09:55:58.632286Z 0 [Note] InnoDB: Using Linux native AIO
mysql_1  | 2020-01-16T09:55:58.632901Z 0 [Note] InnoDB: Number of pools: 1
mysql_1  | 2020-01-16T09:55:58.633235Z 0 [Note] InnoDB: Using CPU crc32 instructions
mysql_1  | 2020-01-16T09:55:58.634985Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M
mysql_1  | 2020-01-16T09:55:58.644127Z 0 [Note] InnoDB: Completed initialization of buffer pool
mysql_1  | 2020-01-16T09:55:58.646140Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority().
mysql_1  | 2020-01-16T09:55:58.660112Z 0 [Note] InnoDB: Highest supported file format is Barracuda.
mysql_1  | 2020-01-16T09:55:58.702341Z 0 [Note] InnoDB: Creating shared tablespace for temporary tables
mysql_1  | 2020-01-16T09:55:58.702446Z 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ...
mysql_1  | 2020-01-16T09:55:58.916889Z 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB.
mysql_1  | 2020-01-16T09:55:58.917944Z 0 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active.
mysql_1  | 2020-01-16T09:55:58.917995Z 0 [Note] InnoDB: 32 non-redo rollback segment(s) are active.
mysql_1  | 2020-01-16T09:55:58.919329Z 0 [Note] InnoDB: 5.7.29 started; log sequence number 2629932
mysql_1  | 2020-01-16T09:55:58.919668Z 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool
mysql_1  | 2020-01-16T09:55:58.920063Z 0 [Note] Plugin 'FEDERATED' is disabled.
mysql_1  | 2020-01-16T09:55:58.922411Z 0 [Note] InnoDB: Buffer pool(s) load completed at 200116  9:55:58
mysql_1  | 2020-01-16T09:55:58.926524Z 0 [Note] Found ca.pem, server-cert.pem and server-key.pem in data directory. Trying to enable SSL support using them.
mysql_1  | 2020-01-16T09:55:58.926589Z 0 [Note] Skipping generation of SSL certificates as certificate files are present in data directory.
mysql_1  | 2020-01-16T09:55:58.927349Z 0 [Warning] CA certificate ca.pem is self signed.
mysql_1  | 2020-01-16T09:55:58.927419Z 0 [Note] Skipping generation of RSA key pair as key files are present in data directory.
mysql_1  | 2020-01-16T09:55:58.929692Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_1  | 2020-01-16T09:55:58.938200Z 0 [Note] Event Scheduler: Loaded 0 events
mysql_1  | 2020-01-16T09:55:58.938729Z 0 [Note] mysqld: ready for connections.
mysql_1  | Version: '5.7.29'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server (GPL)
mysql_1  | 2020-01-16 09:55:59+00:00 [Note] [Entrypoint]: Temporary server started.
mysql_1  | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
mysql_1  | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
mysql_1  | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
mysql_1  | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.
mysql_1  | 
mysql_1  | 2020-01-16 09:56:06+00:00 [Note] [Entrypoint]: Stopping temporary server
mysql_1  | 2020-01-16T09:56:06.599321Z 0 [Note] Giving 0 client threads a chance to die gracefully
mysql_1  | 2020-01-16T09:56:06.599402Z 0 [Note] Shutting down slave threads
mysql_1  | 2020-01-16T09:56:06.599412Z 0 [Note] Forcefully disconnecting 0 remaining clients
mysql_1  | 2020-01-16T09:56:06.599422Z 0 [Note] Event Scheduler: Purging the queue. 0 events
mysql_1  | 2020-01-16T09:56:06.599572Z 0 [Note] Binlog end
mysql_1  | 2020-01-16T09:56:06.600707Z 0 [Note] Shutting down plugin 'ngram'
mysql_1  | 2020-01-16T09:56:06.600763Z 0 [Note] Shutting down plugin 'partition'
mysql_1  | 2020-01-16T09:56:06.600771Z 0 [Note] Shutting down plugin 'BLACKHOLE'
mysql_1  | 2020-01-16T09:56:06.600777Z 0 [Note] Shutting down plugin 'ARCHIVE'
mysql_1  | 2020-01-16T09:56:06.600781Z 0 [Note] Shutting down plugin 'PERFORMANCE_SCHEMA'
mysql_1  | 2020-01-16T09:56:06.600812Z 0 [Note] Shutting down plugin 'MRG_MYISAM'
mysql_1  | 2020-01-16T09:56:06.600817Z 0 [Note] Shutting down plugin 'MyISAM'
mysql_1  | 2020-01-16T09:56:06.600831Z 0 [Note] Shutting down plugin 'INNODB_SYS_VIRTUAL'
mysql_1  | 2020-01-16T09:56:06.600836Z 0 [Note] Shutting down plugin 'INNODB_SYS_DATAFILES'
mysql_1  | 2020-01-16T09:56:06.601155Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESPACES'
mysql_1  | 2020-01-16T09:56:06.601163Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN_COLS'
mysql_1  | 2020-01-16T09:56:06.601167Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN'
mysql_1  | 2020-01-16T09:56:06.601170Z 0 [Note] Shutting down plugin 'INNODB_SYS_FIELDS'
mysql_1  | 2020-01-16T09:56:06.601173Z 0 [Note] Shutting down plugin 'INNODB_SYS_COLUMNS'
mysql_1  | 2020-01-16T09:56:06.601176Z 0 [Note] Shutting down plugin 'INNODB_SYS_INDEXES'
mysql_1  | 2020-01-16T09:56:06.601179Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESTATS'
mysql_1  | 2020-01-16T09:56:06.601182Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLES'
mysql_1  | 2020-01-16T09:56:06.601186Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_TABLE'
mysql_1  | 2020-01-16T09:56:06.601189Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_CACHE'
mysql_1  | 2020-01-16T09:56:06.601192Z 0 [Note] Shutting down plugin 'INNODB_FT_CONFIG'
mysql_1  | 2020-01-16T09:56:06.601195Z 0 [Note] Shutting down plugin 'INNODB_FT_BEING_DELETED'
mysql_1  | 2020-01-16T09:56:06.601198Z 0 [Note] Shutting down plugin 'INNODB_FT_DELETED'
mysql_1  | 2020-01-16T09:56:06.601201Z 0 [Note] Shutting down plugin 'INNODB_FT_DEFAULT_STOPWORD'
mysql_1  | 2020-01-16T09:56:06.601204Z 0 [Note] Shutting down plugin 'INNODB_METRICS'
mysql_1  | 2020-01-16T09:56:06.601207Z 0 [Note] Shutting down plugin 'INNODB_TEMP_TABLE_INFO'
mysql_1  | 2020-01-16T09:56:06.601211Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_POOL_STATS'
mysql_1  | 2020-01-16T09:56:06.601214Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE_LRU'
mysql_1  | 2020-01-16T09:56:06.601217Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE'
mysql_1  | 2020-01-16T09:56:06.601220Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX_RESET'
mysql_1  | 2020-01-16T09:56:06.601223Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX'
mysql_1  | 2020-01-16T09:56:06.601226Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM_RESET'
mysql_1  | 2020-01-16T09:56:06.601229Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM'
mysql_1  | 2020-01-16T09:56:06.601232Z 0 [Note] Shutting down plugin 'INNODB_CMP_RESET'
mysql_1  | 2020-01-16T09:56:06.601236Z 0 [Note] Shutting down plugin 'INNODB_CMP'
mysql_1  | 2020-01-16T09:56:06.601239Z 0 [Note] Shutting down plugin 'INNODB_LOCK_WAITS'
mysql_1  | 2020-01-16T09:56:06.601242Z 0 [Note] Shutting down plugin 'INNODB_LOCKS'
mysql_1  | 2020-01-16T09:56:06.601245Z 0 [Note] Shutting down plugin 'INNODB_TRX'
mysql_1  | 2020-01-16T09:56:06.601248Z 0 [Note] Shutting down plugin 'InnoDB'
mysql_1  | 2020-01-16T09:56:06.601457Z 0 [Note] InnoDB: FTS optimize thread exiting.
mysql_1  | 2020-01-16T09:56:06.602096Z 0 [Note] InnoDB: Starting shutdown...
mysql_1  | 2020-01-16T09:56:06.703296Z 0 [Note] InnoDB: Dumping buffer pool(s) to /var/lib/mysql/ib_buffer_pool
mysql_1  | 2020-01-16T09:56:06.703781Z 0 [Note] InnoDB: Buffer pool(s) dump completed at 200116  9:56:06
mysql_1  | 2020-01-16T09:56:08.246243Z 0 [Note] InnoDB: Shutdown completed; log sequence number 12441955
mysql_1  | 2020-01-16T09:56:08.249558Z 0 [Note] InnoDB: Removed temporary tablespace data file: "ibtmp1"
mysql_1  | 2020-01-16T09:56:08.249620Z 0 [Note] Shutting down plugin 'MEMORY'
mysql_1  | 2020-01-16T09:56:08.249628Z 0 [Note] Shutting down plugin 'CSV'
mysql_1  | 2020-01-16T09:56:08.249632Z 0 [Note] Shutting down plugin 'sha256_password'
mysql_1  | 2020-01-16T09:56:08.249635Z 0 [Note] Shutting down plugin 'mysql_native_password'
mysql_1  | 2020-01-16T09:56:08.249751Z 0 [Note] Shutting down plugin 'binlog'
mysql_1  | 2020-01-16T09:56:08.253458Z 0 [Note] mysqld: Shutdown complete
mysql_1  | 
mysql_1  | 2020-01-16 09:56:08+00:00 [Note] [Entrypoint]: Temporary server stopped
mysql_1  | 
mysql_1  | 2020-01-16 09:56:08+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
mysql_1  | 
mysql_1  | 2020-01-16T09:56:09.018888Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
mysql_1  | 2020-01-16T09:56:09.020190Z 0 [Note] mysqld (mysqld 5.7.29) starting as process 1 ...
mysql_1  | 2020-01-16T09:56:09.024040Z 0 [Warning] InnoDB: Using innodb_file_format is deprecated and the parameter may be removed in future releases. See http://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html
mysql_1  | 2020-01-16T09:56:09.024328Z 0 [Note] InnoDB: PUNCH HOLE support available
mysql_1  | 2020-01-16T09:56:09.024371Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
mysql_1  | 2020-01-16T09:56:09.024378Z 0 [Note] InnoDB: Uses event mutexes
mysql_1  | 2020-01-16T09:56:09.024382Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier
mysql_1  | 2020-01-16T09:56:09.024388Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11
mysql_1  | 2020-01-16T09:56:09.024395Z 0 [Note] InnoDB: Using Linux native AIO
mysql_1  | 2020-01-16T09:56:09.024922Z 0 [Note] InnoDB: Number of pools: 1
mysql_1  | 2020-01-16T09:56:09.025108Z 0 [Note] InnoDB: Using CPU crc32 instructions
mysql_1  | 2020-01-16T09:56:09.026850Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M
mysql_1  | 2020-01-16T09:56:09.036547Z 0 [Note] InnoDB: Completed initialization of buffer pool
mysql_1  | 2020-01-16T09:56:09.038578Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority().
mysql_1  | 2020-01-16T09:56:09.061020Z 0 [Note] InnoDB: Highest supported file format is Barracuda.
mysql_1  | 2020-01-16T09:56:10.650530Z 0 [Note] InnoDB: Creating shared tablespace for temporary tables
mysql_1  | 2020-01-16T09:56:10.650727Z 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ...
mysql_1  | 2020-01-16T09:56:10.920702Z 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB.
mysql_1  | 2020-01-16T09:56:10.922096Z 0 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active.
mysql_1  | 2020-01-16T09:56:10.922158Z 0 [Note] InnoDB: 32 non-redo rollback segment(s) are active.
mysql_1  | 2020-01-16T09:56:10.922898Z 0 [Note] InnoDB: Waiting for purge to start
mysql_1  | 2020-01-16T09:56:10.973211Z 0 [Note] InnoDB: 5.7.29 started; log sequence number 12441955
mysql_1  | 2020-01-16T09:56:10.973628Z 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool
mysql_1  | 2020-01-16T09:56:10.973931Z 0 [Note] Plugin 'FEDERATED' is disabled.
mysql_1  | 2020-01-16T09:56:10.979959Z 0 [Note] InnoDB: Buffer pool(s) load completed at 200116  9:56:10
mysql_1  | 2020-01-16T09:56:10.981172Z 0 [Note] Found ca.pem, server-cert.pem and server-key.pem in data directory. Trying to enable SSL support using them.
mysql_1  | 2020-01-16T09:56:10.981228Z 0 [Note] Skipping generation of SSL certificates as certificate files are present in data directory.
mysql_1  | 2020-01-16T09:56:10.981958Z 0 [Warning] CA certificate ca.pem is self signed.
mysql_1  | 2020-01-16T09:56:10.982025Z 0 [Note] Skipping generation of RSA key pair as key files are present in data directory.
mysql_1  | 2020-01-16T09:56:10.982602Z 0 [Note] Server hostname (bind-address): '*'; port: 3306
mysql_1  | 2020-01-16T09:56:10.982676Z 0 [Note] IPv6 is available.
mysql_1  | 2020-01-16T09:56:10.991740Z 0 [Note]   - '::' resolves to '::';
mysql_1  | 2020-01-16T09:56:10.991902Z 0 [Note] Server socket created on IP: '::'.
mysql_1  | 2020-01-16T09:56:10.994292Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_1  | 2020-01-16T09:56:11.005402Z 0 [Note] Event Scheduler: Loaded 0 events
mysql_1  | 2020-01-16T09:56:11.005836Z 0 [Note] mysqld: ready for connections.
mysql_1  | Version: '5.7.29'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
4. プロセスの確認
Terminal
$ docker-compose ps
              Name                            Command               State           Ports         
--------------------------------------------------------------------------------------------------
capistrano_sample_app_v1_app_1     /bin/sh -c rm -f /app/tmp/ ...   Up      0.0.0.0:8080->8080/tcp
capistrano_sample_app_v1_mysql_1   docker-entrypoint.sh mysql ...   Up      3306/tcp, 33060/tcp   
capistrano_sample_app_v1_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp      
5.appのコンテナにアクセスできるか確認
Terminal
$ docker exec -it capistrano_sample_app_v1_app_1 /bin/bash 
root@a72845229f9c:/var/www/capistrano_sample_app_v1# 
root@a72845229f9c:/var/www/capistrano_sample_app_v1# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   2384   744 pts/0    Ss+  18:55   0:00 /bin/sh -c rm -f /app/tmp/pids/server.pid && bin/rails s -p 8080 -b '0.0.0.0'
root         7  0.2  6.3 1125704 130296 pts/0  Sl+  18:55   0:04 puma 3.12.2 (tcp://0.0.0.0:8080) [capistrano_sample_app_v1]
root        27  1.0  0.1   5748  3400 pts/1    Ss   19:28   0:00 /bin/bash
root        32  0.0  0.1   9388  2992 pts/1    R+   19:28   0:00 ps aux
root@a72845229f9c:/var/www/capistrano_sample_app_v1# ls
Capfile  Dockerfile  Gemfile  Gemfile.lock  README.md  Rakefile  app  bin  config  config.ru  db  docker-compose.yml  lib  log  package.json  spec  storage  tmp  vendor
root@a72845229f9c:/var/www/capistrano_sample_app_v1# bundle exec rails c
from /var/www/capistrano_sample_app_v1/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap.rb:22:in `setup': The 'disable_trace' method is not allowed with this Ruby version. current: 2.6.5, allowed version: < 2.5.0
Loading development environment (Rails 5.2.4.1)
irb(main):001:0> 
irb(main):002:0> Rails.env
=> "development"
irb(main):003:0> 
irb(main):004:0> quit
root@a72845229f9c:/var/www/capistrano_sample_app_v1# 
6.mysqlのコンテナにアクセスできるか確認
Terminal
$ docker exec -it capistrano_sample_app_v1_mysql_1 /bin/bash 
root@38fa1f53821a:/# mysql -udeveloper -p capistrano_sample
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.29 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show tables
    -> ;
Empty set (0.00 sec)

ここまでで、開発のためのコンテナの設定は完了しています
尚、コンテナを実行しているホストマシンのワーキングコピーで変更があった場合、
dockerを stop / start することで変更反映できます

7. Webアプリのコンテナをセットアップする
Terminal
$ docker exec -it capistrano_sample_app_v1_app_1 /bin/bash

# bundle exec rails db:migrate RAILS_ENV=development
8.URLアクセス

ローカルホストのRailsにアクセスする

スクリーンショット 2020-01-16 22.08.04.png

ワーキングスペースの開発など

1. bundle install
Terminal
$ bundle _2.1.4_ install --path vendor/bundle
Your Ruby version is 2.3.7, but your Gemfile specified 2.6.5となる場合
  • 上記、bundle installをしないと、systemのruby versionを見るようです
mysql2の原因でエラーが出た方はこちら参考に
Terminal
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib --with-cppflags=-I/usr/local/opt/openssl@1.1/include"


$ bundle _2.1.4_ install --path vendor/bundle
[DEPRECATED] The `--path` flag is deprecated because it relies on being remembered across bundler invocations, which bundler will no longer do in future versions. Instead please use `bundle config set path 'vendor/bundle'`, and stop using this flag
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
...途中省略...
Fetching mysql2 0.5.3
Installing mysql2 0.5.3 with native extensions
...途中省略...
Bundle complete! 37 Gemfile dependencies, 148 gems now installed.
Bundled gems are installed into `./vendor/bundle`
2. 機能を追加してみる
Terminal
$ bundle exec rails generate scaffold v1::Event game:string description:string event_date:date join_limit:integer latitude:string longitude:string --skip-assets --skip-helper --skip-stylesheets --skip-view-specs --skip-jbuilder --skip-migration
      invoke  active_record
      create    app/models/v1/high_store.rb
      create    app/models/v1.rb
      invoke    rspec
      create      spec/models/v1/event_spec.rb
      invoke  resource_route
       route    namespace :v1 do
  resources :events
end
      invoke  scaffold_controller
      create    app/controllers/v1/events_controller.rb
      invoke    rspec
      create      spec/controllers/v1/events_controller_spec.rb
      create      spec/routing/v1/events_routing_spec.rb
      invoke      rspec
      create        spec/requests/v1/events_spec.rb

$ rm app/models/v1.rb

リポジトリ

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

system_specテストにおいて特定のリンクをクリックする方法

テストにおいて特定のリンクをクリックする

スクリーンショット 2020-01-16 16.14.02.jpeg

前提条件

taskアプリを作成してある。(scaffoldでもOK)
gem 'capybara', '>= 2.15'
gem 'rspec-rails'
インストール済

上記画像の黒丸に囲まれた(詳細を確認する)をクリックする

task_spec.rb
#省略
visit tasks_path
tds = page.all('td')
tds[24].click
#省略

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

変数で見るRubyとPythonの違い

ローカル変数のスコープ

rubyではローカル変数の参照は定義されたスコープ内のみです。

ruby
a = 1

def func
  puts a
end

func #=> error (undefined local variable)

pythonでは子孫スコープからも参照できます。

python
a = 1

def func():
  print(a)

func() #=> 1

さらに、ローカル変数は定義されているスコープが消滅しても、子孫スコープなどから参照されている限り消去されません。この性質を応用してインスタンス変数を持つクラスのようなものを作ることもできます。このクラスもどきのインスタンスメソッドもどきをクロージャーと言います。

python
def cls(): # クラスもどき
  x = {} # インスタンス変数もどき

  def a(): # クロージャー
    x['a'] = 1

  def b(): # クロージャー
    x['b'] = 1

  def g(): # クロージャー
    return x

  return a, b, g

# xのスコープは関数呼び出し終了とともに消滅する
a, b, g = cls()
a(); b();
print(g()) #=> {'a': 1, 'b': 1}

定数クロージャー

rubyでも定数ならクロージャーが存在します。例を挙げる前に、少しだけrubyの説明をします。

rubyでは定数と変数は全く別の手法で管理されています。変数のスコープは関数定義(do構文等含まず)やクラス定義で変化します。一方、定数のスコープはモジュール定義(クラス定義含む)でのみ変化します。そして定数の探索は、

ネストが存在する場合、この定数はそのネストの要素の中で順に探索される
(定数の自動読み込みと再読み込みより)

という様に行われます(但しオープンクラスの場合は探索しない)。つまり、定数は子孫スコープからも参照できます。

この性質を使うと、クロージャーを作ることが出来ます。

ruby
module A
  X = 'x' # インスタンス変数もどき

  module B # クラスもどき
    def show # クロージャー
      puts X
    end
  end
end

class C # インスタンスもどき
  extend A::B
end

C.show #=> x

ローカル変数に見えるインスタンスメソッド

定義が見当たらないローカル変数への参照は、実は引数なしのインスタンスメソッド呼び出しかもしれません。本当によく出会います。

ruby
class C
  def hoge # インスタンスメソッド
    return "hoge"
  end

  def show
    fuga = "fuga"

    puts hoge # インスタンスメソッド呼び出し
    puts fuga # ローカル変数参照
  end
end

さらに、クラスマクロのattr_accessor、attr_reader、attr_writerを使えば、ローカル変数に見えるインスタンスメソッドを簡単に作れます。変数の実体は、引数のシンボルに@を前置した名前のインスタンス変数です。

ruby
class C
  attr_accessor :hoge # インスタンス変数@hogeが実体

  def initialize
    self.hoge = "hoge" # インスタンスメソッドhoge=を呼び出し
  end

  def show
    fuga = "fuga" # ローカル変数fugaを定義&初期化

    puts hoge # インスタンスメソッドhogeを呼び出し
    puts fuga # ローカル変数fugaを参照
  end
end

C.new.methods.include?(:hoge) #=> true
C.new.methods.include?(:hoge=) #=> true

pythonの場合は、インスタンスメソッド呼び出しにselfが付いていたり、関数呼び出しの括弧が省略できなかったりするので、簡単に見分けられます。それよりも、インスタンスメソッド呼び出しとクラスメソッド呼び出しを区別するのが大変です。

python
class C:
  def hoge(self):
    return "hoge"

  @classmethod
  def piyo(cls):
    return "piyo"

  def show(self):
    fuga = "fuga"

    print(self.hoge()) # インスタンスメソッド呼び出し
    print(fuga) # ローカル変数参照

    print(self.piyo()) # クラスメソッド呼び出し

ローカル変数による上書き

(scivola様のコメントを元に追加しました)

先ほどの例でインスタンスメソッドhoge=を呼び出す際、selfをレシーバーとして明示しました。

ruby(再掲)
class C
  attr_accessor :hoge

  def initialize
    self.hoge = "hoge" # ここ
  end
  ...

これは、selfを明示しないとローカル変数hogeの定義として扱われるからです。

ruby
class C
  attr_accessor :hoge

  def initialize
    hoge = "hoge" # ローカル変数hogeの定義
  end

  def show
    puts hoge
  end
end

C.new.show #=> nil

同じことが、pythonでも起こり得ます。例えば、先ほど紹介したクラスもどきの場合、インスタンス変数もどきは更新できません。

python
def cls():
  x = {} # インスタンス変数的振る舞いをする

  def u():
    x = 'updated' # 実はローカル変数xの定義

  def g():
    return x

  return u, g

# xのスコープは関数呼び出し終了とともに消滅する
u, g = cls()
u();
print(g()) #=> {}

pythonではselfを省略できないので、インスタンス変数の更新の際に間違うことはないでしょう。

python
class C:
  def __init__(self):
    self.hoge = "not updated"

  def wrong(self):
    hoge = "updated" # 間違いに気付き易い

  def correct(self):
    self.hoge = "updated"

  def show(self):
    #print(hoge) #=> error (name 'hoge' is not defined)
    print(self.hoge)

c = C()
c.wrong(); c.show(); #=> not updated
c.correct(); c.show(); #=> updated

include先のインスタンス変数を参照

ruby
module M
  def show
    puts @message
  end
end

class C
  include M
  def initialize
    @message = "accessible"
  end
end

C.new.show #=> accessible

pythonでも似たことが出来ます。

python
class S:
  def show(self):
    print(self.message)

class C(S):
  def __init__(self):
    self.message = "accessible"

C().show() #=> accessible

関数を変数に代入

rubyでは関数呼び出しの括弧を省略できます。そのため、関数を変数に代入することは出来ません。

ruby
def func
  return "called"
end

a = func # 関数funcの呼び出し
puts a #=> called

Object#methodインスタンスメソッドを使って、関数をMethodオブジェクトに変換すると、変数に代入できます。

ruby
def func
  return "called"
end

a = method(:func) # Methodオブジェクトの代入
puts a #=> #<Method: main.func>
puts a.call #=> called

一方、pythonでは関数を変数に代入できます。

python
def func():
  return "called"

a = func # 関数funcの代入
print(func) #=> <function func at 0x...>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPCメモ

自身のメモとしてgRPCをまとめてみました。

gRPCとは

protocol buffersをMessage interchange format(メッセージのI/Oに使うための形式)として使えるリモートプロシージャコールシステムです。

protocol buffersとは

IDL:Interface Definition Language (インタフェース定義言語)を用いたファイルフォーマットです。

gRPCで何ができるのか

クライアントが別のマシンにあるメソッドをまるでローカルにあるかのように使えるようになります。
下の図のように様々なプログラミング言語に対して実装できる。

image.png

雑に言うと

今までjsonなどを介してAPIを叩いていたようなところを、もっと確固たる定義をもった上でより高パフォーマンスでやりとりができる…という感じでしょうか。

protocol bufferの定義の仕方

やりとりするmessageの構成は、以下のようにname-valueでfieldを定義します。

person.proto
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

gRPC Serverの定義はrpcメソッドを使って行います。

greeter.proto
// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

protoファイルののコンパイル

上のgreeter.protoをGo言語ようにコンパイルする場合は以下のようなコマンドを叩きます。
protoc --go_out=plugins=grpc:./ ./greeter.proto

このコンパイルでClient側のメソッドもServer側のメソッドも作成されます。

もし、何かのディレクトリの中にあるすべての.protoをコンパイルしたい場合は、以下のような感じでワイルドカードを使うこともできます。
protoc --go_out=plugins=grpc:./ ./*.proto

もし、どこかのディレクトリに入れたいという場合は、grpc:後のところを書き換えます。
以下の場合はprotoディレクトリの中にコンパイルされたデータが書き出されます。
protoc --go_out=plugins=grpc:proto ./*.proto

もし、どこかのディレクトリからどこかのディレクトリに書き出したい場合は、以下のような書き方になります。
protoc -I protobuf --go_out=plugins=grpc:proto protobuf/*.proto
-I が無いとprotoディレクトリ配下にprotbufディレクトリが作成され、そのなかにコンパイルされたデータが書き出されます。
-I はimportの基点となる場所をしてしてやるオプションと考えてもらえば良いかもしれません。

--go_out=plugins=grpc:と書いているのですが、protocol buffer自体は別にgRPCのためだけのものではないので、このようにgRPCのための書き出しですよーというオプションを書いておく必要があります。

Ruby用にコンパイルしたい場合は --go_outを書き換えます。(複数書けば複数の言語のファイルを同時に書き出せます。)
protoc -I protobuf --ruby_out=plugins=grpc:proto protobuf/*.proto

gRPCサーバーのリクエストとレスポンスの種類

種類は4つあります。

Unary RPC

一番シンプルな方法で、単一のメッセージのリクエストに対して、単一のメッセージをレスポンスで返す方法です。

greeter.proto
rpc SayHello(HelloRequest) returns (HelloResponse) {
}

Server streaming RPC

単一のメッセージのリクエストに対して、複数のメッセージをレスポンスで返す方法です。
例えばですが、何かのデータの一覧が欲しいなどと言うときに、この方法を使えたりします。

greeter.proto
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {
}

Client streaming RPC

複数のメッセージのリクエストを受けた上で、単一のメッセージのレスポンスを返します。
greeter.proto
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

Bidirectional streaming RPC

これはリクエストもレスポンスも複数のメッセージをレスポンスを扱う方法ですが、
すべてのメッセージを待って、すべてのメッセージを書き出して返すことも、一つメッセージを受け取る毎に、メッセージを書き出していき、最後にまとめてメッセージを返すことも、サーバー側の実装次第で返ることができます。

greeter.proto
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {
}

サーバー側の実装

主な処理は以下のような感じです。

main.go
package main
import (
    pb "github.com/{リポジトリ名}/{デフォルトならproto}"
    "flag"
    "net"
    "log"
    "sync"
    "fmt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/testdata"
)

var (
    tls        = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
    certFile   = flag.String("cert_file", "", "The TLS cert file")
    keyFile    = flag.String("key_file", "", "The TLS key file")
    jsonDBFile = flag.String("json_db_file", "", "A json file containing a list of features")
    port       = flag.Int("port", 10000, "The server port")
)

func main() {
    flag.Parse() // 引数を入れて実行したい時用にflagを使ってます。

    // hostとportを設定します。
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // 必要であればcredentialの設定をします。
    var opts []grpc.ServerOption
    if *tls {
        if *certFile == "" {
            *certFile = testdata.Path("server1.pem")
        }
        if *keyFile == "" {
            *keyFile = testdata.Path("server1.key")
        }
        creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
        if err != nil {
            log.Fatalf("Failed to generate credentials %v", err)
        }
        opts = []grpc.ServerOption{grpc.Creds(creds)}
    }

    // gRPCのサーバーを初期化
    grpcServer := grpc.NewServer(opts...)
    // protocでコンパイルされたコードからサーバーを登録するメソッドを実行
    pb.RegisterGreeterServer(grpcServer, newServer())
    // サーバーを起動
    grpcServer.Serve(lis)
}

上の記述で書いていないのですがnewServer()という関数が重要なのですが、Register*Serverの第二引数にはprotoのServer内で定義したメソッドを持ったinterfaceを入れる形になっています。

その為に以下のような形の定義もmainに入れておきます。(例えば、Server streaming RPCだけ定義している場合)

main.go
type greeterServer struct {}
// nilかerrを返せばstreamingが終了します。
func(s *greeterServer) LotsOfReplies(req *pb.LotsOfRepliesRequest, stream pb.Greeter_LotsOfRepliesServer) error {
    hs := []pb.HelloReply{
        pb.HelloReply{ Name: "ohayo" },
        pb.HelloReply{ Name: "konnichiwa" },
        pb.HelloReply{ Name: "konbanwa" },
    }
    for _, h := range hs {
        if err := stream.Send(&h); err != nil {
            return err
        }
    }
    return nil
}

func newServer() *greeterServer {
    s := &greeterServer{}
    return s
}

その他

repeated

protoの定義の中でmessageの中に配列をいれたい場合はRepeatedが使えます。

greeter.proto
message HelloResponse {
  repeated string name = 1;
}

こうするとnameの配列を返す事ができます。
複数のmessageを返したい場合はstreamでmessageの中で処理できるならrepeatedというところでしょうか。

oneof

messageに入るのがstringの可能性もあるし、int32の可能性もある…なんて場合に使えます。

greeter.proto
message HelloResponse {
    oneof name {
        string text = 1;
        int32 id = 2;
    }
}

リファレンス

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

graphql-rubyでresolverを使ってqueryをシンプルに保つ

概要

graphql-rubyを使っていると、query_type.rbが肥大化してしまう傾向にあります。

GitHubのIssueを見ていたところ、query_type.rbにおいてResolverを使うことで肥大化を(なるべく)回避するベストプラクティスが紹介されていて、公式ガイドにも反映されていなかったようなので共有したいと思います。

参考Issue:
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

ちなみに確認したバージョンは以下の通りです。

ライブラリ バージョン
ruby 2.6.5
graphql-ruby 1.19.5

また、GraphQLのレスポンスとしては、ユーザー単体とリストを返却することを想定するものとします。

ユーザーのモデルは以下のイメージです。

db/schema.rb
create_table "users", force: :cascade do |t|
  t.string "email"
  t.string "password"
  t.string "first_name"
  t.string "last_name"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

ちなみに表示に用いるUserTypeは以下を想定しています。

app/graphql/types/user_type.rb
module Types
  class UserType < BaseObject
    field :id, Int, null: false
    field :email, String, null: false
    field :password, String, null: false
    field :first_name, String, null: true
    field :last_name, String, null: true
  end
end

普通のQuery

graphql-rubyにおいて普通にquery_typeを書いていくと、例えばこんな感じになるかと思います。

app/graphql/types/query_type.rb
module Types
  class QueryType < BaseObject
    field :users, Types::UserType.connection_type, null: false do
      argument :id, Int, required: false
      argument :email, String, required: false
      argument :first_name, String, required: false
      argument :last_name, String, required: false
    end
    def users(**args)
      res = User.all
      args.each do |k, v|
        # argumentsはGraphQLの仕様上、keyがキャメルケースになる。argsにはスネークケースで入っている
        argument = self.class.arguments[k.to_s.camelize(:lower)]
        # 各argumentで絞り込む
        res = res.where("#{k} = ?", v)
      end
      res
    end

    field :user, Types::UserType, null: false do
      argument :id, Int, required: true
    end
    def user(id:)
      User.find(id)
    end
  end
end

出力するデータのカスタマイズをする場合、どうしてもquery_type自身に実装を行なっていく必要があります。

もちろんもう少しシンプルに記載することもできるかもしれませんが、いずれにせよREADに関わる全クエリをquery_type.rbに記載しないといけないため、queryの種類が増えるほど見通しが悪くなっていきます。

Resolverを使ったQuery

Queryにはresolveオプションがありますが、これを用いて処理をResolverに投げることで処理を外出しします。

app/graphql/types/query_type.rb
module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::UserConnectionResolver
    field :user, resolver: Resolvers::UserResolver
  end
end

query_type.rbに関して言えばだいぶシンプルで見通しが良くなりました。

当然ながら、具体的に何を返すかについてはResolverで実装が必要です。
とはいえquery_type.rbにあった処理をただResolver側に移すだけになります。

app/graphql/resolvers/user_connection_resolver.rb
module Resolvers
  class UserConnectionResolver < GraphQL::Schema::Resolver
    type UserType.connection_type, null: false

    argument :id, Int, required: false
    argument :email, String, required: false
    argument :first_name, String, required: false
    argument :last_name, String, required: false

    def resolve(**args)
      res = User.all
      args.each do |k, v|
        argument = self.class.arguments[k.to_s.camelize(:lower)]
        res = res.where("#{k} = ?", v)
      end
      res
    end
  end
end
app/graphql/resolvers/user_resolver.rb
module Resolvers
  class UserResolver < GraphQL::Schema::Resolver
    type UserType, null: false

    argument :id, Int, required: true

    def resolve(id:)
      User.find(id)
    end
  end
end

このように処理をResolverとして切り出すことで、query_type.rbをGraphQLのルーティングを行うファイルにして、各resolver.rbでController的に処理を実装していくといった構成にすることができます。

Resolverの使用は注意が必要?

Resolverのパターンを紹介しましたが、公式ガイドでは本当にResolverを使う必要があるか?とResolverの使用に懐疑的なようです。

  • テストがしづらくなる
  • graphql-rubyの更新
  • Resolverが肥大化していってしまう

といったことが懸念視されているようです。

ただここで紹介したテクニックに関しては、参考Issue内で作者も良いパターンだと認めておりドキュメントの更新をしてくれという風にコメントしているので、今後ガイド側も更新されるかもしれません。

どんなケースでもResolverを使えばいいというわけではなさそうなので、他のケースに当てはめるときは注意が必要そうです。

ということでresolverを使ったパターンの紹介でした。

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

master以外のブランチからherokuにpushする方法

railsアプリを作って、herokuをデプロイする(masterブランチ以外からデプロイする方法も同時に載せています。)

1 コミット

$ git add .
$ git commit -m "heroku"
$ git push origin ブランチ名

2 herokuにアプリケーションを新規作成する

$ heroku create

3-1 herokuにデプロイする(masterブランチにいる時)

$ git push heroku master

3-2 herokuにデプロイする(master以外のブランチにいる時)

$ git push heroku <現在いるブランチ名>:master

※3-1 or 3-2 どちらかを実行してください

4 herokuにデータベースを作成する

$ heroku run rails db:migrate

5 アプリケーションを開く

$ heroku open

以上で、完成です。

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

Redis の WebAPI (Ruby CGI)

こちらで定めた仕様を満たすサーバーサイドのプログラムです。
Nginx + fcgiwrap で動作を確認しました。
Redis の WebAPI を作成

redis_read.rb
#! /usr/bin/ruby
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
#   redis_read.ruby
#
#                   Jan/16/2020
#
# ---------------------------------------------------------------------
require 'redis'
require "cgi"
require "json"
#
STDERR.puts "*** 開始 ***"
#
$cgi=CGI.new
key = $cgi["key"]
# key = "t1852"

redis = Redis.new(:host => "localhost", :port => 6379)
value = redis.get key

puts "Content-type: text/json; charset=UTF-8\n\n"
puts value
#
STDERR.puts "*** 終了 ***"
# ---------------------------------------------------------------------
redis_insert.rb
#! /usr/bin/ruby
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
#   redis_insert.ruby
#
#                   Jan/16/2020
#
# ---------------------------------------------------------------------
require 'redis'
require "cgi"
require "json"
#
STDERR.puts "*** 開始 ***"
#
$cgi=CGI.new
key = $cgi["key"]
value = $cgi["value"]

redis = Redis.new(:host => "localhost", :port => 6379)
redis.set key,value

puts "Content-type: text/json; charset=UTF-8\n\n"
#
puts value
#
STDERR.puts "*** 終了 ***"
redis_list.rb
#! /usr/bin/ruby
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
#   redis_list.ruby
#
#                   Jan/16/2020
#
# ---------------------------------------------------------------------
require 'redis'
require 'json'
#
STDERR.puts "*** 開始 ***"
#

redis = Redis.new(:host => "localhost", :port => 6379)
keys = redis.keys
json_str = JSON.pretty_generate(keys)

puts "Content-type: text/json; charset=UTF-8\n\n"
puts json_str
#
STDERR.puts "*** 終了 ***"
# ---------------------------------------------------------------------
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Ruby Error class message / raise second args or Error class initialized message

raise StandardError.new('ERROR in class')
# StandardError: ERROR in class

raise StandardError, 'ERROR in raise arg'
# StandardError: ERROR in raise arg

raise StandardError.new('ERROR in class'), 'ERROR in raise arg'
# StandardError: ERROR in raise arg

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2948

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

rails g migrate で外部キーを追加する

ポートフォリオ作成中に後から外部キーを追加しよとしたら、コマンドに詰まったので忘れないために記事にしておく。

articleテーブルにuser_idを追加する

rails g migration AddUserRefToArticles user:references

成功したら

rake db:migrate

を実行する。

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

RubyonRailsを勉強していてずっとわかっていなかったこと

このQiitaはRubyを勉強している中で
私が「ずっと理解できていなかったこと」と「その理由」と「勉強して理解したこと」をまとめたものです。

まだまだ間違っている部分もあるかもしれないのと
私自身まだわからないところ( :question: の部分 )もあるため
その点踏まえてみてください :bow:

ずっと理解できていなかったこと

MVCモデルについての理解

※以下、私の昔の理解なのでところどころ間違ってます!
iOS の画像.jpg
・ルーティング…リクエストに対して適切な場所に移動させる役割。
ブラウザからリクエストが来る。リクエストに対してルーティングで送り先を、コントローラとアクションを指定。

・コントローラー…実装したい機能を決める場所。
・実装したい機能に対して適切なアクションを設定。その際にはRailsで決められてる7つのアクションのどれかに沿って決める。
【Rails】7つのアクションとそれぞれの役割
・そのアクションの中にDBからデータを持って来るために変数を定義しないといけない。それは@からはじまる。
・変数を定義するためにはrailsであらかじめ定められたメソッドで定義できるっぽい。

・ビュー…DBから送られてきたデータを表示する場所
・HTMLの中で <% %> とか <%= %> で囲むとコントローラーで定義した変数を使えるっぽい。そこは動的になる。
・それ以外は普通のHTMLとCSSを書いていくと静的になる。

・モデル…コントローラーからDBへの橋渡し役。DB設計に何か命令する役目?
・テーブル作成と同時にrails g modelをしなければいけない。
・modelを作成した後は特に編集とかは要らないっぽい。

・マイグレーションファイル…DB設計を記述するところ。

その他理解できなかったこと

・なぜクラスとインスタンスが存在するのか?
・MVCモデルにおいてクラスはどれをさしているのか?
・クラスからインスタンスが生成されるというのはMVCにおけるどの部分をさしているのか?
・controllerのアクションの中には何を定義するのか?
・引数はなんで使うのか?

なぜ理解をしていなかったか

Rubyでは、 すべてのデータがオブジェクトとして扱われ、オブジェクトごとに性質や使えるメソッドを持つ という概念を理解していなかった。

(もちろんRubyを習い始めた時に、「オブジェクト指向」についての説明は出てきたが、その概念が初めてで理解できなかったし、そんなに大事なものだと思っていなかった。)

勉強して理解したいこと

勉強して理解ことを「User model」と「Tweet model」の2つをもつツイートアプリの例とともに説明。

モデルについて

モデルの存在意義

・データベースの設計図。そのデータベースに定義したいメソッドを書く場所。(アソシエーション、バリデーション、そのモデル独自のメソッドなど)
・そのためモデルはDB設計が必要なものしかつくらない。静的なサイトであればモデルはいらない。

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable #ユーザーのログイン機能に関する記述
  has_many :tweets #tweet modelに対するアソシエーション

  validates :nickname, presence: true, length: { maximum: 6 } #バリデーション
end
tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user #user modelに対するアソシエーション
end

Railsで定義されているメソッドの存在

・上記のようにモデルに記述がされていなくても、もともとrailsで定義されているメソッドがある。

①テーブルのカラム名

例えば、tweets tableを以下のように定義している場合

migrationfile
class CreateTweets < ActiveRecord::Migration[5.2]
  def change
    create_table :tweets do |t|
      t.string      :name
      t.text        :text
      t.text        :image
      t.timestamps null: true
    end
  end
end

tweet modelには記述されていないが以下のようなメソッドが定義されている。

tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user #user modelに対するアソシエーション

  #以下、記述はされていないが使えるメソッド
  def name
    @name 
  end

  def text
    @text 
  end

  def image
    @image 
  end
end
②Application Recordクラスのメソッド(クラスの継承)

rails g modelコマンドで生成されるモデルクラスは全てApplicationRecordというクラスを継承している。

ApplicationRecordというクラスには テーブルにアクセスして情報を取得するためのメソッドが定義されており 、モデルクラスはそれを継承し利用することでテーブルから情報を取得している。

記述されていないがtweet modelには以下のようなメソッドが定義されている。

tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user #user modelに対するアソシエーション

  #以下、記述はされていないが使えるメソッド
  def all
    # ここの記述がわかんない
  end

  def new
    # ここの記述がわかんない
  end

  def save
    # ここの記述がわかんない
  end

  def create
    # ここの記述がわかんない
  end
end

:question: それぞれの中に書いてある記述

コントローラーについて

コントローラーの存在意義

モデルに定義してあるメソッドを使う役割。

users_controller.rb
class UsersController < ApplicationController
  def show
    user = User.find(params[:id]) # User modelで最初から定義されているfindメソッドを用いてidを引数にuserを見つけて来る
    @nickname = user.nickname # User modelで最初から定義されているnicknameをメソッド使って、user tableからnicknameを見つけてくる
    @tweets = user.tweets.page(params[:page]).per(5).order("created_at DESC")
  end
end
tweets_controller.rb
class TweetsController < ApplicationController
  before_action :move_to_index, except: :index

  def index
    @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
  end

  def new
  end

  def create
    Tweet.create(image: tweet_params[:image], text: tweet_params[:text], user_id: current_user.id)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    if tweet.user_id == current_user.id
      tweet.destroy
    end
  end

  def edit
    @tweet = Tweet.find(params[:id])
  end

  def update
    tweet = Tweet.find(params[:id])
    if tweet.user_id == current_user.id
      tweet.update(tweet_params)
    end
  end

  def show
    @tweet = Tweet.find(params[:id])
  end

  def search
    @tweets = Tweet.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end

  private
  def tweet_params
    params.permit(:image, :text)
  end

  def move_to_index
    redirect_to action: :index unless user_signed_in?
  end
end

:question: アクション内の記述方法

Q.controllerのアクションの中には何を定義するのか?

AかBのいづれか。

A.モデルに定義してあるメソッドを使って、データを引き出す
B.モデルに定義してあるメソッドを使って、データを引き出しインスタンス変数を定義してビューに渡す

その際にUser ControllerではUser modelで定義されているメソッドもつかえるし、tweet modelで定義されているメソッドも使える。

クラスとインスタンス

Q.なぜクラスとインスタンスが存在するのか?

・Rubyでは、すべてのデータがオブジェクトとして扱われ、オブジェクトごとに性質や使えるメソッドを持つ
・オブジェクトを効率的に生成・管理するために、クラスとインスタンスという概念が存在する
・オブジェクト同士の共通の属性・メソッドをまとめて定義したものがクラスである
・クラスに基いて生成されたオブジェクトがインスタンス

例)Userモデルにはクラスとしてログイン機能をつけたり、ニックネームの登録の際のバリデーションをつけておく。
そこから実際に登録されたユーザー1人1人がインスタンス。

Q.MVCモデルにおいてクラスはどれをさしているのか?

・Controller
・Model
・table
(オブジェクトに共通の属性・メソッドを
まとめて定義したものはすべてクラス。)

Q.クラスからインスタンスが生成されるというのはMVCにおけるどの部分をさしているのか?

(Controllerで)Modelのメソッドを使うとき。
必ずインスタンスが生成される。

:question: modelのメソッドを使うのはcontrollerだけでまちがいないか?

引数について

Q.引数はなぜ使うのか

メソッドを異なる条件によって使うため。

tweets_controller.rb
  #このままだと毎回idが1のツイートしか編集できない
  def edit
    @tweet = Tweet.find(1)
  end

  #引数をわたすことによって様々なidのツイートの編集ができるようになる
  def edit
    @tweet = Tweet.find(params[:id])
  end

以上。
間違っているところがあればご指摘ください。

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

rubyでファイル末尾の改行を消す

git diffしたときの
「\ No newline at end of file」がウザいのでrubyで置換した

・パターン1

ruby -i -ne 'print ARGF.eof? ? $_.chomp : $_' test.txt

・パターン2

ruby -i -e 'print ARGF.read.strip' test.txt

sedでやりたかったな

もっといい方法あれば教えて下さい

viで↓をやるのがめんどくさかった
https://qiita.com/lemtosh469/items/b3a5b3b40d9f44dfbd06

(本当は末尾に改行がある方が正しいらしい)

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

[Ruby]ネストされたHash(連想配列)をフラット(1階層)にする②

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

Hash(連想配列)を取り除く

前回からの続きです!
[Ruby]ネストされたHash(連想配列)をフラット(1階層)にする①

ここからやっていきたいのは配列に入っている要素がHashだったら変数に変換する処理します。
要は、配列に入っている要素をチェックしてHashだったら特定の処理をしたら良いのです。

#配列の場合
family_array = [ "sazae", "masuo", "tarao" ]      
pry(main)> family_array.is_a?(Hash)
=> false

#連想配列の場合
family_hash = { "tsuma" => "sazae", "otto" => "masuo", "kodomo" => "tarao" }
pry(main)> family_hash.is_a?(Hash)
=> true

Hashかどうかを判定しつつ任意の{key: value}を持っているかどうかを判定するメソッドを作った

配列の要素を確認する処理
配列に対する繰り返し
Ruby eachが初心者でも書ける!知っていると便利な知識とは?

array = hash.each_with_object([]) {|(k, v), array| array.concat v}
pry(main)> i=0
pry(main)> array.each do |var|
pry(main)*   if var.is_a?(Hash)
pry(main)*     params = var.keys
pry(main)*     array.delete_at(i)
pry(main)*     array.concat(params)
pry(main)*     print(array)
pry(main)*   end
pry(main)*   i=i+1;
pry(main)* end
=> [:name,
 :gender,
 :birthday,
 :primary,
 :clinic,
 :status,
 :age,
 :cycle,
 :history_test,
 :culture_test,
 :math_test,
 :house,
 :address,
 :favorite,
 :score,
 :history_score,
 :culuture_score,
 :math_score,
 :levels,
 :experience]

できた!

メソッドにしていく!

フラットにする為の処理をメソッドにしていきます!
Rubyのメソッドについて

メソッドとは

メソッドは色々な処理をひとつにまとめて定義して、何度も再利用(実行)できるようにしたものです。

def メソッドの名前
  やりたい処理
end

ではメソッド化していきます!

def flat_params(array)
  i=0
  array.each do |var|
     if var.is_a?(Hash)
       params = var.keys
       array.delete_at(i)
       array.concat(params)
     end
     i=i+1;
  end
end

array = hash.each_with_object([]) {|(k, v), array| array.concat v}
array = flat_params(array)
=> [:name,
 :gender,
 :birthday,
 :primary,
 :clinic,
 :status,
 :age,
 :cycle,
 :history_test,
 :culture_test,
 :math_test,
 :house,
 :address,
 :favorite,
 :score,
 :history_score,
 :culuture_score,
 :math_score,
 :levels,
 :experience]

これでフラットにできました!

参考記事

class Array
【Ruby入門】ハッシュ(hash)をeachで取り出す!その他ハッシュの応用について
Rubyで配列の要素数を調べるための3つのメソッドまとめ
【Ruby入門】Rubyにおけるメソッド(関数)の使い方

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

heroku 登録から(rails mysql)デプロイまで

ローカルでは動作確認済みのRailsアプリを、heroku初デプロイ

環境

  • heroku
  • MacBookpro
  • ruby 2.6.3
  • bundrer 2.0.2
  • rails 6.0.1
  • Mysql2 0.5.3
  • GitHub にて git clone(もしくは push) してある状態

<今後の時短のために記録> 今回Qiita自己用記録も含め、10時間かかった様子。
他でデプロイに14日かけてうまく行かず...原因がMVCではない事を再認識できた。知識不足、書籍等を活用する予定。
heroku1日で出来るのは有難い。


①heroku にアカウント登録
https://jp.heroku.com/

<<AWS と違い、基本ローカルからコマンド 入力 >>

②config/routes.rbのindex編集

Rails.application.routes.draw do
resources :blogs 
root 'home#top'   #トップページ:homeコントローラのtopアクションに設定

gem mysql (の確認。今回はローカルから記入済みなので、もし変更したらbundle install)

$ git add . 
$ git commit  
$ git push origin master   (GitHubへ)

③herokuのインストール

$ brew tap heroku/brew && brew install heroku

$ heroku -v(ersion)    確認

④$ heroku login のため sshキー設定 <④は省略内容にはなる為 他サイト参考必須>

(ローカルの.ssh に移動)
$ ssh-keygen -t rsa

:作成したいキー名_rsa
ls確認。

$ eval "$(ssh-agent -s)"

cd デプロイ先の階層に戻る

$ heroku keys:add ~/.ssh/作成した_rsa.pub

........SSH key... done  で成功。
$ heroku keys で確認可 *キーはherokuのHPでも確認可


⑤ $ heroku create (herokuのHPで作成したアプリ名)

$ git remote heroku (herokuのHPで作成したいアプリ名)

Name (作成したいアプリ名) is already taken。。。 で作成された

<<< $ git remote -v

origin
 origin git@github.com:。。。。。。。

(GitHubのしかないため、リモート:herokuを設定。アプリ名を指定しないとherokuリモートは自動で
作成される様子。)

$ heroku git:remote -a (作成したアプリ名)

set git remote heroku to https://git.heroku.com/(作成したアプリ名)  と出る

$ git remote -v で確認。
heroku
origin

>>>

⑥ DB設定
< [公式heroku参考] https://elements.heroku.com/addons/cleardb

$ heroku create cleardb:ignite

 Please verify your account to install this add-on plan (please enter a credit card) For more
 ▸    information, see https://devcenter.heroku.com/categories/billing Verify now at
 ▸    https://heroku.com/verify

heroku hpにてクレカの登録


⑦環境変数の設定

$ heroku config ($ git config --list)で内容確認)


> CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
$ heroku config:add DB_NAME='<データベース名>'heroku_b7dabcdb1795021
$ heroku config:add DB_USERNAME='<ユーザー名>'b1716d8a771568
$ heroku config:add DB_PASSWORD='<パスワード>'e006146a
$ heroku config:add DB_HOSTNAME='<ホスト名>'us-cdbr-iron-east-05.cleardb.net
$ heroku config:add DB_PORT='3306'
$ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

$ heroku config で設定内容 再確認

⑧ config/environments/production.rb を記入

config/environments/production.rb
config.assets.compile = true
config.assets.initialize_on_precompile=false

<herokuへpush>

$ git add .
$ git commit 


$ git push heroku master

⑨database.yml 設定

変更)
production:
  <<: *default
  database: [データベース名]
  username: [ユーザ名]
  password: <%= ENV['DATABASE_PASSWORD'] %>

(10) herokuのDBのマイグレーション

$ heroku rake db:migrate

$ heroku open

remote: Verifying deploy... done.  成功。

[上のURLの方。ターミナ下方のURLではない。

https://(各自)...herokuapp.com/  (← これ => deployed to Heroku ]

<urlに接続>

<ログを表示>

$ heroku logs    
(heroku logs --tail)
↑ 見ても原因が良くわからない為 ↓で今回はエラー解決した

$ heroku run rails console の方が分かる


<urlに接続したが、エラーindex.htmlに繋がる。>

スクリーンショット 2020-01-16 3.19.59.png

ここからエラー内容3回目

$ heroku restart 一応。
(firebaseデプロイ時は時差があった、が変わらず。)
$ heroku logs --tail

内容が分からず、

$ heroku run rails console  で原因が分かりやすい。

Could not load the 'mysql' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile. (LoadError)

ここで、Gemfile に合わせ mysql → mysql2 に変更した。

デプロイ成功

②config/routes.rbで指定したindex.htmlが表示された。



ここから↓ git push heroku master成功までの

実際のエラー対処記載 一部を参考に残す。

⑨ ←ここでエラー内容1回目

Could not load the 'mysql' A....database.yml も確認しろと出る。

 database.yml 確認修正↓

⑦の $ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

を Gemfile に合わせ mysql → mysql2 に最終的に変更。かつ bundle install

エラー内容2回目

...
Could not detect rake tasks
remote:  !     ensure you can run `$ bundle exec rake -P` ...URI::InvalidURIError: bad URI(is not URI?): mysql2:....
$ rbenv -v
rbenv 1.1.2
$ ruby -v
ruby '2.6.4'

$ gem install bundler -v 2.0.2
$ bundle install( + update)
$ rbenv -v が1.1.2のままの為、
$ rm Gemfile.lock
$ bundle install でGemfile.lock 再 bundler -v 2.0.2へ。
(add .  commit )
$ git push heroku master
変わらず同エラー。
今回は公式サイトを参考に、 ruby '2.6.3'に下げる。
$ rbenv install 2.6.3
$ rbenv local 2.6.3
(Gemfileも変更。$ bundle install $ rbenv rehash)
変わらず同エラー。
$ heroku config 設定ミスか確認。
$ heroku config:add DATABASE_URL='mysql://...をmysql2:...
に変更。
変わらず同エラー。
<直接な解決なのかは不明だが、今回は、$ heroku create アプリ名を指定しないで再度やり直す。このエラーは解決>
$ heroku create
$ git remote で自動herokuリモートが作成された。
$ git push heroku master

bad URI(is not URI?): mysql2:...エラーは解決したが、
新たなエラー1回目

remote:  !
remote:  !     Precompiling assets failed.

Precompil なので、

config/application.rbに
config.assets.initialize_on_precompile = true を追記。
config/environments/production.rbの
onfig.assets.compile = false を trueに。

変わらず。
$ RAILS_ENV=production (bundle exec) rake assets:precompile 
変わらず。

webpacker.yml .gitignore の編集で解決した。
webpacker.yml 内に
production:
<<: *default
compile: true がある為と思われる。全て知識不足が原因。


実際には③で 小さいエラーが出て bundle update install 等をプラス作業した
その後、push。

<基本だが一応残す>

rails -v
ruby -v
mysql -v

。。。ローカルの諸々の確認はした前提の内容。

③で 

...Warning: heroku update available from 7.35.0 to 7.35.1.....

$ heroku uodate を実行。
$ heroku -v 確認

⑨ mysql2 エラー

($ brew update && brew upgrade ruby​​-build できなかった)


GitHub 接続確認
$ ssh -T github git@github.com 
他サーバーにて設定変更してしまった為今回 再設定。


参考にしたサイト

mysqlを使ったRailsアプリをHerokuにデプロイする流れ

追えなくなってしまいましたが、その他、多々多々参考にさせて頂きました。

herokuチュートリアル
一部コマンドが公式の方が新しい様子です...

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

[Ruby]ネストされたHash(連想配列)をフラット(1階層)にする①

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

Arra(配列)とHash(連想配列)

ChefTips: RubyのArray(配列)とHash(連想配列)入門

Array(配列) とは

Arrayは、イメージしやすく言うと複数のValue(値)が入っている入れ物です。順番(添字)をつけてValue(値)を管理します。順番は1からではなく、0から始まります。

# Array family_array を定義
family_array = [ "sazae", "masuo", "tarao" ]      

Hash(連想配列) とは

ArrayはValue(値)を管理するために添字を使いますが、Hashは添字の代わりにKeyを使います。KeyとValueのペアは "=>"(ハッシュロケット) で表現します。
Keyは数字と違いなんらかの意味を持つので、コードが読みやすくなるという利点があります。下のサンプルコードを見てください。なんとなく、sazaeが妻で、mazuoが夫で、taraoが子供であることがコードを呼んでいる人に伝わりますよね。

# Hash family_hash を定義
family_hash = { "tsuma" => "sazae", "otto" => "masuo", "kodomo" => "tarao" }  

今回やりたい事

こんなネストされたHashがあるとします、これをフラット(1階層)にしていきます!
ネストがあるhashの、keyとvalueの取得について

これを

hash =
{:basic=>[:name, :gender, :birthday],
 :history=>
  [:primary, :clinic, :status, :age],
 :test=>
  [:cycle,
   :history_test,
   :culture_test,
   :math_test,
   {:score=>[], :history_score=>[], :culuture_score=>[], :math_score=>[]}
  ],
 :tarou_infomation=>[:house, :address,:favorite, {:levels=>[], :experience=>[]}],
 }

こうしたい!パターンです!

hash =
[
:name,
:gender,
:birthday,
:primary, 
:clinic, 
:status, 
:age,
:cycle,
:history_test,
:culture_test,
:math_test,
:score, 
:history_score, 
:culuture_score, 
:math_score,
:house, 
:address,
:favorite,
:levels, 
:experience
]

色々試してみる

キーだけを消してバリューの要素だけとる。
Rubyでハッシュオブジェクトからキーや値を取り出す方法【初心者向け】
ハッシュを配列に変換する

pry(main)> hash.values
=> [[:name, :gender, :birthday],
 [:primary, :clinic, :status, :age],
 [:cycle, :history_test, :culture_test, :math_test, {:score=>[], :history_score=>[], :culuture_score=>[], :math_score=>[]}],
 [:house, :address, :favorite, {:levels=>[], :experience=>[]}]]

次はこれをフラットに!
flattenメソッドを使用して配列を平坦化する。
flatten, flatten! (Array) - Rubyリファレンス

pry(main)> hash.values.flatten
=> [:name,
 :gender,
 :birthday,
 :primary,
 :clinic,
 :status,
 :age,
 :cycle,
 :history_test,
 :culture_test,
 :math_test,
 {:score=>[], :history_score=>[], :culuture_score=>[], :math_score=>[]},
 :house,
 :address,
 :favorite,
 {:levels=>[], :experience=>[]}]

よし!フラットにできたけどHashが〜

ちなみにeach_with_objectを使えば一発でできました。
Ruby: injectとeach_with_objectをうまく使い分ける

pry(main)> hash.each_with_object([]) {|(k, v), array| array.concat v}
=> [:name,
 :gender,
 :birthday,
 :primary,
 :clinic,
 :status,
 :age,
 :cycle,
 :history_test,
 :culture_test,
 :math_test,
 {:score=>[], :history_score=>[], :culuture_score=>[], :math_score=>[]},
 :house,
 :address,
 :favorite,
 {:levels=>[], :experience=>[]}]

これは配列の中に連想配列がネストされているので
連想配列には別の対処が必要です。

配列にしたいHashをインデックスで指定

pry(main)> hash.values.flatten[11]
=> {:score=>[], :history_score=>[], :culuture_score=>[], :math_score=>[]}

これをキーだけの配列にはできる

 pry(main)> hash.values.flatten[11].keys
=> [:score, :history_score, :culuture_score, :math_score]

ゴリ押しで新しい変数にhashの要素だけdelete_atメソッドで削除してkesで作成した配列をconcatメソッドで1つの配列に結合する方法もありますがあまりしたくない。
【Ruby】配列の要素を追加・削除
Rubyで複数の配列を1つの配列に結合するために色々やってみた

しかも、そこそこ記述が多くなりそうなのでメソッドにするか〜
という事で次回は、Hash(連想配列)をフラット(1階層)にする記述をメソッドにしていきます!
[Ruby]ネストされたHash(連想配列)をフラット(1階層)にする②

参考記事

扱いにくい階層の深いHashをフラット(1階層)にする
how to permit an array with strong parameters
Ruby :: ハッシュを配列に変換する
[Ruby]配列をマージ・結合する
【Ruby入門】defについてまとめてみました(return,self,defined)

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