20211012のRubyに関する記事は14件です。

Ruby(Open-URI)とNokogiriでスクレイピングした際に出会ったエラー対応(404エラーのハンドリング)

はじめに Ruby(Rails)でWebスクレイピングをしたとき URLが404 Not Foundになり指定したURLのリソースが見つからず、以下のエラーを出力して止まってしまって困りました。 OpenURI::HTTPError 今回はこのエラーハンドリングの話をします。 ちなみに、Webスクレイピングで用いたライブラリやGemは、OpenURIとNokogiriです。 用語の説明 OpenURI httpやftpを通じてネットワーク上のリソースを取得することができます。こちらはRuby標準のライブラリです こちらだけでもスクレイピングは行えます Nokogiri こちらも、Webスクレイピングをサポートしてくれるやつです。こちらは、HTMLの情報を全て取得したい場合に楽に扱えます 特定のDOMを取得する際の 僕はよく使うsearch など、スクレイピングをする上でひつようなメソッドがたくさんあるので、よく用いています 現象 ざっくり、以下のようなコード書いていたら require 'open-uri' require 'nokogiri' url = 'https://www.test.jp/' # open-uriでurlにアクセスしてhtmlを取得し、Nokogiriでパースする doc = Nokogiri::HTML(open(url), nil, 'utf-8') 特定のページで404が発生し、エラーが発生し、それ以上スクレイピングできなくなりました。 対策 困るので、エラーハンドリングしつつ、スクレイピングを続けられるようにしましょう。 require 'open-uri' require 'nokogiri' url = 'https://www.test.jp/' begin # open-uriでurlにアクセスしてhtmlを取得し、Nokogiriでパースする doc = Nokogiri::HTML(open(url), nil, 'utf-8') rescue OpenURI::HTTPError => e # 例外処理 end これでハンドリングできました。 さいごに 当たり前ですが、このスクリプトを間髪を入れずに連続して実行すると対象となるサーバへ連続的にアクセスが行われてしまうので絶対にしないようにしてください。 本日は、以上です。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

N+1問題を回避してRubyでマスキング処理を実装

ある案件でRubyでマスキング処理を実装しました。本記事はその記録となります。 今回実装したのは、usersテーブルのusernameカラムとemailカラムの各フィールドをダミーデータに置き換えるというものです。イメージとしては、下の図のような感じです。 id username email 1 fukuzawa yukichi fukuzawa@gmail.com 2 kimura takuya kimura@icloud.com 3 kitano takeshi kitano@gmail.com 4 suzuki ichiro suzuki@live.com 5 kataoka tsurutaro kataoka@yahoo.co.jp            ⬇️ id username email 1 dummy_username_1 dummy_email_1@gmail.com 2 dummy_username_2 dummy_email_2@icloud.com 3 dummy_username_3 dummy_email_3@gmail.com 4 dummy_username_4 dummy_email_4@live.com 5 dummy_username_5 dummy_email_5@yahoo.co.jp 第一感では簡単に実装できそうに思いました。しかし、N+1問題という落とし穴があり、それに嵌まらないように今回のプログラムの作成では努める必要がありました。 そもそもN+1問題とは? N + 1問題とは、本来なら1回のクエリ実行で済む処理であるにもかかわらず、無駄にN回ものクエリをループ処理の中で実行してしまい、処理速度が低下してしまうことをいいます(少なくとも私はそう理解しています)。 なぜ処理速度が低下するのかと言うと、仮に処理するレコードの数が一定だとすると、SQLの実行回数の多さに比例して処理時間が掛かるからです。そのため、「100レコードを1回で処理する時間」と「1レコードずつ100回処理する時間」は等しくなく、前者の方が短くなります。 具体的に言うと、SQL文のパースや実行計画生成/評価といったオーバーヘッドの時間が余分に掛かるのです。特にSQL文のパースは、SQL文が発行されデータベースがこれを受理するたびに実行され、1回当たり0.1〜1秒も掛かってしまいます。他のオーバーヘッドならミリ秒単位しか掛からないことを踏まえると、処理速度への影響が非常に大きい要素であると言えます。 要するに、簡単に実装できるからと言って、ループの中で何度もSQLを発行するコーディングにするのはご法度なわけです。 どう対処したか usersテーブルのusernameとemailにマスキング処理をかける場合、①SQLでテーブルからデータを取ってくる、②usernameとemailをダミーデータに置き換える、③SQLでテーブルを更新する、という手続きをコードに落とし込むことでマスキング処理は実現できます。 今回作成したプログラムでは、①と③の2回にとどめてSQLを実行する設計にしました。また、①と③を実行するためにActiveRecordを導入しています。 ②では、①で取得したuserテーブルデータ(カラム名がキーでフィールドの内容がバリューとなったハッシュが配列になっている)に対してmapメソッドを実行し、ダミーデータと置き換わった新しい配列を作成しています。mapで新しい配列を作ったのは、既製の変数の内容を変更するという副作用を避けるためです。 もちろん②については、一意制約を回避するために(usernameとemailの両カラムには一意制約がかかっています)、配列のインデックスをダミーデータの末尾に記述するようにしました。with_indexに引数として1を与えることで、idと同じ数字が記載されるようにしています。 ③に関しては、一括での更新(バルクアップサート)を実現するためにactiverecord-importというgemを導入しました。 実装したコードは以下になります。 masking.rb require "activerecord-import" require_relative "./active_record" require_relative "./user" require_relative "./mask_processing" user_list = User.all masked_users = MaskProcessing.masking_user(user_list) User.import masked_users, on_duplicate_key_update: %i[username email], validate: true mask_processing.rb class MaskProcessing def self.dummy_user(user, id) user["username"] = "dummy_username_#{id}" email_domain = user["email"].split("@")[1] user["email"] = "dummy_email_#{id}@#{email_domain}" user end def self.masking_user(records) records.map.with_index(1) do |record, index| dummy_user(record, index) end end private_class_method :dummy_user end その他のファイルは以下になります。 余談ですが、ソースコードをgithubに上げるに当たり、環境変数を管理するためのgemであるdotenvを導入しました。これを使えば、環境変数を定義した.envファイルをルート配下に設置するだけで、定義した環境変数が自動で読み込まれるようになります。 なお、環境変数を利用する場合、環境変数を<%= ENV['DB_HOST'] %>のように<%= %>で囲む必要があるので、RubyファイルでもERB記法を使えるように工夫しております。 active_record.rb require "dotenv" require "mysql2" require "active_record" require "yaml" require "erb" Dotenv.load yaml_file = File.read("./config/database.yml") config = YAML.safe_load(ERB.new(yaml_file).result) ActiveRecord::Base.establish_connection(config["db"]) user.rb class User < ActiveRecord::Base self.table_name = "users" validates_presence_of :username validates_presence_of :email end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動的と静的を学ぶ!

Webで様々なページを見たり、サービスを利用したりすることがあります! そこには、静的と動的という分類があります! それぞれの特徴を書いて行こうと思います! ①.動的 動的とは、Twitterのように、常に最新のツイートを取得し表示する仕組みや、ユーザーごとで表示されるものが変わるような仕組みです! そのような仕組みのサービスは、動的サイトや動的コンテンツと呼ばれます! Webアプリケーションは、動的コンテンツであることがほとんどです! ②.静的 静的とは、誰がいつ見ても、常に同じ内容が表示されるような仕組みです! そのような仕組みのサービスは、静的サイトや静的コンテンツと呼ばれます! HTMLとCSSで作成したWebページは、静的コンテンツです! ③.まとめ どちらも簡単に言うと、 動的コンテンツとは、ユーザーごとで表示されるものが切り替わるような仕組みのこと! 静的コンテンツとは、誰がいつ見ても、常に同じ内容が表示されるような仕組みのこと! こんな感じですね! 基本的な事なので、これはしっかり把握しておこうと思います! なにか説明が間違っていたら教えてください(_ _)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSL2(Ubuntu)環境下でのDockerを使ったRailsの開発環境構築

概要 初投稿です。 WSL2(Ubuntu)の環境下でDockerを使ったRailsの開発環境の記事が無くて、構築が面倒だったので作りました。 作成する開発環境 Ruby 3.0.2 Rails 6.1.4 MySQL 8.0 ソースコード ソースコードは下記のとおりです。 docker-rails.sh 使い方 前提条件 WSL2(Ubuntu) Docker version 20.10.8以降 Docker Compose v2.0.0-rc.1以降 shellの実行 ディレクトリを作成しdocker-rails.shを配置します。 mkdir sample_app mv docker-rails.sh ./sample_app/ 作成したディレクトリへ移動し、docker-rails.shを実行します。 cd sample_app bash docker-rails.sh -n {project_name} -p {mysql_password} {project_name}は作成するRailsプロジェクトの名前になります。 {mysql_password}は作成するMySQLのrootパスワードになります。 ディレクトリ構成 shellを実行すると下記のようなディレクトリ構成が作成されます。 sample_app ──| ├── README.md ├── docker-compose.yml ├── docker-rails.sh ├── mysql │ ├── Dockerfile | ├── .dockerignore │ ├── data │ ├── docker-entrypoint-initdb.d │ └── mysql-confd ├── rails │ ├── Dockerfile | ├── .dockerignore │ └── entrypoint.sh └── {project_name} ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── babel.config.js ├── bin ├── config ├── config.ru ├── db ├── lib ├── log ├── node_modules ├── package.json ├── postcss.config.js ├── public ├── storage ├── test ├── tmp ├── vendor └── yarn.lock 個人的なこだわりですが、Docker関連のファイルをプロジェクトのソースコードと 同じディレクトリに置きたくないので分けました。 また、rootもあまり使いたくないので、可能な限り使わない形にしました。 解説 shellで何をやっているか学びたい人向けに解説していきます。 shellの作成方針 Mac環境だとDockerの権限などを自動で上手くやってくれますが、 WSL2(Ubuntu)やLinux環境はDocker Deamonが全てrootで動くため権限で引っかかったりします。 このような場合、Rootless Dockerを使うといいのですがWSL2には対応していませんでした。 権限設定など手動で開発環境を構築するのが面倒だったのもあったので、自動構築するshellを作成しました。 1.Rails環境用entrypoint.sh,Gemfile,Dockerfileの作成 DockerにRails環境を作成するにあたり、下記のファイルを作成します entrypoint.sh Gemfile & Gemfile.lock Dockerfile entrypoint.sh gosuを使ってroot以外の任意のユーザーでRailsを実行するshellファイルです。 今回は「rails」というユーザーを作成しています。ユーザーIDはLinuxデフォルトの1000を設定しています。 cat <<'EOF' > entrypoint.sh #!/bin/bash # Railsの仕様でDocker上だと余分なserver.pidが残り続けてエラーになるため、 # Railsを実行するたびに削除する rm -f /$project_name/tmp/pids/server.pid # デフォルトUID&GIDを設定 USER_ID=${LOCAL_UID:-1000} GROUP_ID=${LOCAL_GID:-1000} # Railsを実行するユーザーを作成 groupadd -r --gid $GROUP_ID rails useradd -u $USER_ID -o -m -g $GROUP_ID -G sudo rails export HOME=/home/rails # ユーザーrailsに/usr/srcへの権限を付与 chown -R rails:rails /usr/src # Railsはsudo以上の権限でないとインストールできないため、 # 作成したユーザーrailsにsudo権限の付与 echo "rails ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # 作成したユーザーrailsでDockerを実行 exec /usr/sbin/gosu rails "$@" EOF こちらはほぼDocker公式のRails Quickstartのとおりです。 Gemfile & Gemfile.lock RailsをインストールするためのGemfileです。 Rubyがこれらを参照してRailsをインストールします。 cat <<EOF > Gemfile source 'https://rubygems.org' gem 'rails', "~>6.1.4" EOF Gemfile.lockは空で作成します。 こちらもほぼDocker公式のRails Quickstartのとおりです。 Dockerfile Rails環境のDockerfileです。 こちらもほぼDocker公式のRails Quickstartのとおりです。 cat <<EOF > Dockerfile FROM ruby:3.0.2 ENV LANG C.UTF-8 ENV TZ Asia/Tokyo WORKDIR $app_root # gosuなど必要なライブラリのインストール RUN set -ex && \ apt-get update -qq && \ apt-get install -y sudo && \ : "Install node.js" && \ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - && \ apt-get update -qq && \ apt-get install -y nodejs && \ : "Install yarn" && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \ apt-get update -qq && \ apt-get install -y yarn && \ apt-get -y install gosu # ローカルのGemfileおよびGemfile.lockをDockerコンテナにコピー COPY ./$project_name/Gemfile $app_root/Gemfile COPY ./$project_name/Gemfile.lock $app_root/Gemfile.lock # 上記のGemfileを元にDockerコンテナへRailsをインストール RUN bundle install # ローカルのentrypoint.shをDockerコンテナへコピーし、 # Dockerコンテナ上で実行できるように権限を付与。 # DcokerコンテナのENTRYPOINTで実行するように設定。 COPY ./rails/entrypoint.sh /usr/bin/entrypoint.sh RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["/usr/bin/entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] EOF 2.MySQL用のlocale.gen,my.cnf,init_mysql.sql,Dockerfileを作成 DockerにMySQL環境を作成するにあたり、下記のファイルを作成します locale.gen my.cnf init_mysql.sql Dockerfile locale.gen,my.cnfの作成 locale.gen cat <<EOF > locale.gen ja_JP.UTF-8 UTF-8 EOF my.cnf cat <<EOF > my.cnf [mysqld] default_authentication_plugin=mysql_native_password character-set-server=utf8mb4 collation-server=utf8mb4_general_ci [client] default-character-set=utf8mb4 EOF locale.genとmy.cnfはmysqlの設定ファイルです。 init_mysql.sqlの作成 init_mysql.sql cat <<EOF > init_mysql.sql # ユーザーが存在しない場合のみ、新規ユーザーを作成するsql SET @sql_found='SELECT 1 INTO @x'; SET @sql_fresh='GRANT ALL ON *.* TO "$project_name"@''%'''; SELECT COUNT(1) INTO @found_count FROM mysql.user WHERE user='"$project_name"' AND host='%'; SET @sql=IF(@found_count=1,@sql_found,@sql_fresh); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; EOF init_mysql.sqlはmysqlで最初に実行されるsqlです。 これでmysqlユーザーを作成します。 Dockerfileの作成 cat <<EOF > Dockerfile FROM mysql:8.0 # ユーザーmysqlのUID,GIDをLinuxのUID,GIDと合わせる RUN groupmod -g 1000 mysql && usermod -u 1000 -g 1000 mysql # ユーザーrailsにsudo権限を付与します。 # ユーザーrailsにsudo権限を付与していないと、rails db:createコマンドでエラーになります。 RUN echo "rails ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers COPY ./mysql/mysql-confd/locale.gen /etc/locale.gen RUN sed -i 's@archive.ubuntu.com@ftp.jaist.ac.jp/pub/Linux@g' /etc/apt/sources.list RUN set -ex && \ apt-get update -qq && \ : "Install locales" && \ apt-get install -y --no-install-recommends locales && \ : "Cleaning..." && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ locale-gen ja_JP.UTF-8 ENV LC_ALL ja_JP.UTF-8 COPY ./mysql/mysql-confd/my.cnf /etc/mysql/conf.d/my.cnf COPY ./mysql/docker-entrypoint-initdb.d/init_mysql.sql /docker-entrypoint-initdb.d/init_mysql.sql EOF mysqlのDockerファイルです。 MySQL Daemonもrootユーザーで動作するので、 Dockerファイルでsudo権限を付与します。 プロジェクトの作成&docker-composeの起動 Dockerコンテナでrails newを実行し、プロジェクトを作成します。 その後、docker-compose buildを行うことでbundle installが実行されます。 docker-compose run --no-deps web rails new . --force --database=mysql docker-compose build --no-cache 続いて、下記のコマンドでrailsプロジェクトのdatabase.ymlの内容を書き換えます。 docker-compose run web /bin/sh -c "sed -ie '0,/password:/ s/password:/password: <%= ENV.fetch('\"'MYSQL_PASSWORD'\"') { '\"'$root_password'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/host: localhost/host: <%= ENV.fetch('\"'MYSQL_HOST'\"') { '\"'db'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/username: root/username: <%= ENV.fetch('\"'MYSQL_USER'\"') { '\"'$project_name'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/username: $project_name/username: <%= ENV.fetch('\"'MYSQL_USER'\"') { '\"'$project_name'\"' } %>/g' ./config/database.yml" その後、db:createを実行DBを作成します。 docker-compose run web rails db:create 最後にdocker-compose buildをもう一度行なったあとに コンテナを起動すると、完了です。 docker-compose build --no-cache docker-compose up -d 以上が完了後、localhost:3000へアクセスすると、RailsのTOP画面へ遷移します。 参考 参考にした記事は下記のとおりです。作者様からは許可をいただいております。 丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSL2(Ubuntu)環境下でのDockerを使ったRailsの開発環境構築するshellを作った

概要 初投稿です。 WSL2(Ubuntu)の環境下でDockerを使ったRailsの開発環境の記事が無くて、構築が面倒だったのでshellを作りました。 作成する開発環境 Ruby 3.0.2 Rails 6.1.4 MySQL 8.0 ソースコード ソースコードは下記のとおりです。 docker-rails.sh 使い方 前提条件 WSL2(Ubuntu) Docker version 20.10.8以降 Docker Compose v2.0.0-rc.1以降 shellの実行 ディレクトリを作成しdocker-rails.shを配置します。 mkdir sample_app mv docker-rails.sh ./sample_app/ 作成したディレクトリへ移動し、docker-rails.shを実行します。 cd sample_app bash docker-rails.sh -n {project_name} -p {mysql_password} {project_name}は作成するRailsプロジェクトの名前になります。 {mysql_password}は作成するMySQLのrootパスワードになります。 ディレクトリ構成 shellを実行すると下記のようなディレクトリ構成が作成されます。 sample_app ──| ├── README.md ├── docker-compose.yml ├── docker-rails.sh ├── mysql │ ├── Dockerfile | ├── .dockerignore │ ├── data │ ├── docker-entrypoint-initdb.d │ └── mysql-confd ├── rails │ ├── Dockerfile | ├── .dockerignore │ └── entrypoint.sh └── {project_name} ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── babel.config.js ├── bin ├── config ├── config.ru ├── db ├── lib ├── log ├── node_modules ├── package.json ├── postcss.config.js ├── public ├── storage ├── test ├── tmp ├── vendor └── yarn.lock 個人的なこだわりですが、Docker関連のファイルをプロジェクトのソースコードと 同じディレクトリに置きたくないので分けました。 また、rootもあまり使いたくないので、可能な限り使わない形にしました。 解説 shellで何をやっているか学びたい人向けに解説していきます。 shellの作成方針 Mac環境だとDockerの権限などを自動で上手くやってくれますが、 WSL2(Ubuntu)やLinux環境はDocker Deamonが全てrootで動くため権限で引っかかったりします。 このような場合、Rootless Dockerを使うといいのですがWSL2には対応していませんでした。 権限設定など手動で開発環境を構築するのが面倒だったのもあったので、自動構築するshellを作成しました。 1.Rails環境用entrypoint.sh,Gemfile,Dockerfileの作成 DockerにRails環境を作成するにあたり、下記のファイルを作成します entrypoint.sh Gemfile & Gemfile.lock Dockerfile entrypoint.sh gosuを使ってroot以外の任意のユーザーでRailsを実行するshellファイルです。 今回は「rails」というユーザーを作成しています。ユーザーIDはLinuxデフォルトの1000を設定しています。 cat <<'EOF' > entrypoint.sh #!/bin/bash # Railsの仕様でDocker上だと余分なserver.pidが残り続けてエラーになるため、 # Railsを実行するたびに削除する rm -f /$project_name/tmp/pids/server.pid # デフォルトUID&GIDを設定 USER_ID=${LOCAL_UID:-1000} GROUP_ID=${LOCAL_GID:-1000} # Railsを実行するユーザーを作成 groupadd -r --gid $GROUP_ID rails useradd -u $USER_ID -o -m -g $GROUP_ID -G sudo rails export HOME=/home/rails # ユーザーrailsに/usr/srcへの権限を付与 chown -R rails:rails /usr/src # Railsはsudo以上の権限でないとインストールできないため、 # 作成したユーザーrailsにsudo権限の付与 echo "rails ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # 作成したユーザーrailsでDockerを実行 exec /usr/sbin/gosu rails "$@" EOF こちらはほぼDocker公式のRails Quickstartのとおりです。 Gemfile & Gemfile.lock RailsをインストールするためのGemfileです。 Rubyがこれらを参照してRailsをインストールします。 cat <<EOF > Gemfile source 'https://rubygems.org' gem 'rails', "~>6.1.4" EOF Gemfile.lockは空で作成します。 こちらもほぼDocker公式のRails Quickstartのとおりです。 Dockerfile Rails環境のDockerfileです。 こちらもほぼDocker公式のRails Quickstartのとおりです。 cat <<EOF > Dockerfile FROM ruby:3.0.2 ENV LANG C.UTF-8 ENV TZ Asia/Tokyo WORKDIR $app_root # gosuなど必要なライブラリのインストール RUN set -ex && \ apt-get update -qq && \ apt-get install -y sudo && \ : "Install node.js" && \ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - && \ apt-get update -qq && \ apt-get install -y nodejs && \ : "Install yarn" && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \ apt-get update -qq && \ apt-get install -y yarn && \ apt-get -y install gosu # ローカルのGemfileおよびGemfile.lockをDockerコンテナにコピー COPY ./$project_name/Gemfile $app_root/Gemfile COPY ./$project_name/Gemfile.lock $app_root/Gemfile.lock # 上記のGemfileを元にDockerコンテナへRailsをインストール RUN bundle install # ローカルのentrypoint.shをDockerコンテナへコピーし、 # Dockerコンテナ上で実行できるように権限を付与。 # DcokerコンテナのENTRYPOINTで実行するように設定。 COPY ./rails/entrypoint.sh /usr/bin/entrypoint.sh RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["/usr/bin/entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] EOF 2.MySQL用のlocale.gen,my.cnf,init_mysql.sql,Dockerfileを作成 DockerにMySQL環境を作成するにあたり、下記のファイルを作成します locale.gen my.cnf init_mysql.sql Dockerfile locale.gen,my.cnfの作成 locale.gen cat <<EOF > locale.gen ja_JP.UTF-8 UTF-8 EOF my.cnf cat <<EOF > my.cnf [mysqld] default_authentication_plugin=mysql_native_password character-set-server=utf8mb4 collation-server=utf8mb4_general_ci [client] default-character-set=utf8mb4 EOF locale.genとmy.cnfはmysqlの設定ファイルです。 init_mysql.sqlの作成 init_mysql.sql cat <<EOF > init_mysql.sql # ユーザーが存在しない場合のみ、新規ユーザーを作成するsql SET @sql_found='SELECT 1 INTO @x'; SET @sql_fresh='GRANT ALL ON *.* TO "$project_name"@''%'''; SELECT COUNT(1) INTO @found_count FROM mysql.user WHERE user='"$project_name"' AND host='%'; SET @sql=IF(@found_count=1,@sql_found,@sql_fresh); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; EOF init_mysql.sqlはmysqlで最初に実行されるsqlです。 これでmysqlユーザーを作成します。 Dockerfileの作成 cat <<EOF > Dockerfile FROM mysql:8.0 # ユーザーmysqlのUID,GIDをLinuxのUID,GIDと合わせる RUN groupmod -g 1000 mysql && usermod -u 1000 -g 1000 mysql # ユーザーrailsにsudo権限を付与します。 # ユーザーrailsにsudo権限を付与していないと、rails db:createコマンドでエラーになります。 RUN echo "rails ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers COPY ./mysql/mysql-confd/locale.gen /etc/locale.gen RUN sed -i 's@archive.ubuntu.com@ftp.jaist.ac.jp/pub/Linux@g' /etc/apt/sources.list RUN set -ex && \ apt-get update -qq && \ : "Install locales" && \ apt-get install -y --no-install-recommends locales && \ : "Cleaning..." && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ locale-gen ja_JP.UTF-8 ENV LC_ALL ja_JP.UTF-8 COPY ./mysql/mysql-confd/my.cnf /etc/mysql/conf.d/my.cnf COPY ./mysql/docker-entrypoint-initdb.d/init_mysql.sql /docker-entrypoint-initdb.d/init_mysql.sql EOF mysqlのDockerファイルです。 MySQL Daemonもrootユーザーで動作するので、 Dockerファイルでsudo権限を付与します。 3.プロジェクトの作成&docker-composeの起動 Dockerコンテナでrails newを実行し、プロジェクトを作成します。 その後、docker-compose buildを行うことでbundle installが実行されます。 docker-compose run --no-deps web rails new . --force --database=mysql docker-compose build --no-cache 続いて、下記のコマンドでrailsプロジェクトのdatabase.ymlの内容を書き換えます。 docker-compose run web /bin/sh -c "sed -ie '0,/password:/ s/password:/password: <%= ENV.fetch('\"'MYSQL_PASSWORD'\"') { '\"'$root_password'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/host: localhost/host: <%= ENV.fetch('\"'MYSQL_HOST'\"') { '\"'db'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/username: root/username: <%= ENV.fetch('\"'MYSQL_USER'\"') { '\"'$project_name'\"' } %>/g' ./config/database.yml" docker-compose run web /bin/sh -c "sed -ie 's/username: $project_name/username: <%= ENV.fetch('\"'MYSQL_USER'\"') { '\"'$project_name'\"' } %>/g' ./config/database.yml" その後、db:createを実行DBを作成します。 docker-compose run web rails db:create 最後にdocker-compose buildをもう一度行なったあとに コンテナを起動すると、完了です。 docker-compose build --no-cache docker-compose up -d 以上が完了後、localhost:3000へアクセスすると、RailsのTOP画面へ遷移します。 参考 参考にした記事は下記のとおりです。作者様からは許可をいただいております。 丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バリデーションメモ

初めてユニークネスを使ったのでメモ uniqueness 必ず一意であること。 など、同じ情報を保存しないようにする 例)メールアドレスや電話番号など 今回使った状況としては、Userは投稿を追加することができ、さらにその中で投稿に紐づく商品の情報を追加することができるようにしてるが同じ商品は追加できないようにしていた。 Postに関連する商品としていたのでPostProductモデルとする モデルでバリデーションを仕込む class PostProduct < ApplicationRecord belongs_to :user belongs_to :post validates :title, presence: true, uniqueness: true validates :image, presence: true, uniqueness: true validates :product_url, presence: true, uniqueness: true end 今かけているバリデーションは、 titleは空にはできず、同じ値も認めないこと imageは空にはできず、同じ値も認めないこと product_urlは空にはできず、同じ値も認めないこと みたいなことを書いています。 同じ処理を書いているのでまとめちゃいましょう! class PostProduct < ApplicationRecord belongs_to :user belongs_to :post with_options presence: true, uniqueness: true do validates :title validates :image validates :product_url end end これでスッキリしましたね! 実際に動くかやってみたところ、ちゃんとユニークネスは動いてくれたのですがここで問題が。 このコードだと、emailや電話番号くらいにしか使えません。 これは早いもの勝ちみたいな感じであるuserがこのemailを使用したら他のuserは使えねーよ?みたいな処理なんです。 emailや電話番号では正直問題にはなりません。 ですが他の機能だとちょっとめんどくさくなります。 たとえばtwitterでは1つの投稿に対していろんなuserが「いいね」や「リツイート」をします。 もしそのときに最初の書き方では、投稿には1いいねしかつけられません。 そこでuniquenessでは"scope"を使うことで期待通りの動きをしてくれます。 class PostProduct < ApplicationRecord belongs_to :user belongs_to :post with_options presence: true, uniqueness: {scope: :post_id} do #ここを追加 validates :title validates :image validates :product_url end end こう書くことで、post_idがn番の中では重複した情報を入れることができませんよ!となります。 つまりpost_idが1番と2番では同じ商品を追加することができるということになりました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

仕事で使うREDMINEに関する考察(6)

Redmineの事である。 仕事で使うということで、3か月余り検討してきたのであるが、 一つ課題があることが分かった wiki 現状のRedmineでは、Wikiはすべてプロジェクトに依存している。 しかしながら、Wikiは共有知識であって、そもそもプロジェクトに従属するものではない。 (今のところ、PJTに従属する方が、アクセスしやすいという考えはあるが) そもそも、PJT自体が永続するものでもないし、やがて終了してしまったあとの扱いが困難である。 やりたいこと とりあえずWikiペディアは入り口は一つで十分であり、入り口は出来れば独立させたい。 現状の機能としては、HTML,PDFへの書き出しはあるが、別の場所にコンテンツサイトを構築するまでもない。 できればRedmineの中で、簡単にWIKIペディアを編集したい。 基本的には、階層構造を作って、全体像が見えるようにしたい。 またRedmineを会社、自宅で分けている場合には、やはりファイル形式でインポート、エクスポートしたい。 今後の方針 現在は個人レベルでredmimeを使っているが、そのうち関係者に普及するうえでは、 自分自身が感じている不都合さを、事前につぶしておく必要がある。 余談であるが、これは個人で作っているブログでも同じである。 どこかで階層構造を整理しなければ、全体の見通しが悪くなり、 また記事へのアクセス性が低下するものである。 情報整理のススメ 情報を活用するには、情報をいつでも引き出せるように、整理することが重要だ。 (同様に部屋の整理も同じで、いつでも物を引き出せるようにしなければならない) 今のところファイルのエクスプローラにも、ブラウザのブックマークにも、 整理機能はついておらず、唯一、デスクトップアイコンを整列する機能ぐらいしか、 PCは提供してくれない。 PCやスマホが普及して、すっかり便利になった時代だというのに、 この難解な整理作業から解放されるのは、一体いつの時代になるのであろうか? 参考図書 「超」AI整理法 無限にためて瞬時に引き出す /野口 悠紀雄 (著) https://amzn.to/2YH027P
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カラム数に制限を設ける

モデルに独自のバリデーションを書かずにコントローラー内で処理をする方法を考えてみました。 環境 ruby 2.6.5 rails 5.2 Mac OS 必要な gem 無し テーブルは Post : comment が 1対多になってます。 実装したいこと userの投稿にたとえばコメントがあったとして、そのコメント数を制限する。 みたいな感じの機能です。 大雑把な流れとして userが投稿詳細ページからコメントを追加 ↓ createアクションが動く ↓ 投稿に対するコメント数が条件より少ない場合、そのまま作成され 条件を超えた場合はflashと共に元のページにリダイレクトさせる 何を使うか コメントのカラムには自身が持つ"id"の他に外部キーとして"post_id"を持たせています。 これによって「post_idがn番のコメント」と表現することができます。 今回数えたいのはまさにこのpost_idです。 情報を取得するメソッドはたくさんあるのでそのなかから最適なものを探します。 all find または find_by select where くらいでしょうか。 allはモデルの情報全て持ってくるし、findはidのレコード持ってくるだけだし、find_byも同じく使わないし、selectは指定したカラムの情報を全て返すし、みたいな感じでいろいろ試していったところ 「where」が1番適していました。 各メソッドの書き方 ALL: @posts = Post.all   インスタンス  =  モデル名 FIND: @post = Post.find(id)     #引数には必要なレコードのidを指定 FIND_BY: @post = Post.find_by(email: params[:email]) ←他にも指定できます。     #カラムと実際の値を記載する SELECT: @comment = Post.select(:post_id) #カラムを指定 WHERE: @comments = Post.where(post_id: params[:post_id]) #カラムと実際の値を記載 みてわかる通り、find_byは1件のレコードを返すのに対して、whereは指定された値のカラムを全て返します。 Commentモデル内に10個くらいデータがあったとして、そのうちpost_idが"3"のものがいくつあるか知りたい場合は @comments = Comment.where(post_id: 3) とすることでpost_idが3のものが帰ってきます。 注意としてはここで返ってきた情報に対して、他のカラムを参照したりすることはできないです。 あくまでpost_idが3のものを表示しているだけなので 、 findやfind_byなどはidに対応するレコードを持ってくるので @user = User.find(1) @user.id @user.nickname @user.age こう書いてカラムを呼び出すことができます。 実装 さっき書いた comments = Comment.where(post_id: post_id) これをコントローラーのcreateアクションに書いて def create @comment = Comment.new(comment_params) comments = Comment.where(post_id: params[:post_id]) if comments.count < 10 if @comment.save? flash[:notice] = "成功しました" redirect_to root_path else flash[:alert] = "失敗しました" redirect_to post_path(@post) end else flash[:alert] = "これ以上追加できません" redirect_to post_path(@post) end end こんな感じで10件まで追加できるようになりました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails4.2】Postgresqlの配列リテラルを配列に変換

背景 少々古いプロジェクトでPostgresqlの配列型が使われていた。 ActiveRecord経由であれば取得カラムは自動で配列に変換されるが個別に変換したい場合があった 概要 Rails側で以下の変換を行いたい 半角スペースや改行などの要エスケープ文字がある要素だけダブルクォート囲みされたりしている アポストロフィーエス 's なども少々変換が必要らしい input output {} [] {1,2,3} [1,2,3] {赤,青,緑} ["赤","青","緑"] {"徳川 家康",豊臣秀吉,"織田 信長"} ["徳川 家康","豊臣秀吉","織田 信長"] 実装 モデルのconcernを追加する形にしてみた app/models/concerns/varying_array_parsable.rb module VaryingArrayParsable extend ActiveSupport::Concern class VaryingArrayParser # @see {https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb} # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create # a large amount of Ruby objects that will need to be garbage # collected. pg_array_parser has a C and Java extension begin require 'pg_array_parser' include PgArrayParser rescue LoadError require 'active_record/connection_adapters/postgresql/array_parser' include ActiveRecord::ConnectionAdapters::PostgreSQL::ArrayParser end end module ClassMethods private # @param value [String] varying arrayカラムの値 # @return [Array] 型キャスト後の配列 def cast_to_array(value) @pg_parser ||= VaryingArrayParser.new @pg_parser.parse_pg_array(value) end end end 使用例 app/models/hoge.rb class Hoge < ActiveRecord::Base include VaryingArrayParsable class << self # @param str [String] # @return [Array] def hogehoge(str) cast_to_array(str) end end end まとめ? pgのarray型の文字列→railsの配列 への変換ができるようになった エスケープなどの考慮もされていた postgresqlには array_to_string 関数や unnest 関数があるのでSQL側でこれを使うのもアリだった もっと良い書き方がありそう。ActiveRecordにすでに実装済みのものを真似ているだけなので 参考 rails4.2がActiveRecordでやってる部分 https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb pg_array_parseのgem https://github.com/DavyJonesLocker/pg_array_parser#usage
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyとStimulusを使って勤怠打刻システムを個人開発した話

これはなに? DHHとBasecampが開発しているJavascriptフレームワークStimulusを使ってみて、勤怠打刻システムを個人開発したのでその紹介とStimulusに対する感想です。 Stimulusとは StimulusはHTMLタグのアノテーションに設定された内容に応じてDOMに挙動を実装できるフレームワークです。 https://stimulus.hotwired.dev/ 作ったもの 打刻修正もこんな感じで可能 こだわったポイント タイムライン表示 よくあるテーブルに数字が羅列した画面でなく、このようにタイムラインで表示されたほうがチームメンバーの状況把握をしやすいんじゃないか?ということで、稼働時間をタイムラインで表示する形式にしました。 スクショがありませんが、月間表示・週間表示・メンバー一覧表示などあります。 カレンダーは日本の祝日に対応 カレンダーにはdaterangepickerを使っているのですが、サーバーから返した祝日データと照らし合わせて、祝日なら赤くするような実装になっています。 https://www.daterangepicker.com/ これらのサードパーティライブラリもStimulusでラップしてDOMに振る舞いとして実装しています。 Stimulusの標準のライフサイクルを使えば大体のことは困らなそうかなと思います。 祝日データの生成はholidays Gemを使っています。 https://github.com/holidays/holidays Stimulusの感想 ReactやVueも普段から使うのですが、Railsを使って小規模でサクッと開発するにはとても使いやすいと思います。 ただ他のJavascriptフレームワークと流儀が異なりすぎていて、なかなか広まりにくそうな気がします。 (HTMLのアノテーションにDOMの振る舞いを定義できるので)RailsにおいてはStimulusとヘルパーメソッドとの相性がよく、「viewでヘルパーメソッドに情報を渡したら、その情報がStimulusまで到達して渡した情報をjsで扱える」という状態まで簡単に持っていけるので生産性は高そうです。 コード 実はこれを作ったのは1年ほど前なので、Railsは6系でした。 今なら(まだアルファですが)Rails 7はStimulusが使えるようになるgemがデフォルトで入っていますし、Stimulusも 2.0 3.0が出ているので新しい環境で作りたい。 コードはこちら https://github.com/ryooo/timestamper なお、ログイン機能、組織ごとでのユーザー管理機能、組織管理機能とか、一通り必要そうなものは入っています。 以上、読んでくださりありがとうございました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでArrayを使う方法

RubyでArrayを使う方法です。 #数字を入力します m = gets.to_i #Array配列を定義します c = Array.new(m) m.times { |i| c[i] = gets.chomp } #数字を入力します n = gets.to_i #文字列を入力します s = Array.new(n) n.times { |i| s[i] = gets.chomp } c.each do |d| s.each do |t|   #文字列が含まれているかチェックします if t.include? d puts 'YES' else puts 'NO' end end end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raccでかんたんな自作言語のパーサを書いた

<自作言語処理系の説明用テンプレ> 自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。 小規模: コンパイラ部分は 1,000 行程度 pure Ruby / 標準ライブラリ以外への依存なし 独自VM向けにコンパイルする ライフゲームのために必要な機能だけ 変数宣言、代入、反復、条件分岐、関数呼び出し 演算子: +, *, ==, != のみ(優先順位なし) 型なし(値は整数のみ) Ruby 以外の言語への移植(コンパイラ部分のみ) セルフホスト版(別リポジトリ) 下記も参照してください。 <説明用テンプレおわり> もともとパーサ部分は手書きの再帰下降パーサでしたが、Racc 版を作ってみました。 Racc で四則演算のパーサを作る方法は分かったがもう少しプログラム言語らしきものを扱っているサンプルを見たいとか、パースしたそばから実行する方式(インタプリタ方式)ではなく構文木が欲しいんだけど、という感じの人には参考になるかもしれません(昔の自分に送ってあげたい)。 できたもの vgparser_racc.y を追加したブランチです。 300行弱なので全部貼ってもいいのですが、雰囲気程度ということで途中を省略したものを貼ります。全体は GitHub の方で見てください。 class Parser prechigh left "+" "*" preclow rule program: top_stmts { top_stmts = val[0] result = ["top_stmts", *top_stmts] } top_stmts: top_stmt { top_stmt = val[0] result = [top_stmt] } | top_stmts top_stmt { top_stmts, top_stmt = val result = [*top_stmts, top_stmt] } top_stmt: func_def func_def: "func" IDENT "(" args ")" "{" stmts "}" { _, fn_name, _, args, _, _, stmts, _, = val result = ["func", fn_name, args, stmts] } args: # nothing { result = [] } | arg { arg = val[0] result = [arg] } | args "," arg { args, _, arg = val result = [*args, arg] } arg: IDENT | INT stmts: # nothing { result = [] } | stmt { stmt = val[0] result = [stmt] } | stmts stmt { stmts, stmt = val result = [*stmts, stmt] } stmt: stmt_var | stmt_set | stmt_return | stmt_call | stmt_call_set | stmt_while | stmt_case | stmt_vm_comment | stmt_debug stmt_var: "var" IDENT ";" { _, ident, _ = val result = ["var", ident] } | "var" IDENT "=" expr ";" { _, ident, _, expr = val result = ["var", ident, expr] } # ... 途中省略 ... ---- header require "json" require_relative "common" ---- inner def next_token @tokens.shift end def to_token(line) token = Token.from_line(line) return nil if token.nil? if token.kind == :int Token.new(token.kind, token.value.to_i) else token end end def read_tokens(src) tokens = [] src.each_line do |line| token = to_token(line) next if token.nil? tokens << token end tokens end def to_racc_token(token) kind = case token.kind when :ident then :IDENT when :int then :INT when :str then :STR else token.value end [kind, token.value] end def parse(src) tokens = read_tokens(src) @tokens = tokens.map { |token| to_racc_token(token) } @tokens << [false, false] do_parse() end ---- footer if $0 == __FILE__ ast = Parser.new.parse(ARGF.read) puts JSON.pretty_generate(ast) end 実行の例 入力とするプログラムの例です。 // sample.vg.txt func main() { return 1 + (2 * 3); } 次のように実行するとASTに変換できます。 ## vgparser_racc.rb を生成 $ bundle exec racc -t -o vgparser_racc.rb vgparser_racc.y $ ruby vglexer.rb sample.vg.txt > tmp/tokens.txt $ ruby vgparser_racc.rb tmp/tokens.txt [ "top_stmts", [ "func", "main", [ ], [ [ "return", [ "+", 1, [ "*", 2, 3 ] ] ] ] ] ] スタックの動きを見てみる せっかく Racc を使っているので、適当なサンプルコード(下記)をパースさせてスタックの動きを図にしてみました。 func add(a, b) { var c = a + b; return c; } func main() { var x = -1; var i = 0; while (i != 10) { case when (i == 1) { call_set x = add(i, x); } when (i == 2) { set x = 1 + 2; } when (1) { set x = 1 + (2 * 3); } set i = i + 1; } } 図の描き方については Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする - Qiita を参照。 左端の、全体の 1/5 くらいの小さめの山が add 関数で、残りが main 関数ですね。 メモ レキサはすでにあるのでそれを流用した to_racc_token メソッドで Token オブジェクトを Racc が期待する形式に変換 1時間くらいで書けた。これより大きめの SQL パーサを少し前にすでに書いていてある程度慣れていたのと、パーサのテストがそのまま使えたのが良かった。 関連 他に Racc 関連で書いたもの。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでkaminariを使ってpaginationを作りそのテストをRspecで書こうとしてハマりました

概要 勉強とポートフォリオ作成を兼ねてrails tutorialをなぞりながらtestをminitestではなくRspecで書いて見ようと試みていたところ、 第10章のPaginationの箇所でハマったので(他にも色々ハマっていますが)投稿記事として無難かなという思いと個人的な備忘録としても書いとこうと思います。 環境 rails tutorial 6版とはGemのverが一部異なります。 kaminariとrspec-railsを入れてます Gemfiles.rb # 抜粋です ruby '2.7.4' gem 'bootstrap', '~> 4.6.0' gem 'kaminari' gem 'rails', '6.0.3' gem 'rails-i18n' group :test do gem 'capybara', '3.28.0' gem 'factory_bot_rails' gem 'rails-controller-testing', '1.0.4' gem 'rspec-rails' gem 'selenium-webdriver', '3.142.4' gem 'webdrivers', '4.1.2' end ハマったところ tutorial リスト 10.48のテストについて やっていることを下記します 1. ログインする 2. ユーザ一覧ページへ移動 3. assert_template 4. paginationがあるか 5. 1page目に表示されているすべてのuserのリンクが正しいことの確認 rspecに書く際、3.はtitleを確認することでお茶を濁しています。 ハマったのは5.です。 結論 1ページのレコード数(今回はユーザー数)はperメソッドで変更してもrspecでのtestはデフォルトの25で検査を強行されるので、 kaminari_config.rbを作ってデフォルトを変更するべし 状況 user_controller.rb # 抜粋 def index # pageとperがkaminariで定義されたメソッド perメソッドの引数にどれだけのレコード # (この場合はユーザー)が表示されたらページを増やすかを指定する @users = User.page(params[:page]).per(10) end RspecはSystem_specでやってます spec/systems/users_index_spec.rb require 'rails_helper' RSpec.describe 'user_index', type: :system do describe 'ユーザ一覧を開いたとき' do let!(:first_user) { FactoryBot.create(:user) } it 'ページネーションが存在しページ遷移ができること' do # create_listでFactoryBotから模擬ユーザを量産 create_list(:user, 25) # login_asヘルパーメソッドはsupport/test_helper内で定義 login_as(first_user) visit users_path expect(page.title).to eq full_title('ユーザ一覧') # ページネーションが上下で2つあることを確認する expect(page).to have_selector '.pagination', count: 2 # 最初のページ2へのリンクをクリックする click_on '2', match: :first # ユーザーフレンドリー+ページキャッシュのために下記で通るようにroutes.rbで細工してる expect(current_path).to eq '/users/page/2' click_on '1', match: :first User.page(1).each do |user| expect(page).to have_link href: "/users/#{user.id}" # この部分でエラーが起こる end end end end ご参考にroutes.rbの細工箇所です routes.rb #下記でpagination 2page目のPathが/users/page/2 とできる resources :users do get 'page/:page', action: :index, on: :collection end 解決策 結論で書きましたが、kaminari_config.rbを作ります。 $ rails g kaminari:config create config/initializers/kaminari_config.rb 作ったファイルでデフォルトのper_pageを変更します。 config/initializers/kaminari_config.rb # frozen_string_literal: true Kaminari.configure do |config| config.default_per_page = 10 # ここの部分のコメントを外し25 ⇨ 10にしました # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 config.left = 2 config.right = 1 # config.page_method_name = :page # config.param_name = :page # config.max_pages = nil # config.params_on_first_page = false end user_controller内のper(10)は不要なので消しときます。 これでtest通るようになりました。 ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Active_hashの結合テストコードの書き方【Rspec】

ユーザー新規登録を例に、Active_hashを含む結合テストコードのやり方をアウトプットしていきます。 前提条件 ・Rubyバージョン2.6.5 ・Railsバージョン6.0.0 ・Gemファイル内の:development, :testに  「gem 'rspec-rails'」「gem 'factory_bot_rails'」を導入し、bundle install済み ・Active_hashを作成してページに表示済み 結論 select '選択肢', from: 'name属性'とする。 書き方 フォームに入力する動作をテストしたい時はfill_inを用いますが、 Active_hashのプルダウン形式は「入力」ではなく「選択」となります。 そのためselectを使って選択する動作をテストしていきます。 require 'rails_helper' RSpec.describe "ユーザー新規登録", type: :system do before do @user = FactoryBot.build(:user) sleep 0.2 end context 'ユーザー新規登録ができる時' do it '正しい情報を入力すればユーザー新規登録ができてトップページに移動する' do # トップページに移動する visit root_path # トップページにサインアップページへ遷移するボタンがあることを確認する expect(page).to have_content('新規登録') # 新規登録ページへ移動する visit new_user_registration_path # ユーザー情報を入力する fill_in 'email', with: @user.email fill_in 'password', with: @user.password fill_in 'password-confirmation', with: @user.password_confirmation select "男性", from: 'user[gender_id]' //ここから下4行がselectの例です select '1994', from: 'user[birthday(1i)]' select '9', from: 'user[birthday(2i)]' select '18', from: 'user[birthday(3i)]'   〜省略〜 <「選択肢」と「name属性」の調べ方> 検証ツールを活用してselect '選択肢', from: 'name属性'に必要な情報を集めます。 ①Active_hashの選択ボックスが表示されているページで「⌘ + option + i」(もしくは右クリックから選択)を実行して検証ツールを起動します。 ②検証ツール枠内の一番左上にある矢印をクリックすると、ページ内のあらゆる要素に照準を合わせることができるので、その状態で選択ボックスをクリックします。 ③すると検証ツールのElements内に、フォームのHTMLコードが表示されます。 これは性別のフォームをクリックした時の表示です。 ここから次の2つのことがわかります。 ・optionタグに囲まれている3つの選択肢(---, 男性, 女性) ・name属性に指定されているuser[gender_id] ぶっちゃけoptionタグに囲まれている選択肢はここを見ずとも、自分で設定したものなので分かると思います。 それよりも大事なのはname属性です。 下記のように、erbファイルのコードではname属性を記述していないのに、HTMLコードに変換されたものではしっかりとname属性が表示されています。 //name属性の記述はどこにも見当たらない <%= f.collection_select(:gender_id, Gender.all, :id, :name, {}, {class:"select-box"}) %> この現象にはform_withの特性が関与しています。 form_withの特性 実はform_withで作成されているフォームには、何も指定しなくてもid名とname属性が自動で付与されるようになっています。 ・id名:modelオプションに指定されている変数名_カラム名 ・name属性:modelオプションに指定されている変数名[カラム名] 今回で言えば、 ①form_withのmodelオプションに指定されている@user ②入力された値を保存するカラム名gender_id そのため次のようになります。 ・id名:user_gender_id ・name属性:user[gender_id] なので仮に「model: @post」と「カラム名 :prefecture_id」であれば、 ・id名:post_prefecture_id ・name属性:post[prefecture_id] 以上のことから、select '選択肢', from: 'name属性'に必要な情報は、検証ツールを使うことで見つけることができ、Active_hashを含む結合テストコードが可能となります。 これでActive_hashを含む結合テストコードの書き方の説明は終わりです!お疲れ様でした。 ご指摘などあれば、ご教授いただけると幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む