20210226のRubyに関する記事は18件です。

Couldn't find User without an ID エラーを解決した

rails初学者です。
現在オリジナルアプリを開発しております。
ユーザー詳細機能を実装しようとして、ユーザー詳細画面へのリンクを設定し、動作確認してみたら
「Couldn't find User without an ID」
というエラーが発生したため、エラー解決をしました。

環境

ruby '2.6.5'
rails '6.0.0'

ビュー

<%= link_to "#{current_user.name}", user_path(current_user) %>

このように、ログイン中のユーザーの名前を表示し、名前にユーザー詳細ページへ飛ぶリンクを設定しました。

Usersコントローラー

def show
  @user = User.find(params[:id])
end

Usersコントローラーのshowアクションで、パラメータのidを受け取って
そのidに合致するユーザーを@userに格納しました。

挙動確認

予想される挙動は、「ユーザー名をクリックするとユーザー詳細ページに遷移する」
ですが、いざクリックしたところ、Usersコントローラーのshowアクションで下記のようなエラーメッセージが出ました。

ActiveRecord::RecordNotFound in UsersController#show

Couldn't find User without an ID

簡単に言うとIDがありませんよ、ということです。

パラメーターの確認

showアクションにbinding.pryを仕込み、パラメーターを確認してみました。
すると下記のような結果でした。

 pry(#<UsersController>)> params
=> <ActionController::Parameters {"controller"=>"users", "action"=>"show", "format"=>"1"} permitted: false>

本来ならばid="1"と出て欲しいところが、format="1"となっており
パラメーターにidが含まれていないようです。

結論

ルーティングの記述にミスがありました。

resource :users, only: :show

'resources'と書かなければならないところを、'resource'と書いてしまっておりました。

resourceメソッドはresourcesメソッドに対して、indexアクションとid付パスを生成してくれないみたいです。
そのため、パラメーターにidが含まれていませんでした。

解決策

ルーティングを

resources :users, only: :show

と書き直しました。

再度挙動を確かめたところ、問題なくユーザー詳細ページに遷移しました。

参考

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

Rails「paramsの使用法」

はじめに

今回、テキストの一覧ページを作成する際、link_toとhelperよりparamsの使用方法を復習できたので共有します。

やりたいこと

・viewで各教材の一覧ページへのリンクを設定する
・controllerで各教材の一覧ページに適した教材を取得する
・helperで一覧ページのタイトルを表示する処理を書く

viewで各教材の一覧ページへのリンクを設定する

_header.html.erb
<%= link_to "Ruby/Rails教材", texts_path, class: "dropdown-item" %>
<%= link_to "AWS講座", texts_path(genre: "AWS"), class: "dropdown-item" %>
<%= link_to "PHPテキスト教材", texts_path(genre: "Php"), class: "dropdown-item" %>

<%= link_to "表示名",次のアクションへ遷移するパス(パラメーター),class: "クラス名" %>
となるよう記述します。

各教材の一覧ページへのパスtext_pathにパラメーターとしてgenreを指定することで、表示する教材を制限します。

controllerに条件分岐を記述

texts_controller.rb
  def index
    if params[:genre] == nil
      @texts = Text.where(genre: ["Basic", "Git", "Ruby", "Ruby on Rails"])
    else
      @texts = Text.where(genre: params[:genre])
    end
  end

パラメーターgenreの指定がない場合とある場合で条件分岐し、条件にあった教材だけ取得するようにします。

helperに変換部分を作り教材毎にページタイトルが変わるようにする

texts_helper.rb
module TextsHelper
  def page_title
    if params[:genre] == nil
      "Ruby/Rails"
    else
      params[:genre]
    end
  end
end

helperにpage_titleというメソッドを定義し、パラメーターgenreの指定がない場合はRuby/Railsを表示し、指定がある場合はその教材のgenre名を表示するよう処理を書きます。

helperは、定義しておくことでview内でその処理を呼び出すことができ、viewをよりシンプルに書くことができます。

一覧ページのview

index.html.erb
<div class="text-title">
  <h3><%= page_title %>テキスト教材</h3>
</div>

<% @texts.each do |text| %>
  <div class="card-body">
   <p class="card-text"><%= text.title %></p>
   <p class="card-text"><%= text.genre %></p>
  </div>
<% end %>

viewで <%= page_title(helperで定義したメソッド) %> を記述し、ページ毎にタイトルを表示するようにします。

controllerで定義した@textsはeachの繰り返し処理でtitleとgenreを表示するようにします。

まとめ

このようにcontrollerとhelperでパラメーターを元に条件分岐することで、一覧ページごとに適したタイトルとテキストを表示することができました。

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

MacbookにRuby導入

環境

MacOS:Catalina
MacbookPro初期の状態からRubyRailsMySQLをインストールします。

手順は以下です。
・Zshをデフォルトに
・Command line tools導入
・Homebrew導入
・Rubyインストール
・MySQLを導入
・Railsを導入
・Node.jsの導入
・yarnの導入

Zshをデフォルトに

# zshをデフォルトに設定
% chsh -s /bin/zsh

# ログインシェルを表示
% echo $SHELL
# 以下のように表示されれば成功
/bin/zsh

パスワード求められたらPCのパスワードを入力します。

Command line tools導入

以下をターミナルで

% xcode-select --install

インストールをクリックして進める。

Homebrew導入

Homebrewというソフトウェア管理ツールを導入します。
以下をターミナルで。

% cd  # ホームディレクトリに移動
% pwd  # ホームディレクトリにいるかどうか確認
% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # コマンドを実行

PCパスワードを求められます。
途中エンターキー入力あります。

以下でインストールできたか確認。

% brew -v

最新の状態にします。

% brew update

Homebrewの権限変更

%  sudo chown -R `whoami`:admin /usr/local/bin

Rubyインストール

Rubyの土台となるrbenvとruby-buildを、Homebrewを用いてインストール

% brew install rbenv ruby-build

rbenvをどこからも使用できるようにする。

% echo 'eval "$(rbenv init -)"' >> ~/.zshrc

zshrcの変更を反映

% source ~/.zshrc

ターミナルのirb上で日本語入力を可能にする設定を行うために、以下のコマンドでインストール

% brew install readline

すでにインストール済と出る場合あります。

readlineをどこからも使用できるようにします。

% brew link readline --force

rbenvを利用してRubyをインストール。

% RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"
% rbenv install 2.6.5

時間がかかるコマンドです。

rbenvを読み込んで変更を反映。

% rbenv rehash

Rubyのバーションを確認。

% ruby -v

MySQLを導入

% brew install mysql@5.6

MySQLの自動起動設定。
MySQLは本来であればPC再起動のたびに起動し直す必要がありますが、それは面倒であるため、自動で起動するように。

% mkdir ~/Library/LaunchAgents 
% ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents
% launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist 

mysqlコマンドをどこからでも実行できるように。

% echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc # mysqlのコマンドを実行できるようにする設定
% source ~/.zshrc #  設定を読み込むコマンド
% which mysql # mysqlのコマンドが打てるか確認する

# 以下のように表示されれば成功
/usr/local/opt/mysql@5.6/bin/mysql

MySQLの起動を確認。

% mysql.server status # MySQLの状態を確認するコマンド

# 以下のように表示されれば成功
 SUCCESS! MySQL running

Railsを導入

Rubyの拡張機能(gem)を管理するためのbundler(バンドラー)をインストールします。

% gem install bundler --version='2.1.4'

Railsをインストール。

% gem install rails --version='6.0.0'

処理に時間かかります。

rbenvを再読み込み。

% rbenv rehash

Railsが導入できたか確認。

% rails -v
Rails 6.0.0  # 「Rails」のあとに続く数字は変わる可能性があります

Node.jsの導入

Railsを動かすためにはNode.jsが必要となり、それをHomebrewを用いてインストールします。

% brew install node@14

Warningでても問題ないです。

Node.jsへのパスを設定。

% echo 'export PATH="/usr/local/opt/node@14/bin:$PATH"' >> ~/.zshrc
% source ~/.zshrc

Node.jsが導入できたか確認。

% node -v
v14.15.3 # 数値は異なる場合があります

yarnの導入

yarnのインストール。

% brew install yarn

yarnが導入できたか確認。

% yarn -v

以上で、Railsでアプリ作成ができる環境が整いました。

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

*.js.erbファイルは読み込めているのに表示されないときの対処方法

  • 「Completed 200 OK」となっているのに*.js.erbに書いたjavascriptのコードが動かないとき。
  • javascriptのエラーも発生していないとき。

原因

https://github.com/rails/rails/issues/33115
どうやらrails-ujsが求めているContent-Typeが一致しないことが原因っぽいです。

対策

【対策1】レイアウトファイル名を変更する

views/layouts/*.erb

views/layouts/*.html.erb

【対策2】render時にlayout: falseを指定する

*_controller.rb
  def create
    @sending_user = User.find(current_user.id)
    @receiving_user = User.find(params[:user_id])
    new_like_balance = @sending_user.like_balance - 1
    @sending_user.update(like_balance: new_like_balance)
    @like = Like.create(sender_id: @sending_user.id, receiver_id: @receiving_user.id)
    # ここ
    render formats: :js, layout: false
  end

どちらの方法でも解決できました。

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

初めてのRuby On Rails

Ruby On Railsとは

オープンソースのWebアプリケーションフレームワークである。RoRまたは単にRailsと呼ばれる。その名にも示されているようにRubyで書かれている。またModel View Controller(MVC)アーキテクチャに基づいて構築されている。
 
Ruby_on_Rails - Wikipedia

初めてRubyに触れるので、Progateを使って学習してみました。忘れそうなことや頭を整理するために、記載しています。
Ruby on Rails5 - Progate

Railsアプリケーションの作成

以下コマンドにより必要なファルダ等が作成される

ターミナル
rails new app_name

ツリー構造

 app_name/
    ├ app/
    ├ config/
    ├ db/
    └ その他

サーバーの起動

ターミナル
rails server

アプリ表示

サーバーを起動後に、ブラウザでlocalhost:3000にアクセス

トップページの作成(新しくページを作るコマンド)

以下のコマンドにより自動でtopというページが作成される

ターミナル
rails generate controller home top

home: コントローラー名
top: アクション名

※すでにhomeコントローラーがある場合には別アクションでは使用不可

トップ画面localhost:3000/home/topにアクセスできる

ページを表示するのに必要な3ファイル

  1. ビュー(view)
  2. コントローラー(controller)
  3. ルーティング(routing)

ビュー(view)

ビューとは、ページの「見た目」を作るためのHTMLファイルである
ブラウザとRailsのやりとりの中で、Railsからビューが返され、ページが表示される

ツリー構造

viewsフォルダの中にhomeフォルダtop.html.erbというファイルが作成される

 app/ アプリケーションのメインフォルダ
  └ view/ ビューフォルダ
    │ ┌──────────┐     
    └ │ home/       │
      │  └ top.html.erb   │
      └──────────┘

コントローラー(controller)

ページを表示するとき、Railsの中ではコントローラを経由してビューをブラウザに返している

ツリー構造

controllersフォルダの中にhome_controller.rbというファイルが作成される

 app/ アプリケーションのメインフォルダ
  └ controllers/ コントローラーフォルダ
    │ ┌──────────┐     
    └ │ home_controller.rb  │
      └──────────┘

ターミナル
rails generate controller home top

上記実行時にhome_controller.rbというコントローラのファイルが作成される
ファイルの中にtopメソッドが追加される
コントローラ内のメソッドをアクションと呼ぶ

home_controller.rb
class HomeController < ApplicationController
  def top
  end
end

アクションは、コントローラと同じ名前のビューフォルダから、アクションと同じ名前のHTMLファイルを探してブラウザに返す

ルーティング(routing)

ルーティングはブラウザとコントローラを繋ぐ役割を担う
送信されたURLに対して「どのコントローラの、
どのアクション」で処理するかを決める「対応表」のこと

ページが表示されるまでに、ルーティング→コントローラ→ビューという順で処理

トップ画面localhost:3000/home/topにアクセスした時を例に考えてみよう

  1. URL(home/top)に対応するHTMLファイルをリクエスト
ルーティング(対応表)
URL コントローラ アクション
home/top home top

2.homeコントローラーのtopアクションを呼び出す
3.URLに対応したHTMLファイルを送信

ツリー構造

configフォルダの中にroutes.rbというファイルが作成される

  config/ 設定情報に関するフォルダ
   │ ┌────────┐     
   └ │ routes.rb     │
     └────────┘

routes.rb
Rails.application.routes.draw do
  get "home/top" => "home#top"
end

まとめ

初めてRubyOnRailsを学習しました。
学習して、改めて整理すると意外とわかりやすいと思いました。これからも追加で記事を作成していきます。

参考サイト

ディレクトリ構成図を書くときに便利な記号
https://qiita.com/paty-fakename/items/c82ed27b4070feeceff6
Markdown記法 サンプル集
https://qiita.com/tbpgr/items/989c6badefff69377da7
Qiitaのテーブルの書き方についてまとめた
https://qiita.com/zakuroishikuro/items/f33929eab9d55c5bd073

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

初めてのRuby On Rails その1

Ruby On Railsとは

オープンソースのWebアプリケーションフレームワークである。RoRまたは単にRailsと呼ばれる。その名にも示されているようにRubyで書かれている。またModel View Controller(MVC)アーキテクチャに基づいて構築されている。
 
Ruby_on_Rails - Wikipedia

初めてRubyに触れるので、Progateを使って学習してみました。忘れそうなことや頭を整理するために、記載しています。
Ruby on Rails5 - Progate

Railsアプリケーションの作成

以下コマンドにより必要なファルダ等が作成される

ターミナル
rails new app_name

ツリー構造

 app_name/
    ├ app/
    ├ config/
    ├ db/
    └ その他

サーバーの起動

ターミナル
rails server

アプリ表示

サーバーを起動後に、ブラウザでlocalhost:3000にアクセス

トップページの作成(新しくページを作るコマンド)

以下のコマンドにより自動でtopというページが作成される

ターミナル
rails generate controller home top

以下のようにgenerategに省略可能

ターミナル
rails g controller home top

home: コントローラー名
top: アクション名

※すでにhomeコントローラーがある場合には別アクションでは使用不可

トップ画面localhost:3000/home/topにアクセスできる

ページを表示するのに必要な3ファイル

  1. ビュー(view)
  2. コントローラー(controller)
  3. ルーティング(routing)

ビュー(view)

ビューとは、ページの「見た目」を作るためのHTMLファイルである
ブラウザとRailsのやりとりの中で、Railsからビューが返され、ページが表示される

ツリー構造

viewsフォルダの中にhomeフォルダtop.html.erbというファイルが作成される

 app/ アプリケーションのメインフォルダ
  └ view/ ビューフォルダ
    │ ┌──────────┐     
    └ │ home/       │
      │  └ top.html.erb   │
      └──────────┘

コントローラー(controller)

ページを表示するとき、Railsの中ではコントローラを経由してビューをブラウザに返している

ツリー構造

controllersフォルダの中にhome_controller.rbというファイルが作成される

 app/ アプリケーションのメインフォルダ
  └ controllers/ コントローラーフォルダ
    │ ┌──────────┐     
    └ │ home_controller.rb  │
      └──────────┘

ターミナル
rails generate controller home top

上記実行時にhome_controller.rbというコントローラのファイルが作成される
ファイルの中にtopメソッドが追加される
コントローラ内のメソッドをアクションと呼ぶ

home_controller.rb
class HomeController < ApplicationController
  def top
  end
end

アクションは、コントローラと同じ名前のビューフォルダから、アクションと同じ名前のHTMLファイルを探してブラウザに返す

ルーティング(routing)

ルーティングはブラウザとコントローラを繋ぐ役割を担う
送信されたURLに対して「どのコントローラの、
どのアクション」で処理するかを決める「対応表」のこと

ページが表示されるまでに、ルーティング→コントローラ→ビューという順で処理

トップ画面localhost:3000/home/topにアクセスした時を例に考えてみよう

  1. URL(home/top)に対応するHTMLファイルをリクエスト
ルーティング(対応表)
URL コントローラ アクション
home/top home top

2.homeコントローラーのtopアクションを呼び出す
3.URLに対応したHTMLファイルを送信

ツリー構造

configフォルダの中にroutes.rbというファイルが作成される

  config/ 設定情報に関するフォルダ
   │ ┌────────┐     
   └ │ routes.rb     │
     └────────┘

routes.rb
Rails.application.routes.draw do
  get "home/top" => "home#top"
end

まとめ

初めてRubyOnRailsを学習しました。
学習して、改めて整理すると意外とわかりやすいと思いました。これからも追加で記事を作成していきます。

参考サイト

ディレクトリ構成図を書くときに便利な記号
https://qiita.com/paty-fakename/items/c82ed27b4070feeceff6
Markdown記法 サンプル集
https://qiita.com/tbpgr/items/989c6badefff69377da7
Qiitaのテーブルの書き方についてまとめた
https://qiita.com/zakuroishikuro/items/f33929eab9d55c5bd073

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

RailsのSQLインジェクション対策まとめ

find、find_by

普通に使っている分にはSQLインジェクションは発生しないのであまり気にする必要なし。

(参考)
ActiveRecord::Base#find is SQL injection free? - Rails - Ruby-Forum

シンプルなwhere句

#  NG
User.where("name='#{params[:name]}'")
# OK
User.where(name: params[:name])
User.where("name = ?", params[:name])

複数条件で検索

# NG
User.where("name = #{params[:name]} or age < #{params[:age]}")

# OK
User.where("name = ? or age < ?", params[:name], params[:age])

Likeを使ったあいまい検索

# NG
User.where('name LIKE ?', "%#{params[:name]}%") 

# OK
User.where('name LIKE ?', "%#{sanitize_sql_like(params[:name])}%") 

参考

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

【rails】モデルに定義するメソッドのselfについて

モデルにメソッドを書く際に、”self.メソッド名”と書くことがあるかと思います。
まだ完全には理解できていませんが、わかった内容を記事にまとめます。

モデル

cook.rb
class Cook < ApplicationRecord
  def self.ranks
    Cook.find(Like.group(:cook_id).order(Arel.sql('count(cook_id) desc')).limit(9).pluck(:cook_id))
  end
end

とある記事を参考にして、いいねのランキング機能を実装しました。
ですが、このコード省略は以下のように省略できます。

cook.rb
class Cook < ApplicationRecord
  def self.ranks
    find(Like.group(:cook_id).order(Arel.sql('count(cook_id) desc')).limit(9).pluck(:cook_id))
  end
end

何が変わったのかというと、3行目の'Cook'を外しました。

なぜ省略できるのか

今回の記事のポイントはここです。まず、2行目のselfについて。
selfとは"自分自身"という意味なので、今回の場合、selfはCookを指します。(ここでのCookは1行目class CookのCookです)
説明文書的に表現すると、「selfの実行主体はCookである」ということです。
そして3行目のCook.find~の「findの実行主体もCook」です。
つまり、

"self.メソッド名"と"self.メソッド名の中で実行しているメソッド"の実行主体が同じであれば、省略可能

となります。

最後に

そもそもほとんど記事のコピペでランキング機能を実装してしまったのがダメでしたね。コピペでコードを使うのは良くないことを実感しました笑
また、selfが表しているものについては何となく理解できてきた気はしますが、どんなときにselfを使うのかはボンヤリしています。。。

以上です!ありがとうございました!

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

Railsアプリの標準言語を日本語に設定する

環境

macOS Big Sur Ver11.2.1
Rails6.0.0
Ruby2.6.5

目的

Railsで開発中のアプリケーションの標準言語を日本語に変更すること。
これにより、エラーメッセージが日本語表示されること。
※今回のアプリではdeviseを導入しているので、deviseによるデフォルトのメッセージも日本語化します!

手順

①アプリケーション自体の言語設定を日本語にする

以下の一文を追加

config/application.rb
require_relative 'boot'

require 'rails/all'

Bundler.require(*Rails.groups)

module アプリ名
  class Application < Rails::Application

    config.load_defaults 6.0

    # 日本語の言語設定を追記する
    config.i18n.default_locale = :ja

  end
end

②必要なGemのインストール

Gemfileに以下のgemを追記して、ターミナルでbundle installする。
すべての環境に適用されるよう、最下部とかに記述する。
参考:rails-i18n

Gemfile
# アプリケーションの日本語対応用gem
gem 'rails-i18n'

ここまででアプリの設定は完了!

③deviseの言語設定

上記までの作業である程度の日本語化は完了しますが、deviseを導入してユーザー管理機能を構築しているので、deviseに関係するエラーメッセージはまだ英語表記のはず。
そこで、以下のファイルを作成し、config/locales 内に配置する。
ファイルの中身は、コピペでOK
devise-i18n
これでdeviseに関連する部分はすべて日本語化されます。

④独自に追加したカラムなどの日本語化

deviseを導入していてもusersテーブルに独自にカラムを追加している場合は、これまでの作業を実施しても独自カラムの表記は英語のままです。
おそらくほとんどの場合は上記までの作業では不十分なはず。
ここからは、自分で設定用ファイルを作って、設定も自分で記述していく必要がある。

ということで、config/localesに、「ja.yml」というファイルを作成し、その中に自分で設定を記述していく!
例えばこんな感じ。

ja:
  activerecord:
    attributes:
      user:
        nickname: ニックネーム
        birthday: 生年月日
      item:
        name: 商品名
        explain: 商品の説明

  # フォームオブジェクトを用いている場合
  activemodel:
    attributes:
      order_shipping:
        postal_code: 郵便番号
        prefecture_id: 都道府県
        token: カード情報   

attributesは属性、userはモデルのファイル名(フォームオブジェクトならクラスのファイル名)、nicknameはモデルやクラスで定義している属性名といった感じでしょうか。
モデルやクラスの継承先に気をつけて記述をしてください!
記述したらサーバーを再起動して、エラーメッセージが全て日本語化できているか確認してください。

以上、Railsアプリケーションの日本語化の手順でした。

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

[memo]Heroku利用方法

Herokuを使ってアプリケーションをデプロイする方法をまとめます。

[手順]
●はじめてデプロイをする場合
・Herokuにアカウント登録する
・Heroku CLIをインストールする
・masterブランチへcommitする
・Heroku上にアプリケーションを作成する
・MySQLを使用できるように設定する
・master.keyを環境変数として設定する
・Herokuへアプリケーションの情報をpushする
・Heroku上でマイグレーションを実行する

●デプロイ済みのアプリケーションに変更修正を加えた場合
・変更修正をcommitする
・ブランチを作成していた場合は、masterブランチへマージする
・Heroku上にpushする
(テーブルに変更を加えた場合は)Heroku上でマイグレーションを実行する

Herokuの導入

Heroku CLIをインストール

# ターミナル
% brew tap heroku/brew && brew install heroku

# 完了確認コマンド
% heroku --version

# バージョンが出力されれば成功
heroku/7.40.0 darwin-x64 node-v12.16.2

インストールについての公式ドキュメント
https://devcenter.heroku.com/articles/heroku-cli

Herokuにログイン

% heroku login --interactive
  => Enter your Heroku credentials.
# メールアドレスを入力し、エンターキーを押す
  => Email:
# パスワードを入力して、エンターキーを押す
  => Password:

Heroku上にアプリケーションを作成

アプリ作成公式ドキュメント
https://devcenter.heroku.com/articles/getting-started-with-rails6

# 作成したいアプリのディレクトリへ移動し以下コマンド実行
% heroku create アプリ名

アプリ名はアンダーバー_ではなくハイフン-を使う
例:meisai-app 等

Heroku上でMySQLを使えるようにする

# ClearDBアドオンを追加
% heroku addons:add cleardb

# 下記のように出力されれば成功
Creating cleardb on ⬢ meisai-app... free
Created cleardb-vertical-00000 as CLEARDB_DATABASE_URL
Use heroku addons:docs cleardb to view documentation


# 設定を変更
# ClearDBデータベースのURLを変数heroku_cleardbに格納
% heroku_cleardb=`heroku config:get CLEARDB_DATABASE_URL`

# データベースのURLを再設定
% heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5}
→mysql2というGemを使用しているので、DATABASE_URLの冒頭がmysql2://に変更されている

Heroku上で非公開の値を管理する

credentials.yml.encファイル

Railsにて、外部に漏らしたくない情報を扱う際に用いるファイル。
通常時は、英数字の文字列で構成された暗号文が表示され、ファイル内に何が書かれているのか分からないようになっています。このcredentials.yml.encと対になるmaster.keyが存在する場合、credentials.yml.encの暗号文を復号し、ファイル内の記述を確認できます。

master.keyファイル

credentials.yml.encの暗号文を復号する、鍵の役割を持ったファイルです。特定のcredentials.yml.encと対になっているため、その他のcredentials.yml.encへは、効果を発揮しません。
master.keyはデフォルトで.gitignoreに記述されており、Gitで管理されない仕組みになっています。

credentials.yml.encの中身を確認

アプリのディレクトリを開き、config/credentials.yml.encを開くと暗号文が確認できる

credentials.yml.encをmaster.keyによって復号

# ターミナル
% EDITOR="vi" bin/rails credentials:edit

# 確認後は、「escキー」→「:」→「q」と入力し、「enterキー」を押して credentials.yml.encを閉じる

Heroku上にmaster.keyを設置

Herokuへアプリケーションのコードをデプロイします。
しかし、その際にデプロイされるコードというのは、Gitで管理されているコードになります。
つまり、master.keyはこのままだとHeroku上へデプロイできず、credentials.yml.encもHeroku上で扱えないということになります。

そこで、Heroku上に別途master.keyを設置し、Heroku上でもcredentials.yml.encを扱えるようにする必要があります。
環境変数という「どのディレクトリ・ファイルからでも参照できる変数」を使いmaster.keyの値を設置します。

heroku configコマンドの使用
Heroku上で環境変数の参照・追加・削除等をする場合に用います。環境変数の追加であればheroku config:set 環境変数名="値"と実行します。そうすることによって、Heroku上で環境変数を追加できます。

Heroku上で環境変数を設定

# Heroku上に環境変数を設定
% heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

# Heroku上で環境変数を確認
% heroku config

# RAILS_MASTER_KEYという変数名で値が設定されていれば成功

アプリケーションをプッシュ

# Rubyのバージョン2.6.5が動作するStack(動作環境)を指定
% heroku stack:set heroku-18 -a アプリ名

# アプリケーションをHerokuへ追加
% git push heroku master

エラーが出る場合

git push heroku master 実行時に「remote: ! Could not detect rake tasks」「remote: ! ensure you can run $ bundle exec rake -P against your app」とエラーが表示される場合は、bundlerのバージョンがエラーの原因である可能性が高いので次の手順を試す。

# 現在入っているbundlerを削除[何度か確認を求められますが、「y」を入力してエンター]
% gem uninstall bundler

# bundlerのバージョン2.1.4を指定してインストール
%  gem install bundler -v '2.1.4'

# ディレクトリ内の Gemfile.lock を削除

# Gemfile.lockを作り直す
%  bundle install

# 変更をGitHubへ反映
  GitHubDesktopより、commit と push

# Herokuにアプリケーションの情報を追加
% git push heroku master

Herokuデータベースにマイグレーションの情報を反映

# Heroku上でマイグレーションを実行
% heroku run rails db:migrate

公開を確認

# Herokuにデプロイされたアプリケーションの情報を確認
% heroku apps:info

===meisai-app
Addons:         cleardb:ignite
Auto Cert Mgmt: false
Dynos:          web: 1
Git URL:        https://git.heroku.com/meisai-app.git
Owner:          sample@sample.com
Region:         us
Repo Size:      165 KB
Slug Size:      56 MB
Stack:          heroku-18
Web URL:        https:/meisai-app.herokuapp.com/

エラーが出る場合のログの確認

# ログの最後の10行を表示するためのtailオプションを使いログ表示
% heroku logs --tail --app アプリ名

# 出力結果
2020-05-08T09:03:30.572301+00:00 app[web.1]: F, [2020-05-08T09:03:30.572206 #4] FATAL -- : [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]
2020-05-08T09:03:30.572302+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e] ActionView::Template::Error (undefined method `checked' for #<Post:0x000055f0ec57ec88>):
2020-05-08T09:03:30.572303+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]      7: <div id="list">
2020-05-08T09:03:30.572303+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]      8: </div>
2020-05-08T09:03:30.572304+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]      9: <% @posts.each do |post| %>
2020-05-08T09:03:30.572305+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]     10:   <div class="post" data-id = <%= post.id %> data-check=<%= post.checked %>>
2020-05-08T09:03:30.572305+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]     11:     <div class="post-date">
2020-05-08T09:03:30.572306+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]     12:       投稿日時:<%= post.created_at %>
2020-05-08T09:03:30.572306+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]     13:     </div>
2020-05-08T09:03:30.572306+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e]
2020-05-08T09:03:30.572307+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e] app/views/posts/index.html.erb:10
2020-05-08T09:03:30.572307+00:00 app[web.1]: [52bf1bef-ea70-4df0-897a-6d0c3d925b1e] app/views/posts/index.html.erb:9
2020-05-08T09:03:30.574057+00:00 heroku[router]: at=info method=GET path="/" host=ajax-app-123456.herokuapp.com request_id=52bf1bef-ea70-4df0-897a-6d0c3d925b1e fwd="125.12.120.68" dyno=web.1 connect=1ms service=79ms status=500 bytes=1827 protocol=https
2020-05-08T09:03:31.014022+00:00 heroku[router]: at=info method=GET path="/favicon.ico" host=ajax-app-123456.herokuapp.com request_id=56eb5531-4288-48d7-9614-b58fed6deb86 fwd="125.12.120.68" dyno=web.1 connect=2ms service=2ms status=304 bytes=48 protocol=https

デプロイ済みのアプリケーションに変更を加えた場合

ファイルの変更履歴が存在する場合

# Githubのmasterブランチへcommitする(Githubデスクトップからでもok)
% git add .
% git commit -m "後から見てわかりやすいコミット名"

# 作成したコミットをHerokuへプッシュ
% git push heroku master

ファイルの変更履歴が存在しない場合

Herokuの仕様上、最新のコミット履歴が存在しない状態でgit push heroku masterコマンドを実行すると「Everything up-to-date(すでに最新の状態に更新されています)」と表示されます。その場合は空のコミットを作成してHerokuにプッシュする方法を使います。

# 空のコミットを生成
% git commit --allow-empty -m "空のcommit" 

# 作成したコミットをHerokuへプッシュ
% git push heroku master
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学び直し Rubyがミニツク Part9

今日の教科書
モジュール

モジュール

モジュールは手続きの部分だけのまとめ。
moduleでモジュールを定義。

module <モジュール名>
  <モジュールの定義>
end
module Foo
  def foo
    puts("module foo")
  end
end

クラスとモジュールの違いは
- モジュールはインスタンスを作ることができない
- モジュールは継承ができない

クラスは継承をおこなうことでスーパークラスから機能を渡せました。
しかし、クラスの継承ではスーパークラスがひとつしか指定できません。
そのため、複数のスーパークラスを継承して新しいサブクラスを作れません。そのようなクラスに異なる機能を渡したい時にRubyではモジュールを使います。このさまざまな機能を混ぜ合わせるやり方のことをMix-inと呼びます。

include

モジュールに含まれているメソッドや定数をクラスの中に取り込む。

module Greeting
  def hello
    puts("Hello, Ruby!")
  end
end
class Foo
  include Greeting
end
class Bar
  include Greeting
end
Foo.hello  #=> Hello, Ruby!
Bar.hello  #=> Hello, Ruby!

同じクラスへ複数のモジュールをインクルード

module Foo
  def foo
    puts("foo")
  end
end
module Bar
  def bar
    puts("bar")
  end
end
class Baz
  include Foo
  include Bar
end
baz = Baz.new
baz.foo  #=> foo
baz.bar  #=> bar

別のモジュールに機能を渡す

module Foo
def foo
puts("foo")
end
end
module Bar
include Foo
end
class Baz
include Bar
end
baz = Baz.new
baz.foo #=> foo

モジュール関数

module Foo
  def foo
    puts("foo")
  end
  module_function :foo
end
Foo.foo #=> foo

モジュールで定義したインスタンスメソッドはレシーバを指定した定式では呼べない。
レシーバにモジュールを指定してメソッド呼び出しを行う。モジュール内でmodule_functionの引数にメソッド名をシンボルで指定することで設定できる。このようなメソッドをモジュール関数と呼ぶ。

名前空間の提供

別の人が開発していたライブラリを使ったり、複数のメンバーで開発をおこなっていると使いたい名前が被ってしまうことがあります。
ただし、同じ名前のクラスやメソッドを定義すると、前に定義していたものを上書きしてしまいます。上書きする前に使っていたものと違う機能になってしまうと、上書きする前の機能を使っていたところでエラーが発生するかもしれません。このような名前が衝突することによって生まれる問題を避け、自由に名前を付けることができることを名前空間と呼びます。Rubyではモジュールを使うことによって、名前空間を提供することができます。

module Foo
  def foo
    puts("module foo")
  end
  module_function :foo
end
module Bar
  def foo
    puts("module bar")
  end
  module_function :foo
end
Foo.foo  #=> module foo
Bar.foo  #=> module bar

特異メソッド

特定の一つのオブジェクトだけで使えるメソッド。
メソッド定義時にオブジェクト.メソッド名で使える。

obj = Object.new
def obj.foo
  puts("foo")
end

クラスやモジュールも特異メソッドを定義できる。クラスメソッドも特異メソッドの一種。
クラスメソッドはクラスをレシーバにして呼び出す。

# def self.メソッド名; end
module Foo
  def self.foo
    puts("foo")
  end
end
Foo.foo  #=> foo

# def モジュール.メソッド名; end
module Bar
  def Bar.bar
    puts("bar")
  end
end
Bar.bar  #=> bar

モジュールには似たようなモジュール関数がある。両方ともレシーバにモジュールを指定する。
モジュール関数ではインクルードした際のインスタンスメソッドとして。
特異メソッドではインクルードしたときに渡さない。

module Foo
  def self.foo
    puts("foo")
  end
end
module Bar
  include Foo
end
Foo.foo  #=> foo
Bar.foo
NoMethodError: undefined method `foo' for Bar:Module
    from (irb):9
    from :0

モジュール関数はインクルードすることによって、インスタンスメソッドとしてインクルード先で使うことができる。

module Foo
  def foo
    puts("foo")
  end
  module_function :foo
end
class Bar
  include Foo
  def bar
    foo
  end
end
bar = Bar.new
bar.foo  #=> foo

extend

オブジェクトに対して引数に指定したモジュールのインスタンスメソッドを特異メソッドとして渡す。

module Foo
  def foo
    puts("foo")
  end
end
class Bar
end
str = ""
str.extend(Foo)
str.foo  #=> foo

Bar.extend(Foo)
Bar.foo  #=> foo

定義しているクラスやモジュールの中でも呼び出せる。

module Foo
  def foo
    puts("foo")
  end
end
module Bar
  extend Foo
end
Bar.foo  #=> foo

組み込みモジュール

Rubyに最初から組み込まれているモジュール。

Comparableモジュール

比較演算子をクラスに加えるモジュールです。ArrayやStringなどの大小関係があるオブジェクトはこのモジュールをインクルードしている。使用時には<=>演算子を定義する。
この演算子を使って比較するメソッドの集まりがComparableモジュール。

class Foo
  include Comparable
  attr_accessor :num
  def initialize(num)
    @num = num
  end
  def <=>(other)
    return @num <=> other.num
  end
end
foo = Foo.new(10)
bar = Foo.new(5)
p foo < bar
p foo > bar

Comparableリファレンス

Enumerableモジュール

繰り返しを行うクラスのためのモジュール。eachメソッドを必要とする。
これで様々なイテレータを定義できる。

class MetaSyntax
  include Enumerable
  def initialize
    @variables = []
  end
  def add(value)
    @variables << value
  end
  def each
    @variables.each do |variable|
      yield variable
    end
  end
end
ary = MetaSyntax.new
ary.add("foo")
ary.add("bar")
ary.add("baz")
ary.each do |i| puts i end
ary.each_with_index do |item, index|
  puts("これは#{index}番目の#{item}です")
end

クラス内にeachを定義すると、Enumerableモジュールをインクルードする。
そしてeach_with_indexメソッドといったメソッドが使えるようになる。

Enumerableモジュール

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

TD Toolbelt (Mac)のインストール

業務でTDを使うことになったので、コマンドラインをインストールしようとして、若干ハマったのでメモ

ホントはBrewでサクッとインストールできればいいと思って、調べてみたけど見つからず・・・・

まずはRubyのバージョンの確認

% ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

※これが原因でインストールに失敗する

$ sudo -s
% gem install td

※RootにならないとPermissionエラーになってインストールがポシャった

"xcrun clang -o conftest -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin19 -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/backward -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT    -g -Os -pipe -DHAVE_GCC_ATOMIC_BUILTINS conftest.c  -L. -L/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib -L. -L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.Internal.sdk/usr/local/lib   -arch x86_64   -lruby.2.6   "
In file included from conftest.c:1:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby.h:33:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: fatal error: 'ruby/config.h' file not found
#include "ruby/config.h"
         ^~~~~~~~~~~~~~~
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: note: did not find header 'config.h' in framework 'ruby' (loaded from '/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks')
1 error generated.
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: int main(int argc, char **argv
4: {
5:   return 0;
6: }
/* end */

こんなエラーが出た

なんとなく、Rubyのバージョンが怪しいと思って、バージョンを上げてみる

参考にしたのは、https://qiita.com/Ficus/items/bdef5c2b504d7a4008fb
めちゃ参考になりました

# ruby --version
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]

% gem install td

Parsing documentation for td-0.16.9
Installing ri documentation for td-0.16.9
Done installing documentation for msgpack, yajl-ruby, hirb, parallel, httpclient, td-client, fluent-logger, td-logger, rubyzip, zip-zip, ruby-progressbar, td after 3 seconds
12 gems installed

インストールできました

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

【Docker】エラー Could not find gem 'mysql2 (~> 0.5)' in any of the gem sources listed in your Gemfile

はじめに

Dockerの環境構築中に発生したエラーの解決した方法を記録します。
ただ、エラーは発生までの経緯で解決方法が違ってくるので、参考程度にしてください。

【エラー文】
Could not find gem 'mysql2 (~> 0.5)' in any of the gem sources listed in your Gemfile

環境

Docker version 20.10.0
docker-compose version 1.27.4

Docker内の環境

ruby:2.6.5
Rails:6.0.0
データベース:mysql

Dockerfile

docker-compose.yml
FROM ruby:2.6.5
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs\
    vim 

WORKDIR /[作成したディレクトリ名]
COPY Gemfile Gemfile.lock /[作成したディレクトリ名]/
RUN bundle install

docker-compose.yml

docker-compose.yml
version: '3'

services:
  web:
    build: .
    ports:
      - 3000:3000
    volumes:
      - '.:/[作成したディレクトリ名]'
    tty: true
    stdin_open: true

結論

結論は以下の2点を事項することで解決に至りました。
・bundle installしてmysqlをインストール
・webpackerをインストール

経緯と対応

dockerc-composeでコンテナを作成後Railsのセットアップを行いサーバーを起動した時に発生しました。
エラー内容はmysql2が見つからないという内容でした。

対応1

Dockerfileにmysqlの記述がないから当然??と思いましたが、Gemfileにはmysqlが記述されてるのでとりあえずコンテナ内でbundle installをしてインストールしてみることにしてみました。

かなり時間が経ってgemfileにインストールされました。

対応2

再びrails s -b 0.0.0.0をして起動しようと試みましたが今度はPlease run rails webpacker:install Error dockerというエラーが出ました。
これも、Dockerfileに書いてないので当然??と思いながら調べてるとwebpackerを使う為にはyarnが必要で、yarnをインストールする為には下記の記述も必要との事でDockerfileを編集してイメージ作成からやり直しました。
最後コンテナ内でwebpackerをインストールするとうまくいきました。

対応2の手順

Dockerfileにyarnを追記します。

Dockerfile
FROM ruby:2.6.5
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs\
    yarn \ ←ここ
    vim 

WORKDIR /exam
COPY Gemfile Gemfile.lock /exam/
RUN bundle install

rails webpacker:installのコマンドを入力してwebpackerをインストールします。

ターミナル
root@c21d03f52523:/exam# rails webpacker:install
.
.
.
省略
Webpacker successfully installed ? ?
root@c21d03f52523:/exam# 

サーバーを起動します。

ターミナル
root@c21d03f52523:/exam# rails s -b 0.0.0.0
=> Booting Puma
=> Rails 6.1.2.1 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.2.1 (ruby 2.6.5-p114) ("Fettisdagsbulle")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 156
* Listening on http://0.0.0.0:3000
Use Ctrl-C to stop

かなり時間がかかりましたが、インストール後rails s -b 0.0.0.0で見事サーバーが立ち上がりました!

この記事では上記のエラー解決のみの内容ですがこの後データベースを作ってDocker内での開発環境を整えて行きます。
もしこの先にもご興味あれば下記の記事を参考にしてみてください。

【Docker】Ruby2.6.5とRails6.0.0とmysql DockerComposeで環境構築
後日更新予定

最後に

Dockerについて完全に理解できておらず、今回の対応も対処療法ですのでこれからも継続学習が必要です。
万が一情報が間違っている場合ご指摘していただけると幸いです。

参考

https://qiita.com/matata0623/items/2ff3125d2cdbd5c13528

https://qiita.com/anx/items/88cb4bbf67ff6c046e32

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

正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した

元になっているのは『プログラミング作法』に載っている、ロブ・パイクが書いたコード(以下「ロブ・パイク版」)ですが、40行以内で正規表現エンジンを構築 | POSTD の方を見て書いてみました。JavaScript の方が読み慣れているのと、リポジトリにテストコードが用意されていたためです。

def drop(text, n)
  text.chars.drop(n).join()
end

def empty?(str)
  str.nil? || str.empty?
end

def match_one(pattern_char, char)
  return true if empty?(pattern_char)
  return false if empty?(char)

  pattern_char == "." || pattern_char == char
end

# ロブ・パイク版では match
def search(pattern, text)
  if pattern[0] == "^"
    match(drop(pattern, 1), text)
  else
    match(".*" + pattern, text)
  end
end

# ロブ・パイク版では matchhere
def match(pattern, text)
  return true if empty?(pattern)
  return true if empty?(text) && pattern == "$"

  case pattern[1]
  # when "?"
  #   match_question(pattern, text)
  when "*"
    match_star(pattern, text)
  else
    match_one(pattern[0], text[0]) &&
      match(drop(pattern, 1), drop(text, 1))
  end
end

# def match_question(pattern, text)
#   (
#     match_one(pattern[0], text[0]) &&
#     match(drop(pattern, 2), drop(text, 1))
#   ) ||
#     match(drop(pattern, 2), text)
# end

def match_star(pattern, text)
  (
    match_one(pattern[0], text[0]) &&
    match(pattern, drop(text, 1))
  ) ||
    match(drop(pattern, 2), text)
end

たったこれだけで正規表現の基本的な機能が実現できるのすごい&おもしろいですね。

ロブ・パイク版では . ^ $ * だけをメタ文字としてサポートする最低限の実装をまず示し、演習問題で ? などを追加する流れになっています(なので、上に貼ったコードでは ? の部分をコメントアウトしてみました)。


書籍『ビューティフルコード』では「1章 正規表現マッチャ」をブライアン・カーニハンが書いており、ロブ・パイク版について解説しています。

このコードが生まれた経緯について書かれていて、ここもおもしろい。

 1998年、ロブ・パイク(Rob Pike)と私は、『プログラミング作法』(原題『The Practice of Programming』、Addison-Wesley刊)という本を執筆していました。(略)
 問題は、既存の正規表現パッケージはどれも大き過ぎたということでした。(略)それでは教育用に適しているとは到底言えません。
 そこで私はロブに、正規表現の基本的な考え方が読み取れる最小限の、ただしそれでいて有用でつまらなくないパターンが書けるようなパッケージを探そうと提案しました。コードが本の1ページに納まるようなら理想的だと思いました。
 ロブは自分の部屋に入って行きました。今思い返してみると、1〜2時間も経たないうちだったと思います。彼は30行のCのコードを携えて部屋から出て来ました。そのコードが、『プログラミング作法』の第9章に掲載されているものです。(略)

参考

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

Ruby 3.x, Rails 6.x, MySQL 8.x の Docker 環境構築。

概要

Docker と docker-compose を使い、アプリケーションサーバを Ruby 3.x, Ruby on Rails 6.x、DB サーバを MySQL 8.x でコンテナの構築するまでの手順となります。

なお、この記事ではセキュリティについての考慮は一切していません。

Windows 10 の WSL2 - Ubuntu 18.04 と Mac で確認していますが、後述する理由により Mac の方がお勧めです。

PostgreSQL の方が好みの方は、以下のページをご確認ください。
cf. クィックスタート: Compose と Rails

前提

下記の環境が設定されていること。

Windows 10

  • WSL2 Ubuntu 18.04+
  • Docker
  • docker-compose
  • MySQL Client(必要に応じて)

Mac

  • Docker
  • docker-compose
  • Docker Desktop for Mac
  • MySQL Client(必要に応じて)

また、Docker のサービスが起動済みであること。

初期ファイル構成

以下のファイルから Rails プロジェクトを新規に作成します。

.
├── docker
│   ├── app
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
│   └── db
│       ├── Dockerfile
│       ├── conf.d
│       │   └── my.cnf
│       └── initdb.d
│           └── init.ddl.sql
├── scripts
│   └── wait-for-it.sh
├── docker-compose.yml
├── Gemfile
└── Gemfile.lock

初期ファイル設定

docker-compose.yml

注意点は app 側の build: context: で基準になるフォルダを root として、Dockerfile のパスを指定しているところです。
これは Dockerfile 内で Gemfile をコピーする必要があるのですが、build: ./docker/app としてしまうとフォルダを遡って Gemfile の操作ができないため、起点を root にしています。
Dockerfile が root にあれば関係ないのですが、今回は docker フォルダ下にしているため、このような対応となります。

また app の command: で先述の wait-for-it.sh を利用し、DB が起動するまで rails server を立ち上げないようにしています。

docker-compose.yml
version: "3.3"

services:
  db:
    container_name: "db"
    build: ./docker/db
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d
      - ./docker/db/initdb.d:/docker-entrypoint-initdb.d
    networks:
      - backend

  app:
    container_name: "app"
    build:
      context: ./
      dockerfile: ./docker/app/Dockerfile
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      BINDING: 0.0.0.0
    tty: true
    depends_on:
      - "db"
    command: ["./scripts/wait-for-it.sh", "db:3306", "--", "bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
    volumes:
      - .:/app
    networks:
      - frontend
      - backend

networks:
  frontend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.10.0/24
  backend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.20.0/24

app 用設定ファイル

docker/app/Dockerfile

ここで Gemfile および Gemfile.lock をホスト(ローカル)からゲスト(コンテナ)にコピーしています。

docker/app/Dockerfile
FROM ruby:3.0

ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends sudo curl apt-transport-https wget build-essential libpq-dev nodejs default-mysql-client

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install --no-install-recommends -y yarn

RUN apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN mkdir /app
WORKDIR /app
COPY Gemfile /Gemfile
COPY Gemfile.lock /Gemfile.lock
RUN bundle install
COPY . /app

COPY docker/app/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

docker/app/entrypoint.sh

こちらは以下のサイトの entrypoint.sh からいただきました。
cf. Quickstart: Compose and Rails

docker/app/entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

db 用設定ファイル

docker/db/Dockerfile

docker/db/Dockerfile
FROM mysql:8.0

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends locales && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    locale-gen ja_JP.UTF-8

RUN sed -i -E 's/# (ja_JP.UTF-8)/\1/' /etc/locale.gen && locale-gen

ENV LANG ja_JP.UTF-8
ENV TZ Asia/Tokyo

docker/db/conf.d/my.cnf

my.cnf は文字コード指定が中心ですが、今回はシンプルに Rails の実行のみを考えているため、default_authentication_plugin=mysql_native_password で認証プラグインを変更しておきます。

docker/db/conf.d/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
default-storage-engine=INNODB
explicit-defaults-for-timestamp=1
general-log=1
general-log-file=/var/log/mysql/mysqld.log

default_authentication_plugin=mysql_native_password

[mysqldump]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4

docker/db/initdb.d/init.ddl.sql

データベースを development 以外に test 用も作成する場合のファイルです。
実際は test 用データベースは rake db:create で作成されるはずですのでなくても問題ないと思われます。
コピー先の /docker-entrypoint-initdb.d フォルダではシェルの実行も可能なので、組み込み次第ではいろいろと対応できるようです。

docker/db/initdb.d/init.ddl.sql
CREATE DATABASE IF NOT EXISTS `app_test`;
GRANT ALL ON app_test.* TO 'user'@'%';

Gemfile

Gemfile

初期は Rails のバージョン指定のみとなります。

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'

Gemfile.lock

初期状態は空ファイルとなります。

Gemfile.lock

初期起動までの手順

rails new

Docker のサービスが起動しているか確認の上、docker-compose.yml があるフォルダで database を MySQL に指定して rails new を実行します。

$ docker-compose run app rails new . --force --database=mysql

問題なく実行が完了すると、実行したフォルダに Rails アプリのファイル群が作成されます。
このとき、Mac の場合は実行したユーザーの権限でファイルが作成されますが、WSL の場合は root 権限となり、そのままではファイルの更新が行えません。

cf. 【Docker】 WSL 2 を利用したコンテナー内開発で権限をどう設定するべきか

根本的な解決ではないとは思いますが、とりあえず以下のコマンドで権限を実行ユーザーに振り替えて対応することは可能です。
ただし、ここだけではなく、scaffold など rails のコマンドでファイルを作成・編集するごとに権限を書き換える必要があります。
(これが WSL よりも Mac をお勧めする理由です)

$ sudo chown -R $USER:$USER .

この時点で、作成された config/database.yml を編集し、MySQL へのアクセス設定を変更します。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: user
  password: password
  host: db

docker-compose build, up

build でサービスを作成し、問題がなければ up でコンテナを起動します。
この際、--no-cache オプションを付けるのは、bundle install 実行時に gem ファイルを巧く取り込めない場合がある(らしい)ためです。

$ docker-compose build --no-cache
$ docker-compose up -d

この時、db および app がほぼ同時に立ち上がりますが、docker-compose.yml で記述した通り、app の rails server は MySQL サーバとの接続が確立するまで実行されないようになっています。
それぞれのコンテナのログは docker logs で確認できるため、以下のように確認して下さい。

$ docker logs app # アプリケーションサーバのログ
$ docker logs db # DB サーバのログ

実行に問題がなければ、ブラウザまたは curl などで http://localhost:3000/ にアクセスすることで、いつもの Rails の初期画面が表示されます。

image.png

scaffold 作成と実行の確認

scaffold で MVC を作成して動作が可能か確認します。

$ docker-compose run app rails g scaffold user name:string email:string

WSL で操作している場合はファイル権限の変更をしてください。

$ sudo chown -R $USER:$USER .

db:migrate でテーブルを作成します。

$ docker-compose run app rails db:migrate

ブラウザで http://localhost:3000/users にアクセスすることで scaffold で作成した Rails 標準の UI が表示され、CRUD の一連の操作が可能なことが確認できます。

http://localhost:3000/users/new
image.png

http://localhost:3000/users
image.png

実際の DB を確認したい場合は MySQL Client が入っていればコマンドで確認できます。
(設定を変えていない場合は、user アカウントのパスワードは password となります)

$ mysql -u user -h 127.0.0.1 -D app_development -p
mysql> show create table users;
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                              |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from users;
+----+------+-----------------+----------------------------+----------------------------+
| id | name | email           | created_at                 | updated_at                 |
+----+------+-----------------+----------------------------+----------------------------+
|  1 | test | aaa             | 2021-02-25 15:56:02.123864 | 2021-02-25 15:56:02.123864 |
|  2 | test | aaa@example.com | 2021-02-25 16:38:33.311981 | 2021-02-25 16:38:33.311981 |
+----+------+-----------------+----------------------------+----------------------------+
2 rows in set (0.00 sec)

今回は以上となります。

参考資料

以下の記事、情報を参考にさせていただきました。
ありがとうございます。

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

includeメソッド学びメモ書き

includesメソッドとは

アソシエーションの関連付けを事前に取得しN +1問題を解決してくれるメソッドのこと。

N + 1問題

SQLが大量に発行されることで動作が重くなる問題のこと

なぜSQLが大量に発行されるのか?

(例)

user_idにuserテーブルのidを外部キーとして参照するfruitsテーブルがあったとして・・・

fruitsテーブル

id name user_id
1 りんご
2 みかん
3 バナナ
4 マンゴー

userテーブル

1 2
1 タケシ
2 サトシ
3 カスミ

UserモデルはFruitモデルに対して1対他の関係になっている

Userモデル
class User < ActiveRecord::Base
    has_many :fruits
end
Fruitモデル
class Fruits < ActiveRecord::Base
    belongs_to :user
end

この関係で、「全てのユーザーのもってるフルーツを一覧表示する」コードを以下のように書くと、
userテーブルに1回アクセスが行われる(=SQLが発行される。)

controller
@users = User.all

また、ビューで以下のように記述して一覧表示すると、user1つずつに対してfruitsテーブルにアクセスするので、
userテーブルのレコードの数だけ(3回)SQLが発行される。

view
@users.each do |user|
  user.fruits.each do |fruit|
    fruit.name
  end
end

この1+3回のSQL発行が1+N問題となる。

includeメソッド

上記の状態で、usersテーブルにアクセスする際に、fruitsテーブルの情報ももってきてくれるのがincludeメソッド。  
引数にアソシエーションで定義した関連名を指定して定義する。(NOTテーブル名)

Userモデル
class User < ActiveRecord::Base
    has_many :fruits #←関連名
end
controller
@users = User.all #変更

@users = User.include(:fruits) #変更後

これでusersテーブルへのアクセス1回、fruitsテーブルへのアクセスは1回の計2回に変更される。

コントローラーの時点で関連テーブルの情報を一括取得することで、ビューが動いた時に必要以上のSQLが発行されない。

includeメソッドの親子関係

次に、このFruitモデルのidをさらに外部キーにする関連Juiceモデルがある場合は、以下のように書くことで、親子の子の情報もまとめて取得することができる(便利!)

Userモデル、Fruitモデル、Juiceモデル
# User.rb
class User < ActiveRecord::Base
    has_many :fruits
end

# fruit.rb
class Fruit < ActiveRecord::Base
    belongs_to :user
    has_many :Juices
end

# juice.rb
class Juice < ActiveRecord::Base
    belongs_to :fruit
end
users_controller.rb
@users = User.includes(fruits: :juices)

上記を実行すると・・・

実行結果
SELECT `users`.* FROM `users`
SELECT `fruits`.* FROM `fruits`  WHERE `fruits`.`user_id` IN (1, 2, 3)
SELECT `juices`.* FROM `juices`  WHERE `juices`.`fruit_id` IN (1, 2, 3, 4)

3回のアクセスで全て取得できる!!  

お疲れ様でした。

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

式展開ってなんなの?

式展開とは?

  • 文字列の中に式を入れることができる機能
  • 書き方は文字列中で#{式}とするだけ
  • 文字列を作るときにダブルクォーテーション"で囲む

記述例と出力例

記述例
"今日で#{20+1}歳になりました"
表示例
 今日で21歳になりました

ターミナルでirbを使用して確認してみる

出力成功例
irb(main):001:0> "今日で#{20+1}歳になりました"
=> "今日で21歳になりました"


出力失敗例
# シングルクォーテーションだと式展開されない
irb(main):002:0> '今日で#{20+1}歳になりました'
=> "今日で\#{20+1}歳になりました"

シングルコーテーションは不可
シングルクォーテーション'で囲んだ場合は式展開が行われません

補足

式展開のに関して

Rubyにおける「式」とは
文字列や数値の他に

  • メソッドの呼び出し
  • 変数
  • 演算子式

などが含まれます。

すなわち、"文字列", 1000, (1 + 5)などはすべて式

Rubyは記述をすべて式と捉えます。

ターミナル【例】irb
irb(main):001:0> "記述はすべて式"

# 式の結果
=> "記述はすべて式"

irbでとくにメソッドも使用せずに文字列や数値の値を確認できていた
これは、式の結果が表示されていたからこそ可能となっていた

irbを使用して想定通りの出力がされない場合は、以下の可能性がある

  • irbを起動できていない(最初にirbとコマンドを入力しないとirbは起動しません)
  • 式展開 #{ }が全角になっている(式展開は半角で記述します)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Strageでファイル形式を指定して保存

今回はrailsでの動画投稿機能実装のアウトプットです。

動画投稿の際にActive Strageでmp4ファイルのみ保存させたかったので、自己流ではありますが今回はcontroller側で制御する形で実装しました。

posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    if @post.valid? && @post.video.content_type == "video/mp4"
      @post.save
      redirect_to posts_path
    else
      render :new
    end
  end

  private
  def post_params
    params.require(:post).permit(:title, :video).merge(user_id: current_user.id)
  end
end
models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_one_attached :video

  validates :title, :video, presence: true
end

今回はcontent_typeで保存するデータのファイル形式を指定できるということに気づくことが出来ました。
ですが本当は他の書き方で、より効率的に、ファイルサイズ等もまとめて指定して書ける方法があるのだと思います。
モデル側で書く方が正しいのかもしれません。

まだまだ自分は初心者なので、
よりDRYな実装ができるよう更に学習を深めて改善に努めていきたいと思います。

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