- 投稿日:2020-02-28T21:03:14+09:00
【Rails】Rails6系とMySQL8系の組み合わせの開発環境をDockerだけでつくる
概要
- この記事は弊社ァ内で行った準備作業の備忘録メモでございますわっ
- 次の新規案件に備え、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 localdocker 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-handshakeAPコンテナ(一時)を建てる
$ # 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のバージョンを決めている
Gemfilesource '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.ymlconfig/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
- ローカルPCで http://localhost:3000 にアクセス
- うまく、いった、みたい、
ソースコードのコピー
- ディレクトリ内をコピーするときは
.
をつけるらしい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 -aDBコンテナ(一時)の削除
- DBコンテナ(一時)も、消す。
$ # host OS (local PC) $ $ docker stop tmp_db_mysql $ docker rm tmp_db_mysql $ $ # 消えていることを確認 $ docker ps -adocker network(一時)の削除
- docker network(一時)も、消す。
$ # host OS (local PC) $ $ docker network rm ${APP_NETWORK} $ $ # 消えていることを確認 $ docker network lsdocker volume(一時)の削除
- docker volume(一時)も、消す。
$ # host OS (local PC) $ $ docker volume rm ${DB_VOLUME} $ docker volume rm ${BUNDLE_VOLUME} $ $ # 消えていることを確認 $ docker volume lsRailsプロジェクトの新規作成まとめ
- 長文になってしまって申し訳ナソス
- ローカル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/DockerfileFROM 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.confcontainers/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/DockerfileFROM 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.ymlversion: '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 にアクセス(初回は時間がかかるのでログを見て落ち着いたら)
- うまく、できた、みたい、
READMEに説明を書く
- やる。
- 僕だけ使えても意味がない。
- もしかしたらこの作業が一番骨折れるかもしれない。
最後に
- 長文失礼。
- ポート開放不親切失礼。必要に応じて書き換え願う
- ruby2.7対応していないgemがいくつかあるみたいで、ログが汚い。
warning
の大量発生や。。。(2020/02ころ現在)- WEBコンテナ追加してnginxとソケット通信する作業でに労力増えてる感ある。
rails sever
のありがたみを感じる。。。- redisの設定は開発時にやるで良いよね。。。
https://hub.docker.com/_/ruby ←←2020/02くらいにここを閲覧した、そして全部取得して比べてみた。 ↩
https://docs.docker.com/storage/volumes/ ←Docker領域内に名前つけて保存されるから、万が一ソースコードごと紛失してもワンチャンある() ↩
https://medium.com/veltra-engineering/how-to-copy-a-directory-using-docker-cp-command-f2c73f9ccf75 ←←こちらのサイトがとても参考になりました ↩
https://docs.docker.com/develop/develop-images/multistage-build/#use-a-previous-stage-as-a-new-stage ↩
https://docs.docker.com/compose/compose-file/compose-versioning/ ↩
https://github.com/puma/puma#binding-tcp--sockets unixソケットにするためにbindのオプションを
config/puma.rb
に書く必要があった。こことかこれとかこのへん読んでも書き方迷いそう。。。 ↩
- 投稿日:2020-02-28T19:20:28+09:00
Capistranoでのデプロイを攻略する
今日の目標
デプロイ作業でよく使うコマンドを整理する
前提
Rails 5.0.7.2
レポジトリ名:freemarket
pemキー名:team_c.pem
ユーザー名:ec2-user
Elastic IP:@18.176.134.115SSH接続
$ cd .ssh/ $ ssh -i team_c.pem ec2-user@18.176.134.115 cd /var/www/freemarketpemキー確認
cd cd .ssh lsデプロイコマンド
bundle exec cap production deployAWSアクセスキー、キーID確認
env | grep AWS_SECRET_ACCESS_KEY env | grep AWS_ACCESS_KEY_IDMySQL
ターミナル(本番環境)状況確認 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/7b20fcae6ef13820d01funicorn
ターミナル(本番環境)状況確認 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.lognginx
ターミナル(本番環境)再起動 sudo service nginx restart ログ ホームディレクトリで sudo less /var/log/nginx/error.logcapistrano
ターミナル(本番環境)ログ less log/capistrano.logvim
ローカル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.rbconfig.consider_all_requests_local = true ← デフォルトはfalsehttps://qiita.com/keitah/items/2aa2ac968c76260e8750
デプロイ作業でよく使うコマンドを備忘録として整理しました。
今後気がついたらちょいちょい追記していきます。
- 投稿日:2020-02-28T18:27:20+09:00
deviseの関連ファイルが作られず、コマンドが適用されなくなった時の対処法
deviseをインストールしていると
config/initializers/devise.rb
と
config/locales/devise.en.yml
ファイルが生成されておらず、、さらにrails g devise user
やrails 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 s
やrails g devise user
やrails 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週間の初心者なので、その目線でわかりやすく書くことを心がけました。
わかりにくい記述、間違っているところがあればご指摘ください。
最後まで見ていただきありがとうございました。
- 投稿日:2020-02-28T15:27:06+09:00
【Rails】入力された値を元にGETリクエストを送ってjsonを取得し、DBに保存する
すること
今回はGET statuses/oembed — Twitter Developersを用いてツイッターカードを表示します。
流れは以下の通りです。
- 入力されたURLからIDを取得
- リクエストを送信
- データを取得
- URL, jsonをデータベースに保存
- 保存したデータを表示
開発環境
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.rbclass 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
create
とupdate
の違いは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) endNet::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) endRuby の 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'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>— 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' %>リンク
例外処理など細かく行う場合
- 投稿日:2020-02-28T13:12:23+09:00
【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.lockomniauth-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.lockomniauth-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-oauth2
は1.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.gemspecgem.add_runtime_dependency 'omniauth-oauth2', '>= 1.6'とバージョン1.6以上が指定されています。
v0.7.0
を指定して見てみると、omniauth-google-oauth2.gemspecgem.add_runtime_dependency 'omniauth-oauth2', '>= 1.5'バージョンは1.5以上になってます。
'rspotify'
でも同じように'omniauth-oauth2’
のバージョン指定を確認していきました。
最新のv2.8.0
で見てみると、rspotify.gemspecspec.add_dependency 'omniauth-oauth2', '~> 1.5.0'バージョン指定は1.5.0以上1.6.0未満となってます。
つまり、
v0.7.0
とv2.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が勝手にコンフリクトのないようにバージョンをインストールしてくれるみたいですが、
今回はうまくできなかったので、自分で指定してからインストールすることになりました。
- 投稿日:2020-02-28T12:32:43+09:00
rubyXLでexcelデータを出力する際に悩んだこと。
備忘録な意味合いで残します。
1. テンプレートとするExcelで完全にブランクなセルに値をセットしようとするとエラーになる。
・罫線などの情報を一切もっていないセルには、
file[sheet_name][row][col].change_contents(value)
こういう指定ができない。
もしこれで、nil:NilClassのエラーがでるようなら、該当のセルにスペースを一度いれておく。とかで取得できるみたい。2. insert_rowで値を挿入すると、その下のセル結合などが壊れる。
・仕様のようなので仕方がないが、UIで操作する単純な行の挿入のイメージとは処理が違う。挿入する行の前後は、セル結合などは極力してない方がベスト。
- 投稿日:2020-02-28T11:58:50+09:00
enumのRSpecの書き方
はじめに
enumのテストコードを書いたので、備忘として残します。
shoulda-matchers
を使用しています。ソースコード
app/models/hoge.rbclass Hoge < ApplicationRecord enum status: { hoge: 0, huga: 1 } endspec/models/hoge_spec.rbRSpec.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-matchers
のdefine_enum_for
を使うと上のように書けます。
with_values
ではなく、with
でもテストは通りますが怒られます。簡単ですね!
参考
- 投稿日:2020-02-28T09:50:28+09:00
Devise のviewを編集可能にする
はじめに
導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。Deviseを導入
実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sassDevise 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:views1.に書かれている通りに
config/environments/development.rb
にURLオプションを記載する。config/environments/development.rbconfig.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:migrateviewの作成
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.rbconfig.scoped_views = true終わりに
deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。
- 投稿日:2020-02-28T09:50:28+09:00
Devise viewのデザインを編集可能にする
はじめに
導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。Deviseを導入
実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sassDevise 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:views1.に書かれている通りに
config/environments/development.rb
にURLオプションを記載する。config/environments/development.rbconfig.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:migrateviewの作成
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.rbconfig.scoped_views = true終わりに
deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。
- 投稿日:2020-02-28T09:25:20+09:00
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 endin句に関して
https://www.sejuku.net/blog/72497
僕の記事より圧倒的にわかりやすい記事を書いている中で発見しました
- 投稿日:2020-02-28T07:46:59+09:00
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は後々使用します。
今回、利用するAPIは次の2つです。
- Maps JavaScript API
- Geocoding API
この2つを有効に設定しておいてください。
gemのインストール
必要なgemをインストールします。
Gemfilegem "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を入れています。.envGOOGLE_MAP_KEY = "あなたが取得したkeyを記述してください"underscore.jsを作成
その後、app/assets/javascripts下にunderscore.jsを作成してください。作成したファイルに次のリンク先のコードをコピペして貼り付けるらしい。
application.jsで読み込み設定
application.jsに次の記述を追記します。
application.js//= require underscore //= require gmaps/googlemodelを編集
modelを次のように編集していきます。
review.rbclass Review < ApplicationRecord has_one :spot, dependent: :destroy accepts_nested_attributes_for :spot endspot.rbclass Spot < ApplicationRecord belongs_to :review geocoded_by :address after_validation :geocode endviewを編集
投稿ページを作成します。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.rbdef 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] ) endshowアクションで記述している
@lat = @review.spot.latitude
@lng = @review.spot.longitude
gon.lat = @lat
gon.lng = @lngでは、controllerで定義した
@lat
と@lng
の変数をJavaScriptでも扱えるように、それぞれgon.lat
とgon.lng
に代入しています。JavaScriptの編集
いよいよJavaScriptでGoogleMapを表示させていきます。
googlemap.jslet 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を表示させる方法
- 投稿日:2020-02-28T07:17:57+09:00
ネストしたindexにYoutube動画埋め込み
ネストしたindexへの動画埋め込み備忘録
https://qiita.com/Kairi_Yasunnde/items/8e931a4670549ba8237e
上記の記事を参考にしたが、自分はgroupテーブルにYoutube_urlを入れてcreateに定義し、groupでネストしたmessages/indexに埋め込もうとして、コントローラーの差異があり出来なかったと推測する。routes.rbresources :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.rbprivate def group_params params.require(:group).permit(:name, :image, :content, :youtube_url, user_ids: []) endcreateまたは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の動画が埋め込まれる
- 投稿日:2020-02-28T06:07:20+09:00
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.rbclass Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } #...略 end本演習で出てくる図 14.22の
user.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'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的に意味がある文字(
<
>
&
"
'
)は、脆弱性につながることを防ぐために自動でエスケープされます。'
もエスケープの対象なので、'
と出力されているのです。というわけで、
response.body
に含まれるのは「HTML的に意味がある文字がエスケープされた文字列」となります。assert_match
による検索文字列もまた、HTML的に意味がある文字をエスケープしなければ、正しい結果を得ることができません。「期待されるHTMLをCGI.escapeHTML
メソッドでエスケープする理由」としては以上です。
CGI.escapeHTML
というのは、「引数として与えた文字列から、HTML的に意味がある文字をエスケープする」というメソッドです。まさに今回のユースケースで必要となる動作ですね。
- 投稿日:2020-02-28T06:02:36+09:00
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#feeddef 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上記のコードは、最終的に以下のような動作をすることになります。
following_ids
メソッドにより、現在フォローしている全てのユーザーを得るためにRDBに問い合わせを行う- 全ての
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#feeddef 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#feeddef 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ページにフィードが表示されている様子は以下のようになります。
本番環境において、Homeページにフィードが表示されている様子は以下のようになります。
- 投稿日:2020-02-28T05:56:41+09:00
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#feeddef 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#feeddef 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#feeddef 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行目です。ということは、想定通りの形でテストが落ちたと言えそうです。
- 投稿日:2020-02-28T05:56:41+09:00
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#feeddef 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#feeddef 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#feeddef 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行目です。ということは、想定通りの形でテストが落ちたと言えそうです。
- 投稿日:2020-02-28T05:52:38+09:00
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.rbclass 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
- ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
- フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
- フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと
以上の機能が実現されている、ということになりますね!
当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。 ↩
- 投稿日:2020-02-28T05:52:38+09:00
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.rbclass 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
- ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
- フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
- フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと
以上の機能が実現されている、ということになりますね!
当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。 ↩
- 投稿日:2020-02-28T05:48:33+09:00
Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード実装の動機と計画(データモデルの解説とテストの実装)
ステータスフィードのデータモデルの基本
例えば、「id 1のユーザーが、id 2、7、8、10のユーザーをフォローしている」という場合、フィードのデータモデルは、
microposts
テーブルのサンプルデータとセットで示すと以下のようなイメージになります。上図の矢印は、「現在のユーザー自身、および現在のユーザーがフォローしているユーザーに対応するユーザーidを持つマイクロポストを取り出す」という操作を表しています。
ステータスフィードの要求仕様と、それに対するテスト
ステータスフィードの要求仕様
どのように実装するかはさておき、この時点では、ステータスフィードの要求仕様として以下を示すことができます。
- ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
- フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
- フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと
上記要求仕様をアサーションとして展開し、テストを実装していきましょう。
「ステータスフィードを表示する」という機能の実装箇所
「ステータスフィードを表示する」という機能は、Userモデルに
feed
メソッドとして実装したのでしたね。以下に現時点におけるapp/models/user.rb#feed
の内容を示しておきます。app/models/user.rb#feedclass User < ApplicationRecord has_many :microposts, dependent: :destroy # ...略 def feed Micropost.where("user_id = ?", id) end #...略 endステータスフィードのテストの実装
前提条件として、以下のfixtureが存在するものとします。
test/fixtures/relationships.ymlone: 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.rbrequire '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「ログインユーザー自身とは別の、フォローしているユーザーのマイクロポストがフィードに表示されていない」という理由でテストが落ちている、ということになりますね。