20200228のRailsに関する記事は19件です。

【Rails】Rails6系とMySQL8系の組み合わせの開発環境をDockerだけでつくる

概要

  • :point_right: この記事は弊社ァ内で行った準備作業の備忘録メモでございますわっ
  • 次の新規案件に備え、RubyとRails最新版のスケルトン(?)なプロジェクトを準備する
  • 各バージョンは以下の予定
    • Ruby2.7系
    • Rails6.0系
    • MySQL8.0系
    • Nginx1.17系
    • redis(いちおう)
  • Docker前提です
    • 新規ならDockerでしょ?って、僕も過激派になっている感。
    • AWS ECSでの運用が前提のため。
  • Rubyのイメージはalpine3.11を使用。カーネル5.4系でセキュリティも向上してるらしい(?)し、軽い1

    $ docker images
    REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
    ruby                      2.7-slim-buster     b913dc62d63c        10 days ago         149MB
    ruby                      2.7-buster          0c1ee6efe061        10 days ago         842MB
    ruby                      2.7-alpine3.10      78005ca97a7f        2 weeks ago         52.9MB
    ruby                      2.7-alpine3.11      1f7033feacdb        3 weeks ago         53.5MB
    
  • docker-compose化する。最終的にはね。

    • そこだけ読みたいならここまでスクロール

やりたいこと

  • 作業PC的な観点で
    • PCローカル内のモダンな過去プロジェクトを汚さずに環境構築したい。
    • PCローカル内のレガシーな過去プロジェクトを汚さずに環境構築したい。
    • PCローカル内のヴィンテージな過去プロジェクトを汚さずに環境構築したい。
    • PC内のパッケージに悪影響が出ないようにしたい。
  • rails new すらもDockerの中でやりたい。
  • dockerコンテナ落としてもDBの内容やbundleのgemが消えないようにしたい。
  • 他の開発者(後輩くん)が使える程度のものを残しておきたい。
  • 運用想定的な観点で(何様)
    • 環境作成
      • AP,DBコンテナの構成で十分
      • rails new してソースコード準備したい
      • できれば動作確認したい
    • 開発環境(弊社ァ内エンジニア用)
      • WEB,AP,DBコンテナに加えてRedisも建てたい
        • ↑↑環境差異による不具合を減らすため、本番環境にできるだけ合わせたい・・・!!(切実)
      • docker-compose化しておいて、少しでも普段遣いのコマンドを減らしたい
    • 本番環境
      • AWS ECSで動かす想定
      • dockerイメージはできるだけ開発環境そのままECRにプッシュする
        • 環境変数だけで環境変えたいよね
      • ECSタスク定義とかRDS設定はdocker-compose見ればほぼ設定できるようにしておきたい

Railsプロジェクトの新規作成

目的

  • docker内でrails newしてWebアプリケーションのソースを用意する
  • ここの見出しでやるのはrails newだけ。開発環境整える方法は次の見出しまでスクロールしてね

やることを決める

  • dockerボリュームの有効活用
    • 環境構築途中で失敗してもdbとgemを消さないようにして時短。
    • dockerのマウントタイプは bind でわなく volume を使う2。Macのファイルシステムへのマウントは遅い(?)ので
  • 動作確認のためDBコンテナも建てる

事前準備

docker network(一時) を作成

$ # host OS (local PC)
$ # コンテナ間通信用のネットワークを用意しておく
$ 
$ APP_NETWORK=tmp_network # 名前をきめてね
$ 
$ docker network create ${APP_NETWORK}
$ 
$ # 確認しよう
$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
c31a38da01e0        tmp_network         bridge              local

docker volume(一時) を作成

$ # host OS (local PC)
$ # DBデータ保持用と、gem保存用の2つのdockerボリュームを作っておく。
$ 
$ DB_VOLUME=tmp_dbdata_vol # 名前をきめてね
$ BUNDLE_VOLUME=tmp_bundle_vol # 名前をきめてね
$ 
$ docker volume create ${DB_VOLUME}
$ docker volume create ${BUNDLE_VOLUME}
$ 
$ # 確認しよう
$ docker volume ls
DRIVER              VOLUME NAME
local               tmp_bundle_vol
local               tmp_dbdata_vol
  • これ以降のコンテナ作成作業中で、エラーが出まくったら、volumeを一度作り直すなどして問題解決していこうかと考えていた。

コンテナの作成

DBコンテナ(一時)を建てる

  • mysqld コマンドのオプションで色々宣言している。 **.cnfを用意するのでも良かったけど、本番はRDS使うだろうし、
$ # host OS (local PC)
$ 
$ # パスワードはあくまで一例。ちゃんと毎回違うランダム文字を使う癖をつけるように教えるの()
$ docker run \
    --detach \
    --env MYSQL_ROOT_PASSWORD=4h%zpW8wrb+G \
    --env TZ="Asia/Tokyo" \
    --mount type=volume,source=${DB_VOLUME},target=/var/lib/mysql \
    --name tmp_db_mysql \
    --network ${APP_NETWORK} \
    --publish 127.0.0.1:13306:3306 \
    --tty \
    mysql:8.0 \
    mysqld \
      --block_encryption_mode aes-256-cbc \
      --character_set_server utf8mb4 \
      --collation-server utf8mb4_bin \
      --default-authentication-plugin mysql_native_password \
      --explicit_defaults_for_timestamp \
      --init-connect 'SET NAMES utf8mb4' \
      --skip-character-set-client-handshake

APコンテナ(一時)を建てる

$ # host OS (local PC)
$ 
$ # DBパスワードはDBコンテナと同じもの
$ docker run \
    --detach \
    --env APP_DATABASE_PASSWORD=4h%zpW8wrb+G \
    --mount type=volume,source=${BUNDLE_VOLUME},target=/usr/local/bundle \
    --name tmp_ap_rails \
    --network ${APP_NETWORK} \
    --publish 127.0.0.1:3000:3000 \
    --tty \
    --workdir /app \
    ruby:2.7-alpine3.11

プロジェクトの新規作成

Railsプロジェクトを新規作成

$ # host OS (local PC)
$ 
$ docker exec -it tmp_ap_rails sh
/app # # guest OS (ruby:2.7-alpine3.11)
/app # 
/app # # 必要そうなパッケージをAlpineLinuxPackagesからインストール
/app # apk update
/app # apk upgrade
/app # apk add --update --no-cache \
          build-base \
          git \
          imagemagick \
          imagemagick-dev \
          libxml2-dev \
          libxslt-dev \
          mysql-client \
          mysql-dev \
          nodejs \
          ruby-dev \
          tzdata \
          yarn
/app # 
/app # # yarnのインストールコマンドを叩いておく
/app # yarn install
  • ローカルPC内でGemfileを作って、それをAPコンテナ(一時)にコピーする
  • ここでrailsのバージョンを決めている
Gemfile
source 'https://rubygems.org'

git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'rails', '~> 6.0', '>= 6.0.2.1'
$ # host OS (local PC)
$ 
$ # cpコマンドでdocker内にファイルをコピーする
$ docker cp Gemfile tmp_ap_rails:/app/Gemfile
$ # host OS (local PC)
$ 
$ docker exec -it tmp_ap_rails sh
/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # # railsをインストールする
/app # bundle install
/app #
/app # rails --version
Rails 6.0.2.1
/app #
/app # # Railsプロジェクトを新規作成する(各オプションはプロジェクトに応じて変える)
/app # rails new . \
      --database=mysql \
      --force \
      --skip-coffee \
      --skip-sprockets \
      --skip-turbolinks \
      --skip-test \
      --webpack=vue

設定値の書き換え

/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # # development と test の設定を変更
/app # vi config/database.yml
config/database.yml
# --- L20..L24
development:
  <<: *default
  database: app_development
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: tmp_db_mysql
# --- L29..L33
test:
  <<: *default
  database: app_test
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: tmp_db_mysql

動作確認

/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # rails db:create
/app #
/app # rails server -b 0.0.0.0

スクリーンショット 2020-02-21 18.37.09.png

  • うまく、いった、みたい、

ソースコードのコピー

  • ディレクトリ内をコピーするときは . をつけるらしい3
  • この作業で新規プロジェクトのソースコードがローカルPCにコピーされるので、これをgithubで管理するなりしておく
$ # host OS (local PC)
$ 
$ # Docker内のソースコードをホスト側にコピーする
$ docker cp tmp_ap_rails:/app/. ./

ゴミ掃除

APコンテナ(一時)の削除

  • APコンテナ(一時)は rails new と動作確認のためだけのコンテナだったので、消す。
$ # host OS (local PC)
$ 
$ docker stop tmp_ap_rails
$ docker rm tmp_ap_rails
$ 
$ # 消えていることを確認
$ docker ps -a

DBコンテナ(一時)の削除

  • DBコンテナ(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker stop tmp_db_mysql
$ docker rm tmp_db_mysql
$ 
$ # 消えていることを確認
$ docker ps -a

docker network(一時)の削除

  • docker network(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker network rm ${APP_NETWORK}
$ 
$ # 消えていることを確認
$ docker network ls

docker volume(一時)の削除

  • docker volume(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker volume rm ${DB_VOLUME}
$ docker volume rm ${BUNDLE_VOLUME}
$ 
$ # 消えていることを確認
$ docker volume ls

Railsプロジェクトの新規作成まとめ

  • 長文になってしまって申し訳ナソス
  • ローカルPCのrubyバージョンに影響されずにrailsプロジェクトが新規作成できた。
    • (rbenvとかを使用してもこの程度はできる気がするけど・・・)
  • これを応用してエンジニア用の開発環境も整えていく形で良さそう。(何様)
  • 上記に記してはいないが、volume設定でgemを別に保持できていたのを確認できているので、感触はOK
    • 直接volume内を見に行くのは少々面倒だが、bindよりは動作速そう(?)。

docker-compose化

目的

  • ここから先が本当にやりたかったことやで。。。
  • 上記までの準備(新規作成したrailsプロジェクト)をもとに、開発環境(弊社ァ内エンジニア用)を整えてゆく

やることを決める

  • docker-compose.ymlを用意する
  • 基本 docker-compose up -d, docker-compose down コマンドだけで動くようにしておく
  • ログローテートはちゃんと設定する(もし無限ログ吐き出されてもPCをパンクさせない教訓)
  • Dockerfileの用意をしてビルド時には必要パッケージがインストールされている状態にする
    • Dockerfileはマルチステージビルド4を仕込んでおき、本番リリースに使えるようにしておく
  • WEBコンテナ(nginx)を追加して、静的ページを表示できるようにする
    • 静的ファイル、ソケットはAPコンテナとつなげておく
      • ソケットはrails6だからpuma(unixソケットにする予定)
    • 起動順の依存関係には一番最後
  • CACHEコンテナ(redis)を追加しておく
    • 起動順の依存関係はAPより前
    • 今回は使わないけど。開発で使うので

Dockerfileの作成

containers/web/Dockerfile
FROM nginx:1.17-alpine

LABEL maintainer="なまえ <メアド>"

# 設定ファイルを上書き
RUN rm -f /etc/nginx/conf.d/*
ADD ./containers/web/default.conf /etc/nginx/conf.d/default.conf

# nginxの起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
containers/web/default.conf
# @note ホストサーバーとのファイル共有は以下
#   - tmp/     # pumaソケット通信用
#   - public/  # rails静的ファイル共有(404ページとかjs,cssとか)

# nginxとpumaのソケット通信の設定
# @see{https://github.com/puma/puma/blob/master/docs/nginx.md}
upstream rails_app {
  server unix:///rails_app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name localhost;

  # HTTPの持続的な接続維持時間(軽量なので0秒(off)でもいいかも)
  keepalive_timeout 5;

  # アップロードサイズの上限を設定
  client_max_body_size 10m;

  # 静的ファイルのパス
  root /rails_app/public;

  # ログ出力(dockerイメージの設定より、このパスは標準出力となる)
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log info;

  # メンテナンスファイルが置かれている場合はメンテ画面を出す
  if (-f $document_root/maintenance.html) {
    rewrite  ^(.*)$  /maintenance.html last;
    break;
  }

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # 静的ファイル
    if (-f $request_filename) {
      break;
    }

    if (-f $request_filename/index.html) {
      rewrite (.*) $1/index.html break;
    }

    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://rails_app;
      break;
    }
  }

  # 後方一致で画像などの静的ファイルが指定された場合はpublic/配下のファイルを直接返す(Railsを介さない)
  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # ファイルパスが存在しない場合はRailsで処理
    if (!-f $request_filename) {
      proxy_pass http://rails_app;
      break;
    }

    expires max;
    break;
  }
}
  • ↓↓マルチステージビルドの使い方が完全に自己流なんだけどこれで良いのかしら・・・。
containers/app/Dockerfile
FROM ruby:2.7-alpine3.11 as dev_mode

LABEL maintainer="なまえ <メアド>"

# ビルド時の作業ディレクトリ
WORKDIR /app

# AlpineLinuxPackagesで必要そうなコマンドをインストール
RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache \
        build-base \
        git \
        imagemagick \
        imagemagick-dev \
        libxml2-dev \
        libxslt-dev \
        mysql-client \
        mysql-dev \
        nodejs \
        ruby-dev \
        tzdata \
        yarn

# yarnインストールの実行(先にlockファイルをコピーしてバージョンを固定)
COPY package.json yarn.lock ./
RUN yarn install

# マルチステージビルドで本番用イメージを作成
FROM dev_mode as prod_mode

# Railsの秘密情報のマスターキー(docker build時にオプションで宣言する)
ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}
# 本番モードで起動する
ENV RAILS_ENV production

# アプリのソースをイメージ内にコピー
COPY ./ ./

RUN bundle install

# フロント用のライブラリのインストールと静的ファイルの作成
RUN rails yarn:install
RUN rails webpacker:compile

# コンパイル後、マスターキー情報はイメージから消しておく
ENV RAILS_MASTER_KEY=

docker-compose.ymlの作成

  • 2020/02くらいに確認したときは、バージョン3.7が最新だった5ので、それで。
  • パスワードはあくまで一例。
docker-compose.yml
version: '3.7'

# @see {https://docs.docker.com/compose/compose-file/}
services:
  # DBコンテナの設定
  db:
    # ビルドに使うイメージ
    image: mysql:8.0
    # 環境変数
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_ROOT_PASSWORD: F-O75%kSG6gz
      TZ: "Asia/Tokyo"
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # ホストOSに開放するポートの指定
    ports:
      - 127.0.0.1:3306:3306
    # ファイルのマウント設定
    volumes:
      # データベースの内容をDocker領域に同期する(データ永続化のため)
      - type: volume
        source: mysql_data_vol
        target: /var/lib/mysql
    # コンテナを永続化
    tty: true
    # 認証を旧式(パスワード)に変更、デフォルトの文字コードとcollate設定を指定、暗号化のモード選択
    command: >
      mysqld
      --block_encryption_mode aes-256-cbc
      --character_set_server utf8mb4
      --collation-server utf8mb4_bin
      --default-authentication-plugin mysql_native_password
      --explicit_defaults_for_timestamp
      --init-connect 'SET NAMES utf8mb4'
      --skip-character-set-client-handshake

  # Redisコンテナの設定
  redis:
    # ビルドに使うイメージ
    image: redis
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # コンテナを永続化
    tty: true

  # APコンテナの設定
  app:
    # ビルド時の設定
    build:
      # プロジェクトのディレクトリでビルドする
      context: .
      # マルチステージビルドのターゲットを指定
      target: dev_mode
      # Dockerfileの場所を指定
      dockerfile: containers/app/Dockerfile
    # コンテナ依存関係
    depends_on:
      - db
      - redis
    # 環境変数
    environment:
      APP_DATABASE_PASSWORD: F-O75%kSG6gz
      # RAILS_MASTER_KEY:
      # RAILS_MAX_THREADS:
      TZ: "Asia/Tokyo"
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # ファイルのマウント設定
    volumes:
      # アプリのソースをホストOSと共有する
      - type: bind
        source: .
        target: /app
      # gemをDocker領域に同期する
      - type: volume
        source: bundle_vol
        target: /usr/local/bundle
    # コンテナを永続化
    tty: true
    # bundle install 、yarn install を行い、pumaサーバーを起動する
    command: >
      sh -c "
      bundle install &&
      rails yarn:install &&
      rails webpacker:compile &&
      pumactl start"

  # WEBコンテナの設定
  web:
    # ビルド時の設定
    build:
      # プロジェクトのディレクトリでビルドする
      context: .
      # Dockerfileの場所を指定
      dockerfile: containers/web/Dockerfile
    # コンテナ依存関係
    depends_on:
      - app
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # ホストOSに開放するポートの指定
    ports:
      - 127.0.0.1:80:80
    # ファイルのマウント設定
    volumes:
      # 静的ファイルの共有
      - type: bind
        source: ./public
        target: /rails_app/public
      # pumaソケットファイルの共有
      - type: bind
        source: ./tmp
        target: /rails_app/tmp

# ネットワークの定義
networks:
  # コンテナ間通信用
  app_net:

# ボリュームの定義
volumes:
  mysql_data_vol:
  bundle_vol:
  • rails側のDB設定も修正修正(ホストをdocker-composeで定義した名前に変えよう)
config/database.yml
# --- L20..L24
development:
  <<: *default
  database: app_development
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: db
# --- L29..L33
test:
  <<: *default
  database: app_test
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: db
  • あと、WEBコンテナとソケット通信させるためにconfigも少し修正が必要だった6
config/puma.rb
# --- L13
# unixソケット通信はポートを使用しない
# port        ENV.fetch("PORT") { 3000 }

# --- 最後に追加
# ソケット通信の設定(WORK_DIRを/appにしているからもう直書きで良いかしら)
bind "unix:///app/tmp/sockets/puma.sock"

動作確認

$ # host OS (local PC)
$ 
$ # いよいよdocker-compose起動するぜ!!
$ docker-compose up -d
$ 
$ # 確認
$ docker-compose ps
        Name                       Command               State                  Ports
----------------------------------------------------------------------------------------------------
skl_rails6021_app_1     sh -c  bundle install && r ...   Up
skl_rails6021_db_1      docker-entrypoint.sh mysql ...   Up      127.0.0.1:3306->3306/tcp, 33060/tcp
skl_rails6021_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp
skl_rails6021_web_1     /bin/sh -c /usr/sbin/nginx ...   Up      127.0.0.1:80->80/tcp
$ 
$ # 確認
$ docker-compose logs --tail 30 app
$ docker-compose logs --tail 30 web
$ 
$ # MySQLもローカルPCから接続できるよね
$ mysql -u root -p
  • ローカルPCで http://localhost:80 にアクセス(初回は時間がかかるのでログを見て落ち着いたら)

スクリーンショット 2020-02-28 20.23.46.png

  • うまく、できた、みたい、

READMEに説明を書く

  • やる。
  • 僕だけ使えても意味がない。
  • もしかしたらこの作業が一番骨折れるかもしれない。

最後に

  • 長文失礼。
  • ポート開放不親切失礼。必要に応じて書き換え願う
  • ruby2.7対応していないgemがいくつかあるみたいで、ログが汚い。warningの大量発生や。。。(2020/02ころ現在)
  • WEBコンテナ追加してnginxとソケット通信する作業でに労力増えてる感ある。rails severのありがたみを感じる。。。
  • redisの設定は開発時にやるで良いよね。。。

  1. https://hub.docker.com/_/ruby ←←2020/02くらいにここを閲覧した、そして全部取得して比べてみた。 

  2. https://docs.docker.com/storage/volumes/ ←Docker領域内に名前つけて保存されるから、万が一ソースコードごと紛失してもワンチャンある() 

  3. https://medium.com/veltra-engineering/how-to-copy-a-directory-using-docker-cp-command-f2c73f9ccf75 ←←こちらのサイトがとても参考になりました :pray::pray::pray: 

  4. https://docs.docker.com/develop/develop-images/multistage-build/#use-a-previous-stage-as-a-new-stage 

  5. https://docs.docker.com/compose/compose-file/compose-versioning/ 

  6. https://github.com/puma/puma#binding-tcp--sockets unixソケットにするためにbindのオプションをconfig/puma.rbに書く必要があった。こことかこれとかこのへん読んでも書き方迷いそう。。。 

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

Capistranoでのデプロイを攻略する

今日の目標 

デプロイ作業でよく使うコマンドを整理する

前提 

Rails 5.0.7.2
レポジトリ名:freemarket
pemキー名:team_c.pem
ユーザー名:ec2-user
Elastic IP:@18.176.134.115

SSH接続

$ cd .ssh/
$ ssh -i team_c.pem ec2-user@18.176.134.115
cd /var/www/freemarket

pemキー確認

cd
cd .ssh
ls

デプロイコマンド

bundle exec cap production deploy

AWSアクセスキー、キーID確認

env | grep AWS_SECRET_ACCESS_KEY
env | grep AWS_ACCESS_KEY_ID

MySQL

ターミナル(本番環境)
状況確認
sudo service mysqld status
起動する
sudo service mysqld start

mysqlにアクセスする
mysql -u root -p

データベースを確認
show databases;
データベースを選択
use freemarket;
テーブルを確認
show tables;
カラムを確認
show columns from テーブル名;

本番環境のデータベースを作り変える(破壊系)場合はこちら
https://qiita.com/keitah/items/7b20fcae6ef13820d01f

unicorn

ターミナル(本番環境)
状況確認
ps auxwww | grep unicorn
ログ
cat log/unicorn.stderr.log

less /var/www/freemarket/current/log/unicorn.stderr.log
または
cat /var/www/freemarket/current/log/unicorn.stderr.log

nginx

ターミナル(本番環境)
再起動
sudo service nginx restart

ログ
ホームディレクトリで
sudo less /var/log/nginx/error.log 

capistrano

ターミナル(本番環境)
ログ
less log/capistrano.log

vim

ローカル
vimエディッタを起動
vim ~/.bash_profile

insertモード
i

環境変数を反映させる
source ~/.bash_profile

PCに設定されている環境変数を確認
$ printenv
exportされている環境変数を確認
$ export -p

insertモードを終了
esc
:wq
本番環境
sudo vim /etc/environment


本番環境で 
env | grep SECRET_KEY_BASE   (SECRET_KEY_BASEの値の確認)
env | grep DATABASE_PASSWORD (DATABASE_PASSWORDの値の確認)

おまけ

we're sorry, but something went wrongの内容を把握する方法

config/environments/production.rb
config.consider_all_requests_local       = true  デフォルトはfalse

https://qiita.com/keitah/items/2aa2ac968c76260e8750

デプロイ作業でよく使うコマンドを備忘録として整理しました。

今後気がついたらちょいちょい追記していきます。

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

deviseの関連ファイルが作られず、コマンドが適用されなくなった時の対処法

deviseをインストールしているとconfig/initializers/devise.rb
config/locales/devise.en.ymlファイルが生成されておらず、、さらにrails g devise userrails db:migrate:statusしても

NoMethodError: undefined method `devise' for Install (call 'Install.connection' to establish a connection):Class

というエラーが出ました。すでにuserモデル以外のデータべースは作ってあるのでrails db:migrate:statusが出来ないということはコマンドが適用されていないということだと思います。
同じ状態でbundle installからやり直しても解決出来ませんでした。

考えた仮説

このときGemfileにはちゃんとgem 'devise'と記述されていました。ターミナル履歴は以下の通りです。

#ターミナル履歴
bundle install
rails g devise:install
rails g devise user
rails db:migrate

ターミナル履歴におかしいところはない。
しかしconfig/initializers/devise.rb
config/locales/devise.en.ymlファイルがインストールがされていないので、rails g devise installに問題があるのではないか?
gemはローカルサーバーの再起動をしないと反映されないので、rails g devise installの前に再起動をしていなかったのではないか?

解決方法

調べると、rails g devise installが反映されていないとrails srails g devise userrails db:migrate:statusなど他のコマンドが競合してしまい、コマンドが反映されなくなってしまうようです。
参考記事 : https://qiita.com/ryouzi/items/9c5324ba567109ab2a22

具体的な対処法
1. もう一度bundle install
2. routes.rbにあるdevise_for :installもしくはdevise_for :usersというコードを削除する
3. 2でrailsコマンドが適用されるようになったのでrails db:rollbackでマイグレーションファイルをロールバックする。
4. rails db:migrate:statusでステータス確認
5. userテーブルやモデルが作られている場合はrails d model userで削除
6. rails g devise:install
7. rails g devise user
8. rails db:migrate

最後に

プログラミングを始めて4週間の初心者なので、その目線でわかりやすく書くことを心がけました。
わかりにくい記述、間違っているところがあればご指摘ください。
最後まで見ていただきありがとうございました。

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

【Rails】入力された値を元にGETリクエストを送ってjsonを取得し、DBに保存する

すること

今回はGET statuses/oembed — Twitter Developersを用いてツイッターカードを表示します。
image.png
流れは以下の通りです。

  1. 入力されたURLからIDを取得
  2. リクエストを送信
  3. データを取得
  4. URL, jsonをデータベースに保存
  5. 保存したデータを表示

開発環境

Ruby 2.6.5
Rails 5.2.3
MySQL 8.0.19

実装する

モデル

事前に、jsonを入れる予定のt_jsonカラムをマイグレーションで作成しておきます。

tweet.rb
# == Schema Information
#
# Table name: tweets
#
#  id         :bigint           not null, primary key
#  url        :string(255)      not null
#  t_json     :json             not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Tweet < ApplicationRecord
  validates :url, presence: true, length: { maximum: 200 }

  def tweet_id
    url.slice(-19..-1)
  end
end

t_jsonはバリデーションチェックした後に代入するので、ここではバリデーションを入れていません。

入力するURLはhttps://twitter.com/Interior/status/507185938620219395という形になりますが、今回必要なのは507185938620219395の部分だけです。
ということで、tweet_idというインスタンスメソッドで、入力されたURLからIDとなる文字列(19桁の数字)を抜き出します。

コントローラ

tweets_controller.rb
class TweetsController < ApplicationController

  def create
    @tweet = Tweet.new(tweet_params)
    if @tweet.valid?
      @tweet.t_json = tweet_json(@tweet.tweet_id)
      @tweet.save!
    else
      render :index
    end
  end

  def update
    @tweet = Tweet.find(params[:id])
    @tweet.assign_attributes(tweet_params)
    if @tweet.valid?
      @tweet.t_json = tweet_json(@tweet.tweet_id) if tweet.url_changed?
      @tweet.save!
    else
      render :index
    end
  end

  private

  def tweet_params
    params.require(:tweet).permit(:url)
  end
end

createupdateの違いはif tweet.url_changed?の有無だけです。
@tweetに入力されたURLを格納し、有効な場合はt_jsonにデータを格納し、保存するという流れになります。

@tweet.t_json = tweet_json(@tweet.tweet_id)

では、この行で必要なtweet_jsonメソッドですが、役割としては、入力されたURLを元にGETリクエストを送り、jsonを取得し、返すというものです。
private以下に記述します。

open_uriを用いる方法

  def tweet_json(tweet_id)
    url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}"
    responce = OpenURI.open_uri(url)
    ActiveSupport::JSON.decode(responce.read)
  end

Net::HTTPを用いる方法

  def tweet_json(tweet_id)
    url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}"
    uri = URI.parse(url)
    json = Net::HTTP.get(uri)
    JSON.parse(json)
  end

Ruby の HTTP リクエストを送る方法の性能比較によると、Net::HTTPの方が速いようです。

そして、これで以下のようなjson形式のデータをt_jsonに保存できました。

{
  "url": "https://twitter.com/Interior/status/507185938620219395",
  "author_name": "US Dept of Interior",
  "author_url": "https://twitter.com/Interior",
  "html": "<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Happy 50th anniversary to the Wilderness Act! Here&#39;s a great wilderness photo from <a href="https://twitter.com/YosemiteNPS">@YosemiteNPS</a>. <a href="https://twitter.com/hashtag/Wilderness50?src=hash">#Wilderness50</a> <a href="http://t.co/HMhbyTg18X">pic.twitter.com/HMhbyTg18X</a></p>&mdash; US Dept of Interior (@Interior) <a href="https://twitter.com/Interior/status/507185938620219395">September 3, 2014</a></blockquote>n<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>",
  "width": 550,
  "height": null,
  "type": "rich",
  "cache_age": "3153600000",
  "provider_name": "Twitter",
  "provider_url": "https://twitter.com",
  "version": "1.0"
}

ビュー

Twitterカードを表示するには"html"をそのまま出力すればいいので、ビューファイルで以下のように記述します。

view.html.erb
<%= @tweet.t_json['html'].html_safe %>

ツイート情報を取得する際のオプション

"https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}&omit_script=true"

このように&でつなげます。(パラメータの種類については公式ページ参照)
omit_script=trueにすると、<script>部分を除いたHTMLが返されるので、1ページに複数のツイートを表示する場合はおすすめです。

view.html.erb
<% @tweets.each do |tweet| %>
  <%= tweet.t_json['html'].html_safe %>
<% end %>

<%= javascript_include_tag '//platform.twitter.com/widgets.js' %>

リンク

例外処理など細かく行う場合

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

【Rails】依存関係にあるGemのバージョンコンフリクト「Bundler could not find compatible versions」

概要

依存関係にあるgemのバージョン問題を解決する。
今回は以下2点のgemにおいて発生したエラーの解決までの流れになります。

omniauth-google-oauth2
https://github.com/zquestz/omniauth-google-oauth2

rspotify
https://github.com/guilhermesad/rspotify

エラー発生

omniauth-google-oauth2を利用してGoogleSNS認証を実装し、
ログインが完了した後にアプリ内のとあるviewで以下のエラーが発生するようになってしまいました。

エラーメッセージ「key not found: :ciphers」

  require 'rspotify'
  RSpotify.authenticate(ENV['SPOTIFY_CLIENT_ID'], ENV['SPOTIFY_SECRET_ID'])

SpotifyAPIを利用するために取得していたkeyが見つからないとのこと。
Google認証機能を加える前は問題なくAPI連携できていました。
行ったことはGoogle認証機能を実装するために、’omniauth-google-oauth2’を最新バージョンにアップデートしたことでした。

SpotifyAPIの連携には’rspotify’というラッパーを使っています。
Gemfile.lockを確認するとrspotifyのバージョンが変わってしまっていることがわかりました。

【omniauthバージョンアップ前】

Gemfile.lock
omniauth-google-oauth2 (0.2.6)
      omniauth (> 1.0)
      omniauth-oauth2 (~> 1.1)

rspotify (2.4.0)
      omniauth-oauth2 (~> 1.3.1)
      rest-client (~> 2.0.2)

【omniauthをバージョンアップした後】

Gemfile.lock
omniauth-google-oauth2 (0.8.0)
      jwt (>= 2.0)
      omniauth (>= 1.1.1)
      omniauth-oauth2 (>= 1.6)

    rspotify (1.15.4)
      omniauth-oauth2 (~> 1.1)
      rest-client (~> 1.7)


Gemfile内の記号の意味

例えばomniauth-oauth2 (~> 1.3.1)であれば、
~>は上記を例にすると1.3.1以上1.4.0未満のバージョンを指定することになる。
>は指定バージョンより大きいバージョンを。
>=は指定バージョン以上のバージョンをインストールします。

原因解明

原因は’rspotify’と’omniauth-google-oauth2’の両方に依存関係があるgem’omniauth-oauth2’でした。
omniauth-google-omniauth2の最新バージョンをインストールすると、依存関係にあるomniauth-oauth2>=1.6となっています。

rspotifyのバージョンを戻すため、バージョン指定でinstallしてみました。
gem 'rspotify', '>= 2.4.0'



すると、


Bundler could not find compatible versions for gem "omniauth-oauth2":
  In snapshot (Gemfile.lock):
    omniauth-oauth2 (= 1.6.0)

  In Gemfile:
    omniauth-google-oauth2 was resolved to 0.8.0, which depends on
      omniauth-oauth2 (>= 1.6)

    rspotify (>= 2.4.0) was resolved to 2.4.0, which depends on
      omniauth-oauth2 (~> 1.3.1)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

このエラーコードを読み解くと、’omniauth-google-oauth2’のバージョン’0.8.0’に対して依存するomniauth-oauth21.6.0以上が指定されると、

rspotifyのバージョン2.4.0に対して依存するomniauth-oauth2’~> 1.3.1’つまり1.3.1以上1.4.0未満が指定される。
だからバージョンのコンフリクトが起きて、インストールできないと。

rspotify (1.15.4)
omniauth-oauth2 (~> 1.1)が依存関係で指定されるので、バージョンは1.1以上2.0未満という指定です。
そのためomniauth-oauth2(>= 1.6.0)とも共存できるという認識をしました。
間違ってるかも・・・

解決方法

エラーメッセージの通りにbundle updateをしてみてもrspotifyのバージョンは変わりませんでした。

依存関係にあるgemのバージョンを確認する。

'omniauth-oauth2'のバージョンコンフリクトを解消するために'rspotify'’omniauth-google-oauth2’のgithubを確認。

githubのBranch:masterをクリックして、
Tagsを選択すれば、様々なバージョンごとにソースコードが分かれています。


ここで最新のv0.8.0では依存関係のある'omniauth-oauth2'

omniauth-google-oauth2.gemspec
gem.add_runtime_dependency 'omniauth-oauth2', '>= 1.6'

とバージョン1.6以上が指定されています。

v0.7.0を指定して見てみると、

omniauth-google-oauth2.gemspec
gem.add_runtime_dependency 'omniauth-oauth2', '>= 1.5'

バージョンは1.5以上になってます。


'rspotify'でも同じように'omniauth-oauth2’のバージョン指定を確認していきました。
最新のv2.8.0で見てみると、

rspotify.gemspec
spec.add_dependency 'omniauth-oauth2', '~> 1.5.0'

バージョン指定は1.5.0以上1.6.0未満となってます。

つまり、v0.7.0v2.8.0でインストール指定すればコンフリクトが解消され、rspotifyが最新バージョンになるのでkey取得の問題も解消されるのではないかと!

tag指定でgemをインストールする

Gemfileにtag指定でインストールするように記載します。

gem 'omniauth-google-oauth2', git: "git@github.com:zquestz/omniauth-google-oauth2.git", tag: "v0.7.0"

gem 'rspotify', git: "git@github.com:guilhermesad/rspotify.git", tag: "v2.8.0"

一度Gemfile.lockを消して、bundle installすれば、コンフリクトなくバージョン指定することができ、エラーも解消されました!

普通はbundlerが勝手にコンフリクトのないようにバージョンをインストールしてくれるみたいですが、
今回はうまくできなかったので、自分で指定してからインストールすることになりました。

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

rubyXLでexcelデータを出力する際に悩んだこと。

備忘録な意味合いで残します。

1. テンプレートとするExcelで完全にブランクなセルに値をセットしようとするとエラーになる。
・罫線などの情報を一切もっていないセルには、
  file[sheet_name][row][col].change_contents(value)
 こういう指定ができない。
 もしこれで、nil:NilClassのエラーがでるようなら、該当のセルにスペースを一度いれておく。とかで取得できるみたい。

2. insert_rowで値を挿入すると、その下のセル結合などが壊れる。
・仕様のようなので仕方がないが、UIで操作する単純な行の挿入のイメージとは処理が違う。挿入する行の前後は、セル結合などは極力してない方がベスト。

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

enumのRSpecの書き方

はじめに

enumのテストコードを書いたので、備忘として残します。
shoulda-matchersを使用しています。

ソースコード

app/models/hoge.rb
class Hoge < ApplicationRecord
  enum status: {
    hoge: 0,
    huga: 1
  }
end
spec/models/hoge_spec.rb
RSpec.describe Hoge, type: :model do
  describe 'enum' do
    it { is_expected.to define_enum_for(:status).with_values(hoge: 0, huga: 1) }
  end
end

解説

shoulda-matchersdefine_enum_forを使うと上のように書けます。
with_valuesではなく、withでもテストは通りますが怒られます。

簡単ですね!

参考

【RSpec】shoulda-matchersでenum,delegateのテスト

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

Devise のviewを編集可能にする

はじめに

導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。

Deviseを導入

実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sass

Devise gemをインストール

以下をGemfileに記入しbundle installを実行する。

gem 'devise'

Deviseのファイルを作成する。

$rails g devise:install
Running via Spring preloader in process 55270
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

1.に書かれている通りにconfig/environments/development.rbにURLオプションを記載する。

config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

2.ではrootを設定しなければいけないのでしていない方は設定してください。

3.flashを表示させる為にapplication.rb<body>タグに記載する。

app/views/layouts/application.html.erb
<body>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
</body>

モデルの作成

deviseに応じたUserモデルを作成する。

$ rails g devise User

生成されたmodels/user.rbのモジュールを追加したり,マイグレーションファイルdb/migrate/生成されたファイルのカラムを変更したりできますが今回はここを省きます。

$rails db:migrateを実行します。

$ rails db:migrate

viewの作成

deviseはviewを作成できて便利です。

$ rails g devise:views

これによりapp/views/deviseに生成されます。

・しかし、今回の場合、作成したモデルUserに合わせたいので(app/views/deviseだと分かりづらい。)、viewをusersと指定します。

$ rails g devise:views users

Running via Spring preloader in process 59595
Expected boolean default value for '--markerb'; got :erb (string)
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_links.html.erb
      invoke  form_for
      create    app/views/users/confirmations
      create    app/views/users/confirmations/new.html.erb
      create    app/views/users/passwords
      create    app/views/users/passwords/edit.html.erb
      create    app/views/users/passwords/new.html.erb
      create    app/views/users/registrations
      create    app/views/users/registrations/edit.html.erb
      create    app/views/users/registrations/new.html.erb
      create    app/views/users/sessions
      create    app/views/users/sessions/new.html.erb
      create    app/views/users/unlocks
      create    app/views/users/unlocks/new.html.erb
      invoke  erb
      create    app/views/users/mailer
      create    app/views/users/mailer/confirmation_instructions.html.erb
      create    app/views/users/mailer/email_changed.html.erb
      create    app/views/users/mailer/password_change.html.erb
      create    app/views/users/mailer/reset_password_instructions.html.erb
      create    app/views/users/mailer/unlock_instructions.html.erb

反映されない問題

生成されたviews/userファイルをカスタマイズしてブラウザで確認してみると反映されてません。
なぜかというと、デフォルトでスコープされているのがviews/deviseになっており正確にviews/usersが呼び出せていませんでした。

解決方法

デフォルトのスコープを変更してあげることでviews/usersのカスタマイズが反映されます。
#config.scoped_views = trueをコメントアウトします。

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

終わりに

deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。

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

Devise viewのデザインを編集可能にする

はじめに

導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。

Deviseを導入

実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sass

Devise gemをインストール

以下をGemfileに記入しbundle installを実行する。

gem 'devise'

Deviseのファイルを作成する。

$rails g devise:install
Running via Spring preloader in process 55270
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

1.に書かれている通りにconfig/environments/development.rbにURLオプションを記載する。

config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

2.ではrootを設定しなければいけないのでしていない方は設定してください。

3.flashを表示させる為にapplication.rb<body>タグに記載する。

app/views/layouts/application.html.erb
<body>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
</body>

モデルの作成

deviseに応じたUserモデルを作成する。

$ rails g devise User

生成されたmodels/user.rbのモジュールを追加したり,マイグレーションファイルdb/migrate/生成されたファイルのカラムを変更したりできますが今回はここを省きます。

$rails db:migrateを実行します。

$ rails db:migrate

viewの作成

deviseはviewを作成できて便利です。

$ rails g devise:views

これによりapp/views/deviseに生成されます。

・しかし、今回の場合、作成したモデルUserに合わせたいので(app/views/deviseだと分かりづらい。)、viewをusersと指定します。

$ rails g devise:views users

Running via Spring preloader in process 59595
Expected boolean default value for '--markerb'; got :erb (string)
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_links.html.erb
      invoke  form_for
      create    app/views/users/confirmations
      create    app/views/users/confirmations/new.html.erb
      create    app/views/users/passwords
      create    app/views/users/passwords/edit.html.erb
      create    app/views/users/passwords/new.html.erb
      create    app/views/users/registrations
      create    app/views/users/registrations/edit.html.erb
      create    app/views/users/registrations/new.html.erb
      create    app/views/users/sessions
      create    app/views/users/sessions/new.html.erb
      create    app/views/users/unlocks
      create    app/views/users/unlocks/new.html.erb
      invoke  erb
      create    app/views/users/mailer
      create    app/views/users/mailer/confirmation_instructions.html.erb
      create    app/views/users/mailer/email_changed.html.erb
      create    app/views/users/mailer/password_change.html.erb
      create    app/views/users/mailer/reset_password_instructions.html.erb
      create    app/views/users/mailer/unlock_instructions.html.erb

反映されない問題

生成されたviews/userファイルをカスタマイズしてブラウザで確認してみると反映されてません。
なぜかというと、デフォルトでスコープされているのがviews/deviseになっており正確にviews/usersが呼び出せていませんでした。

解決方法

デフォルトのスコープを変更してあげることでviews/usersのカスタマイズが反映されます。
#config.scoped_views = trueをコメントアウトします。

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

終わりに

deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。

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

Rails初心者の「N+1って何ですか?」にのベテランエンジニアさん超絶わかりやすいアンサー!!!

僕 「N+1ってデータを何回もやりとりしてしまうことのことですよね?(ぼんやり)」
先輩 「そうそう!具体的にいうとね。この例を見て欲しい」

ProductモデルとPatronモデルはアソシエーションしている前提

products = Product.all #全体の数をN件とする 
#先輩「ここでN件分の一回呼び出されるよね?正確にはeachしたときなんだけど」
products.each do |product| #先輩「ここではさっきN件分呼び出されたものを、一個一個取り出す。つまりN回分呼ぶよね」
 product.patrons.each do |patron|
  patron.title
 end
end

先輩「そうすると結局N+1回分データを取り出す作業していることはわかった?」
僕「なるほど!わかりました!」

先輩「データが少ないときには特に問題にならないかもしれないのだけど、データが数万とかたくさんのデータを扱うときにN+1問題はパフォーマンスを低下させてしまう原因になってしまうんだ。」
僕 「確かにそうですよね!解消する手段はあるんですか?」
先輩「ある!結論から言うとincludesメソッドを使うんだ。このメソッドを使うと下記の例のように2回のやりとりで済ませることができる」

products = Product.includes(:patrons).all #先輩「includedsメソッドはSQLでin句を発行してくれるin句に関しては下に解説を載せておいたから見ていみるといい。」
products.each do |product| #先輩「viewの内容は変わらないんだ。ただincludesメソッドがついているものだとここでのデータのやりとりを1回で済ませることができる。」
 product.patrons.each do |patron|
  patron.title
 end
end

in句に関して

https://www.sejuku.net/blog/72497

僕の記事より圧倒的にわかりやすい記事を書いている中で発見しました

https://nyoken.com/rails-includes

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

Google Mapを「投稿画面」と「詳細画面」の2か所に実装してみた。

はじめに

こんにちは!現在、アウトプットの一環として個人開発を行っているんですが、エラーでボコボコにされてる今日この頃です!
という話は置いといて、本記事ではレビューの投稿機能にGoogleMapを表示させてみた話をしたいと思います!

実装したいこと

  • 投稿ページ(new)にて住所(または地名)を入力してもらう。
  • 投稿ページのGoogleMapにマーカーを落とす。
  • 詳細ページ(show)のGoogleMapを表示。投稿時に指定した場所にマーカーがある。

開発条件

Ruby 2.5.1
Rails 5.2.3
Haml・Sass記法

データベース

reviewテーブル

Column Type Options
title string null: false
description text null: false

Association

  • has_one :spot

spotテーブル

Column Type Options
address string null: false
latitude float null: false
longitude float null: false
review_id references foreign_key: true, null: false

Association

  • belongs_to :review

実装手順

それでは、実装です。実装手順は参考記事に則ってます。

APIの利用

まず、GoogleMapのAPIを取得する必要があります。そのため、下記のリンクにアクセスしてAPIのkeyを取得してください。発行されたkeyは後々使用します。

Google Maps Platform

今回、利用するAPIは次の2つです。

  • Maps JavaScript API
  • Geocoding API

この2つを有効に設定しておいてください。

gemのインストール

必要なgemをインストールします。

Gemfile
gem "gmaps4rails"
gem "geocoder"
gem "gon"
gem "dotenv-rails"

記入できたらbundle installをしてください。
それぞれの役割をしては

  • GoogleMapを簡単に作成できるgem "gmaps4rails"
  • 地名から緯度経度に変換できるgem "geocoder"
  • JSでcontrollerの変数を使えるようにするgem "gon"
  • GoogleMapAPIのkeyを隠すためのgem "dotenv-rails"

になります。gem無しでも実装はできるみたいですが、結構複雑みたいなのでgemを使用しました。

JSを導入

application.html.hamlを編集

application.html.haml
%head
  《中略》
  = include_gon
  = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body
  = yield
  %script{src: "https://maps.googleapis.com/maps/api/js?key=#{ENV["GOOGLE_MAP_KEY"]}&callback=initMap"}
  %script{src: "//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js"}
  %script{src: "//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js", type:"text/javascript"}

%headにはgem "gon"を使えるようにするための記述をします。
また、%bodyにはJSを使うための記述をしています。はじめは%script%headに記述していましたが、GoogleMapが表示されないというエラーが起きたので%bodyの最後に記述しました。おそらくは読み込みの順番の問題かと思います。
ENV["GOOGLE_MAP_KEY"]には.envファイルに隠したAPIkeyを入れています。

.env
GOOGLE_MAP_KEY = "あなたが取得したkeyを記述してください"

underscore.jsを作成

その後、app/assets/javascripts下にunderscore.jsを作成してください。作成したファイルに次のリンク先のコードをコピペして貼り付けるらしい。

underscore.js

application.jsで読み込み設定

application.jsに次の記述を追記します。

application.js
//= require underscore
//= require gmaps/google

modelを編集

modelを次のように編集していきます。

review.rb
class Review < ApplicationRecord
  has_one :spot, dependent: :destroy
  accepts_nested_attributes_for :spot
end
spot.rb
class Spot < ApplicationRecord
  belongs_to :review

  geocoded_by :address
  after_validation :geocode
end

viewを編集

投稿ページを作成します。GoogleMapを表示させる記述以外は省略してます。

new.html.haml
= form_with(model: @review, local: true, multipart: true) do |f|
  .spot
    = f.fields_for :spot do |s|
      = s.label :address, "レビュー場所(Google Mapで検索)", class: 'spot__title'
      = s.text_field :address, placeholder: "スポットを入力", id: "address", class: 'spot__text'
    %input{onclick: "codeAddress()", type: "button", value: "検索する"}
    .map{id: "map", style: "height: 320px; width: 640px;"}

次に、投稿したレビューを表示させるページを作成します。

show.html.haml
.show
  .show__address
    = @review.spot.address
  .show__maps{id: "show_map", style: "height: 320px; width: 400px;"}

controllerを編集

controllerも編集しておきます。

reviews_controller.rb
def new
  @review = Review.new
  @review.spot.build
end

def create
  @review = Review.new(review_params)
  if @review.save
    redirect_to root_path
  else
    redirect_to new_review_path
  end
end

def show
  @review = Review.find(params[:id])
  @lat = @review.spot.latitude
  @lng = @review.spot.longitude
  gon.lat = @lat
  gon.lng = @lng
end

private

def review_params
  params.require(:review).permit(
    :title,
    :description,
    spot_attributes: [:address]
  )
end

showアクションで記述している

@lat = @review.spot.latitude
@lng = @review.spot.longitude
gon.lat = @lat
gon.lng = @lng

では、controllerで定義した@lat@lngの変数をJavaScriptでも扱えるように、それぞれgon.latgon.lngに代入しています。

JavaScriptの編集

いよいよJavaScriptでGoogleMapを表示させていきます。

googlemap.js
let map //変数の定義
let geocoder //変数の定義

function initMap(){ //コールバック関数
  geocoder = new google.maps.Geocoder() //GoogleMapsAPIジオコーディングサービスにアクセス
  if(document.getElementById('map')){ //'map'というidを取得できたら実行
    map = new google.maps.Map(document.getElementById('map'), { //'map'というidを取得してマップを表示
      center: {lat: 35.6594666, lng: 139.7005536}, //最初に表示する場所(今回は「渋谷スクランブル交差点」が初期値)
      zoom: 15, //拡大率(1〜21まで設定可能)
    });
  }else{ //'map'というidが無かった場合
    map = new google.maps.Map(document.getElementById('show_map'), { //'show_map'というidを取得してマップを表示
      center: {lat: gon.lat, lng: gon.lng}, //controllerで定義した変数を緯度・経度の値とする(値はDBに入っている)
      zoom: 15, //拡大率(1〜21まで設定可能)
    });

    marker = new google.maps.Marker({ //GoogleMapにマーカーを落とす
      position:  {lat: gon.lat, lng: gon.lng}, //マーカーを落とす位置を決める(値はDBに入っている)
      map: map //マーカーを落とすマップを指定
    });
  }
}

function codeAddress(){ //コールバック関数
  let inputAddress = document.getElementById('address').value; //'address'というidの値(value)を取得

  geocoder.geocode( { 'address': inputAddress}, function(results, status) { //ジオコードしたい住所を引数として渡す
    if (status == 'OK') {
      let lat = results[0].geometry.location.lat(); //ジオコードした結果の緯度
      let lng = results[0].geometry.location.lng(); //ジオコードした結果の経度
      let mark = {
          lat: lat, //緯度
          lng: lng  //経度
      };
      map.setCenter(results[0].geometry.location); //最も近い、判読可能な住所を取得したい場所の緯度・経度
      let marker = new google.maps.Marker({
          map: map, //マーカーを落とすマップを指定
          position: results[0].geometry.location //マーカーを落とす位置を決める
      });
    } else {
      alert('該当する結果がありませんでした');
    }
  });   
}

今回は、地図を表示させる場所が2か所あり、それぞれ別の場所をマップの中心にしたかったので、initMap関数内で「指定のidの有無」による条件分岐を設けることで対応しています。

JavaScriptについては知識が浅く、コメントアウトで解説した部分で解釈の違いがあるかもしれません。その際にはご指摘頂けると有り難いです。

おわりに

GoogleMapは多くのサイトで利用されているので、どのように実装すればいいかを多少なり知ることができて良かったです!GoogleMapAPIにはまだまだ使ってない機能が沢山あるので、時間があれば色々チャレンジしていきたいと思います!!

参考記事

gonを使ったRailsとJavascriptの連携について
ジオコーディング備忘録
RailsでGoogleMapを表示させる(gem 'gmaps4rails'の使い方)
Rails Google Mapを表示させる方法

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

ネストしたindexにYoutube動画埋め込み

ネストしたindexへの動画埋め込み備忘録

https://qiita.com/Kairi_Yasunnde/items/8e931a4670549ba8237e
上記の記事を参考にしたが、自分はgroupテーブルにYoutube_urlを入れてcreateに定義し、groupでネストしたmessages/indexに埋め込もうとして、コントローラーの差異があり出来なかったと推測する。

routes.rb
  resources :groups, only: [:new, :create, :edit, :update, :show, :destroy] do
    resources :messages, only: [:index, :create, :destroy]
  end

なのでYoutube_urlを生値のまま取り出し、ビュー上で末尾11桁をとり出すようにした。
※Youtubeは末尾11桁の文字、数字が各動画に割り当てられている。

手順

まずは、groupにYoutube_urlのカラムを追加する。
ターミナル上で、groupテーブルにyoutube_urlをstring型でカラムを追加。↓

rails g migration AddYoutube_urlToGroups youtube_url:string

追加後、rails db:migrateで実行

次に、コントローラーでYoutube_urlの値をpermitで許可する↓

groups_controller.rb
  private
  def group_params
    params.require(:group).permit(:name, :image, :content, :youtube_url, user_ids: [])
  end

createまたはupdateアクションでurlをテーブルに入れたいのでフォームを作る↓
※_formはnewとeditアクションの部分テンプレート

groups/_form.html.erb
<%= f.text_field :youtube_url, class: "form-control", placeholder: "Youtube_url" %>

そして、ビュー上で末尾11桁の値をlastメソッドで取り出し、Youtube動画埋め込みのテンプレートにはめこむ↓
<%=(@groups.youtube_url).last(11)%>の部分

messages/index.html.erb
<iframe width="560" height="315" src="https://www.youtube.com/embed/<%=(@groups.youtube_url).last(11)%>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

以上で埋め込み完了
createまたはupdateアクションで入力したurlの動画が埋め込まれる

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

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード

何をするのか

Railsチュートリアル本文13.3.3にて、「ユーザー自身のポストを含むマイクロポストのフィード」を実装しました。さらに、14章ここまでで「ユーザーのフォロー」という機能も実装しました。今度は「ユーザー自身のポストと、フォローしているユーザーのポストの両方を含むマイクロポストのフィード」を実装しよう、というわけです。

ステータスフィードの完成形は、Railsチュートリアル本文においては、図 14.21にて示されています。

動機と計画

別記事にて解説しています。

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード実装の動機と計画(データモデルの解説とテストの実装)

演習 - 動機と計画

1. マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。

ヒント: 13.1.4で実装したdefault_scopeを思い出してください。

そもそもuser.feed.map(&:id)というのは、「ステータスフィードに含まれる全てのマイクロポストのidが含まれる配列を返す」というメソッドチェーンです。また、Micropostsモデルのdefault_scopeの実装が以下のようになっているため、「ステータスフィードに含まれるマイクロポストは、作成日時の降順に整列された状態である」というのもポイントです。

app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  #...略
end

本演習で出てくる図 14.22user.feedを前提とし、また、「単純にidが大きくなるごとにマイクロポストの作成日時も新しくなっていく」という関係が成り立つとすると、user.feed.map(&:id)の戻り値は以下のようになると想定されます。

>> user.feed.map(&:id)
=> [10, 9, 7, 5, 4, 2, 1]

フィードを初めて実装する

別記事にて解説しています。

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - フィードを初めて実装する

演習 - フィードを初めて実装する

別記事にて解説しています。

Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フィードを初めて実装する」

サブセレクト

別記事にて解説しています。

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - サブセレクト

演習 - サブセレクト

1. Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

test "feed on Home Page" do
  get root_path
  @user.feed.paginate(page: 1).each do |micropost|
    assert_match CGI.escapeHTML(micropost.content), response.body
  end
end

上記コードの動作におけるポイントは以下です。

  • マイクロポストの内容のみを得るには、micropost.contentとする
  • assert_matchメソッドで、期待するマイクロポストの内容がresponse.bodyに含まれていることをテストする
    • 期待するマイクロポストは、「@user.feed.paginate(page: 1)に含まれるマイクロポスト」である
特定のMicropostオブジェクトの内容
#<Micropost id: 300, content: "Accusamus veniam voluptatibus voluptatum sapiente ...", user_id: 6, created_at: "2020-01-29 23:14:31", updated_at: "2020-01-29 23:14:31", picture: nil>

CGI.escapeHTMLというメソッドは、上記Micropostオブジェクトの内容そのものを受け取ることはできません。本当に必要なのはmicropost.contentということですね。

では、実際に当該テストを実行してみましょう。

# rails test test/models/user_test.rb
Running via Spring preloader in process 560
Started with run options --seed 933

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.28291s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips

無事テストが通りました。

2. リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。

ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。

上記のようにdebuggerを挿入し、response.bodyの内容を見てみることとします。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 110
Started with run options --seed 1594

  7/2: [==========                          ] 28% Time: 00:00:04,  ETA: 00:00:13
[58, 67] in /var/www/sample_app/test/integration/following_test.rb
   58:   end
   59: 
   60:   test "feed on Home Page" do
   61:     get root_path
   62:     @user.feed.paginate(page: 1).each do |micropost|
   63:       debugger
=> 64:       assert_match CGI.escapeHTML(micropost.content), response.body
   65:     end
   66:   end
   67: end
(byebug) response.body

すると、response.bodyには以下のような文字列が含まれているのがわかります。

I&#39;m sorry. Your words made sense, but your sarcastic tone did not.

上記の文字列は、test/fixtures/microposts.ymlの以下の部分に相当します。

test/fixtures/microposts.yml(抜粋)
tone:
  content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
  created_at: <%= 10.minutes.ago %>
  user: :rusami

以下がポイントです。

  • response.bodyの内容はERBのテンプレートに基づいて生成される
  • content中にアポストロフィ(')が含まれている

ERBのテンプレートに文字列が与えられた場合、HTML的に意味がある文字(<>&"')は、脆弱性につながることを防ぐために自動でエスケープされます。'もエスケープの対象なので、&#39;と出力されているのです。

というわけで、response.bodyに含まれるのは「HTML的に意味がある文字がエスケープされた文字列」となります。assert_matchによる検索文字列もまた、HTML的に意味がある文字をエスケープしなければ、正しい結果を得ることができません。「期待されるHTMLをCGI.escapeHTMLメソッドでエスケープする理由」としては以上です。

CGI.escapeHTMLというのは、「引数として与えた文字列から、HTML的に意味がある文字をエスケープする」というメソッドです。まさに今回のユースケースで必要となる動作ですね。

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

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - サブセレクト

前準備 - whereメソッド内の変数に、キーと値のペアを使うようにする

whereメソッドの第1引数であるSQL文において、Rails側の変数の内容を使う部分は、これまで?(疑問符)として与えてきました。以下のようなメソッド呼び出しがその例です。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

whereメソッドは、実は「上記?の部分に、?ではなくRubyのシンボルを与える」という使い方ができます。以下のようなメソッド呼び出しがその例です。

Micropost.where(
  "user_id IN (:following_ids) OR user_id = :user_id",
  following_ids: following_ids,
  user_id: id
)

結果、app/models/user.rb内のfeedメソッドは以下のように変更できます。

app/models/user.rb#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where(
+     "user_id IN (:following_ids) OR user_id = :user_id",
+     following_ids: following_ids,
+     user_id: id
+   )
  end

このような変更をするからには、「following_idsもしくはuser_idを複数箇所で使う」という実装が発生するということなのでしょう。

「フィードを初めて実装する」の実装の問題点

「フィードを初めて実装する」の実装では、「投稿されたマイクロポストの数が膨大になった際にうまくスケールしない」という問題点があります。Railsチュートリアル本文には以下のようにあります。

フォローしているユーザーが5,000人程度になるとWebサービス全体が遅くなる可能性があります

現状の実装は、一体どのような点でスケールしないのでしょうか。「よりスケールする実装」というのは、一体どのような実装なのでしょうか。

現状の実装ではどういう処理がされているのか、現状の実装は何が問題なのか

「フィードを初めて実装する」の実装におけるfeedメソッドの実装内容は、以下のようなものでした。

def feed
  Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end

上記のコードは、最終的に以下のような動作をすることになります。

  1. following_idsメソッドにより、現在フォローしている全てのユーザーを得るためにRDBに問い合わせを行う
  2. 全てのMicropostオブジェクトを得るため、前述following_idsメソッドの戻り値を条件に、RDBのmicropostsテーブル全体を対象として問い合わせを行う

今回開発しているアプリケーションのユースケースでは、idが上記1.の集合に内包されているかどうかだけをチェックするため、RDBに2回問い合わせを行う現状の動作はどうにもまどろっこしいです。また、Railsが介入する必要がないであろうところにRailsが介入しているのはよろしくありません。

より効率的な方法はないのでしょうか。いや、こうした集合計算に特化した言語であるSQLなら、より効率的な方法はきっとあるはずです。

どういう処理だとよりいいのか - サブクエリ(サブセレクト)を用いたクエリの使用

今回行おうとしている処理の場合、「SELECT文の結果そのものを、(Railsに渡すことなく)次段のSELECT文の評価の対象とする」という形にすることによって、全てをRDBMS内で完結させることができます。RDBMSはこうした処理に最適化されているので、全てをRDBMS内で完結させることができれば、途中でRailsが介在する実装より高速な処理が実現できます。

例えば現在のユーザーのidが1である場合、このような処理を実現するためのSQL文は以下のようになります。

SELECT * FROM microposts
WHERE user_id IN (
  SELECT followed_id FROM relationships
  WHERE follower_id = 1
) OR user_id = 1

上記SQL文のように、「SELECT文の結果そのものを、次段のSQL文の評価の対象とする」処理は「サブクエリを用いたクエリ」と呼ばれます。()の内側のSQL文は「サブクエリ(もしくはサブセレクト)」と呼ばれます。

このようなサブクエリを用いたクエリにおいては、集合を組み立てるロジックはRDBMS内で完結します。

サブクエリを用いたクエリをwhereメソッドで使用する

Micropost.where(
  "user_id IN (:following_ids) OR user_id = :user_id",
  following_ids: following_ids,
  user_id: id
)

上記Micropost.whereの引数:following_idsは、前述「サブクエリを用いたクエリの使用」を踏まえて、以下のように書き換えることができます。

following_ids =
  "SELECT followed_id FROM relationships
  WHERE follower_id = :user_id"

結果、User#feedメソッドの実装は以下のように書き換えられる、ということになるわけです。

User#feed
def feed
  following_ids =
    "SELECT followed_id FROM relationships
    WHERE follower_id = :user_id"
  Micropost.where(
    "user_id IN (#{following_ids}) OR user_id = :user_id",
    user_id: id
  )
end

フィードの最終的な実装

ここまでの内容を踏まえると、app/models/user.rbに対する変更の内容は、以下のようになります。

app/models/user.rb#feed
  def feed
-   Micropost.where(
-     "user_id IN :following_ids, OR user_id = :user_id",
-     following_ids: following_ids,
-     user_id: id
-   )
+   following_ids =
+     "SELECT followed_id FROM relationships
+     WHERE follower_id = :user_id"
+   Micropost.where(
+     "user_id IN (#{following_ids}) OR user_id = :user_id",
+     user_id: id
  end

test/models/user_test.rbを対象としたテストも実行しておきましょう、

# rails test test/models/user_test.rb
Running via Spring preloader in process 428
Started with run options --seed 8307

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.29007s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips

無事テストが通りました。

開発環境において、Homeページにフィードが表示されている様子は以下のようになります。

スクリーンショット 2020-02-26 18.25.04.png

本番環境において、Homeページにフィードが表示されている様子は以下のようになります。

スクリーンショット 2020-02-26 18.32.27.png

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

Railsチュートリアル 第14章 ユーザーをフォローする - 演習 - フィードを初めて実装する

1. リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

「(フォローしているユーザーの投稿は含まれるが)現在のユーザー自身の投稿が含まれない」というのは、以下のようなSQL文になるはずです。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>)

上記SQL文に対応するwhereメソッドの記述は、以下のようになります。

Micropost.where("user_id IN (?)", following_ids)

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where("user_id IN (?)", following_ids)
  end

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# 自分自身の投稿を確認
rhakurei.microposts.each do |post_self|
  assert rhakurei.feed.include?(post_self)
end

私の環境では、上記のテストは、test/models/user_test.rbの105〜108行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 217
Started with run options --seed 20808

 FAIL["test_feed_should_have_the_right_posts", UserTest, 0.9165929999999207]
 test_feed_should_have_the_right_posts#UserTest (0.92s)
        Expected false to be truthy.
        test/models/user_test.rb:107:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:106:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.26620s
15 tests, 29 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは106行目と107行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

2. リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

「(自分自身の投稿は含まれるが)フォローしているユーザーの投稿が含まれない」というのは、以下のようなSQL文になるはずです。

SELECT * FROM microposts
WHERE user_id = <user id>

上記SQL文に対応するwhereメソッドの記述は、以下のようになります。

Micropost.where("user_id = ?", id)

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where("user_id = ?", id)
  end

(これって、本文14章「ステータスフィード」に取り掛かる前のUser#feedの実装内容と同じですよね)

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# フォローしているユーザーの投稿を確認
rusami.microposts.each do |post_following|
  assert rhakurei.feed.include?(post_following)
end

私の環境では、上記のテストは、test/models/user_test.rbの101〜104行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 230
Started with run options --seed 53063

 FAIL["test_feed_should_have_the_right_posts", UserTest, 2.1687096999994537]
 test_feed_should_have_the_right_posts#UserTest (2.17s)
        Expected false to be truthy.
        test/models/user_test.rb:103:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:102:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.66360s
15 tests, 27 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは102行目と103行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

3. リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。

「自分自身・自分をフォローしているユーザー・自分をフォローしていないユーザー全てのマイクロポストの集合」というのは、「全てのマイクロポスト」と等しくなります。以下のようなSQL文になるはずです。

SELECT * FROM microposts

上記SQL文に対応する記述は、以下のようになります。

Micropost.all
  end

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.all
  end

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# フォローしていないユーザーの投稿を確認
mkirisame.microposts.each do |post_unfollowed|
    assert_not rhakurei.feed.include?(post_unfollowed)
end

私の環境では、上記のテストは、test/models/user_test.rbの109〜112行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 269
Started with run options --seed 15243

 FAIL["test_feed_should_have_the_right_posts", UserTest, 1.1897443999987445]
 test_feed_should_have_the_right_posts#UserTest (1.19s)
        Expected true to be nil or false
        test/models/user_test.rb:111:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:110:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.61430s
15 tests, 63 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは110行目と111行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

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

Railsチュートリアル 第14章 ユーザーをフォローする - 演習 「フィードを初めて実装する」

1. リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

「(フォローしているユーザーの投稿は含まれるが)現在のユーザー自身の投稿が含まれない」というのは、以下のようなSQL文になるはずです。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>)

上記SQL文に対応するwhereメソッドの記述は、以下のようになります。

Micropost.where("user_id IN (?)", following_ids)

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where("user_id IN (?)", following_ids)
  end

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# 自分自身の投稿を確認
rhakurei.microposts.each do |post_self|
  assert rhakurei.feed.include?(post_self)
end

私の環境では、上記のテストは、test/models/user_test.rbの105〜108行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 217
Started with run options --seed 20808

 FAIL["test_feed_should_have_the_right_posts", UserTest, 0.9165929999999207]
 test_feed_should_have_the_right_posts#UserTest (0.92s)
        Expected false to be truthy.
        test/models/user_test.rb:107:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:106:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.26620s
15 tests, 29 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは106行目と107行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

2. リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

「(自分自身の投稿は含まれるが)フォローしているユーザーの投稿が含まれない」というのは、以下のようなSQL文になるはずです。

SELECT * FROM microposts
WHERE user_id = <user id>

上記SQL文に対応するwhereメソッドの記述は、以下のようになります。

Micropost.where("user_id = ?", id)

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where("user_id = ?", id)
  end

(これって、本文14章「ステータスフィード」に取り掛かる前のUser#feedの実装内容と同じですよね)

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# フォローしているユーザーの投稿を確認
rusami.microposts.each do |post_following|
  assert rhakurei.feed.include?(post_following)
end

私の環境では、上記のテストは、test/models/user_test.rbの101〜104行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 230
Started with run options --seed 53063

 FAIL["test_feed_should_have_the_right_posts", UserTest, 2.1687096999994537]
 test_feed_should_have_the_right_posts#UserTest (2.17s)
        Expected false to be truthy.
        test/models/user_test.rb:103:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:102:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.66360s
15 tests, 27 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは102行目と103行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

3. リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。

「自分自身・自分をフォローしているユーザー・自分をフォローしていないユーザー全てのマイクロポストの集合」というのは、「全てのマイクロポスト」と等しくなります。以下のようなSQL文になるはずです。

SELECT * FROM microposts

上記SQL文に対応する記述は、以下のようになります。

Micropost.all
  end

User#feedの変更内容は以下のようになります。

User#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.all
  end

上記のUser#feedの実装では、以下のテストが落ちるはずです。

# フォローしていないユーザーの投稿を確認
mkirisame.microposts.each do |post_unfollowed|
    assert_not rhakurei.feed.include?(post_unfollowed)
end

私の環境では、上記のテストは、test/models/user_test.rbの109〜112行目に記述されています。

test/models/user_test.rbを対象として、実際にテストを実行してみます。

# rails test test/models/user_test.rb
Running via Spring preloader in process 269
Started with run options --seed 15243

 FAIL["test_feed_should_have_the_right_posts", UserTest, 1.1897443999987445]
 test_feed_should_have_the_right_posts#UserTest (1.19s)
        Expected true to be nil or false
        test/models/user_test.rb:111:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:110:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.61430s
15 tests, 63 assertions, 1 failures, 0 errors, 0 skips

失敗が発生したのは110行目と111行目です。ということは、想定通りの形でテストが落ちたと言えそうです。

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

Railsチュートリアル 第14章 ユーザーをフォローする - フィードを初めて実装する

フィードに必要となるSQLクエリと、対応するwhereメソッドの引数

要件は以下です。

  • 対象ユーザーがフォローしているユーザーのユーザーidを持つマイクロポストを全て選択する
  • かつ、対象ユーザー自身のマイクロポストも全て選択する

以上の要件を満たす最小限のSQLクエリを模式的に書くと、以下のようになります。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

「ログインしているユーザーがフォローしているユーザーのid」というのは、多くの場合複数となります。ゆえに、それらをクエリ問い合わせに使う場合、単一の値ではなく集合として与えなければなりません。そのような場合に使うSQLのキーワードがINとなります。

Active Recordでは、whereメソッドを使うことによって、SQL文のWHERE以下に相当する検索条件を与えることができます。上記のSQL文に対応するwhereメソッドの引数の与え方は以下のようになります。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

following_idsの内容は、「ユーザーidをカンマ区切りで列挙したもの」となります。となると、次に問題となるのは、「カンマ区切りでユーザーidを列挙したものを文字列で得るためにはどうすればいいか」ということになりますね。

参考…自身のマイクロポスト全てを取得するためのSQL文と、whereメソッドの引数の与え方

「対象ユーザー自身のマイクロポストを全て選択する」という要件を満たす最小限のクエリは、以下のような内容になります。

SELECT * FROM microposts
WHERE user_id = <user id>

上記のSQL文に対応するwhereメソッドの引数の与え方は以下のようになります。

Micropost.where("user_id = ?", id)

ユーザーidの列挙を、カンマ区切りの文字列として得る方法

Rubyにおいては、「オブジェクトの列挙から、各オブジェクトの特定の属性値を文字列化し、その結果の列挙を配列として得る」という操作は、「当該オブジェクトの列挙に対して、ブロック内でto_sメソッドを呼び出す形でmapメソッドを実行する」という処理を行うことによって実現できます。

>> [1, 2, 3, 4].map { |i| i.to_s}
=> ["1", "2", "3", "4"]

上記のmapメソッドの呼び出しでは、各要素に対してto_sメソッドが実行されています。mapメソッド(やeachメソッド等)に与えるブロックにおいて、内部の処理が「単一のメソッドを実行する」というものである場合、「mapメソッド(やeachメソッド等)の引数として、ブロック中で実行するメソッドのシンボルを&に続けて記述したものを与える」という省略表記を使うことが可能です。

>> [1, 2, 3, 4].map(&:to_s)      
=> ["1", "2", "3", "4"]

上記結果に対してjoinメソッドを使うと、以下のように、idの集合をカンマ区切りで繋げた文字列を得ることができます。

>> [1, 2, 3, 4].map(&:to_s).join(", ")
=> "1, 2, 3, 4"

実際に、Userオブジェクトの列挙からidの集合をカンマ区切りの文字列として得てみる

データベースの最初のユーザーを対象として、フォローしている全ユーザーのidを配列として得てみます。

User.first.following.map(&:id)
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

このような呼び出しは、実際のアプリケーションでも頻繁に行われます。そのため、Active Recordでは次のようなメソッドも用意されています1

>> User.first.following_ids
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

ここまでで得られたのはあくまで配列です。最終的には、得られた配列を「カンマ区切りの文字列」に変換しなければなりません。お目当ての出力を得るためには、join(', ')をメソッドチェーンの最後につないでやる必要がある、ということですね。

>> User.first.following_ids.join(', ')
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

これで「カンマ区切りでユーザーidを列挙したものを文字列で得る」という出力結果を得ることができました。

whereメソッドの引数内でfollowing_idsメソッドを使う

user.following_ids.join(', ')というのは、あくまで説明用のコードであり、実際のRailsアプリケーションでこのようなコードを使うことはありません。実際のRailsアプリケーションにおいて、whereメソッドの引数内でfollowing_idsメソッドを使う場合、特に引数を取ることなくfollowing_idsと書けばいいのです。whereの第一引数で正しく?が使われていれば、それで「カンマ区切りでユーザーidを列挙したものを含めたSQLクエリを文字列で得る」ことができるのです。RDBMS依存の非互換性の一部も含め、全てはRailsがよしなにしてくれます。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

app/models/user.rbの実装を変更する

ここまでの内容を踏まえ、実際にapp/models/user.rbに加える変更の内容は以下のようになります。

app/models/user.rb
  class User < ApplicationRecord
    ...略

    # 試作feedの定義
    # 完全な実装は次章の「ユーザーをフォローする」を参照
    def feed
-     Micropost.where("user_id = ?", id)
+     Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    end

    ...略
  end

test/models/user_test.rbに対し、改めてテストを実行する

ここまでの実装を反映すれば、test/models/user_test.rbに対するテストが通るようになります。

# rails test test/models/user_test.rb
Running via Spring preloader in process 178
Started with run options --seed 54454

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.41146s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
  • ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
  • フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
  • フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと

以上の機能が実現されている、ということになりますね!


  1. 当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。 

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

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - フィードを初めて実装する

フィードに必要となるSQLクエリと、対応するwhereメソッドの引数

要件は以下です。

  • 対象ユーザーがフォローしているユーザーのユーザーidを持つマイクロポストを全て選択する
  • かつ、対象ユーザー自身のマイクロポストも全て選択する

以上の要件を満たす最小限のSQLクエリを模式的に書くと、以下のようになります。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

「ログインしているユーザーがフォローしているユーザーのid」というのは、多くの場合複数となります。ゆえに、それらをクエリ問い合わせに使う場合、単一の値ではなく集合として与えなければなりません。そのような場合に使うSQLのキーワードがINとなります。

Active Recordでは、whereメソッドを使うことによって、SQL文のWHERE以下に相当する検索条件を与えることができます。上記のSQL文に対応するwhereメソッドの引数の与え方は以下のようになります。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

following_idsの内容は、「ユーザーidをカンマ区切りで列挙したもの」となります。となると、次に問題となるのは、「カンマ区切りでユーザーidを列挙したものを文字列で得るためにはどうすればいいか」ということになりますね。

参考…自身のマイクロポスト全てを取得するためのSQL文と、whereメソッドの引数の与え方

「対象ユーザー自身のマイクロポストを全て選択する」という要件を満たす最小限のクエリは、以下のような内容になります。

SELECT * FROM microposts
WHERE user_id = <user id>

上記のSQL文に対応するwhereメソッドの引数の与え方は以下のようになります。

Micropost.where("user_id = ?", id)

ユーザーidの列挙を、カンマ区切りの文字列として得る方法

Rubyにおいては、「オブジェクトの列挙から、各オブジェクトの特定の属性値を文字列化し、その結果の列挙を配列として得る」という操作は、「当該オブジェクトの列挙に対して、ブロック内でto_sメソッドを呼び出す形でmapメソッドを実行する」という処理を行うことによって実現できます。

>> [1, 2, 3, 4].map { |i| i.to_s}
=> ["1", "2", "3", "4"]

上記のmapメソッドの呼び出しでは、各要素に対してto_sメソッドが実行されています。mapメソッド(やeachメソッド等)に与えるブロックにおいて、内部の処理が「単一のメソッドを実行する」というものである場合、「mapメソッド(やeachメソッド等)の引数として、ブロック中で実行するメソッドのシンボルを&に続けて記述したものを与える」という省略表記を使うことが可能です。

>> [1, 2, 3, 4].map(&:to_s)      
=> ["1", "2", "3", "4"]

上記結果に対してjoinメソッドを使うと、以下のように、idの集合をカンマ区切りで繋げた文字列を得ることができます。

>> [1, 2, 3, 4].map(&:to_s).join(", ")
=> "1, 2, 3, 4"

実際に、Userオブジェクトの列挙からidの集合をカンマ区切りの文字列として得てみる

データベースの最初のユーザーを対象として、フォローしている全ユーザーのidを配列として得てみます。

User.first.following.map(&:id)
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

このような呼び出しは、実際のアプリケーションでも頻繁に行われます。そのため、Active Recordでは次のようなメソッドも用意されています1

>> User.first.following_ids
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

ここまでで得られたのはあくまで配列です。最終的には、得られた配列を「カンマ区切りの文字列」に変換しなければなりません。お目当ての出力を得るためには、join(', ')をメソッドチェーンの最後につないでやる必要がある、ということですね。

>> User.first.following_ids.join(', ')
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

これで「カンマ区切りでユーザーidを列挙したものを文字列で得る」という出力結果を得ることができました。

whereメソッドの引数内でfollowing_idsメソッドを使う

user.following_ids.join(', ')というのは、あくまで説明用のコードであり、実際のRailsアプリケーションでこのようなコードを使うことはありません。実際のRailsアプリケーションにおいて、whereメソッドの引数内でfollowing_idsメソッドを使う場合、特に引数を取ることなくfollowing_idsと書けばいいのです。whereの第一引数で正しく?が使われていれば、それで「カンマ区切りでユーザーidを列挙したものを含めたSQLクエリを文字列で得る」ことができるのです。RDBMS依存の非互換性の一部も含め、全てはRailsがよしなにしてくれます。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

app/models/user.rbの実装を変更する

ここまでの内容を踏まえ、実際にapp/models/user.rbに加える変更の内容は以下のようになります。

app/models/user.rb
  class User < ApplicationRecord
    ...略

    # 試作feedの定義
    # 完全な実装は次章の「ユーザーをフォローする」を参照
    def feed
-     Micropost.where("user_id = ?", id)
+     Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    end

    ...略
  end

test/models/user_test.rbに対し、改めてテストを実行する

ここまでの実装を反映すれば、test/models/user_test.rbに対するテストが通るようになります。

# rails test test/models/user_test.rb
Running via Spring preloader in process 178
Started with run options --seed 54454

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.41146s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
  • ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
  • フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
  • フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと

以上の機能が実現されている、ということになりますね!


  1. 当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。 

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

Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード実装の動機と計画(データモデルの解説とテストの実装)

ステータスフィードのデータモデルの基本

例えば、「id 1のユーザーが、id 2、7、8、10のユーザーをフォローしている」という場合、フィードのデータモデルは、micropostsテーブルのサンプルデータとセットで示すと以下のようなイメージになります。

User_Feed.png

上図の矢印は、「現在のユーザー自身、および現在のユーザーがフォローしているユーザーに対応するユーザーidを持つマイクロポストを取り出す」という操作を表しています。

ステータスフィードの要求仕様と、それに対するテスト

ステータスフィードの要求仕様

どのように実装するかはさておき、この時点では、ステータスフィードの要求仕様として以下を示すことができます。

  • ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
  • フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
  • フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと

上記要求仕様をアサーションとして展開し、テストを実装していきましょう。

「ステータスフィードを表示する」という機能の実装箇所

「ステータスフィードを表示する」という機能は、Userモデルにfeedメソッドとして実装したのでしたね。以下に現時点におけるapp/models/user.rb#feedの内容を示しておきます。

app/models/user.rb#feed
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  # ...略
  def feed
    Micropost.where("user_id = ?", id)
  end
  #...略
end

ステータスフィードのテストの実装

前提条件として、以下のfixtureが存在するものとします。

test/fixtures/relationships.yml
one:
  follower: rhakurei
  followed: skomeiji

two:
  follower: rhakurei
  followed: rusami

three:
  follower: skomeiji
  followed: rhakurei

four:
  follower: mkirisame
  followed: rhakurei

上記fixtureからは、以下の関係があることがわかります。

  • rhakureiはrusamiをフォローしている
  • rhakureiはmkirisameをフォローしていない

この状況では、「rhakureiのステータスフィードには、rusamiとrhakureiのマイクロポストは見えるが、mkirisameのマイクロポストは見えない」というのが要求仕様通りの実装となります。

テストの対象はUserモデルなので、テストの実装箇所はtest/models/user_test.rbとなります。以下は実際のテストの実装内容です。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  #...略

  test "feed should have the right posts" do
    rhakurei  = users(:rhakurei)
    mkirisame = users(:mkirisame)
    rusami  = users(:rusami)
    # フォローしているユーザーの投稿を確認
    rusami.microposts.each do |post_following|
      assert rhakurei.feed.include?(post_following)
    end
    # 自分自身の投稿を確認
    rhakurei.microposts.each do |post_self|
      assert rhakurei.feed.include?(post_self)
    end
    # フォローしていないユーザーの投稿を確認
    mkirisame.microposts.each do |post_unfollowed|
      assert_not rhakurei.feed.include?(post_unfollowed)
    end
  end
end

ステータスフィードのテストの実装に関する、マイクロポストのfixtureの注意事項

※上記テスト「feed should have the right posts」で使用する全てのユーザーについて、1つ以上のマイクロポストをtest/fixtures/microposts.ymlで定義しておく必要があります。1つもマイクロポストを定義していないユーザーがテスト「feed should have the right posts」に含まれていると、テスト「feed should have the right posts」はうまく動きません。

当該テストを初めて実装した時点における、ステータスフィードのテストの動作結果

現時点において、test/models/user_test.rbを対象としたテストの結果は以下のようになります。

# rails test test/models/user_test.rb
Running via Spring preloader in process 127
Started with run options --seed 63017

 FAIL["test_feed_should_have_the_right_posts", UserTest, 3.487605000000258]
 test_feed_should_have_the_right_posts#UserTest (3.49s)
        Expected false to be truthy.
        test/models/user_test.rb:103:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:102:in `block in <class:UserTest>'

  15/15: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.55564s
15 tests, 27 assertions, 1 failures, 0 errors, 0 skips

テストは以下のコードで失敗しています。

test/models/user_test.rb(102〜104行目)
rusami.microposts.each do |post_following|
  assert rhakurei.feed.include?(post_following)
end

「ログインユーザー自身とは別の、フォローしているユーザーのマイクロポストがフィードに表示されていない」という理由でテストが落ちている、ということになりますね。

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