20200715のRailsに関する記事は27件です。

【Rails】link_toメソッドで指定する引数の順番について

Q. link_toメソッドで指定する引数の順番は決まっているのか?

⬇️

A. ink_toは 表示したい文字→パス→クラスとメソッド と構文の様に順番が決まっている。

? <%= link_to 'ツイート一覧へ', '/tweets', class: 'sample' %>
? <%= link_to 'ツイート一覧へ', class:, 'sample’, ‘/tweets’ %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + Rails6 + PostgreSQL 環境構築

はじめに

今までRails5 + MySQL での開発しか行ってこなかった自分でしたが、初めてRails6 + PostgreSQL での環境構築を行ったため備忘録も兼ねて記述していきたいと思います。

必要なファイル

作業フォルダを作成します。
今回は sample_app とします。

$ mkdir sample_app

そして必要なファイルを以下の構成で作成します。

$ touch ○○(ファイル名)

- sample_app
    - Dockerfile
    - docker-compose.yml
    - Gemfile
    - Gemfile.lock
    - entrypoint.sh

ではそれぞれを編集していきます。

Dockerfile

Rails6 からWebpackerがデフォルトで導入されたためインストールする必要があります。
そのため、DockerfileにはWebpackerをインストールするために必要なyarnNode.jsをインストールするように記述します。

FROM ruby:2.6.5

# 必要なライブラリインストール
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client

# yarnパッケージ管理ツールをインストール
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

# Node.jsをインストール
RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \
  apt-get install nodejs

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

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

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

docker-compose.yml

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

Gemfile

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

Gemfile.lock

ファイルを作成だけして、編集は何もしません。

entrypoint.sh

entrypoint.sh
set -e

rm -f /myapp/tmp/pids/server.pid

exec "$@"

Railsアプリの作成

作成したDockerfile元にイメージがbuildし、生成されたコンテナ内でrails newを実行します

$ docker-compose run web rails new . --force --no-deps --database=postgresql --skip-bundle

bundle install

rails newコマンドによりGemfile内の記述が変更されているためbundle installをする必要があります。
イメージをbuildすることでbundle installも実行されるため以下のコマンドを実行します。

$ docker-compose build

database.yml

rails newで作成されたdatabase.ymlを以下のように編集します。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password:
  pool: 5

development:
  <<: *default
  database: myapp_development


test:
  <<: *default
  database: myapp_test

webpackerのインストール

$ docker-compose run web bundle exec rails webpacker:install

コンテナを起動

$ docker-compose up

DB作成

$ docker-compose run web rake db:create

おわり

これでhttp://localhost:3000/にアクセスするとサーバーが立ち上がっていることを確認できるかと思います。
最後まで見ていただき、ありがとうございました。

参考記事
DockerでRuby on Railsの環境構築を行うためのステップ【Rails 6対応】
Rails6 開発時につまづきそうな webpacker, yarn 関係のエラーと解決方法
PostgreSQL(Docker)にRails(Docker)が接続できなくなったから調べてみた。(could not translate host name "db" to address: Name or service not known)

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

初心者がWeb開発するために Docker + Rails6 + PostgreSQL の環境構築をする話

はじめに

いろいろあって友人とWeb開発を行うことになりました。
初心者のため勉強しながらの開発となるので、
これからは勉強した内容のアウトプットのためにいろいろ記事にしようと思います。
まず第一歩として環境構築からです。

環境

ubuntu 18.04

0.前提

Dockerインストール済み
まだの方はこちらから
https://docs.docker.com/compose/install/

1.必要なディレクトリ・ファイル準備

開発したい場所に開発をするディレクトリを作成します。
今回ディレクトリの名前は 'myapp'

$ mkdir myapp
$ cd myapp

作成したディレクトリの中にアプリケーション構築に必要なファイルを作成します。

  • Dockerfile
  • Gemfile
  • Gemfile.lock
  • entrypoint.sh
  • docker-compose.yml
$ touch Dockerfile Gemfile Gemfile.lock entrypoint.sh docker-compose.yml

作成したファイルに下記の内容を記述します。

Dockerfile
FROM ruby:2.6.6

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

RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.3'
Gemfile.lock

Gemfileは何も書かなくていいです。

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 "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

これで必要なファイルは揃いました。

2.プロジェクトの構築

作成した5つのファイルを利用してアプリケーションを生成します。
まずターミナルを使ってdocker-compose runを実行します。

$ docker-compose run web rails new . --force --no-deps --database=postgresql

最後に下記のようなWebpackerのインストール成功メッセージが表示されたら生成完了です。
Webpacker successfully installed ? ?

次はdocker-compose builをします。

$ docker-compose build

最後に下記のようなSuccess情報が表示されたら成功です。
Successfully built xxxxxxxxxx
Successfully tagged myapp_web:latest

3.データベースの設定・作成

まずデータベースの設定を行うために以下のファイルを編集します。

  • config/database.yml

編集内容は下記を記述してください

database.yml
# 設定箇所のみ抜粋
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: 5

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

編集を終えたら下記のコマンドを実行し、データベースを作成します。

$ docker-compose run web rake db:create

最後に下記のようなメッセージが最後に表示されたら作成成功です。
Starting myapp_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'

4.Dockerを起動

dockerを起動するために下記のコマンドを実行します。

$ docker-compose up

実行が完了すると下記の内容がコンソールに表示されます。

myapp_db_1 is up-to-date
Starting myapp_web_1 ... done
Attaching to myapp_db_1, myapp_web_1
(省略)
web_1  | => Booting Puma
web_1  | => Rails 6.0.3.2 application starting in development
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 4.3.5 (ruby 2.6.6-p146), codename: Mysterious Traveller
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop

表示されたら http://localhost:3000 にアクセスして下の画像と同じページが表示されれば、構築完了です。
Screenshot from 2020-07-15 22-01-17.png

最後に

この環境構築で下記のような問題が発生して、3日ほど時間をかけてようやく解決しました。
この記事で書こうか迷ったのですが構築まできれいにまとめたかったので、解決方法は後日記事に書こうと思います。

Creating network "myapp_default" with the default driver
Creating myapp_db_1 ... done
sh: 1: node: not found
Webpacker requires Node.js >= 8.16.0 and you are using 4.8.2
Please upgrade Node.js https://nodejs.org/en/download/

あと冒頭に書いてある”いろいろあって友人とWeb開発することになった”という話は、
Web開発が終了してリリースしたときに一緒に記事にして投稿します。

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

初心者がWeb開発するためにDocker+Rails6+Postgresqlの環境構築をする話

はじめに

いろいろあって友人とWeb開発を行うことになりました。
初心者のため勉強しながらの開発となるので、
これからは勉強した内容のアウトプットのためにいろいろ記事にしようと思います。
まず第一歩として環境構築からです。

環境

ubuntu 18.04

0.前提

Dockerインストール済み
まだの方はこちらから
https://docs.docker.com/compose/install/

1.必要なディレクトリ・ファイル準備

開発したい場所に開発をするディレクトリを作成します。
今回ディレクトリの名前は 'myapp'

$ mkdir myapp
$ cd myapp

作成したディレクトリの中にアプリケーション構築に必要なファイルを作成します。

  • Dockerfile
  • Gemfile
  • Gemfile.lock
  • entrypoint.sh
  • docker-compose.yml
$ touch Dockerfile Gemfile Gemfile.lock entrypoint.sh docker-compose.yml

作成したファイルに下記の内容を記述します。

Dockerfile
FROM ruby:2.6.6

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

RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.3'
Gemfile.lock

Gemfileは何も書かなくていいです。

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 "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

これで必要なファイルは揃いました。

2.プロジェクトの構築

作成した5つのファイルを利用してアプリケーションを生成します。
まずターミナルを使ってdocker-compose runを実行します。

$ docker-compose run web rails new . --force --no-deps --database=postgresql

最後に下記のようなWebpackerのインストール成功メッセージが表示されたら生成完了です。
Webpacker successfully installed ? ?

次はdocker-compose builをします。

$ docker-compose build

最後に下記のようなSuccess情報が表示されたら成功です。
Successfully built xxxxxxxxxx
Successfully tagged myapp_web:latest

3.データベースの設定・作成

まずデータベースの設定を行うために以下のファイルを編集します。

  • config/database.yml

編集内容は下記を記述してください

database.yml
# 設定箇所のみ抜粋
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: 5

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

編集を終えたら下記のコマンドを実行し、データベースを作成します。

$ docker-compose run web rake db:create

最後に下記のようなメッセージが最後に表示されたら作成成功です。
Starting myapp_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'

4.Dockerを起動

dockerを起動するために下記のコマンドを実行します。

$ docker-compose up

実行が完了すると下記の内容がコンソールに表示されます。

myapp_db_1 is up-to-date
Starting myapp_web_1 ... done
Attaching to myapp_db_1, myapp_web_1
(省略)
web_1  | => Booting Puma
web_1  | => Rails 6.0.3.2 application starting in development
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 4.3.5 (ruby 2.6.6-p146), codename: Mysterious Traveller
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop

表示されたら http://localhost:3000 にアクセスして下の画像と同じページが表示されれば、構築完了です。
Screenshot from 2020-07-15 22-01-17.png

最後に

この環境構築で下記のような問題が発生して、3日ほど時間をかけてようやく解決しました。
この記事で書こうか迷ったのですが構築まできれいにまとめたかったので、解決方法は後日記事に書こうと思います。

Creating network "myapp_default" with the default driver
Creating myapp_db_1 ... done
sh: 1: node: not found
Webpacker requires Node.js >= 8.16.0 and you are using 4.8.2
Please upgrade Node.js https://nodejs.org/en/download/

あと冒頭に書いてある”いろいろあって友人とWeb開発することになった”という話は、
Web開発が終了してリリースしたときに一緒に記事にして投稿します。

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

【memberとcollection】

memberとcollectionとは

現在ツイッターの写真投稿版のようなアプリを作成しているのですが、その作成途中で投稿の検索機能を実装する際に出てきたmemberとcollectionについてとそれぞれの違いについてアウトプットします。

memberとcollectionはルーティングを設定する際に使用しすることができ、生成されるURLと実行されるコントローラを任意にカスタムすることができます。

collectionはルーティングに:idがつかない、memberはつくという違いがあります。

search1.png

上の画像のようにtweetsコントローラーのネストに入れてcollectionを記述すると、
以下の画像のルーティングになります。

search2.png

URIを見てもidが入っていません。

一方、memberで記述した場合

search3.png

この場合のルーティングは以下の画像のようになります。

search4.png

このようにルーティングにidが入っています。

今回作成した写真投稿アプリの場合は検索結果としてあくまで一覧表示させたいだけでidごとの詳細ページは必要ないのでcollectionを使用しました。

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

初心者がはまりがちなDockerのエラーに対する解決法

記事の背景

Le Wagon Tokyo (https://www.lewagon.com/ja/tokyo) という、主にRubyとRailsを教えるフランス発の英語のプログラミングスクールを卒業後、Dockerを勉強し始めました。卒業後に取り組んだプロジェクトでDockerの構築を行ったのですが、多くのエラーに直面しました。独学で試行錯誤しながらエラー解決を行ったので、非常に多くの時間がかかってしまいました。私のように、Dockerを初めて触ってエラーに苦労した人も多いと思うので、私が使った解決方法を参考にしていただければと思い、この記事を書くことにしました。

1 PermissionError (Permission denied :‘~~~/your_app/tmp/db’)

Dockerの公式ドキュメントを参照する限り、このエラーはLinuxユーザーだけのようですが、LinuxユーザーにとってPermissionは厄介になりうるので共有します。このエラーは、Dockerが一時的なデータベース(dbフォルダ)を作るのですが、現在ログインしているLinuxユーザーがそのデータベースを使う権利がないため発生します。

まずはこのコマンドでそのテンポラリーフォルダに移動します。

$ cd ~~~(ご自身のアプリまでのパス)/(ご自身のアプリ名)/tmp

以下のコマンドでどのユーザーがdbフォルダを使えるか確認します。

$ ls -la

現在ログインしているLinuxユーザーの名前が表示されていなければ、そのユーザーに以下のコマンドで権限を与えます。

$ sudo chown -R (Linuxユーザーの名前) . 
password for (Linuxユーザーの名前):

sudoコマンドなので、パスワードの入力が求められます。ここまで行けば、"docker-compose build"や"docker-compose up"など主要なDockerコマンドは問題なく実行できるはずです。

注:
似たようなエラーで、私は"FATAL: could not open file “
global/pg_filenode.map”: Permission denied"というエラーをよく見たのですが、こちらは"docker-compose stop"→"docker-compose up"でリスタートすれば大丈夫です。

2 No space left on your device

Dockerfileの設定によって、Dockerイメージの容量が大きくなってしまった場合に起きるエラーです。不要なイメージ並びにコンテナは以下のコマンドで簡単に削除できます。

$ docker image prune
$ docker container prune

これらのコマンドを入力すると、以下のメッセージが表示されます。

WARNING! This will remove all dangling images (もしくはcontainers).
Are you sure you want to continue? [y/N]

dangling imagesは不要なイメージという意味なので、"y"を入力してください。容量が確保され、PermissionError解決時と同様の主要なDockerコマンドを実行できるようになっているはずです。

もしもさらに削除する必要がある場合は、以下で削除するイメージを探します。

$ docker images

消したいイメージのIDを確認した後、以下で削除します。

$ docker image rm -f (消したいイメージのID)

参考文献

英語ですが、この本はRuby on RailsのプロジェクトでDockerを使う方法をかなり詳細に論じているので、おすすめです。

Docker for Rails Developers: https://pragprog.com/titles/ridocker/

残念ながら、英語も含めてRails用のDockerの資料はまとまっているものが少ないので、特にRails歴が浅く、Dockerについての易しい資料が読みたいということであれば、英語ですが読む価値はあると思います。

Mediumの英語の記事

本テーマに関してより詳細に、Mediumに英語で記事を書いたので、そちらもご覧になってみてください。

https://medium.com/@shogohida_81081/5-solutions-to-common-errors-of-docker-for-beginners-c04dc1237c78

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

そうだ、画像をプレビューしよう。~part5~

前提

  • ruby on rails 6.0.0 を使用。
  • ユーザー機能はdeviseにより導入されているものとする。
  • viewファイルは全てhaml形式とする。
  • ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。

はじめに

前回(part4)のあらすじです。複数画像を含んだ商品情報の登録、編集機能までが実装できました。ひと通りの作業は終わった感じですね。

ちなみに前置きや手順などは part1 に詳しく記載してあるので気になったら是非見てやってください。

さぁ!残るはプレビューのみだ!

プレビューします

それではまずjsから記述していきます。
やることしてはフォームのときと似たような感じです。

html生成用の関数を作って、新しいフォーム同時に表示させます。画像を削除した場合はプレビューも消えるようにしましょう。

app/assets/javascripts/product.js
$(function() {
  // ~省略~
  const buildImg = (index, url)=> {
    const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`;
    return html;
  }
  // ~省略~
  $('#image-box').on('change', '.file', function(e) {
    const targetIndex = $(this).parent().data('index');
    const file = e.target.files[0];
    const blobUrl = window.URL.createObjectURL(file);

    if (img = $(`img[data-index="${targetIndex}"]`)[0]) {
      img.setAttribute('src', blobUrl);
    } else {
      $('#image-box').append(buildFileField(fileIndex[0]));
      fileIndex.shift();
      fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
    }
  });
  // ~省略~
  $('#image-box').on('click', '.remove', function() {
    // ~省略~
    $(`img[data-index="${targetIndex}"]`).remove();
  });
});

長くなってしまいましたがこんなところです。複雑なのは真ん中の部分だけなのでそう身構えることもないと思います。

それでは順番に解説をつけていきましょう。

const buildImg = (index, url)=> {
    const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`;
    return html;

まずはhtmlを生成するための関数ですが、特に難しいことは何もないので大丈夫だと思います。引数として渡されたurlで、大きさを指定しながら画像を表示しています。

$('#image-box').on('change', '.file', function(e) {
    // フォームに割り振られた固有のインデックスを取得。
    const targetIndex = $(this).parent().data('index');
    // 画像ファイルのweb上におけるURLを取得。
    const file = e.target.files[0];
    const blobUrl = window.URL.createObjectURL(file);
    // 該当するインデックスを持つ画像の有無で条件分岐
    if (img = $(`img[data-index="${targetIndex}"]`)[0]) {
      // 前行で取得した画像のURLを差し替える。
      img.setAttribute('src', blobUrl);
    } else {
      $('#previews').append(buildImg(targetIndex, blobUrl));
      $('#image-box').append(buildFileField(fileIndex[0]));
      fileIndex.shift();
      fileIndex.push(fileIndex[fileIndex.length - 1] + 1)

次に画像を選択した際の処理です。内容としてはこんな感じ。

難しいのは5,6行目のURLの取得ですが、ここに関してはいまいち自分でも理解できていないので後ほど詳しく調べようと思っています。動作自体はこれでうまくいくのでひとまず安心してください。

ちなみにelse以降の部分は元々あった記述にプレビュー表示を追加しただけなので詳しくは前のpartをご覧くださいな。

こいつらをelse以下に移動した理由としては、今までの状態では画像を差し替えるだけでも新しいフォームが表示されてしまっていたためです。なのでこいつらをelse以下に置くことで、画像追加時のみフォームが追加されるようにしてます。

  $(`img[data-index="${targetIndex}"]`).remove();

最後はこいつですが、ただ削除ボタンに合わせてプレビューを削除しているだけです。ほんとにそれだけ。

これでjsの処理が完成したので、今度は編集画面にあらかじめ表示されるべきプレビューを追加していきます。

app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    #previews
      - if @product.persisted?
        - @product.images.each_with_index do |image, i|
          = image_tag image.src.url, data: { index: i }, width: '100', height: '100'
   = f.fields_for :images do |i|
      // ~省略~
  = f.submit 'SEND'

追記したのは @product.persisted? の部分です。productに紐づいた画像をurlで1枚ごと取り出し、image_tagを用いて表示させています。

ちなみに.each_with_index とは、引数を二つ設定することで、.eachと同時に一つずつ番号を割り振るメソッドです。

さて、これでプレビュー機能も実装完了となります。
ということは、

最後に

遂に完成です!!結構な時間がかかりましたが、なんとか終えることができました。ふぅ。

完成とは言いつつもできたのは機能面だけなので、あとはゆっくりマークアップをしていく感じになります。

とはいえ仕様書の内容は全て実装することができました。実際のところ大して難しいことをしてるわけでもないのに長々と書いてしまってすみません。

いないとは思いますが、最後まで読んだ方がいらっしゃいましたら長らくどうもありがとうございました。何かの参考になれば幸いです。

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

【Postmanを使用】APIレスポンステストで詰まったところ 

APIレスポンステストとは

自身が設定したHTTPのレスポンスのステータスコードと実際のデータを含んだレスポンスボディが正しく返ってくるかテストします!
これらは、実際にAPI仕様書等見れば記載があると思いますので、確認してみてください:v:

※補足です

ステータスコードとは

今回のAPIによって返されるステータスコードは以下になります。
APIの動きによってこちらのコードと照らし合わせる事でテストします。

  • 200: OK - リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
  • 201: OK - POSTリクエストでデータが一つ追加される。
  • 401: Unauthorized - 認証失敗。認証が必要である。
  • 403: Forbidden - 禁止されている。リソースにアクセスすることを拒否された。
  • 404: Not Found - 未検出。リソースが見つからなかった。
  • 500: internal server error - インストールの失敗、互換性の問題、サーバーのアクセス権限の設定ミスなどが原因として上げられます。

レスポンスボディとは

自身が設定したカラムのデータの内容です。
GETリクエストの場合は単にリクエストボディに要求したデータが入っているかをテストします。
POST、PUT、 DELETE リクエストの際にはそれぞれ要求したデータ通りの動きをするかテストします。

Postmanとは

APIの動作を確認するツールです:point_up:
それだけではなく認証、テスト、ドキュメント作成、バージョン管理など幅広く活用できる便利なツール&サービスになっています!

今回は、認証やテストしか触れていませんが、他の機能も今後触れていきたいですね:grin:
まずは下記URLでPostmanをダウンロードしてください!!
※この記事では基本的な使い方は紹介していないので、詳しい使い方は最後の行に参考URL記載しています!!
https://www.postman.com/
image.png

APIレスポンステストで詰まったところの例

ここからはAPIレスポンステストを具体例を挙げて紹介します!
割と基本的なところで詰まってしまったりしたんですけど、同じ様なところで詰まっている方いれば、参考にしてください:relaxed:

  • 画像の挿入の仕方 今回は、form_dataを用いて画像挿入しました:smiley: もし、base64文字列を読み込む設定がアプリにあるなら画像をbase64のコードにデコードしてjson形式で読み込めます!
  1. Bodyからform-dataを選択して、KEY入力欄のプルダウンから、Text → Fileに変更
  2. VALUEで挿入したい画像を選択 スクリーンショット 2020-07-15 11.46.25.png
  • ストロングパラメーター設定時の入れ方

例えば、あるコントローラーで下記の様に設定されている場合
スクリーンショット 2020-07-15 11.50.41.png

下記の様に許可しているキーを指定してあげないとparam is missing or the value is emptyとなってしまいます。
スクリーンショット 2020-07-15 12.02.05.png

  • 関連付けしているカラムの入れ方

またまた、下記の様なコントローラーの設定があったします。
スクリーンショット 2020-07-15 21.12.04.png

見ての通り多対多の関連づけがされているものがあります。
こういう時Postmanではどの様に記載するのかといいますと下記の様になります。
スクリーンショット 2020-07-15 21.17.40.png

  • ログイン状態の維持 アプリにbrfore_action: authenticate!の様な認証設定があれば、ログイン状態でテストしたい時があります。そういう時は、下記の様にAutherizaitonのキーを押してTokenTypeを選択して、認証をすればログイン状態でテストできます:thumbsup:
  1. まずはログインしたいユーザーでログインしてアクセストークンを取得します。
    スクリーンショット 2020-07-15 17.35.58.png

  2. 取得したアクセストークンを下記の様に入力すれば、ログイン状態でAPIのテストが実施できます。
    ※今回はBarear Tokenを指定しています。アプリによって設定が違うと思いますので、確認してみてください!!
    スクリーンショット 2020-07-15 17.35.37.png

まとめ

Postmanをしようして詰まったところをまとめてみました!
これから同じ様な内容で詰まる方の参考になれれば嬉しいです:relaxed:

参考URL

  • 今回の記事ではPostmanの使い方を詳しく紹介していないので下記に簡単に紹介されています!

https://blog.linkbal.co.jp/4447/#:~:text=Postman%E3%81%AF%E3%80%81Web%20API%E3%81%AE,%E3%81%AA%E3%81%A9%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82

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

rubyのバージョン上げてrails sしようとしたらLibrary not loaded

エラーの抜粋

/Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require': dlopen(/Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
Referenced from: /Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle
Reason: image not found - /Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle

Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
と言っているのでbundle doctor

The following gems are missing OS dependencies:
 * mysql2: /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib
 * mysql2: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib

mysqlが依存しているopenssl系の何かが足りないと言っている。
rubyのバージョンアップに伴ってopensslを入れ替えたからか。
ということでmysqlをinstallしなおして依存しているgemライブラリを入れ直す

bundle exec gem uninstall mysql2
bundle install # mysqlのgemも一緒に入るはず

rails s すると正常に動いた

$ bundle exec rails s -p 3099
=> Booting Puma
=> Rails 5.2.4.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3099
* Listening on tcp://[::1]:3099

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

未経験初学者がRails Serverを立てられなくて困ってた話。

はじめまして。たくまです。
自分なりに困ったことがあり解決ができた経験をアウトプットしてみようと思います。

経緯

ProgateのWeb開発パスを勧めていくうちにうまくいかない箇所が出て来たので自分で行った解決法を共有しようと思います。
理解が浅くフワッとした内容なのでご指摘があればよろしくお願いします。

問題

Web開発パス(Ruby on Rails)のSTEP4、[Ruby on Railsの環境構築をしてみよう!(macOS)]では最終的にはブラウザにてRailsサーバを立てて終わりになります。
しかし、エディタにコマンドを入力しても立てることができない!!
具体的には

~
Could not find a JavaScript runtime.
~

といった内容が表示されブラウザにRailsの表示ができませんでした。
ターミナルをいじる経験もあまりなかったため、エラーコードをググってみると下記のような記事が見つかりました。

解決

どうやらJavaScriptに関することを言っているようです。
そこでエラーコードにあった"therubyracer"についてググってみると以下のような記事が見つかりました。

開発環境構築に関して知識のなかった自分としてはこの記事を見ることにより落ち込まずに学習を続けることができました。

開発環境構築が鬼門な点だ。rubyにはrubygemsというライブラリの管理システムがある。Railsはたくさんのライブラリ(gem)を起動前にインストールする必要があるのだが、これが理不尽なエラーとともに失敗することが頻発する。慣れると対応策は分かってくるのだが、初心者だと「立ち尽くすしかない」と思う。

内容としてはtherubyracerという部品が古くなっており、新しくmini_racerという部品が広く普及していると言った内容。
→なんとなくAppleのSidecarと乗り物繋がりでフロントよりの技術てことでJavaScriptと関係あるかもなんて思ってた。

実施

改めてレッスンで作成した sample_app < Gemfile 内容を確認するとmini_racerがコメントアウトされてる...
こいつのコメントアウトを外してあげて編集点を保存してくれる?らしい"bundle update"を実施しアップデート。
後に"Rails s"を実施すると無事にブラウザでServerを立ち上げることができました。

あとがき

今回、はじめて問題点に関する解決法を記事にしてみましたが、gemの内容や自分と一緒の場所でつまずいている方が仮にこの記事を参考にしてくれた場合の動線の意識など反省点がや改善点が多々ありました。
今後もこのようなアウトプットを続けて行こうと思います。

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

高速なのは Range#cover? or Range#include? or ActiveSupport::TimeWithZone#between? それとも?

rubocop-performance を確認していたら、

Performance/RangeInclude: Use Range#cover? instead of Range#include?.

が出たのでベンチマークを取ってみました。

require 'bundler/setup'
require 'benchmark_driver'

Benchmark.driver do |x|
  x.prelude <<~RUBY
  require 'active_support/time'
  t = Time.current + 5.minutes
  RUBY

  x.report 'include?', %[ (Time.current..Float::INFINITY).include?(t) ]
  x.report 'cover?', %[ (Time.current..Float::INFINITY).cover?(t) ]
  x.report 'between?', %[ t.between?(Time.current, Float::INFINITY) ]
  x.report 'raw_function', %[ t >= Time.current && t <=  Float::INFINITY ]
end

結果

Warming up --------------------------------------
            include?    85.071k i/s -     86.456k times in 1.016282s (11.75μs/i)
              cover?   131.933k i/s -    135.600k times in 1.027793s (7.58μs/i)
            between?   276.108k i/s -    285.615k times in 1.034434s (3.62μs/i)
        raw_function   285.200k i/s -    291.396k times in 1.021725s (3.51μs/i)
Calculating -------------------------------------
            include?   125.985k i/s -    255.212k times in 2.025732s (7.94μs/i)
              cover?   133.774k i/s -    395.799k times in 2.958713s (7.48μs/i)
            between?   304.505k i/s -    828.322k times in 2.720227s (3.28μs/i)
        raw_function   304.348k i/s -    855.600k times in 2.811255s (3.29μs/i)

Comparison:
            between?:    304504.7 i/s
        raw_function:    304348.1 i/s - 1.00x  slower
              cover?:    133774.0 i/s - 2.28x  slower
            include?:    125985.1 i/s - 2.42x  slower

include? から cover? に変更したほうが早いが、between? や便利機能使わずに普通に書いた raw_function の方が早くなりました。 後者の2つは実行するタイミングで順位がわかりましたが、ほぼ誤差の範囲だと思います。

ちなみに benchmark_driver では、 ymlで入力することもサポートしているので、

# range.yml
prelude: |
  require 'bundler/setup'
  require 'active_support/time'
  t = Time.current + 5.minutes

benchmark:
  include?: (Time.current..Float::INFINITY).include?(t)
  cover?: (Time.current..Float::INFINITY).cover?(t)
  between?: t.between?(Time.current, Float::INFINITY)
  raw_function: t >= Time.current && t <= Float::INFINITY

のようなファイルを用意して、

$ benchmark-driver range.yml

と書いても似たような結果を得られました。

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

【form_withのmodel:の意味】

form_withに記述するmodel:の意味

いまいち理解できていなかったのですがようやく意味がわかったのでアウトプットとして記述していきます。

model-.png

上の画像はツイートの詳細画面からコメントを投稿するためのフォームを表示させるためのコードを記述しているのですが、
form_withに続くmodel:@tweet, @commentの二つをなぜ使用しているのかいまいち理解できていませんでしたが解決できましたので以下に理由を記述します。

1点目

今回の場合はツイートに対してコメントをするという状況でありコメントは一つのツイートに紐付いている状態なのでコメント単体でフォームの送信先に指定することはできないため@tweet, @commentと紐付いているものとセットにして送信先を指定しているという理由。

2点目

@tweet,@commentという記述自体がルーティングを指定しているということ。
どういうことかといいますと、以下の画像を見てください。

model2.png

今回の場合はコメントを作成したいので発動させたいアクションはcommentsコントローラーのcreateアクションです。
そして一番左のprefixの項目を見ると、tweet_commentsというパスが記述されています。
要するにフォームを送信してコメントを作成するのであればこのルーティングを指定しなければいけないということです。

そのルーティングを指定しているのがmodel: [@tweet,@comment]だというのが二点目の理由です。

以上です。

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

Unicornを再起動する

前提

RailsアプリをEC2にデプロイし、Unicornをアプリケーションサーバーとして使用しています。

開発環境でしたら、rails sで起動すれば良かったのですが、本番環境はサーバーの起動&停止方法が変わったので、メモとしてまとめました。

サーバーを停止したい場合の手順

Unicornの起動確認

ps -ef | grep unicorn | grep -v grep
vinaka     15533     1  0 08:02 ?        00:00:01 unicorn_rails master -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production                                                                                    
vinaka     15537 15533  0 08:03 ?        00:00:00 unicorn_rails worker[0] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production                                                                                 
vinaka     15538 15533  0 08:03 ?        00:00:00 unicorn_rails worker[1] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production 

master、worker[0]、worker[1]
三つ出てきた場合、Unicornが起動しているようです。

killする

今確認した、三つのうちの一番上の番号(master)15533killします(毎回番号が変わるので都度ps -ef | grep unicorn | grep -v grepで番号を確認)

kill -9 15533

Unicornの停止確認

ps -ef | grep unicorn | grep -v grep

すると何も表示されないはず。
表示されていないと、Unicornが停止しています。

再度サーバーを立ち上げる時の手順

Unicornの停止確認

ps -ef | grep unicorn | grep -v grep

Unicorn起動!

 bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production 

何も出ないと、ちゃんと起動しているようです。

一応確認もする。

ps -ef | grep unicorn | grep -v grep
vinaka     15740     1  1 08:48 ?        00:00:01 unicorn_rails master -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production                                                                                    
vinaka     15744 15740  0 08:48 ?        00:00:00 unicorn_rails worker[0] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production                                                                                 
vinaka    15745 15740  0 08:48 ?        00:00:00 unicorn_rails worker[1] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production  

三つ表示されました!起動されている。
さっきと番号が変わっているはず!

ちなみに

master failed to start, check stderr log for details

とエラーが出た場合は

cat log/unicorn.log

でエラーの内容を確認する。
Unicornのバージョン(5.5以上だとエラーが出るみたいなのでGem fileでバージョン指定するのが良いです。)

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

また

unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production

だと怒られることがあるので、bundle execをつけてあげた方が無難。

bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production

nginxも再起動

Unicornが無事に立ち上がったら、nginxも再起動してあげて完了です。

sudo nginx -s reload

終わりです。

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

Progate Ruby on Rails5 振り返り

プロゲート Ruby on Railsコース所感

プログラミングの学習に200時間ほど費やし、Railsコースに着手しました。
今までの学習ではスムーズに進んでこれたものの、今回は苦戦。
道場コース4の内容は完全には理解できていません。
復習できるように、メモとして記録を残します。


マイグレーションファイル関連

マイグレーションファイルとは:データベースを作成するときの設計図。
モデルとは:テーブルを操作するための特殊なクラス
  
モデルの作成

rails g model Post content:text 

rails g model モデル名 カラム名:データ型
※モデル名は単数系で指定する。
  
マイグレーションファイルの作成

rails g migration add_image_name

URLから値を取得する

ルーティングでハッシュを指定する。

routes.rb
get "posts/:id" => "posts#show"

params[ハッシュ]とすることで取得できる

controller.rb
def show
  @id = params[:id]
end

※params[:~~]で取得できる値は文字列であるため、比較等を行う場合には整形すること。

フォームの送信先の設定

posts/new.html.erb
<%= form_tag("/posts/create") do%>
<% end %>

doを忘れないこと

post時のリンク記述方法

第三引数としてメソッドのpostを指定する。

<%= link_to("削除する","/posts/#{@post.id}/destroy",{method: "post"})%>
<% end %>

データベースへの保存処理での分岐

if ~~~の部分で保存処理は実行されていることに注意
(真偽値を返すだけではない)

def update
  @post = Post.find_by(id: params[:id])
  if @post.save
    redirect_to("/posts/index")
  else
    redirect_to("/posts/#{@post.id}/edit")
  end
end

データベースを経由せず、直接ビューに表示する(render)

render(コントローラー名/ビュー名)と指定する

def update
  @post = Post.find_by(id: params[:id])
  if @post.save
    redirect_to("/posts/index")
  else
    render("posts/edit")
  end
end

画像の送信欄

typeの指定を忘れないこと

<input name="image" type="file">

画像の送信フォーム

{multipart: true}を指定する

<%= form_tag("~~~",{multipart: true}) do%>

送信された画像の保存処理

受け取ったparams[:image]に対して、readメソッドを活用し画像データの中身を取得する。
File.binwrite("ファイルの場所","ファイルの中身")

  def update
    @user = User.find_by(id: params[:id])
    @user.name = params[:name]
    @user.email = params[:email]
    @user.image_name = "#{@user.id}.jpg"
    image = params[:image]

    if params[:image]
      File.binwrite("public/user_images/#{@user.image_name}",image.read)
    end
  end

セッションの設定

ページを移動しても、ユーザー情報を保持し続けるために、sessionという特殊な変数を活用する。
sessionに代入された値はブラウザに保存される。

session[:user_id] = @user.id

共通変数の定義

application.html.erbは全アクションから呼び出される。
そのため、全アクションで活用する変数を定義すると効率が良い。
before_action :アクション名 とすることでアクションが呼び出される前に「アクション名」が実行される。

application.rb
before_action :set_current_user

def set_current_user
  @current_user = User.find_by(id: sessino[:user_id])
end

投稿テーブルに持たせたuser_idから別テーブルに存在するユーザー情報を取得する

使用するデータが存在するテーブル(ここではPostモデル)に対してアクションを定義する

post.rb
def user
  return User.find_by(id: self.user_id)
end

活用例(投稿から、それに紐づくユーザーを取得する)

posts_controller.rb
def show
  @post = Post.find_by(id: params[:id])
  @user = @post.user
end

ユーザーテーブルに持たせたidから別テーブルに存在する投稿情報を取得する

使用するデータが存在するテーブル(ここではUserモデル)に対してアクションを定義する

user.rb
def posts
  return Post.where(user_id: self.id)
end

活用例(ユーザーから、それに紐づく投稿を全て取得し表示する)

show.html.erb
<% @user.posts.each do |post|%>
  <img src="<%= "/user_images/#{post.user.image_name}" %>">
  <%= link_to(post.user.name, "/users/#{post.user.id}") %>
  <%= link_to(post.content, "/posts/#{post.id}") %>
<% end%>

各ユーザーがいいねした投稿の一覧表示

ルーティングの指定

routes.rb
get "users/:id/likes" => "users#likes"

アクションの定義

users.controller.rb
def likes
  @user = User.find_by(id: params[:id])
  @likes = Like.where(user_id: @user.id)
end

ビューでの表記

likes.html.erb
<% @likes.each do |like|%>
  <% post = Post.find_by(id: like.post_id)%>
  <img src="<%= "/user_images/#{post.user.image_name}" %>">
  <%= link_to(post.user.name, "/users/#{post.user.id}") %>
  <%= link_to(post.content, "/posts/#{post.id}") %>
<% end%>

二行目のLikeテーブルのpots_idを用いて、対応する投稿を取得する部分を忘れないように。


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

「商品画像を差し替える」と心の中で思ったならッ!その時スデに行動は終わっているんだッ!~part4~

前提

  • ruby on rails 6.0.0 を使用。
  • ユーザー機能はdeviseにより導入されているものとする。
  • viewファイルは全てhaml形式とする。
  • ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。

はじめに

前回(part3)のあらすじです。jQueryを使用して新規登録画面にあんなことやこんなことを実装しました。

現在の状態としては、画像を選択すると新しいフォームが出現する、削除を押したら消える、という感じです。一見出来上がったように見えますが、これらは全てjsによるアクションなのでリロードされた編集画面では消えてしまいます。

なので今回のpartでは、編集画面にも新しいフォームを出現させる、といった作業になります。

ちなみに前置きや手順などは part1 に詳しく記載してあるので気になったら是非見てやってください。

やってみよう

やることとしては主に以下の3つです。

  • 編集画面に新しいフォームを表示させる。
  • 削除ボタンで、すでに登録された情報も削除できるようにする。
  • 画像フォームがもつ固有のインデックスが被らないようにする。

こんな感じですね。さっそくやっていきますが、ビューファイルをいじる前にコントローラに少し追記をする必要があるので、そちらからいきましょう。

app/controllers/products_controller.rb
#~省略~
  private
  def product_params
    params.require(:product).permit(:name, images_attributes: [:src, :_destroy, :id]).merge(user_id: current_user.id)
  end
#~省略~
end

ストロングパラメータに少し記述を増やしました。この_destroyというのは、関連づいた子モデルの情報を削除してくれるキーです。見慣れない形ですがちゃんとしたキーなので安心しましょう。

それではビューファイルを記述していきます。

app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    // ~省略~
    - if @product.persisted?
      .group{ data: { index: @product.images.count } }
        = file_field_tag :src, name: "product[images_attributes][#{@product.images.count}][src]", class: 'file'
        .remove
          削除
  = f.submit 'SEND'

追記したのは@product.persisted?の部分です。難しく見えますが、やっていることとしては前回jQueryで追加したフォーム部分をhaml形式にしただけです。新しいフォームを表示させようってやつですね。

このpersisted?ですが、こいつは利用したインスタンスが保存済みか否かを判断してくれます。要は新規なのかすでに登録された情報なのかってことです。とても便利なので覚えておきましょう。

表示はできましたが、現在の削除ボタンではすでにデータベースに登録された情報を削除することができません。なので先ほど追加した_destroyキーを使って削除できるようにする必要があります。

app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    = f.fields_for :images do |i|
      // ~省略~
      - if @product.persisted?
        = i.check_box :_destroy, data: { index: i.index }, class: 'hidden'
  - if @product.persisted?
    // ~省略~
  = f.submit 'SEND'

先ほどとは別に、image-boxの内部にもpersisted?で文を追加しました。_destroyキーを持ったチェックボックスにチェックを入れると、データベース上から対応するレコードが削除される、といった記述です。なぜそうなるのかは詳しく書きませんが(自分が理解しきれていないので...)、こういう書き方があるのか程度に覚えておくといいと思います。

さて、これで仕組み自体はできました。ですが削除ボタンと別にチェックボックスがあるのはよろしくないのでこいつらを連動させていきます。

app/assets/javascripts/product.js
$(function() {
  // ~省略~
  $('.hidden').hide();
  $('#image-box').on('click', '.remove', function() {
    // フォームに割り振られた固有のインデックスを取得。
    const targetIndex = $(this).parent().data('index')
    // 取得したインデックスに対応するチェックボックスを取得。
    const hiddenCheck = $(`input[data-index="${targetIndex}"].hidden`);
    // チェックボックスが存在する場合チェックを入れる。
    if (hiddenCheck) hiddenCheck.prop('checked', true);
    ~省略~
  });
});

image-boxのクリックイベントに処理を追加しました。一行ずつの解説も書いておきました。やっていること自体は簡単なので、jQueryの基礎が分かっていれば問題はないと思います。

削除ボタンを押せばチェックもできるようになったのでチェックボックスは非表示にしておきましょう。今回はjsの.hide()を使用してやりましたが、cssで display: none にしていただいても構いません。

それでは最後にインデックス被りを防止して実装は完了となります。

app/assets/javascripts/product.js
$(function() {
  // ~省略~
  let fileIndex = [1,2,3,4,5]
  lastIndex = $('.group:last').data('index');
  fileIndex.splice(0, lastIndex);
  // ~省略~
});

fileIndexの定義部分に記述を追加しましょう。考え方としては、現在使われている最後のインデックスを取得し、その値でfileindexの値を入れ替える、といった感じです。

ちなみに.spliceについてですが、こいつは指定した要素から数えて好きな分だけ取り除き、ついでに要素の追加もできてしまう優れものです。

今回は第一引数で指定した数以降の値を全て取り除き、第二引数で指定した値を挿入してくれています。やれることが多い分、書き方に多少複雑な部分があるので是非詳しく調べてみてください。

さて、以上で編集機能自体の実装は終了となります!

最後に

とうとう出来上がりましたね。いやぁ長かった。

仕様書でいう「画像をの差し替えを一枚ごとにできる。」は達成できたとしていいと思います。あとはわかりやすいように画像のプレビューをつければ完成です。

アプリで画像をプレビューする前に記事に画像をプレビューしろよって天の声が聞こえますが気にしないことにします。読みづらくてすみません。

それではまた次のpartで!!

商品情報編集機能を実装したい~part5~

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

アクセスが多いテーブルのカラムを安全に変更(削除やリネーム)する方法

概要

これはアクセスの多いテーブルのカラムを変更したときに、
カラムを新しく変更したはずなのに、なぜかコードは(すでに存在しない)古いカラムを参照してしまい、エラーが発生してしまう問題を解決するための方法を記したものです。

前提条件

Rails 5.1
heroku

解決策1「heroku restart」

先日、自分のカラム の英語の命名ミスに気づき、
AnswerHistoryテーブルのweekly_continuationというカラムを、continuation_all_weekに変更しました。

その後、カラム変更に伴うコードも変更してデプロイし、
heroku run rails db:migrateしたのですが、その後しばらくSentryより、以下の報告を繰り返し繰り返し受け続けました。

PG::UndefinedColumn: ERROR: column answer_histories.weekly_continuation does not exist

つまり、DBのカラム名はきちんと変更されているにも関わらず、コードは古いカラム名を参照し続けているということです。

僕は最初この警告を、一時的な不具合だとばかり思っており、無視していました。
「ユーザーがページを遷移したり更新すれば直るやろ」と。
しかし、自分の端末でも同じ問題が起こり、かつページを遷移しても更新しても解決しなかったとき、これがサービス運営者が解決すべき障害だと気づきました。
(それもこの障害は、自分のサービスの中でももっともアクセスが多いページで起きていました....無念....)

結局このときは、heroku restartを行うことで障害を解決することができました。

「なぜheroku restartで障害を解決できたのか?」
解決しておきながら、自分はその理由がわかりませんでした。

よりベテランの開発者の方にこのことを聞くと、
どうやら、heroku run rails db:migrate する直前にデプロイされたとき、Railsのアプリケーションサーバーがschema informationのキャッシュを持ってしまっていたがために、アプリケーションサーバーを再起動してキャッシュを消す必要があり、
そのためheroku restartをして、dynoごとアプリケーションサーバーを再起動する必要があったそうです。

なので、もし教科書的に、
git push heroku
heroku run rails db:migrate
を行いカラムを変更する場合には、
追加でheroku restart もしておくことで、
heroku のdynoと一緒に、アプリケーションサーバーも再起動されるおかげで、schema informationのキャッシュもクリアされるので、コードがきちんと新しいカラムを参照してくれるようになります。

解決策2「Procfileからリリース時にdb:migrateを行う」

今回の障害で初めて知ったのですが、
herokuではProcfileなるものを設定することで、
heroku run rails db:migrate のようなコマンドをリリース時に自動で実行してくれるようになります。
参照: https://devcenter.heroku.com/articles/release-phase

このProcfileでdb:migrateを行うメリットとしては、「デプロイした後」ではなく「デプロイ直前」にdb:migrateしてくれるという点です。
herokuではデプロイ時に、自動で真新しい別のDynoを立ててくれます。
そして真新しいDynoはheroku restartされた状態と同じなため、
新しいDynoにあるアプリケーションサーバーはschema informationのキャッシュをもつこともなく、結果として、heroku restartせずとも、カラムの変更によるアプリケーションのエラーに見舞われることがないそうです。

ただしエラーが起きないのは、新しいDynoにアクセスしてきたユーザーのみで、古いDynoにアクセスしていたユーザーは、エラーに見舞われます。

Procfileからデプロイ直前に自動でdb:migrateまで行うには、次のように設定します。

Procfile
release: bin/rails db:migrate

解決策3『段階的にデプロイする』

より安全にカラムを変更するときに、実務においては段階的にデプロイする手法が採用されているそうです。

db:migrateを伴うリリースだけ先にデプロイし、本番環境でdb:migrateを行ってから、
別のタイミングで、変更したカラムを利用するソースコードをリリースするという形です。

たとえば今回のように、AnswerHistoryのweekly_continuationをcontinuation_all_weekにリネームする場合には、次のように3回のデプロイします。

1, continuation_all_weekカラムを新しく追加するmigrationファイルを作成。
2, デプロイ&db:migrate。
3, weekly_continuationカラムではなく、continuation_all_weekカラムを参照するように、コードを変更する
4, デプロイ&weekly_continuationのデータをcontinuation_all_weekにコピー。
5, weekly_continuationがコードから呼ばれていない状態にする。
6, weekly_continuationカラムを削除するmigrationファイルを作成。
7, デプロイ&db:migrate。

端的にまとめると、
テーブルへのアクセスが多い場合は、古いカラムをリネームするのではなく、新しいカラムを追加して、コードが古いカラムを参照しない状態をつくった後に、古いカラムを削除することで「リネームしたように見せること」が良い、ということらしいです。
 
面倒かもしれませんが、いちばん安全な方法ですね。
 
呼ばれる頻度の低いテーブルのカラムなら、これほど気を使わなくても良いかもしれませんが、
今回僕が障害を発生させたAnswerHistoryテーブルは、1日に1万件以上も生成されるほど利用頻度の高いものだったので、以降、カラムのリネームなどを行う場合は、こちらの段階的なデプロイを採用したいと思います。

以上となります!
僕は恐ろしく無知なので、他にもアドバイスなどございましたら、ぜひ教えてください。

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

アクセスが多いテーブルのカラムを安全に変更(削除やリネーム)する方法【Rails,heroku】

概要

これはアクセスの多いテーブルのカラムを変更したときに、
カラムを新しく変更したはずなのに、なぜかコードは(すでに存在しない)古いカラムを参照してしまい、エラーが発生してしまう問題を解決するための方法を記したものです。

前提条件

Rails 5.1
heroku

解決策1「heroku restart」

先日、自分のカラム の英語の命名ミスに気づき、
AnswerHistoryテーブルのweekly_continuationというカラムを、continuation_all_weekに変更しました。

その後、カラム変更に伴うコードも変更してデプロイし、
heroku run rails db:migrateしたのですが、その後しばらくSentryより、以下の報告を繰り返し繰り返し受け続けました。

PG::UndefinedColumn: ERROR: column answer_histories.weekly_continuation does not exist

つまり、DBのカラム名はきちんと変更されているにも関わらず、コードは古いカラム名を参照し続けているということです。

僕は最初この警告を、一時的な不具合だとばかり思っており、無視していました。
「ユーザーがページを遷移したり更新すれば直るやろ」と。
しかし、自分の端末でも同じ問題が起こり、かつページを遷移しても更新しても解決しなかったとき、これがサービス運営者が解決すべき障害だと気づきました。
(それもこの障害は、自分のサービスの中でももっともアクセスが多いページで起きていました....無念....)

結局このときは、heroku restartを行うことで障害を解決することができました。

「なぜheroku restartで障害を解決できたのか?」
解決しておきながら、自分はその理由がわかりませんでした。

よりベテランの開発者の方にこのことを聞くと、
どうやら、heroku run rails db:migrate する直前にデプロイされたとき、Railsのアプリケーションサーバーがschema informationのキャッシュを持ってしまっていたがために、アプリケーションサーバーを再起動してキャッシュを消す必要がありました。
heroku restartすることでdynoが再起動したので、アプリケーションサーバーのschema informationも新しくなり障害が解決したということでした。

なので、もし教科書的に、
git push heroku
heroku run rails db:migrate
を行いカラムを変更する場合には、
追加でheroku restart もしておくことで、
heroku のdynoと一緒に、アプリケーションサーバーも再起動されるおかげで、schema informationのキャッシュもクリアされるので、コードがきちんと新しいカラムを参照してくれるようになります。

解決策2「Procfileからリリース時にdb:migrateを行う」

今回の障害で初めて知ったのですが、
herokuではProcfileなるものを設定することで、
heroku run rails db:migrate のようなコマンドをリリース時に自動で実行してくれるようになります。
参照: https://devcenter.heroku.com/articles/release-phase

このProcfileでdb:migrateを行うメリットとしては、「デプロイした後」ではなく「デプロイ直前」にdb:migrateしてくれるという点です。
herokuではデプロイ時に、自動で真新しい別のDynoを立ててくれます。
そして真新しいDynoはheroku restartされた状態と同じなため、
新しいDynoにあるアプリケーションサーバーはschema informationのキャッシュをもつこともなく、結果として、heroku restartせずとも、カラムの変更によるアプリケーションのエラーに見舞われることがないそうです。

ただしエラーが起きないのは、新しいDynoにアクセスしてきたユーザーのみで、古いDynoにアクセスしていたユーザーは、エラーに見舞われます。

Procfileからデプロイ直前に自動でdb:migrateまで行うには、次のように設定します。

Procfile
release: bin/rails db:migrate

解決策3『段階的にデプロイする』

より安全にカラムを変更するときに、実務においては段階的にデプロイする手法が採用されているそうです。

db:migrateを伴うリリースだけ先にデプロイし、本番環境でdb:migrateを行ってから、
別のタイミングで、変更したカラムを利用するソースコードをリリースするという形です。

たとえば今回のように、AnswerHistoryのweekly_continuationをcontinuation_all_weekにリネームする場合には、次のように3回のデプロイします。

1, continuation_all_weekカラムを新しく追加するmigrationファイルを作成。
2, デプロイ&db:migrate。
3, weekly_continuationカラムではなく、continuation_all_weekカラムを参照するように、コードを変更する
4, デプロイ&weekly_continuationのデータをcontinuation_all_weekにコピー。
5, weekly_continuationがコードから呼ばれていない状態にする。
6, weekly_continuationカラムを削除するmigrationファイルを作成。
7, デプロイ&db:migrate。

端的にまとめると、
テーブルへのアクセスが多い場合は、古いカラムをリネームするのではなく、新しいカラムを追加して、コードが古いカラムを参照しない状態をつくった後に、古いカラムを削除することで「リネームしたように見せること」が良い、ということらしいです。
 
面倒かもしれませんが、いちばん安全な方法ですね。
 
呼ばれる頻度の低いテーブルのカラムなら、これほど気を使わなくても良いかもしれませんが、
今回僕が障害を発生させたAnswerHistoryテーブルは、1日に1万件以上も生成されるほど利用頻度の高いものだったので、以降、カラムのリネームなどを行う場合は、こちらの段階的なデプロイを採用したいと思います。

以上となります!
僕は恐ろしく無知なので、他にもアドバイスなどございましたら、ぜひ教えてください。

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

Railsで学ぶXSS脆弱性

webセキュリティについて勉強中なので、学習内容を公開します
スカスカ記事ですがお許しください

演習環境

  • Ubuntu 18.04.4
  • Ruby (2.6.6)
  • Rails (5.2.4.3)
    • database adapter mysql2 (0.5.3)
  • mysql (5.7.30)

記事投稿、コメント投稿、ブックマークなどが出来る演習用アプリ「TARGET」
GitHubリポジトリ https://github.com/Fumitaka1/target

XSS脆弱性とは

XSSには反射型、持続型、DOMベースの3タイプに分類され、単にXSSというと反射型XSSを指すことが多いようです。

上記の3タイプに共通する性質として「外部からのパラメータを用いてwebページを出力しているアプリケーションにおいて、任意のhtml要素や属性が挿入出来てしまう」所がXSS脆弱性の肝です。

挿入されたスクリプトによってセッションや入力データが流出したり、有害な操作やリダイレクトが可能になるなど多くの被害が発生します。

例 演習用アプリでは投稿にコメントする機能があります
正規のコメント.png

スクリプトタグを含む不正なコメントを投稿すると、ブラウザはスクリプトタグとして解釈します。
悪意のコメント.png
攻撃成功.png
演習用アプリではRailsの機能によりcookieのHttponly属性が付与されているので、Javascriptでセッションを取得することはできません。
しかし任意のページにリダイレクトしたり、不正なフォームをもとのページに重ねて表示することが出来ます。

なぜ発生するのか

上記の例では外部からのパラメータを用いてHTML要素内容を出力する際に、特殊文字[" ' & < >]をエスケープしていないことが原因。
等号記号が2つのERB式<%== @comment.content %>で意図的にエスケープを回避している。

XSS脆弱性が発生しうる箇所には①HTML属性値②HTML要素内容③リンク④イベントハンドラなどがあり、それぞれエスケープの方法が異なるので場所にあったエスケープを行う必要がある。
各々のエスケープ方法については後日記事にします。

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

【Rails】本番環境でも開発環境と同じようにエラー画面を確認する方法【デプロイ】

【経緯】

本番環境だと、ローカル環境のようなエラー画面が出ないため、エラーが特定しにくいものです。と思っていたのですが、本番環境でもエラー画面を出力する方法がありました。この方法を行うことでエラー解決がグッと近くなります!

【バージョンやら環境】

Rails 5.2.4.3
Ruby 2.5.1
macOS Catalina 10.15.4
Capistrano 3.14.1

【前提】

Capistranoが動く(Capistranoの自動デプロイが成功している)

【解決法】

config/environments/production.rb
config.consider_all_requests_local = false

config/environments/production.rb
config.consider_all_requests_local = true

にしましょう。こうすると、本番環境でも開発環境(ローカル)と同じエラー画面が出ます。

おなじみのこいつです。(参考画像)
error.png

(ただし、最終的にfalseに戻すのを忘れないようにしましょう。
一般の方や企業の方が本番環境を見た時にエラー画面が出たら変ですからね?)



この後、

①Githubのプッシュ(最新の状態に)

②デプロイを行う。(ローカルで直してもデプロイしないと意味がないので!)

ターミナル(アプリのディレクトリにて)
bundle exec cap production deploy

【一部参考にしたサイト】

Railsガイド:Rails アプリケーションを設定する

【あとがき】

デプロイの作業は地道ですね。
成功した時の喜びはかなり大きい。
がんばれデプロイ担当者(いつもいってる)

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

【N+1問題】

N+1問題とは

アソシエーションでモデル同士を関連づけている場合に起こる問題のこと。

例えばツイッターのようなアプリでユーザーと投稿が紐付いている場合に
トップページで投稿を一覧表示させる時に各投稿ごとに紐付いているユーザーを検索するために毎回毎回データベースにアクセスしているとアプリのパフォーマンスが下がってしまう。

1万投稿ある場合は1万回データベースにアクセスしなければいけないということ。

この問題のことをN+1問題という。

この問題を解決するためのメソッドが

includeメソッド

と言われるメソッド。

このメソッドは毎回毎回データベースにアクセスして紐づくデータを検索していたところを、
一回で全データを取得してくれる。

そのため1万投稿あるとしてもデータベースへのアクセス「は一度で済む。

n+1.png

上の画像のようにindexアクションの処理内容の箇所にincludes(:紐づくモデル)とすれば使用できる。

これでツイートなどを一覧表示する際に投稿がいくら増えたとしてもパフォーマンスを下げずに済む。

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

rails generateコマンドで不要なファイルを生成しない

はじめに

rails generate ...は必要な関連ファイルを自動生成してくれる便利なコマンドですが、「このファイルは不要だな」というものがあった場合、生成する度に削除していては手間がかかります。

そこで、自動生成するファイルを設定する(不要なファイルは生成しない)方法について書いています。

参考までに環境はこちら。

  • Ruby 2.5.1
  • Rails 5.2.4

generateコマンドで自動生成されるファイル

まず、どのコマンドでどのファイルが自動生成されるかを次の一覧にまとめています。

コマンド名 コントローラ ビュー モデル マイグレーション アセット ルート テスト ヘルパー
scaffold
scaffold_controller × × × ×
controller × ×
model × × × × ×
migration × × × × × ×

application.rbを編集する

設定方法は簡単で、application.rbに設定を書き込めばOKです。

例えば何も設定せず次のコマンドを打った場合、

rails g controller StaticPages home

次のファイルが生成されます。

      create  app/controllers/static_pages_controller.rb
       route  get 'static_pages/home'
      invoke  erb
      create    app/views/static_pages
      create    app/views/static_pages/home.html.erb
      invoke  test_unit
      create    test/controllers/static_pages_controller_test.rb
      invoke  helper
      create    app/helpers/static_pages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/static_pages.coffee
      invoke    scss
      create      app/assets/stylesheets/static_pages.scss

ここで、以下のファイルが不要だと思ったとします。

  • CSS, JavaScriptファイル
  • testファイル

その場合、次のように設定します。

module XXX
  class Application < Rails::Application
    config.load_defaults 5.2

    config.generators do |g|  # ここから追記
      g.assets false          # CSS, JavaScriptファイル生成せず
      g.skip_routes false     # trueならroutes.rb変更せず、falseなら通常通り変更
      g.test_framework false  # testファイル生成せず
    end                       # ここまで
  end
end

ちなみにg.assetsは、g.javascriptsg.stylesheetsに分けて記述することもできます。

この状態で再度rails generate ...を行うと、次のように生成されるファイルが減っていることがわかります。

      create  app/controllers/static_pages_controller.rb
       route  get 'static_pages/home'
      invoke  erb
      create    app/views/static_pages
      create    app/views/static_pages/home.html.erb
      invoke  helper
      create    app/helpers/static_pages_helper.rb

だいぶスッキリしました。以上。

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

【Rails】Scopeの使い方

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

基本構文

class モデル名 < ApplicationRecord
  scope :スコープ名, -> { 条件式 }
end

使用例

下記のようユーザーのIDを降順にして5つだけ表示させたいと仮定します。

users_controller.rb
User.order(id: desc).limit(5)

1.引数なし

models/user.rb
class User < ApplicationRecord
  scope :recent, -> { order(id: :desc).limit(5) }
end
users_controller.rb
User.recent

2.引数あり

models/user.rb
class User < ApplicationRecord
  scope :recent, -> (count) { order(id: :desc).limit(count) }
end
users_controller.rb
User.recent(5)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsアプリをawsでインフラ構築する際に出たエラー

今回はこちらの記事を参考に、始めて作成したrailsのappをAWSを使用してインフラ構築に挑戦した。
https://qiita.com/naoki_mochizuki/items/814e0979217b1a25aa3e

EC2インスタンスの環境構築

$git make gcc-c++ patch
git: 'make' is not a git command. See 'git --help'.
The most similar commands are blame, merge, stage

というエラーが出た。これはコメントを削除し、1行のコマンドとして実行する必要があった。
$sudo yum install git make gcc-c++ patch openssl-devel libyaml-devel libffi-devel libicu-devel libxml2 libxslt libxml2-devel libxslt-devel zlib-devel readline-devel mysql mysql-server mysql-devel ImageMagick ImageMagick-devel epel-release

$sudo mkdir www

mkdir: ディレクトリ `www' を作成できません: File exists
というエラーが出た。既に存在しているらしいが作成した記憶はない、、、
そのまま使用するか迷ったが、一応別名(XYZ)でファイルを作成して代用した。

gitとの連携、アプリのクローン

$cat aws_git_rsa.pub
cat: aws_git_rsa.pub: No such file or directory

これは公開鍵を作成した際に通常のコマンド入力と操作が違ったため、:aws_git_rsaのうしろに半角スペースを付けてしまったのが原因だった。(凡ミス。。)

$ ssh-keygen -t rsa

Enter file in which to save the key ():aws_git_rsa(この部分に半角スペース)
鍵を再度作成したら無事に公開鍵の中身を出現させることができた。

MySQLの設定

Mysqlの起動を試みたところ、次のエラーが発生
$sudo service mysqld start #mysqldの起動
Redirecting to /bin/systemctl start mysqld.service
Failed to start mysqld.service: Unit not found.
以下の記事を参考に次のコマンドを実行したところうまくいった。
https://qiita.com/yuta-38/items/4074f5ada9e22088c8dd
$ sudo yum install -y mariadb-server
$ sudo systemctl enable mariadb
$ sudo service mariadb start

また、Amazon Linux2ではyumでmysqlをインストールしようとするとmariaDBをインストールしようとするらしい。
私は実践しなかったが、以下の記事が参考になるかもしれない。
https://qiita.com/hamham/items/fd77bb0bb167a150dc8e#mysql57%E3%81%AE%E5%B0%8E%E5%85%A5

Nginxの起動

以下のコマンドを入力したがNginxが起動しない。
$ sudo service nginx start
Redirecting to /bin/systemctl start nginx.service
Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.

エラーの指示通りに以下のコマンドを実行
$ systemctl status nginx.service
nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 月 2020-07-13 09:10:04 UTC; 57s ago
Process: 11360 ExecStart=/usr/sbin/nginx (code=exited, status=1/FAILURE)
Process: 11356 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 11355 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)

原因が分からなかったので以下も実行。
$sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

設定ファイルに問題はなさそうだったので、困ったことになった。
色々調べているうちに以下のコマンドを発見。
$ sudo service nginx status -l
Redirecting to /bin/systemctl status -l nginx.service
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 月 2020-07-13 09:10:04 UTC; 27min ago
Process: 11360 ExecStart=/usr/sbin/nginx (code=exited, status=1/FAILURE)
Process: 11356 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 11355 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
7月 13 09:10:02 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] still could not bind()
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: nginx.service: control process exited, code=exited status=1
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: Failed to start The nginx HTTP and reverse proxy server.
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: Unit nginx.service entered failed state.
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: nginx.service failed.

上記によると、設定した80番ポートは既に起動中のようだ。
原因を考えた結果、以前あるweb講座でApacheのインストール及び自動起動設定をしていたのを思い出した。
$ sudo systemctl status httpd.service
● httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
Active: active (running) since 日 2020-07-12 06:24:44 UTC; 2 days ago
Docs: man:httpd.service(8)
Main PID: 21760 (httpd)
Status: "Total requests: 149; Idle/Busy workers 100/0;Requests/sec: 0.00084; Bytes served/sec: 2 B/sec"
CGroup: /system.slice/httpd.service
├─20766 /usr/sbin/httpd -DFOREGROUND
├─21049 /usr/sbin/httpd -DFOREGROUND
├─21055 /usr/sbin/httpd -DFOREGROUND
├─21056 /usr/sbin/httpd -DFOREGROUND
├─21760 /usr/sbin/httpd -DFOREGROUND
├─21761 /usr/sbin/httpd -DFOREGROUND
├─21762 /usr/sbin/httpd -DFOREGROUND
├─21763 /usr/sbin/httpd -DFOREGROUND
├─21764 /usr/sbin/httpd -DFOREGROUND
├─21765 /usr/sbin/httpd -DFOREGROUND
└─21887 /usr/sbin/httpd -DFOREGROUND
案の定Apacheが既に動いていた。自動起動を無効&停止後に、NginXの起動コマンドを実行したら無事に起動できた。

インフラ構築前にUdemyで購入した以下の講座が効いてテンポよく理解できた。
https://www.udemy.com/course/aws-and-infra/

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

logrageではRails.loggerのログ出力がJSON形式にならない問題

はじめに

RailsログをJSON形式にしたいだけなんですが、解決方法が意外とめんどくさかったので、ググってたどり着いた誰かのためにメモを残しておきます。

「Railsログ JSON」で雑にググるとlogrageというgemをいれるとよさそうな感じです。
https://github.com/roidrage/lograge

gem入れるだけでバーンといいかんじのJSONになることを期待したのですが、以下の記事に書いてあるとおり、このgemはコントローラのアクセスログや例外しかJSON形式で出ません。
https://qiita.com/amanekey/items/b921ef73d871dac299eb

おもむろに Rails.logger.info("foo") とかしてもJSONにならなくてそのまま出力されちゃう。
つまりこんなかんじ。

{"method":"GET","path":"/","format":"html","controller":"RootPagesController","action":"index","status":200,"duration":1471.86,"view":1429.04,"db":11.39,"level":"INFO","tag":"rails.controller","uuid":"1ecfefb7-7c5e-4bb7-ada8-711e6743a634","ip":"172.20.0.1","time":"2020-07-10T13:42:11+09:00"}
foo

1行目がlogrageが出力するアクセスログ、
2行目が普通にアプリケーション内でおもむろに Rails.logger.info("foo") した場合のログです。
ダメじゃん。

上記の参考記事では最終的にRails.loggerを使用せずに、独自に定義したMyLogger.runという専用のインターフェースを使ってログを出力していました。
しかしながら、既存のコード量が多いと、Rails.loggerを使っている箇所を全部書き換えるなど現実的ではないです。
一方、単純に Logger::Formatter を継承した独自のログフォーマッタを定義してRails.loggerの出力をJSON形式にしても、logrageで出力できるような request_id などのメタデータを付与することができません。

解決策

なんかよい方法はないかとlogrageのissueを漁っていたら以下を見つけました。

https://github.com/roidrage/lograge/issues/255#issuecomment-592212635

要約すると、まず Logger::Formatter を継承した独自のログフォーマッタを定義してRails.loggerの出力をJSON形式にしておきます。次に ActiveSupport::TaggedLogging 経由で request_id などのコンテキストのメタデータをログの本文に付与できるので、ログの本文から request_id などのタグ部分をパースして、JSONを構築しようというハックです。
なるほど。その発想はなかった。

上記のissueのコメントでは、 -> という区切り文字をマジックワードとしてタグの最後に使って、ログの本文(msg変数)からタグ部分とメッセージ部分を分離しているようでしたが、もうちょっと調べると、 Logger::Formatter#call の中で current_tags が見えることに気づいたので、もうちょっとマシに書けそうです。
また上のissueのサンプルコードでは、Rails.loggerの出力全体をJSONにしているので、logrageが生成するJSON形式のログがmsg変数に入ってきて、JSON in JSONになってしまい若干イマイチです。できれば入れ子のJSONはパースしてマージしたい。

というわけで、もうちょっとマシにしてみたのがこちらです。issueのコメントにも書いてみましたが、若干補足コメント付きで、コードのサンプルをここにも貼っておきます。

https://github.com/roidrage/lograge/issues/255#issuecomment-657328032

手元の環境はRails5.2+lograge v0.11.2です。

class JsonLogFormatter < Logger::Formatter
  def call(severity, _time, _progname, msg)
    log = {
      time: _time.iso8601(6),
      level: severity,
      progname: _progname,
      type: "default",
    }

    # タグが付いてる場合
    unless current_tags&.empty?
      # タグのkey=valueのmapを作ってマージ 
      tagged = Rails.application.config.log_tags.zip(current_tags).to_h
      log.merge!(tagged)
      # msgの先頭に入ってるタグを削除
      msg = msg&.split(' ', current_tags.size + 1)&.last
    end
    begin
      # msgがJSON形式の場合は JSON in JSON をフラットにしてマージする
      parsed = JSON.parse(msg).symbolize_keys
      log.merge!(parsed)
    rescue JSON::ParserError
      # JSON形式ではない場合は、meessageというkeyに保存
      log.merge!({message: msg})
    end
    log.to_json + "\n"
  end
end

lograge自体の設定は、各自よしなにして下さい。

config/initializers/lograge.rb
Rails.application.configure do
  config.lograge.enabled = true
  config.lograge.formatter = Lograge::Formatters::Json.new

  config.lograge.custom_options = lambda do |event|
    data = {
      level: 'INFO',
      type: 'access',
    }

    # customize other fields
    # (snip.)

    data
  end
end

config/environments/*.rb などに定義したログフォーマッタを差し込みます。
付与するタグはコントローラの request 経由で見えるものなら付与できますが、最低限 request_id があれば紐付け可能。

config/environments/development.rb
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = ::JsonLogFormatter.new
config.log_tags = [:request_id]
config.logger = ActiveSupport::TaggedLogging.new(logger)

Rails.logger.info("foo") のログ

{"time":"2020-07-13T09:57:47.558838+09:00","level":"INFO","progname":null,"type":"default","request_id":"5a48d951-a04b-43fe-b462-f57277b55291","message":"foo"}

logrageのログ

{"time":"2020-07-13T09:57:48.744866+09:00","level":"INFO","progname":null,"type":"access","request_id":"5a48d951-a04b-43fe-b462-f57277b55291","method":"GET","path":"/","format":"html","controller":"RootPagesController","action":"index","status":200,"duration":1675.8,"view":1150.5,"db":15.37,"user_id":null,"params":"{}"}

原理的にlogrageのコンテキストをすべてRails.logger側のログに出力することはできないんですが、最低限request_idがどちらにも含まれていれば、紐付けできるしいいやという作戦です。

おわりに

RailsログをJSON形式にしたいだけなんですが、なんでこんなにめんどくさいんでしょうか?
もっとマシな解決方法を知ってる人がいたら教えて下さい。

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

[Rails][RSpec]導入と書き方 〜userモデル編〜

これはなに

RailsでのRSpec導入とテストコードの記述方法です。
初学者が備忘録のため書いています。ご指摘等あれば頂けると幸いです。

では、いきましょう!

Gemをインストール

Gemfile
group :development, :test do
 gem 'rspec-rails'
 gem 'factory_bot_rails'
end


group :development do
gem 'web-console', '>= 3.3.0'
end
bundle install

RSpecの基本設定

ターミナル
#RSpec用設定ファイルの作成
rails g rspec:install

#4つのファイルが作成されます
create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

.rspecに以下を追記します。

.rspec
--format documentation

RSpecを走らせるコマンドbundle exec rspecで動作確認。
まだなにもテストコード書いてないので、0 examples, 0 failuresと出ます。
これで動作確認完了です。

ターミナル
$ bundle exec rspec


No examples found.

Finished in 0.00031 seconds (files took 0.19956 seconds to load)
0 examples, 0 failures

ファイル作成

次に、必要なファイルを作ります。
specフォルダ下に以下のように作成。モデルのテストなのでモデルのフォルダを作成。

- app
- spec
    - models
        - user_spec.rb

次にfactory_botのフォルダとファイルを作成します。

- app
- spec
    - factories
        -users.rb 

これで必要なファイルは完成しました。

テストコード書き方

まず、factories/users.rbのファイルを以下のように編集。
とりあえず進みます。

factories/users.rb
FactoryBot.define do
  factory :user do
    name { "abc" }
    email { "aaa@bbb" }
    password              { "000000" }
    password_confirmation { "000000" }
  end
end
medels/user_spec.rb
require 'rails_helper'
describe User do
  describe '#create' do
    it "nameがない場合は登録できないこと" do
      user = build(:user, name: nil)
      user.valid?
      expect(user.errors[:name]).to include("を入力してください")
    end
  end
end
  • 一行目のrequire 'rails_helper'は、rails_helper.rb内の記述を読み込むこんでいます。この1行目の記述は、全ての~_specファイルに書くと思って頂いて大丈夫です。

  • 二行目のdescribe User do ~ endはUserモデルのテストであることを示しています。

  • 三行目のdescribe '#create' do ~ endはcreateメソッド(ユーザー新規登録)であることを示しています。

  • 四行目のit "nameがない場合は登録できないこと" do ~ endはテストの説明です。この間にテストコードを書いていきます。

  • user = build(:user, name: nil)
    こちらは、buildメソッドでfactories/users.rbのuserを作成しています。作成した際、nameだけ空にしたいので、nameカラムを指定し、nilにしています。

  • user.valid?
    こちらはbuildしたuserに対して、valid?でバリデーションにより保存ができない状態であるか」を確かめることができます。

  • expect(user.errors[:name]).to include("を入力してください")
    こちらは、valid?メソッドを利用したuserに対してerrorsメソッドを利用し、なぜバリデーションにより保存ができないのかを確認することができます。
    includeマッチャは引数にとった値が、expectの引数である配列に含まれているかをチェックするマッチャです。
    今回の場合、「nameがnilの場合は、を入力してください、というエラーが出るはずだ」ということがわかっているため、include("を入力してください")のように書くことができます。
    実際にその通りになれば、このコードは意図した動作をすると確認出来ます。

実際にテストを実行してみましょう。

ターミナル
$ bundle exec rspec

〜省略〜
1 example, 0 failures

特に赤文字エラーが出てなく、0 failuresとなっていれば成功です!

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

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

initializeメソッド

initializeメソッド

・クラスからオブジェクトを作成する場合に呼び出されるメソッド

class Food
  def initialize
    puts 'Initialize'
  end
end
Food.new
#=> Initialized.

・initializeメソッドは特殊なメソッドで外部から呼び出すことはできない

food = Food.new
food.initialize
#=> NoMethodError: private method 'initialize' call for #<Food:0x007gfasjfasfjas>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

一部sassが当たらなかった時

解決したかった事

マイページ実装中に一部hamlにsassが当たらない

show.html.haml
.MypageContents
  .MypageContents__side
    .MypageContents__side--nav
      %ul.MypageContents__side--nav--list
        %li.MypageContents__side--nav--list--item
          = link_to "商品一覧", root_path
        %h3.Mypage-nav-head 設定
      %ul.MypageContents__side--nav--list
        %li.MypageContents__side--nav--list--item
          = link_to "ログアウト", root_path
        %li.MypageContents__side--nav--list--item
          = link_to "支払い方法", root_path
  .MypageContents__user
    = current_user.nickname 
    %pさんのマイページ

こちらの
%li.MypageContents__side--nav--list--item
sassが当たらなかった
該当sass
  &__side {
    float: left;
    width: 280px;
    margin: 0 40px 0 0;
    &__side--nav {
      &__side--nav--list {
        &__side--nav--list--item {
          display: block;
          min-height: 48px;
          padding: 16px;
          background-color: #fff;
          font-size: 14px;
          color: #333;
          .Mypage-nav-head {
            font-size: 16px;
          }
        }
      }
    }
  }

解決

修正後sass
  &__side {
    float: left;
    width: 280px;
    margin: 0 40px 0 0;
    &--nav {
      &--list {
        &--item {
          border: 1px solid #eee;
          display: block;
          min-height: 48px;
          padding: 16px;
          background-color: #fff;
          font-size: 14px;
          color: #333;
        }
        .Mypage-nav-head {
          font-size: 16px;
        }
      }
    }
  }

原因

どうやら&の使い方に誤りがあった模様。
sassでの&は親要素のクラス名まで省略してくれるものらしい。
また、.Mypage-nav-headがhamlの入れ子構造とマッチしていなかったのも原因であった。
かなりシンプルに記述する事ができた。

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