20210226のRailsに関する記事は30件です。

プルダウンの作り方

プルダウンの作り方

select要素とoption要素を使用します。

<select>
  <option>スポーツを選択してください</option>
  <option>野球</option>
  <option>サッカー</option>
 <option>バスケットボール</option>
 <option>バレーボール</option>
  <option>水泳</option>
 <option>柔道</option>
</select>

select要素は・・・セレクトボックス
option要素は・・・プルダウンの選択肢

これでプルダウンが簡単に作れます。

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

【Rails】ページネーションの実装

ページネーションとは

一覧表示画面に表示するレコード件数が多くなると、目視での確認が困難になってしまいます。この問題を避けるための方法がページネーションです。
ページネーションとは、レコード件数が一定数を超えた場合に複数のページに分割して表示を行うことです。

kaminariのインストール

Railsでページネーションを実現するgemとして、kaminariがあります。今回はこれを用いてページネーションを進めていきます。
まずは、gemファイルに以下を追加。

Gemfile
gem 'kaminari'

bundleコマンドでkaminari gemをインストールします。

$ bundle

ページ番号に対応する範囲のデータを検索するようにする

PostsControllerのindexアクションを変更します。
このアクションでは、ページ番号がparams[:page]として渡されることを前提とします。ページ番号を用いて、データ範囲を検索する機能は、kaminariのpageスコープで簡単に行えます。
indexアクションを以下のように変更します。

app/controllers/posts_controller.rb
  def index
    @posts =Post.page(params[:page])
  end

ビューにページネーションのための情報を表示する

ビューには、ページネーションための以下の情報を表示するようにします。

  1. 現在どのページを表示しているかの情報
  2. 他のページに移動するためのリンク
  3. 全データが何件なのかという情報

1と2のためにはpaginate、3のためにはpage_entries_infoというkaminariヘルパーメソッドが利用できます。

それでは、ビューを以下のように変更します。


= link_to '新規投稿', new_post_path, class: 'btn btn-primary'

.mb-3
  =paginate @posts
  =page_entries_info @posts

table.table.table-hover

ページネーションの情報を表示する枠として、mb-3というCSSクラスのついたdiv要素を用意し、paginateヘルパー、page_entries_infoヘルパーを呼び出しています。

翻訳ファイルの追加

kaminariが用意している翻訳ファイルはen(英語)のみなので、以下の内容でjaの翻訳ファイルを用意します。

config/kaminari.ja.yml
ja:
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: " &rsaquo;"
      truncate: "&hellip;"
  helpers:
    page_entries_info:
      one_page:
        display_entries:
          zero: "%{entry_name}がありません"
          one: "1件の%{entry_name}が表示されています"
          other: "&{count}件の%{entry_name}が表示されています"
      more_pages:
        display_entries: "全%{total}件中 %{first}&nbsp;-&nbsp;%{last}件の%{entry_name}が表示されています"

デザインの調整

機能は実装できましたが、見た目がいまいちなので修正します。paginateヘルパーが表示に使用するパーシャルテンプレートをアプリ内に用意し、それを自由にカスタマイズすることができます。パーシャルテンプレートはkaminariの提供するジェネレータで生成しますが、その際にテーマ(Thema)と呼ばれるデザインの種類を指定して、好みのパーシャルテンプレートを生成できます(https://github.com/amatsuda/kaminari_themes)。

今回はbootstrap4というテーマのパーシャルテンプレートを生成します。

$ bin/rails g kaminari:views bootstrap4

app/views/kaminari配下にいくつかビューテンプレートが追加されます。

そして、localhost:3000にアクセスすると…
50D4FAEA-8D4D-4627-9560-B905C718ED96.png

無事に格好良くなっています!

参考

現場で使える Ruby on Rails 5速習実践ガイド

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

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で続きを読む

EC2インスタンスにRails + MySQL環境構築

はじめに

AWSEC2インスタンス立ち上げたところからのサーバー構築手順です。

・設定用のツールをインストール
・Node.jsをインストール
・Yarnをインストール
・rbenvとruby-buildをインストール
・Rubyをインストール
・MariaDBをインストール(サーバーにDB設定する場合)
・Githubとの接続
・Unicornをインストール
・Githubからコードをクローン
・gemをインストール
・環境変数の設定
・アセットファイルをコンパイル
・本番環境でRailsを起動

ここまではアプリケーションサーバー構築。
WEBサーバー構築は以下。
EC2サーバーにRails + Nginx

設定用のツールをインストール

yumというコマンドを使ってこのサーバに元々あるプログラムをアップデートします。

$ sudo yum -y update

環境構築に必要なパッケージを諸々インストール。(長いコマンド)

$ sudo yum  -y install git make gcc-c++ patch libyaml-devel libffi-devel libicu-devel zlib-devel readline-devel libxml2-devel libxslt-devel ImageMagick ImageMagick-devel openssl-devel libcurl libcurl-devel curl

Node.jsをインストール

サーバーサイドで動くJavaScriptのパッケージです。

$ sudo curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
$ sudo yum -y install nodejs

Yarnをインストール

Railsに搭載されているJavaScriptのパッケージを管理するためのパッケージマネージャです。

$ sudo yum -y install wget
$ sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo
$ sudo yum -y install yarn

rbenvとruby-buildをインストール

rbenvruby-buildは、Rubyのバージョンを管理する際に組み合わせて使うツールになります。これらはRubyをインストールする前に、インストールする必要があります。
ruby-buildrbenvのプラグインであり、ruby-buildによってRubyの様々なバージョン(2.0.0など)をインストールすることができます。
rbenvを使用することでrubyのバージョンを切り替えることできます。

#rbenvのインストール
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv

#パスを通す(どのディレクトリからもアプリケーションを呼び出せる状態にする)
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile

#rbenvを呼び出すための記述
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

#.bash_profileの読み込み
$ source .bash_profile

#ruby-buildのインストール
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

#rehashを行う
$ rbenv rehash

Rubyをインストール

Rubyのインストールには時間がかかります。

$ rbenv install 2.6.5
$ rbenv global 2.6.5 #EC2インスタンス内で使用するRubyのバージョンを決める
$ rbenv rehash  #rehashを行う
$ ruby -v # バージョンを確認

AWS RDSを使用する場合はMariaDB関連操作は不要。

MariaDBをインストール(サーバーにDB設定する場合)

Amazon Linux 2を利用している場合、MariaDByum コマンドからインストールすることができます。

$ sudo yum -y install mysql56-server mysql56-devel mysql56 mariadb-server mysql-devel

○○○利用できませんと表示されますが問題ありません。

データベースを起動

データベースを起動するためにsystemctlコマンドを利用します。これは、Amazon LinuxCentOSに含まれているもので、インストールしたソフトウェアの起動を一括して行えるツールです。

$ sudo systemctl start mariadb
$ sudo systemctl status mariadb #起動できたか確認

データベースのrootパスワードの設定

yum でインストールしたMariaDBには、デフォルトで root というユーザーでアクセス出来るようになっていますが、パスワードは設定されていません。パスワードを設定していきます。

$ sudo /usr/bin/mysql_secure_installation

対話形式でパスワードを設定します。

データベースへの接続確認

$ mysql -u root -p

パスワードを入力するように表示されるので、先程設定したパスワードを入力して、Enterしてください。

Githubとの接続

EC2サーバのSSH鍵ペアを作成

$ ssh-keygen -t rsa -b 4096

Generating public/private rsa key pair.
Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/ec2-user/.ssh/id_rsa.
Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub.
The key fingerprint is:
3a:8c:1d:d1:a9:22:c7:6e:6b:43:22:31:0f:ca:63:fa ec2-user@ip-172-31-23-189
The key's randomart image is:
+--[ RSA 4096]----+
|    +            |
| . . =           |
|  = . o .        |
| * o . o         |
|= *     S        |
|.* +     .       |
|  * +            |
| .E+ .           |
| .o              |
+-----------------+

全て何も入力せずにEnterキーで進んでください。

生成されたSSH公開鍵を表示し、値をコピーします。

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2E......

catで表示させた公開鍵を、Githubにアクセスして登録していきます。
https://github.com/settings/keys

①Titleは自由に記入
②Keyに公開鍵をペースト
Add SSH Keyを押して保存

Githubに鍵を登録できたら、SSH接続できるか以下のコマンドで確認

$ ssh -T git@github.com
Hi <Githubユーザー名>! You've successfully authenticated, but GitHub does not provide shell access.

Permission denied (publickey). と表示された場合は、SSH鍵の設定が間違っているので、作業を確認してください。

アプリケーションサーバの設定

アプリケーションサーバとは、ブラウザからの「リクエスト」を受け付けRailsアプリケーションを実際に動作させるソフトウェアのことです。

Unicornをインストール

ローカル環境のGemfile を編集

group :production do
  gem 'unicorn', '5.4.1'
end

ローカルのターミナルで

% bundle install

config/unicorn.rbを作成し、内容を以下のように編集

#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory app_path

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/tmp/pids/unicorn.pid"

#ポート番号を指定
listen 3000

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

#以下は応用的な設定なので説明は割愛

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

Githubからコードをクローン

EC2サーバーにSSH接続後に以下の操作

#mkdirコマンドで新たにディレクトリを作成
$ sudo mkdir /var/www/

#作成したwwwディレクトリの権限をec2-userに変更
$ sudo chown ec2-user /var/www/

#移動
$ cd /var/www/

#Githubから「リポジトリURL」を取得しクローンします
$ git clone https://github.com/<ユーザー名>/<リポジトリ名>.git

Swap(スワップ)領域作成

Swap領域は、メモリが使い切られそうになった時にメモリの容量を一時的に増やすために準備されるファイルです。

EC2はデフォルトではSwap領域を用意していないため、これを準備することでメモリ不足の処理エラーを防ぎます。

$ cd
$ sudo dd if=/dev/zero of=/swapfile1 bs=1M count=512
# しばらく待って、以下のように表示されれば成功
512+0 レコード入力
512+0 レコード出力
536870912 バイト (537 MB) コピーされました、 7.35077 秒、 73.0 MB/秒

$ sudo chmod 600 /swapfile1
$ sudo mkswap /swapfile1
# 以下のように表示されれば成功
スワップ空間バージョン1を設定します、サイズ = 524284 KiB
ラベルはありません, UUID=74a961ba-7a33-4c18-b1cd-9779bcda8ab1

$ sudo swapon /swapfile1

#右へ長いコマンドです
$ sudo sh -c 'echo "/swapfile1  none        swap    sw              0   0" >> /etc/fstab'

gemをインストール

rbenvでインストールされたRubyが使われているかチェック

$ cd  /var/www/<アプリ名>
$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]

本番環境でgemを管理するためのbundlerをインストールして、bundle installを実行

ローカル環境でバージョン確認。

#ローカル環境 アプリのディレクトリで以下を実行
% bundler -v
Bundler version 2.1.4

ローカルと同じバージョンをEC2サーバー上で以下のコマンド

# ローカルで確認したbundlerのバージョンを導入する
$ gem install bundler -v 2.1.4

$ bundle install

環境変数の設定

環境変数は、Railsからは ENV['<環境変数名>'] という記述でその値を利用することができます。今回は、 SECRET_KEY_BASE という環境変数を指定していきます。

$ rake secret
69619d9a75b78f2e1c87ec5e07541b42f23efeb6a54e97da3723de06fe74af29d5718adff77d2b04b2805d3a1e143fa61baacfbf4ca2c6fcc608cff8d5a28e8d

この長い英数の羅列は、この後利用するのでコピーしておきます。

環境変数は /etc/environment というファイルに保存することで、サーバ全体に適用されます。

$ sudo vim /etc/environment
#前章で設定したデータベースのrootユーザーのパスワードを入力
DATABASE_PASSWORD='データベースのrootユーザーのパスワード'
SECRET_KEY_BASE='先程コピーしたsecret_key_base'

書き込みができたら esc(エスケープキー)を押下後、:wqと入力して内容を保存します。
保存できたら環境変数を適用するために一旦ログアウト。

$ exit
logout
Connection to 52.xx.xx.xx closed.

再度ログイン。

$ ssh -i [ダウンロードした鍵の名前].pem ec2-user@[作成したEC2インスタンスと紐付けたElastic IP]

環境変数が適用されているか確認

$ env | grep SECRET_KEY_BASE
SECRET_KEY_BASE='secret_key_base'

$ env | grep DATABASE_PASSWORD
DATABASE_PASSWORD='データベースのrootユーザーのパスワード'

ポートを開放

config/unicorn.rblisten 3000 と記述しましたが、これはRailsのサーバを3000番ポートで起動するということを意味します。

EC2インスタンスのセキュリティグループを編集しカスタムTCPでポート3000を許可してください。

database.ymlの本番環境の設定

config/database.yml(ローカル)を以下のように設定。

  <<: *default
  database: ~~~(それぞれのアプリによって異なる)
  username: root
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: /var/lib/mysql/mysql.sock

RDSなど使用の場合はまた異なります。

ローカルでの編集をコミットして、GitHubにプッシュし、EC2へ再度クローンします。

$ git pull origin master

データベースを作成しマイグレーションを実行

EC2サーバー上で以下のコマンド。

$ rails db:create RAILS_ENV=production
Created database '<データベース名>'

$ rails db:migrate RAILS_ENV=production

もしデータベースが起動していないときは

sudo systemctl start mariadb

アセットファイルをコンパイル

本番モードでは、事前にアセットをコンパイルする必要があります。

$ rails assets:precompile RAILS_ENV=production

本番環境でRailsを起動

unicorn のgemをインストールしていると unicorn_rails というコマンドが使えるようになっています.
ローカルでいうrails sにあたります。

Unicornのプロセスを確認

$ ps aux | grep unicorn
ec2-user 17877  0.4 18.1 588472 182840 ?       Sl   01:55   0:02 unicorn_rails master -c config/unicorn.rb -E production -D
ec2-user 17881  0.0 17.3 589088 175164 ?       Sl   01:55   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

「unicorn_rails master」と表示されているプロセスがあればkillコマンドで停止してください。

$ kill <確認したunicorn rails masterのPID>

unicornを起動

$ cd /var/www/[リポジトリ]
$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D

RAILS_SERVE_STATIC_FILES=1は、コンパイルされたアセットをRailsが見つけられるような指定です。

ブラウザで確認

http://<Elastic IP>:3000/で正常に表示されれば成功です。

うまく表示されない場合

エラーが起こっているのでログを確認。

$tail -f /var/www/<アプリ名>/log/production.log

または/var/www/<リポジトリ名>/log/unicorn.stderr.logをlessまたはcatコマンドで確認。

設定は以上です。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Active Storageで追加した画像をjavascriptを使ってプレビューする方法

はじめに

active Storageを使って保存する画像を保存前に表示させて確認したいと思って調べたもののまとめです。

前提

すでに画像保存機能は出来上がっている

javascriptファイルの追加

app/javascript/packsの中にプレビュー用のファイルを追加します。今回、名前はpreview.jsとします。

preview.jsの読み込み

前述で追加したpreview.jsをpacks内application.jsで読み込みます。
app/javascript/packs/application.js

require('./preview')

読み込まれたときに動作する関数を定義

まずは動作ようの記述を。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
});

画像を表示するビューファイルに画像表示スペースをつくる

app/views/messages/_form.html.erb

<div id="image-list"></div>

HTMLの要素を、JavaScript側で取得

続いて、今記述した画像を表示するためのHTMLの要素を、JavaScript側で取得します。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');
});

input要素で値の変化が起こったときに呼び出される関数を定義

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');
  document.getElementById('message_image').addEventListener('change', function(e){
    console.log(222);
  });
});

念の為ファイルを選択する」をクリックして適当な画像を選択し、以下のように222と表示できているかを確認しておきましょう。

inputの中にある画像を取得

発火したイベントeの中の、targetの中の、filesという配列に格納されています。

次に取得した画像の情報は、変数fileとして宣言します。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');

  document.getElementById('message_image').addEventListener('change', function(e){
    const file = e.target.files[0];
  });
});

URLオブジェクトのcreateObjectURLメソッドを呼び出し、変数fileを引数として渡す

次はcreateObjectURLメソッドを引数として渡します。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');

  document.getElementById('message_image').addEventListener('change', function(e){
    const file = e.target.files[0];
    const blob = window.URL.createObjectURL(file);
  });
});

実際に画像を表示できるようにする。

preview.jsを以下のように編集して画像を表示します。

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');

  document.getElementById('message_image').addEventListener('change', function(e){
    const file = e.target.files[0];
    const blob = window.URL.createObjectURL(file);

    // 画像を表示するためのdiv要素を生成
    const imageElement = document.createElement('div');

    // 表示する画像を生成
    const blobImage = document.createElement('img');
  });
});

画像URLをimg要素のsrc属性に設定

setAttribute()というdocumentオブジェクトを用いて、画像情報をsrc属性に指定します。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
const ImageList = document.getElementById('image-list');

  document.getElementById('message_image').addEventListener('change', function(e){
    const file = e.target.files[0];
    const blob = window.URL.createObjectURL(file);

    // 画像を表示するためのdiv要素を生成
    const imageElement = document.createElement('div');

    // 表示する画像を生成
    const blobImage = document.createElement('img');
    blobImage.setAttribute('src', blob);
  });
});

appendChild()

指定した親要素の中に要素を追加するappendChildメソッドを使って、HTML要素を追加します。
app/javascript/packs/preview.js

document.addEventListener('DOMContentLoaded', function(){
  const ImageList = document.getElementById('image-list');

  document.getElementById('message_image').addEventListener('change', function(e){
    const file = e.target.files[0];
    const blob = window.URL.createObjectURL(file);

    // 画像を表示するためのdiv要素を生成
    const imageElement = document.createElement('div');

    // 表示する画像を生成
    const blobImage = document.createElement('img');
    blobImage.setAttribute('src', blob);

  // 生成したHTMLの要素をブラウザに表示させる
    imageElement.appendChild(blobImage);
    ImageList.appendChild(imageElement);
  });
});

条件分岐でトップページのエラーを解決

このままではpreview.jsで指定している要素が、新規投稿ページや編集ページにだけ存在し、TOP画面などでは表示されないため、エラーが発生します。
そのため、下記のようにして条件分岐してエラーを起こさないようにします。
app/javascript/packs/preview.js

if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
  document.addEventListener('DOMContentLoaded', function(){
    const ImageList = document.getElementById('image-list');

    document.getElementById('message_image').addEventListener('change', function(e){
      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);

      // 画像を表示するためのdiv要素を生成
      const imageElement = document.createElement('div');

      // 表示する画像を生成
      const blobImage = document.createElement('img');
      blobImage.setAttribute('src', blob);

      // 生成したHTMLの要素をブラウザに表示させる
      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement);
    });
  });
}

表示画像を編集可能にする。

最後に既存のプレビューを削除した後に新しい画像のプレビューを表示することで画像を編集可能にして起きます。

if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
  document.addEventListener('DOMContentLoaded', function(){
    const ImageList = document.getElementById('image-list');

    document.getElementById('message_image').addEventListener('change', function(e){
      // 画像が表示されている場合のみ、すでに存在している画像を削除する
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }
      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);

      const imageElement = document.createElement('div');

      const blobImage = document.createElement('img');
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement);
    });
  });
}

完成

以上で完成です?

  • このエントリーをはてなブックマークに追加
  • 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] deviseを日本語化する方法

前提

Railsにdeviseのgemを導入していること。

日本語化する方法

Gemfileに追記する

gem 'devise' の後に以下を追記して、 bundle install します。

Gemfile
gem 'devise-i18n'

devise.rbを編集する

config/initializers/devise.rb
config.scoped_views = true

そして rails s でサーバーを再起動します。

devise.views.ja.yml を作成する

以下を実行すると config/locales に日本語化ファイル(devise.views.ja.yml)が作成されます。

$ rails g devise:i18n:locale ja
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails・Laravel・Docker・React環境構築一覧

本記事は、TechpitがQiitaで投稿している環境構築記事のまとめです。

今後も適時更新していきます(最終更新日:2021/2/26)

「〇〇の環境構築も掲載してほしい!」
「〇〇のエラーが解決できない」
等ありましたら、ぜひコメントしてください。

記事一覧表

Ruby on Rails

0から Ruby on Rails の環境構築【macOS】 (Homebrew のインストールから Rails のインストールまで)

0からRuby on Railsの環境構築【Cloud9】(Rubyのバージョン変更からRailsのインストールまで)

Laravel

【最新版】はじめてのLaravel!Laradockを用いたLaravel開発環境構築

React

React環境構築【Node.jsのインストールからCreate React Appまで】~macOS編~

React環境構築【Node.jsのインストールからCreate React Appまで】~Windows編~

Docker

?【すぐ開発を始めたい人向け】DockerをMacにインストールする手順?

?WindowsでDockerを始める手順?

?Docker Toolboxのインストール?

今後もプログラミング学習に役立つような情報を発信していきます!アカウントをフォローしてお待ちください!

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

interactor gem についてまとめてみた (1/2)

業務で interactor gem1 を使用する機会があったのですが、日本語の情報がそれほど多くなかったためまとめました。

全2部作となっていて、
1. Interactor 概論(今回)
2. ActiveInteractor Gem で Interactor を使いやすく
という構成でお届けします。
今回は、 interactor gem の README を翻訳したもの +α の内容となっています。

Interactor とは

Interactor とは、アプリケーションのビジネスロジックをカプセル化するために使われるオブジェクトです。それぞれの Interactor はアプリケーションが行う1つの動作を表現します。

ビジネスロジックとは

ビジネスロジックとは、アプリケーションにおいてデータがどのように作られ、保存され、変更されるか( = ビジネスルール)を記述したソースコードのことです。データベースそのものの管理やUIの表示、システム構成やプログラムの様々な部分の連動などとは区別されます。

Context

それぞれの Interactor は、自身が動作するために必要な情報を集めたオブジェクトである Context を持っています。Interactor が動作すると、その Interactor が持つ Context が変更されます。

Context に情報を追加する

context.user = user

このように記述すると、 Context にユーザーの情報を追加することができます。

Context を落とす

Interactor の動作に問題が生じた場合、以下のようにして Context を落とすことができます。

context.fail!

以下のようにしてエラーメッセージを追加することもできます。

context.error = "Boom!"
context.fail!

# または
context.fail!(error: "Boom!")

Context が落ちているかどうかを確認することもできます。

context.failure? # => false
context.fail!
context.failure? # => true

# または
context.success? # => true
context.fail!
context.success? # => false

落ちた Context を処理する

context.fail! が実行されると常に例外タイプ Interactor::Failure が返りますが、この例外を認識することは通常ありません。後に述べるように、コントローラから call クラスメソッドを用いて Interactor を呼び出し、その後 context.success? メソッドを使ってチェックする方法が推奨されます。call クラスメソッドは fail! が実行されても例外を発生させることはありません。
ただし、 Interactor の単体テストにおいて call ではないビジネスロジックメソッドを呼び出している際は、fail! メソッドは Interactor::Failure を返すことに注意してください。

フック

before フック

Interactor が実行される前に Context の準備が必要な場合は、 before フックを使用することができます。引数にはブロックかシンボルを指定することができます。

before do
  context.emails_sent = 0
end

# または
before :zero_emails_sent
def zero_emails_sent
  context.emails_sent = 0
end

after フック

Interactor が実行された後の動作を指定することもできます。

after do
  context.user.reload
end

ただし、 after フックは Interactor が正常終了した場合にのみ実行されます。 fail! が実行された場合は、 after フックは実行されません。

around フック

before フックや after フックと同様に around フックを定義することもできます。 around ブロックやこれに使われるメソッドは1つの引数を受け取り、その引数の call メソッドを呼び出すことで Interactor を呼び出します。

around do |interactor|
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end

# または
around :time_execution
def time_execution(interactor)
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end

fail! メソッドが呼び出された場合、around フックの動作が止まり、 interactor.call の後に記述された処理は実行されません。

フックの実行順

before フックは定義された順番に、after フックは定義された順番と逆に実行されます。around フックはbeforeフックやafterフックの外側で実行されます。
例えば、

around do |interactor|
  puts "around before 1"
  interactor.call
  puts "around after 1"
end

around do |interactor|
  puts "around before 2"
  interactor.call
  puts "around after 2"
end

before do
  puts "before 1"
end

before do
  puts "before 2"
end

after do
  puts "after 1"
end

after do
  puts "after 2"
end

このように定義された場合、実行結果は以下のようになります。

around before 1
around before 2
before 1
before 2
after 2
after 1
around after 2
around after 1

Interactor の Concern

Interactor は以下のようにして、 Concern の中で共通に用いられる複数の before/after フックを定義することができます。

module InteractorTimer
  extend ActiveSupport::Concern

  included do
    around do |interactor|
      context.start_time = Time.now
      interactor.call
      context.finish_time = Time.now
    end
  end
end

Interactor の例

ユーザーの認証を行う場合、次のようにして Interactor を用いることができます。

class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end

Interactor モジュールを include したクラスを作り、その中で call インスタンスメソッドを作成するだけで Interactor を定義することができます。 Interactor は、 call メソッドの中で自身の context にアクセスすることができます。

コントローラ内での Interactor

多くの場合、 Interactor はコントローラの中で用いられます。

class SessionsController < ApplicationController
  def create
    if user = User.authenticate(session_params[:email], session_params[:password])
      session[:user_token] = user.secret_token
      redirect_to user
    else
      flash.now[:message] = "Please try again."
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

このようなコントローラは、先ほどの AuthenticateUser クラスを用いて次のようにリファクタリングされます。

class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

call クラスメソッドを用いることで、 Interactor を動作させることができます。 call に引数として渡したハッシュは Interactor のインスタンスである context に変換され、該当の Interactor 内で定義されたフックと共に実行され、最終的に変更が加えられた Context が返ります。

Interactor の利点

ユーザーの認証を行う先ほどの例では、コントローラは以下のようになりました。

class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

このような単純なユースケースでは、 Interactor を使った方がコードの分量は多くなります。ではなぜ Interactor を使うのでしょうか?

動作が明快になる

Interactor は全ての破壊的変更( POSTPUTDELETE リクエスト)を伴う動作に対して使います。 Interactor は app/interactors ディレクトリに作成されるため、このディレクトリを見れば全ての開発者がアプリケーションの行う動作を理解することができます。

▾ app/
  ▸ controllers/
  ▸ helpers/
  ▾ interactors/
      authenticate_user.rb
      cancel_account.rb
      publish_post.rb
      register_user.rb
      remove_post.rb
  ▸ mailers/
  ▸ models/
  ▸ views/

TIP: Interactor は実装ではなく、ビジネスロジックに由来した名前にしましょう。アカウントの削除を行う Interactor を作成する際に DestroyUser ではなく CancelAccount という名前にすることで、将来的に様々な責任を担うことができるようになります。

将来性2

今回のユーザー認証の例では、ユーザーを認証するというシンプルなタスクであっても、次のように複数の責任を担うことになるでしょう。

  • 久しぶりにログインしたユーザーに対して「おかえりなさい」と言う
  • パスワードを更新するように通知する
  • 何度もログインが失敗するユーザーを凍結する
  • 凍結した旨をメールで通知する

リストが増えていけば行くほど、コントローラの分量も増えていきます。これで長大なコントローラの出来上がりです。
その代わりに Interactor を導入することで、担うべき責任が増えたとしても、コントローラ(とそのテスト)にはほとんど(あるいは全く)変更を加える必要がなくなります。追加される責任に応じて適切に変更すべき Interactor を選ぶことで、追加される動作が変更されることを防ぐことができます。

Interactor の種類

Interactor ライブラリの中には、普通の Interactor と Organizer という2つの種類の Interactor が定義されています。

Interactor

普通の Interactor は Interactor を include し、 call メソッドを定義したクラスのことを指します。

class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end

通常の Interactor はさしずめ建築用のブロックのようなもので、アプリケーションにおいて単一の働きをします。

Organizer

Organizer は Interactor の変種として重要な意味を持ちます。 Organizer は他の Interactor を動作させるためだけに用いられます。

class PlaceOrder
  include Interactor::Organizer

  organize CreateOrder, ChargeCard, SendThankYou
end

PlaceOrder Organizer は、コントローラ内で他の Interactor と同様に用いることができます。

class OrdersController < ApplicationController
  def create
    result = PlaceOrder.call(order_params: order_params)

    if result.success?
      redirect_to result.order
    else
      @order = result.order
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit!
  end
end

Organizer は、自身が organize する Interactor をまとめて順序通り実行します。 Organizer の中の Interactor は、動作して Context を変更した状態で次の Interactor に手渡します。

ロールバック

organize された Interactor のうちどれかの動作が失敗した場合、 Organizer は動作を停止します。 ChangeCard Interactor が失敗した場合、 SendThankYou は呼び出されません。
加えて、すでに実行された Interactor は自身の行った動作を巻き戻すことができます。 rollback メソッドを Interactor に定義し、呼び出すだけで実現できます。

class CreateOrder
  include Interactor

  def call
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end

動作が失敗した Interactor はロールバックされません。全ての Interactor は単一の目的を持っているはずなので、仮にその動作が失敗したとしても、その動作を巻き戻す必要はないと考えられるからです。

Interactor のテスト

適切に書かれていれば、それぞれの Interactor は1つのことを行うだけなので、 Interactor をテストするのは簡単です。

class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end

テストする必要があるのは、この Interactor が行う単一の動作と、 Context に与える影響のみです。

describe AuthenticateUser do
  subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") }

  describe ".call" do
    context "when given valid credentials" do
      let(:user) { double(:user, secret_token: "token") }

      before do
        allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user)
      end

      it "succeeds" do
        expect(context).to be_a_success
      end

      it "provides the user" do
        expect(context.user).to eq(user)
      end

      it "provides the user's secret token" do
        expect(context.token).to eq("token")
      end
    end

    context "when given invalid credentials" do
      before do
        allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil)
      end

      it "fails" do
        expect(context).to be_a_failure
      end

      it "provides a failure message" do
        expect(context.message).to be_present
      end
    end
  end
end

今回は RSpec で記載しましたが、どのテストフレームワークを用いても考え方は同じです。

分離

上のテストにおいて、ユーザーをデータベースに作成せずに User.authenticate をスタブ化したのは、 spec/interactors/authenticate_user_spec.rb の目的が AuthenticateUser Interactor をテストすることだけだったからです。 User.authenticate メソッドは spec/models/user_spec.rb でテストされます。

モデルに独自のインターフェースを定義するのは賢いやり方です。このようにすることで、 Interactor に属するある責任がどのモデルと関連づけられるのかがわかりやすくなり、コードを記述しやすくなります。 User.authenticate メソッドは良い例です。もしこの Interactor が

class AuthenticateUser
  include Interactor

  def call
    user = User.where(email: context.email).first

    # ユーザーを認証する処理が直接記述されている
    if user && BCrypt::Password.new(user.password_digest) == context.password
      context.user = user
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end

このようになっていた場合、この Interactor をテストするのはとても難しくなります。仮にテストできたとしても、共にモデルの関心事であるORMや暗号化アルゴリズムを変更した瞬間に、ビジネスロジックの関心事であるこの Interactor は壊れてしまいます。
1行1行の目的を明確にしましょう。

統合

Interactor のテストを分離することが大事である一方で、統合テストを適切に行うことも重要です。
あるメソッドをスタブ化すると、そのメソッドが壊れたり変更されたり、もしくはそもそも存在しなかったりする場合を隠蔽することができてしまいます。
統合テストを書けば、アプリケーションの個々のパーツが期待通り動くかどうかを確かめることができ、これは Interactor のような新しいレイヤーをプログラムに追加する場合に特に重要です。
TIP: テストカバレッジを見るのであれば、統合テストを書く前に100%のカバレッジを目指しましょう。それから統合テストを書けば、あなたは夜も安心して寝ることができます。

コントローラ

Interactor を用いる利点のひとつは、コントローラやそのテストを完結にすることができることです。 Interactor は単体テストと統合テストでテストされているので(ですよね?)、コントローラのテストからビジネスロジックのテストを省くことができます。

class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
describe SessionsController do
  describe "#create" do
    before do
      expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
    end

    context "when successful" do
      let(:user) { double(:user, id: 1) }
      let(:context) { double(:context, success?: true, user: user, token: "token") }

      it "saves the user's secret token in the session" do
        expect {
          post :create, session: { email: "john@doe.com", password: "secret" }
        }.to change {
          session[:user_token]
        }.from(nil).to("token")
      end

      it "redirects to the homepage" do
        response = post :create, session: { email: "john@doe.com", password: "secret" }

        expect(response).to redirect_to(user_path(user))
      end
    end

    context "when unsuccessful" do
      let(:context) { double(:context, success?: false, message: "message") }

      it "sets a flash message" do
        expect {
          post :create, session: { email: "john@doe.com", password: "secret" }
        }.to change {
          flash[:message]
        }.from(nil).to(I18n.translate("message"))
      end

      it "renders the login form" do
        response = post :create, session: { email: "john@doe.com", password: "secret" }

        expect(response).to render_template(:new)
      end
    end
  end
end

全ての魔法は Interactor の中で起きているので、このコントローラのテストは、アプリケーションが運用され続ける限り小さな変更を加え続けなければいけないでしょう。

Rails

Interactor を Rails で使うときは、 Interactor は app/interactors に格納し、次のように動詞となる名前をつけています。

  • AddProductToCart
  • AuthenticateUser
  • PlaceOrder
  • RegisterUser
  • RemoveProductFromCart

参照: interactor-rails


  1. Copyright (c) 2013 Collective Idea, Released under the MIT license: https://github.com/collectiveidea/interactor/blob/master/LICENSE.txt 

  2. ネタバレになりますが、あなたのユースケースはこれほどシンプルにはならないはずです。 

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

7つのアクション以外にViewを増やす方法

はじめに

オリジナルアプリケーションを作成中に、疑問に思いました。
「7つのアクション以外にviewって作れるのか?」
初心者なら必ずこう思うはず、
そして、作れないことはないはず!!
調べてみたら作れました!!

忘れないために記事に残したいと思います。

やることは、以下の3つになります。
1.コントローラーへの記述
2.ルーティングの設定
3.ビューの作成

順を追って説明します。

バージョン

・Ruby 2.6.5
・Rails 6.0.0

コントローラーの設定

まずは、コントローラーに新しいアクションを設定します。

def index
    @post = Post.includes(:user).order("created_at DESC")
    @post_like = Post.includes(:post_likes).sort {|a,b| b.post_likes.size <=> a.post_likes.size}
  end

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
      if @post.save
        redirect_to root_path
      else
        render :new
      end
  end

  def show
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
    @comment_like = @post.comments.includes(:likes).sort {|a,b| b.likes.size <=> a.likes.size}
  end

  def edit
  end

  def update
    if @post.update(post_params)
      redirect_to post_path
    else
      render :edit
    end
  end


  def destroy
    if @post.destroy
      redirect_to root_path
    end
  end

  def indexabout
  end
# 以下省略

今回は、アプリケーションの概要説明みたいなページを作成しました。
1番下に記述されている、indexaboutが新しく設定したアクションです。
名前はなんでも大丈夫です。
説明が書いてあるだけのページなので、処理も書いていません。

ルーティングの設定

次にルーティングを設定します。

root to: 'posts#index'
  resources :users, only: [:show, :edit, :update, :destroy]
  resources :posts do
    collection do
      get 'indexabout'
    end
# 以下省略

7つのアクション以外のルーティングを設定するには、collectionかmember
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを
任意にカスタムできます。

collectionとmemberの違いは?

collection→ルーティングにidがつかない
member→ルーティングにidがつく

この違いです。
例えば、ユーザー情報の編集ページには、idが必要ですよね。
そういったページを作成する場合は、memberを使います。
今回は、説明だけのページなのでcollectionを使いました。

ルーティングに記述を行った後にターミナルで確認すると

% rails routes

Image from Gyazo

しっかりと設定されているのがわかります。

ビューの設定

最後にビューを設定します。

<div class="what_index">
    <%= link_to "正解のない問題たちとは?",  indexabout_posts_path, class: :nav__btn %>
</div>

リンク先の指定をしてあげる。

Image from Gyazo

該当箇所にビューの設定を行い、
作成したビューに記述すれば完了です。

これで、7つのアクション以外にもビューが作成できました。

初心者のため、まだまだ誤った記述があるかもしれません。
見つけた方はコメントを残していただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

チャット機能追加

相手を指定してチャットルームを作成し、会話ができる機能を追加しましょう。

まずどのような機能とテーブルが必要か考えてみます。

チャットルーム管理機能(新規作成・削除機能)    roomsテーブル
メッセージ管理機能(テキスト投稿・画像投稿機能) messagesテーブル

roomsテーブル

チャットルームの名前

messagesテーブル

チャットの内容
画像情報
メッセージの投稿時刻
投稿したチャットルーム
投稿したユーザー

テーブル間の関係性

usersテーブル と messagesテーブル

どのユーザーがどの投稿したかを管理する

roomsテーブル と messagesテーブル

どのチャットルームで投稿されたメッセージなのかを管理する

usersテーブルとroomsテーブル

ユーザーがどのチャットルームにいるかを管理
チャットルームにどのユーザーが存在するかを管理

アソシエーション

今回のDB設計では「多対多」という関係性が存在します。
関連するテーブルのidをお互いが複数持っている関係性のことで、今回のように「ユーザーは複数のチャットルームに所属する」「チャットルームには複数ユーザーが所属する」という場合の関係性を指します。
この場合は中間テーブルを使用して設計します。

中間テーブル

多対多の関係にある2つのテーブルの間に挟まって、2つの組み合わせパターンだけをレコードとして保存します。
roomer.png

中間テーブルを経由して多対多のテーブルへアソシエーションを組むには、has_manyメソッドにthroughオプションを記述する必要があります。

class Room < ApplicationRecord
  has_many :room_users
  has_many :users, through: :room_users
end
class User < ApplicationRecord
  has_many :messages
  has_many :rooms, through: :room_users
end
class RoomUser< ApplicationRecord
  belongs_to :user
  belongs_to :room
end

それでは追加していきます。

ルーティングを追加

Rails.application.routes.draw do
  #省略
 resources :rooms, only: [:new, :index, :create, :destroy] do
    resources :messages, only: [:index, :create]
  end
end

ユーザーモデルにアソシエーション追記

Userモデル
class User < ApplicationRecord

#省略
  has_many :room_users
  has_many :rooms, through: :room_users
  has_many :messages

end

Roomモデルとテーブルを作成

% rails g model room
マイグレーションファイル
class CreateRooms < ActiveRecord::Migration[6.0]
  def change
    create_table :rooms do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end
% rails db:migrate
Roomモデル
class Room < ApplicationRecord
  has_many  :room_users, dependent: :destroy
  has_many  :users, through: :room_users
  has_many  :messages, dependent: :destroy
  validates :name, presence: true
end

rommsコントローラーを作成

roomsコントローラー
class RoomsController < ApplicationController
  def new
    @room = Room.new
  end

  def create
    @room = Room.new(room_params)
    if @room.save
      redirect_to rooms_path
    else
      render :new
    end
  end

  def index
  end

  def destroy
    room = Room.find(params[:id])
    room.destroy
    redirect_to rooms_path
  end

  private

  def room_params
    params.require(:room).permit(:name, user_ids: [])
  end
end

user_ids: [ ]という記述があります。このように、配列に対して保存を許可したい場合は、キーに対し[ ]を値として記述します。

rooms/newビュー
<div class='chat-room-form'>
  <h1>新規チャットルーム</h1>
  <%=form_with model: @room, local: true do |f|%>
    <div class='chat-room-form__field'>
        <%= f.label :チャットルーム名, class: 'chat-room-form__label'%>  
        <%= f.text_field :name, class: 'chat__room_name chat-room-form__input', placeholder: 'チャットルーム名を入力してください'%>
      </div>
    </div>
        <label class='chat-room-form__label' for='chat_room_チャットメンバー'>チャットメンバー</label>
            <select name="room[user_ids][]">
            <option value="">チャットするユーザーを選択してください</option>
          <% User.where.not(id: current_user.id).each do |user| %>
            <option value=<%= user.id %>><%= user.nickname %></option>
          <% end %>
        </select>
        <input name="room[user_ids][]" type="hidden" value=<%= current_user.id %>>
        <%= f.submit class: 'chat-room-form__action-btn'%> 
  <% end %>
</div>

モデル名.where("条件")と記述すると、条件に一致したレコードを配列として取得でき、続けてnotメソッドを追記すると条件に一致したレコード以外の値を配列として取得できます。

type属性にhiddenを指定することで、画面上には表示されないinput要素を作成できます。そしてselect要素と同様にname属性にroom[user_ids][]を指定し、room[user_ids]をキーに対する値を配列で受け取る仕組みです。value属性にはcurrent_user.idを指定しているため、room[user_ids]キーに対して、ログイン中ユーザーのidを配列として格納し、コントローラーへ送信します。

room[user_ids][]でコントローラーに送られるparamsの中身
{"room" => {"user_ids" => ["選択したユーザーのid", "現在ログインしているユーザーのid"]}}
rooms/indexビュー
<div class='wrapper'>
  <div class='side-bar'>
    <%= render "messages/side_bar" %>
  </div>
</div>

部分テンプレートを使います。

RoomUserモデルを作成

% rails g model room_user
マイグレーションファイル
class CreateRoomUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :room_users do |t|
      t.references :room, foreign_key: true
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end
% rails db:migrate
RoomUserモデル
class RoomUser < ApplicationRecord
  belongs_to :room
  belongs_to :user
end

messageモデル作成

% rails g model massage
マイグレーション
class CreateMessages < ActiveRecord::Migration[6.0]
  def change
    create_table :messages do |t|
      t.string  :content
      t.references :room, foreign_key: true
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

% rails db:migrate
Messageモデル
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
  has_one_attached :image

  validates :content, presence: true, unless: :was_attached?

  def was_attached?
    image.attached?
  end
end

messagesコントローラー
class MessagesController < ApplicationController
  def index
    @message = Message.new
    @room = Room.find(params[:room_id])
    @messages = @room.messages.includes(:user)
  end

  def create
    @room = Room.find(params[:room_id])
    @message = @room.messages.new(message_params)
    if @message.save
      redirect_to room_messages_path(@room)
    else
      @messages = @room.messages.includes(:user)
      render :index
    end
  end

  private

  def message_params
    params.require(:message).permit(:content, :image).merge(user_id: current_user.id)
  end
end

messagesにビューを追加

_main_chat.html.erb
<%= @room.name %>
<%= link_to "チャットを終了する", room_path(@room), method: :delete %>
<%= render partial: 'message', collection: @messages %>
<%= form_with model: [@room, @message], class: 'form', local: true do |f| %>
 <%= f.text_field :content, class: 'form-message', placeholder: 'type a message' %>
    <label class="form-image">
      <span class="image-file">画像</span>
      <%= f.file_field :image, class: 'hidden' %>
    </label>
      <%= f.submit '送信', class: 'form-submit' %>
 <% end %>
_message.html.erb
      <%= message.user.name %>
      <%= l message.created_at %>
      <%= message.content %>
      <%= image_tag message.image.variant(resize: '500x500'), class: 'message-image' if message.image.attached? %>
_side_bar.html.erb
    <%= link_to current_user.nickname, rooms_path %>
    <%= link_to "チャットを作成する", new_room_path %>
   <% current_user.rooms.each do |room| %>
        <%= link_to room.name, room_messages_path(room) %>
  <% end %>
index.html.erb
<div class="wrapper">
<div class="side-bar">
  <%= render "side_bar" %>
   </div>
  <div class="chat">
  <%= render "main_chat" %>
  </div>
</div>

form_withの引数に@room, @messageの2つを渡しているのはmessagesのルーティングがroomsにネストされているため。チャットルームとメッセージとの間には親子関係があります。

form_withのmodelオプションに記述した@roomには「指定のチャットルームのレコード情報」、@messageには「Messageモデルのからのインスタンス(Message.new)」をあらかじめセットしておく必要があります。

時刻設定

module アプリ名
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0 
    config.i18n.default_locale = :ja  #設定
    config.time_zone = 'Tokyo'        #設定
   # 中略

config/localesディレクトリに「ja.yml」というファイルを作成し、以下のような設定を記述

config/locales/ja.yml
ja:
  time:
    formats:
      default: "%Y/%m/%d %H:%M:%S"

l(エル)メソッドは、日付や時刻を表示するRailsのメソッドで、指定した現地時間に対応できます。

以上です。

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

ActionMailerでdefault_url_optionを使って環境毎にURLを変更する

config/environments/**.rb

に環境毎のホスト名を指定して使いたくて調べた。

以下を設定すれば良いとあったのだけど、
config.action_mailer.default_url_options = { host: 'develop.com' }

設定後、どうすればURLが取れるかわからなかった..

しばらく以下の意味がわからなかったけど..
https://railsguides.jp/action_mailer_basics.html#action-mailer%E3%81%AE%E3%83%93%E3%83%A5%E3%83%BC%E3%81%A7url%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B

routesの普段pathの所をurlに変更して引数を指定すれば、URLを返してくれる。

こんなroutesの設定があったら

activate GET    /registrations/:id/activate(.:format)     registrations#activate

以下のようにcontroller ,viewで指定すればdefault_url_optionsを反映したURLが取れる

activate_url(@user.activation_token) 

ふいー

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

Railsコマンドまとめ

よく使うRailsコマンドのメモです。
今まで何となく使っていたのですが、いい機会なので公式ドキュメントから調べてみました。

参考:Railsドキュメント

新しいRailsアプリを作成

# 基本形
$ rails new アプリケーション名 [オプション]

# オプション何もなしで作成
$ rails new sample

# mysqlを使うアプリを作成
$ rails new sample -d mysql

# バージョンを指定してアプリを作成
$rails _6.0.0_ new sample

# バージョン指定とmysql組み合わせ
rails _6.0.0_ new sample -d mysql

コントローラー作成

# 基本形
$ rails g controller コントローラー名

# ビューも一緒に生成
$ rails g controller sample index

今まで何にも知らずに「g」を使っていましたが、これはgenerateを省略していたんだと知りました。
ですので本来の正しい形は

$ rails generate controller コントローラー名

ということです。
コントローラー名は複数形で作成すると勉強したのですが公式をみると複数形になっていないので必須というわけではないのですかね。

モデル作成

# 基本形
$ rails generate model モデル名

# 短縮
$ rails g model モデル名

ローカルサーバー起動

# 基本形
$ rails server

# 短縮
$ rails s

ローカルのhttp://localhost:3000/が起動します。

ルーティング確認

$ rails routes

マイグレーション関係

# マイグレーション実行
$ rails db:migrate

# ロールバック(直前に実行したものだけ)
$ rails db:rollback

# ロールバック(戻したい数を指定することで複数戻すことができる)
$ rails db:rollback STEP=戻したい数

# 状況の確認
$ rails db:migrate:status

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

Railsで新規作成

いや、何回もしてるんですけど、改めてね。

ターミナル

cd ~/(ファイル名)

rails _6.0.0_ new (アプリ名) -d mysql

config/database.yml
データの保存形式変更(人によるかと)

# encoding: utf8mb4
encoding: utf8

DB作成

rails db:create

ローカルで動作確認

rails s

とりあえずこれがスタートライン!
後々、付け足します。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Next.js + RailsでポートフォリオサイトをISR対応&メンテナンスフリー化した

2年ほど前にNuxt.jsを使ってポートフォリオサイトを作成しました。
今回、このサイトをNext.js + Railsでリニューアルしたので、経緯を記事にまとめます。

リニューアル後のページ

https://portfolio.y-uuu.net/
image.png

デザインは前回のものを踏襲していて、ほとんど変わっていません。

リポジトリ

リニューアルの目的

Next.jsを使って何か作りたい

nextjs.png

昨年からReactやNext.jsを触ってノウハウを蓄積するようにしています。私自身普段はRailsを使った開発をしているので、Next.jsを採用するとしたらRailsと組み合わせて使う可能性が高いです。

昨今のフロントエンド界隈の盛り上がりを横目に、フロントエンドにNext.js/バックエンドにRailsを用いて、何か作りたいと思っていました。

デプロイなしで内容を更新したい

image.png

前回、勢いでポートフォリオサイトを作成したものの、単なる静的ページとして公開していたので記載内容を変更するためにはVueコンポーネントを直接編集する必要がありました。

今回は管理ページを別途作成し、ログインすることで記載内容を容易に追加・変更できるようにしています。

メンテナンスフリーにしたい

image.png

image.png

リニューアル後のサイトではQiitaやZennに公開した記事、SpeakerDeckに公開したスライドをを自動的に収集し、メンテナンスしなくても内容が自動更新されるようになっています。

システム構成

Next.jsのデプロイ先としてVercelを、Railsのデプロイ先としてherokuを使っています。
また、画像の格納先としてAWSのS3を利用しました。

Untitled_Portfolio_Site_-_Cacoo.png

ライブラリ・フレームワーク

フロントエンド

  • React
  • Next.js
  • React Hook Form
  • react-dropzone
  • react-spinners
  • react-tippy
  • axios
  • SWR
  • Tailwind CSS

バックエンド

  • Ruby on Rails
  • devise
  • faraday
  • rails_same_site_cookie
  • AWS SDK for Ruby V3

実装上のポイント

ISR(Incremental Static Regeneration)

Next.jsを使ってISRを実現しています。herokuがレスポンスを返す時間に関わらず、来訪者がすぐにページを閲覧できるようにする狙いです。

以下はISRの挙動の解説です。

Next.js(ISR有効)は、アクセスがあった際に生成済みの静的ページをレスポンスします。このとき、herokuへのアクセスは発生しません。

Notification_Center.png

前回のページ生成から指定した時間を経過した後にアクセスが発生すると、静的ページを再生成します。このときNext.jsはサーバーサイドでページを再生成するのを待たず、いったん前回の静的ページをレスポンスします。

Untitled_Portfolio_Site_-_Cacoo.png

再生成が完了すると、以降その静的ページをレスポンスします。

前提として、herokuのFreeプランだと30分間アクセスがない場合にdynoがSleepするので、次にアクセスがあった場合にdynoが起動するまで数十秒ほど待たされてしまうという問題があります。本来の使い方ではないかもしれませんが、バックエンドの処理に時間がかかる場合でも生成済みのページを即時にレスポンスできるという点で、ISRは有用だと感じました。

記事・スライドの自動収集

yuuu-portfolio-v2-api_·_Scheduler___Heroku.png

Heroku Schedulerを使うことで、日次でrakeタスクを実行して、記事・スライドを自動収集しています。

QiitaはAPIを、ZennはFeedを使って、自分自身の記事を収集しDBに保存しています。

namespace :qiita do
  desc "Fetch articles from qiita"
  task fetch: :environment do
    res = Faraday.get('https://qiita.com/api/v2/users/Y_uuu/items?per_page=100')
    return if res.status != 200

    items = JSON.parse(res.body)
    items.each do |item|
      next if Article.find_by(link: item['url']).present?

      item_res = Faraday.get(item['url'])
      next if res.status != 200

      Article.create(
        title: item['title'],
        body: item['body'].truncate(100) + '...',
        published_at: Time.zone.parse(item['created_at']),
        link: item['url'],
      )
    end
  end
end

SpeakerDeckは収集方法を悩んだのですが、よくよく調べると https://speakerdeck.com/yuuu.atom のように、自身のアカウント名の末尾に .atom を付与することでFeedを取得できることがわかったので、これを使って収集するようにしました。

認証

image.png

最初は「SPAの認証はJWT」という思い込みがあったのですが、いろいろ調べていくうちに「cookieを使った認証でも問題ない」との結論に至りました。認証のバックエンドもRailsで、deviseというGemを使ったよくある実装です。

ただし、今回はCrossOriginな構成のためつまづきポイントが多くありました。具体的な実装方法は別記事にまとめたので、興味のある方は参照ください。

Rails 6.1対応版: APIモードのRailsに対してCrossOriginなSPAからSession認証する方法

ファイルアップロード

image.png

当初は、バックエンドがRailsということで、Active Storageを使ってファイルアップロードを実現する予定でした。実装をしていく上で「わざわざActive Storageを使う必要があるのか?」という疑問が生じ、最終的にはS3の署名付きURLを使ってアップロード・閲覧する方式に変更したという経緯があります。

こちらも別記事にまとめたので、興味のある方は参照ください。

RailsをバックエンドとしたSPAでのファイルアップロード機能の作り方に悩んだ話

感想

ISRが良い

image.png

SSRとSSGのいいとこ取りができていて良いです。SSGのようにデプロイのビルドが長くなることもなく、かつSSRを使った場合に比べてページの表示が高速なので満足です。

Vercelが良い

image.png

今回初めてVercelを使ってみたのですが、GitHubのリポジトリを指定するだけで簡単にCI/CDを構築できました。前回Netlifyを使った時も同様の感動があったのですが、とかくNext.jsを使う場合はほとんど設定が不要で、噂通りVercelとの組み合わせがベストだと実感しました。

バックエンドのRails・herokuも良い

image.png

死んだと言われて久しいRailsですが、自分にとってはやはり最速で実装ができるフレームワークです。バックエンドは必要最低限実装しつつ、フロントエンドの実装に注力するスタイルで開発が進められました。

herokuを使うことでデプロイも非常に簡単でした。

まとめ

個人的には十分満足できるポートフォリオサイトが完成しました。

Next.js + Railsで何か作ろうとしている人の参考になれば幸いです。質問・感想などありましたら、ぜひコメントをお願いします。

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

【rails6.0にBootstrap】RailsにBootstrapを導入する方法

はじめに

こちらの記事では、Railsのアプリケーションの開発途中でBootstrapを導入する方法について書いています。

環境

ruby '2.6.5'
rails '6.0.0'

手順

1.必要なGemのインストール

まずは、必要なGemを導入(インストール)します。

Gemfile
gem 'bootstrap', '~> 4.5.0'
gem 'jquery-rails'
ターミナル
% bundle install

2.CSSのファイル名変更

application.cssをapplication.scssと拡張子を変更します。

3.scssの中身を削除

application.scssファイルの中に記述されている物を全て削除します。

app/assets/stylesheet/application.scss
#全て削除

ーーここから削除ーー

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */

ーーここまで削除ーー

4.Bootstrapにapplication.scssにインポート

application.scssにBootstrapを使用するための記述を追記します。

app/assets/stylesheet/application.scss
 @import "bootstrap";

5.他のSCSSファイルを読み込む記述

他のSCSSファイルを適用するためには、ファイルの数だけ記述をおこなう必要があります。
適用するための記述は以下の通りです。

app/assets/stylesheet/application.scss
 @import "読み込みたいSCSSのファイル名";

私の場合、scssファイルが7つあるので全て記述していきます。
スクリーンショット 2021-02-26 1.50.32.png

これでブラウザをリロードすると、CSCCが適用されています。

まとめ

以上がRailsにBootstrapを導入する手順でした。
最後に、今回参考にさせていただいたQiitaの記事を貼っておきます。こちらの方は、アプリケーションの作成から丁寧に解説されています。これからアプリケーションを作成する方はぜひ合わせて読んでみてください!

https://qiita.com/keisukesaito/items/0309f7b76d8aa4d78879

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【環境構築】docker + Vue.js + Rails + MySQL をエラー地獄切り抜け、構築した方法

はじめに

色々なサイトを見ながら、まる三日かけてついに構築できましたので、スムーズに解決できた構築方法をご紹介したいと思います。

・Ruby 2.5.8 (x86_64-linux)
・Ruby on Rails 5.2.4.5

スクリーンショット 2021-02-26 1.09.36.png

初めに、ファイル作成

$ cd 作業するディレクトリ

作業ディレクトリに移動後。

$ touch Gemfile Gemfile.lock docker-compose.yml Dockerfile

ファイル構成はこちら

[project_name]
├── docker-compose.yml
│── Dockerfile
│── Gemfile
├── Gemfile.lock

ファイルの中身を編集していきます

・Gemfile↓

source 'https://rubygems.org'
gem 'rails', '~>5'
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.19
    environment:
      - MYSQL_ROOT_PASSWORD=root
    ports:
      - "3307:3306"
    volumes:
      - ./tmp/db:/var/lib/mysql
  webpacker:
    build: .
    command: bundle exec bin/webpack-dev-server
    volumes:
      - .:/myapp
    ports:
      - "3035:3035"
  web:
    build: .
    environment:
      RAILS_ENV: development
    command: bash -c "bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - bundle-data:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
      - webpacker
volumes:
  bundle-data:

db(データベース用)、webpacker、アプリケーション用、それぞれのコンテナを作成します。

・Dockerfile↓

FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
  && apt-get install -y nodejs

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
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 -y yarn

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

CMD ["rails", "server", "-b", "0.0.0.0"]

Railsの初期画面を表示させる

まず、rails newでプロジェクトの作成をします。

$ docker-compose run web rails new . --force --database=mysql --webpack=vue --skip-coffee

--skip-coffee:CoffeeScriptのセットアップをスキップ。

$ sed -i ".bak" -e "s/host: localhost/host: webpacker/g" config/webpacker.yml

sed -i:テキストファイルをフィルター処理で直接編集します。

buildでイメージの構築をします。

$ docker-compose build

datebase.ymlを編集します。

datebase.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root

# ここからした2行
  password: # ここには、datebase-compose.ymlの、- MYSQL_ROOT_PASSWORD=ここを入力
  host: db

development:
  <<: *default
  database: myapp_development

webコンテナ上で、データベースの作成をします。

$ docker-compose run web rails db:create

イメージ構築〜コンテナ起動までをバックグラウンド(-d)で行います

$ docker-compose up -d

localhost:3000にアクセスすると、yey!Railsの初期画面が表示されたのではないでしょうか?

webpackerをインストールしていく

$ docker-compose run web rails webpacker:install
$ docker-compose run web rails webpacker:install:vue
$ docker-compose build
$ docker-compose up -d

Hello Vue!表示しにいく

スクリーンショット 2021-02-26 1.09.36.png

config/routes.rbを下記のように編集

routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

続いて、home controllerを作成していきます。

$ docker-compose run --rm web rails g controller home index

app/controllers/home_controller.rbと、app/views/home/index.html.erbの編集をする

home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

webpacker は app/javascript/packs/ 配下に設置されたファイルをコンパイルします。
よって、index.html.erbでは、下記のようになります。

index.html.erb
<%= javascript_pack_tag 'hello_vue' %>

localhost:3000を開いてください。

スクリーンショット 2021-02-26 1.09.36.png

お疲れ様でした?

参考

Rails世渡り2【docker × Ruby on Rails × Vue.js】

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む