20200927のRailsに関する記事は22件です。

英語を習っていない小学1年生でも書ける、l(エル)メソッド

l(エル)メソッドとは

 時刻や日時を表すときに使うメソッド。指定した現地時間に対応できる。SNSで「何月何日の何時に投稿した」なんてときに使う。テーブルのcreated_atカラムを表示したいときなどに使う。

現地時間を指定する方法

ステップ1

config/application.rb
class Application < Rails::Application
  config.i18n.default_locale = :ja #日本の場合
  config.time_zone = 'Tokyo' #東京の場合
end

Railsの全ての環境設定のファイルに、上記の記述をする。

ステップ2

ja.ymlファイルを作成する。

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

lメソッドの使い方

 上記の2つの設定をしたら、時刻の入ったカラムを表示させるメソッドの前に「l」と入れるだけ。
ex) 拡張子が.html.erbなら

<%= l name.created_at %> #2020/9/27/ 23:32:09 と表示される

ポイント

  • configディレクトリのapplicationファイルに現地時間の設定をする。
  • localesディレクトリにja.ymlファイルを作成し、時間の表示方法を指定する。
  • 使う時は、メソッドの前に「l」を記述する。

最後に

 投稿機能を実装するときには必須のメソッドとなりそう。

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

Rails: rake taskを良い感じに書く方法

良い感じ = 以下の二点

  1. テストしやすいこと
    • 工数削減のため
  2. (ある程度は)エンジニア間での書き方が統一できること
    • 例えばdryrunの指定方法が書き手によってまちまちだと、商用でのtask実行時に事故が起きる可能性もあるので統一可能であればした方が良い
    • ログのフォーマットが統一されていないと作業効率が悪い

などなど

コード

lib/tasks/issue_6885.rake
require_relative 'helpers/all_user_name_update_helper.rb'

namespace :issue_6885 do

  desc 'これはサンプルです'
  task all_user_name_update: :environment do
    helper = AllUserNameUpdateHelper.new

    helper.main
  end
end
lib/tasks/helpers/all_user_name_update_helper.rb
require_relative 'rake_helper_template'

class AllUserNameUpdateHelper < RakeHelperTemplate
  NEW_NAME = 'bar'

  def main
    template do |logger|
      all_users = get_all_user
      logger.info("対象ユーザー数: #{all_users.size}")

      if all_users.blank?
        logger.info('対象ユーザー数が0件だったため処理を終了します')
        return
      end

      all_users.each { |user| update_name(user, NEW_NAME) }
    end
  end

  private

  def get_all_user
    User.all
  end

  def update_name(user, new_name)
    user.update!(name: new_name)
    user
  end
end

lib/tasks/helpers/rake_helper_template.rb
class RakeHelperTemplate
  # 標準出力も行う
  def make_logger(log_file_path)
    logger = ActiveSupport::Logger.new(log_file_path)
    stdout_logger = ActiveSupport::Logger.new(STDOUT)
    broadcast_logger = ActiveSupport::Logger.broadcast(stdout_logger)
    logger.extend(broadcast_logger)
    logger.formatter = Logger::Formatter.new
    logger
  end

  # ログファイル名はtask名と同じにする(コロンは使わない方が良いので置換する)
  # rakeタスクの実行以外から呼び出されるケース(例: spec)を考慮しておく
  def make_log_file_path
    task_name = Rake.try(:application)&.top_level_tasks&.[](0)&.gsub(':', '_') || Rails.env
    log_file_name = "#{task_name}.log"
    Rails.root.join('log', log_file_name)
  end

  def template
    log_file_path = make_log_file_path

    # 明示的に文字列のfalseを渡さない限りは必ずdryrunにする
    is_dryrun = ENV['is_dryrun'] != 'false'

    logger = make_logger(log_file_path)
    logger.info("Start. is_dryrun: #{is_dryrun}")

    ActiveRecord::Base.transaction do
      yield(logger)
      raise ActiveRecord::Rollback if is_dryrun
    end

    logger.info("Finish. log_file_path: #{log_file_path}")
  end
end

実行例

dryrunにする場合
$ is_dryrun=false bundle exec rake issue_6885:all_user_name_update
I, [2020-09-27T13:33:27.229764 #13040]  INFO -- : Start. is_dryrun: true
I, [2020-09-27T13:33:27.556890 #13040]  INFO -- : 対象ユーザー数: 1
I, [2020-09-27T13:33:27.755933 #13040]  INFO -- : Finish. log_file_path: /app/log/issue_6885_all_user_name_update.log 

ポイント

  1. .rakeファイルはhelperクラスを呼び出すだけにして、rakeのDSL?的なお作法を気にせずに普段書き慣れているclassのメソッドをテストするようにした
  2. yieldを使ってdryrun, logger(の一部)を共通化し、rake taskを作成するたびに毎回定義しなくても済むようにした
    • mainメソッド(名前は何でも良い)には極力そのtaskの処理フローだけを定義するようにし、個別具体的な処理はテストしやすい粒度で別メソッドに切り出すようにする

テンプレートにしてしまえばall_user_name_update_helper.rbのようにやりたいことに集中できる!(^○^)

所感

  • rake taskの書き方は久々に書くと忘れてるのでこの記事は実は備忘録的な意味合いもあったり
  • yieldは正直わかりにくいので使いたくない派だけど、テンプレートのように毎回書いたり読んだりしないものであれば良いかなという考え
  • 他にも良い感じの書き方あるよという方いたら教えてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

指定されたエラー場所にファイルがあるのに、rails sで、`rescue in block in modules_for_hoge': Missing hoge file hoge/hoge_hoge.rb (AbstractController::Helpers::MissingHogeError)と出た時の対処法。

エラー文

qiita.rb
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/abstract_controller/helpers.rb:152:in `rescue in block in modules_for_helpers': Missing helper file helpers/microposts_helper.rb (AbstractController::Helpers::MissingHelperError)
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/abstract_controller/helpers.rb:149:in `block in modules_for_helpers'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/abstract_controller/helpers.rb:145:in `map!'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/abstract_controller/helpers.rb:145:in `modules_for_helpers'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/action_controller/metal/helpers.rb:94:in `modules_for_helpers'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/abstract_controller/helpers.rb:109:in `helper'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actionpack-6.0.3/lib/action_controller/railties/helpers.rb:19:in `inherited'
        from /home/ubuntu/environment/sample_app/app/controllers/application_controller.rb:1:in `<main>'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/zeitwerk-2.4.0/lib/zeitwerk/kernel.rb:27:in `require'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/actiontext-6.0.3/lib/action_text/engine.rb:47:in `block (2 levels) in <class:Engine>'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:428:in `instance_exec'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:428:in `block in make_lambda'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:200:in `block (2 levels) in halting'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:605:in `block (2 levels) in default_terminator'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:604:in `catch'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:604:in `block in default_terminator'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:201:in `block in halting'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:513:in `block in invoke_before'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:513:in `each'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:513:in `invoke_before'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/callbacks.rb:134:in `run_callbacks'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/execution_wrapper.rb:119:in `complete!'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/execution_wrapper.rb:90:in `wrap'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activesupport-6.0.3/lib/active_support/reloader.rb:51:in `reload!'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.0/lib/spring/application.rb:168:in `serve'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.0/lib/spring/application.rb:145:in `block in run'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.0/lib/spring/application.rb:139:in `loop'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.0/lib/spring/application.rb:139:in `run'
        from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.0/lib/spring/application/boot.rb:19:in `<top (required)>'
        from /home/ubuntu/.rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from /home/ubuntu/.rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from -e:1:in `<main>'

対処法

bundle updateして、再度rails sで無事テストできました。

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

f.collection_selectの"選択して下さい"を選んだときのバリデーション

1.やりたいこと

<%= f.collection_select :address_id, @customer.addresses, :id, :full_address, :include_blank => "選択してください" %>
フォームにおいて選択せずに一番上の"選択して下さい"を選んだ際にバリデーションをかけたい。

2.実装する

app/controllers/orders_controller.rb
 params[:order][:address_id] ==  ""
 flash[:notice] = "選択して下さい"
 redirect_to new_order_path

パラメータから送られている値を見ると""となっていたのでイコール文を""に変更。

3.終わりに

これを実装したかった時にQiitaやGoogleで検索しても全然出てこなかったので
投稿しました。
しっかりターミナルを見て返ってきてる値を確認しないといけないと痛感しました。

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

Redmineのコードリーディング環境をDockerで構築する

概要

redmineのソースコードを読むための環境をDockerで構築します。以下の状態をゴールとします。

  • redmineのmasterブランチのコードがDocker上で動作する
  • Debug用のツールが使える

モチベーション

動作環境を構築するのは実際のアプリケーションの挙動とコード内容を頭の中で紐付けながらコードリーディングしたいからです。

なぜコードリーディングしたいのか?

人が書いたコードを読むことは新たな発見や学びに繋がると考えています。
私の場合は、クラスの分け方、命名、テストコードの書き方、エラーハンドリングの仕方などに悩むことが最近多いので、そのあたりの引き出しを増やしたいというモチベーションがあります。

なぜRedmineか?

Redmineを選んだのは以下の理由です。

  • 普段からRedmineを使っているため馴染みがあり、画面や外部仕様をある程度知っている。
  • 長年メンテナンスされており、世界中で使用されている。
  • 普段はPHPをつかっているため、あえて別の言語やFWに触れてみる。

環境構築

以下を参考にすすめていきます。

リポジトリの用意

  1. redmineのレポジトリをフォーク
  2. フォークしたリポジトリをクローン
  3. redmineディレクトリに移動
  4. masterブランチから別ブランチを切る(自分はcode_readingにしました)

Docker関連ファイルの作成

redmineディレクトリ直下に以下のファイルを作成します。

  • Dockerfile
  • docker-compose.yml
  • entrypoint.sh

ファイルの内容は以下の通りにします。

Dockerfile

RedmineのGemfileがconfig/database.ymlを参照しているため、COPY . /redmineをした後にbundle installをする必要があります。
ちなみに参照して何をしているかというと、使用するDBにあわせてインストールするGemを変えています。例えばconfig/database.ymlにpostgresの接続情報が書いてあると、postgresに接続するためのGemがインストールされます。

Dockerfile
FROM ruby:2.5

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

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

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

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /redmine/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

docker-compose.yml

コメントアウトしているのはruby-debug/ruby-debug-ide用の設定です。
導入しましたが自分の環境では動作が安定しなかったので一旦使わないようにしています。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    ports:
        - "5433:5432"
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    # command: bash -c "rm -f tmp/pids/server.pid && bundle exec rdebug-ide --host 0.0.0.0 --port 1234 -- bin/rails s -p 3000 -b 0.0.0.0"
    command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p 3000 -b 0.0.0.0"
    volumes:
      - .:/redmine
    ports:
      - "3003:3000"
      # - "1234:1234"
      # - "26162:26162"
    stdin_open: true
    tty: true
    depends_on:
      - db

デバッグ用のGemの追記

などの記事を参考にGemfile.に以下を追加します。

  • better_errors
  • binding_of_caller
  • pry-rails
  • pry-byebug
Gemfile
group :development do
  gem "yard" #この下に追記する
  gem "better_errors"
  gem "binding_of_caller"
  gem "pry-rails"
  gem "pry-byebug"
  # 安定動作しなかったのでコメントアウト
  # gem "ruby-debug-ide"
  # gem "debase"      
end

database.ymlの設定

config/database.ymlを作成し以下の内容にします。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: postgres
  password: password

development:
  <<: *default
  database: redmine_development

test:
  <<: *default
  database: redmine_test

production:
  <<: *default
  database: redmine

また、better_errosをDocker上の環境で使えるようにするため、development.rbに以下を追記します。

config/enviroments/development.rb
BetterErrors::Middleware.allow_ip! "0.0.0.0/0"

環境の立ち上げ

ファイルの用意が完了したのでコンテナを起動し、アプリケーションを動かします。

# コンテナイメージのビルド
$ docker-compose build
# コンテナの起動
$ docker-compose up -d
# DBの作成
$ docker-compose run --rm web rake db:create
# マイグレーションの実行
$ docker-compose run --rm web bin/bundle exec rake db:migrate
# Redmineのデフォルトデータ投入タスク
$ docker-compose run --rm web bin/bundle exec rake redmine:load_default_data

起動が成功していればdocker-compse psで以下のようにStatusがUpになります。

$ dcom ps
    Name                   Command               State    Ports                                  
--------------------------------------------------------------------------------
redmine_db_1    docker-entrypoint.sh postgres    Up      0.0.0.0:5433->5432/tcp                                                  
redmine_web_1   entrypoint.sh bash -c rm - ...   Up      0.0.0.0:3003->3000/tcp

この後にhttp://localhost:3003にアクセスするとRedmineの画面が表示されます。
また、最初はIDとパスワード共にadminでログインすることができます。
これで環境構築は完了です。

pry-byebugによるデバッグ

docker-compose upでコンテナが起動している状態でコンテナにアタッチします。

$ docker attach redmine_web_1

確認したいコードの該当箇所にbinding.pryを追記します。以下の例はRedmineのルートパスにアクセスした時に実行されるwellcome#indexに追加しています。

app/controllers/welcome_controller.rb
  def index
    binding.pry #調べたいところに追加する
    @news = News.latest User.current
  end

この状態でhttp://localhost:3003にアクセスするとターミナルに以下の内容が表示されステップ実行などができるようになります。

From: /redmine/app/controllers/welcome_controller.rb:25 WelcomeController#index:

    23: def index
    24:   binding.pry
 => 25:   @news = News.latest User.current
    26: end

[1] pry(#<WelcomeController>)> Started GET "/" for 172.18.0.1 at 2020-09-27 07:41:34 +0000
[1] pry(#<WelcomeController>)> 

[1] pry(#<WelcomeController>)>の箇所にコマンドを打つことでステップ実行などができます。

  • next
    • ステップイン
  • step
    • ステップオーバー
  • continue
    • プログラムの実行を続行しpryを終了

以上

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

Redmineのコードリーディング環境をDocker上に構築する

概要

redmineのソースコードを読むための環境をDockerで構築します。以下の状態をゴールとします。

  • redmineのmasterブランチのコードがDocker上で動作する
  • Debug用のツールが使える

モチベーション

動作環境を構築するのは実際のアプリケーションの挙動とコード内容を頭の中で紐付けながらコードリーディングしたいからです。

なぜコードリーディングしたいのか?

人が書いたコードを読むことは新たな発見や学びに繋がると考えています。
私の場合は、クラスの分け方、命名、テストコードの書き方、エラーハンドリングの仕方などに悩むことが最近多いので、そのあたりの引き出しを増やしたいというモチベーションがあります。

なぜRedmineか?

Redmineを選んだのは以下の理由です。

  • 普段からRedmineを使っているため馴染みがあり、画面や外部仕様をある程度知っている。
  • 長年メンテナンスされており、世界中で使用されている。
  • 普段はPHPをつかっているため、あえて別の言語やFWに触れてみる。

環境構築

以下を参考にすすめていきます。

リポジトリの用意

  1. redmineのレポジトリをフォーク
  2. フォークしたリポジトリをクローン
  3. redmineディレクトリに移動
  4. masterブランチから別ブランチを切る(自分はcode_readingにしました)

Docker関連ファイルの作成

redmineディレクトリ直下に以下のファイルを作成します。

  • Dockerfile
  • docker-compose.yml
  • entrypoint.sh

ファイルの内容は以下の通りにします。

Dockerfile

RedmineのGemfileがconfig/database.ymlを参照しているため、COPY . /redmineをした後にbundle installをする必要があります。
ちなみに参照して何をしているかというと、使用するDBにあわせてインストールするGemを変えています。例えばconfig/database.ymlにpostgresの接続情報が書いてあると、postgresに接続するためのGemがインストールされます。
Gemfileで指定するgemを動的に変えるという発想があまりなかったので既に学びがあったと感じています。

Dockerfile
FROM ruby:2.5

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

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

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

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /redmine/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

docker-compose.yml

コメントアウトしているのはruby-debug/ruby-debug-ide用の設定です。
導入しましたが自分の環境では動作が安定しなかったので一旦使わないようにしています。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    ports:
        - "5433:5432"
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    # command: bash -c "rm -f tmp/pids/server.pid && bundle exec rdebug-ide --host 0.0.0.0 --port 1234 -- bin/rails s -p 3000 -b 0.0.0.0"
    command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p 3000 -b 0.0.0.0"
    volumes:
      - .:/redmine
    ports:
      - "3003:3000"
      # - "1234:1234"
      # - "26162:26162"
    stdin_open: true
    tty: true
    depends_on:
      - db

デバッグ用のGemの追記

などの記事を参考にGemfile.に以下を追加します。

  • better_errors
  • binding_of_caller
  • pry-rails
  • pry-byebug
Gemfile
group :development do
  gem "yard" #この下に追記する
  gem "better_errors"
  gem "binding_of_caller"
  gem "pry-rails"
  gem "pry-byebug"
  # 安定動作しなかったのでコメントアウト
  # gem "ruby-debug-ide"
  # gem "debase"      
end

database.ymlの設定

config/database.ymlを作成し以下の内容にします。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: postgres
  password: password

development:
  <<: *default
  database: redmine_development

test:
  <<: *default
  database: redmine_test

production:
  <<: *default
  database: redmine

また、better_errosをDocker上の環境で使えるようにするため、development.rbに以下を追記します。

config/enviroments/development.rb
BetterErrors::Middleware.allow_ip! "0.0.0.0/0"

環境の立ち上げ

ファイルの用意が完了したのでコンテナを起動し、アプリケーションを動かします。

# コンテナイメージのビルド
$ docker-compose build
# コンテナの起動
$ docker-compose up -d
# DBの作成
$ docker-compose run --rm web rake db:create
# マイグレーションの実行
$ docker-compose run --rm web bin/bundle exec rake db:migrate
# Redmineのデフォルトデータ投入タスク
$ docker-compose run --rm web bin/bundle exec rake redmine:load_default_data

起動が成功していればdocker-compse psで以下のようにStatusがUpになります。

$ dcom ps
    Name                   Command               State    Ports                                  
--------------------------------------------------------------------------------
redmine_db_1    docker-entrypoint.sh postgres    Up      0.0.0.0:5433->5432/tcp                                                  
redmine_web_1   entrypoint.sh bash -c rm - ...   Up      0.0.0.0:3003->3000/tcp

この後にhttp://localhost:3003にアクセスするとRedmineの画面が表示されます。
また、最初はIDとパスワード共にadminでログインすることができます。
これで環境構築は完了です。

pry-byebugによるデバッグ

docker-compose upでコンテナが起動している状態でコンテナにアタッチします。

$ docker attach redmine_web_1

確認したいコードの該当箇所にbinding.pryを追記します。以下の例はRedmineのルートパスにアクセスした時に実行されるwellcome#indexに追加しています。

app/controllers/welcome_controller.rb
  def index
    binding.pry #調べたいところに追加する
    @news = News.latest User.current
  end

この状態でhttp://localhost:3003にアクセスするとターミナルに以下の内容が表示されステップ実行などができるようになります。

From: /redmine/app/controllers/welcome_controller.rb:25 WelcomeController#index:

    23: def index
    24:   binding.pry
 => 25:   @news = News.latest User.current
    26: end

[1] pry(#<WelcomeController>)> Started GET "/" for 172.18.0.1 at 2020-09-27 07:41:34 +0000
[1] pry(#<WelcomeController>)> 

[1] pry(#<WelcomeController>)>の箇所にコマンドを打つことでステップ実行などができます。

  • next
    • ステップイン
  • step
    • ステップオーバー
  • continue
    • プログラムの実行を続行しpryを終了

以上

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

Redmineのコードリーディング用環境をDocker上に構築する

概要

redmineのソースコードを読むための環境をDockerで構築します。以下の状態をゴールとします。

  • redmineのmasterブランチのコードがDocker上で動作する
  • Debug用のツールが使える

モチベーション

動作環境を構築するのは実際のアプリケーションの挙動とコード内容を頭の中で紐付けながらコードリーディングしたいからです。

なぜコードリーディングしたいのか?

人が書いたコードを読むことは新たな発見や学びに繋がると考えています。
私の場合は、クラスの分け方、命名、テストコードの書き方、エラーハンドリングの仕方などに悩むことが最近多いので、そのあたりの引き出しを増やしたいというモチベーションがあります。

なぜRedmineか?

Redmineを選んだのは以下の理由です。

  • 普段からRedmineを使っているため馴染みがあり、画面や外部仕様をある程度知っている。
  • 長年メンテナンスされており、世界中で使用されている。
  • 普段はPHPをつかっているため、あえて別の言語やFWに触れてみる。

環境構築

以下を参考にすすめていきます。

リポジトリの用意

  1. redmineのレポジトリをフォーク
  2. フォークしたリポジトリをクローン
  3. redmineディレクトリに移動
  4. masterブランチから別ブランチを切る(自分はcode_readingにしました)

Docker関連ファイルの作成

redmineディレクトリ直下に以下のファイルを作成します。

  • Dockerfile
  • docker-compose.yml
  • entrypoint.sh

ファイルの内容は以下の通りにします。

Dockerfile

RedmineのGemfileがconfig/database.ymlを参照しているため、COPY . /redmineをした後にbundle installをする必要があります。
ちなみに参照して何をしているかというと、使用するDBにあわせてインストールするGemを変えています。例えばconfig/database.ymlにpostgresの接続情報が書いてあると、postgresに接続するためのGemがインストールされます。
Gemfileで指定するgemを動的に変えるという発想があまりなかったので既に学びがあったと感じています。

Dockerfile
FROM ruby:2.5

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

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

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

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /redmine/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

docker-compose.yml

コメントアウトしているのはruby-debug/ruby-debug-ide用の設定です。
導入しましたが自分の環境では動作が安定しなかったので一旦使わないようにしています。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    ports:
        - "5433:5432"
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    # command: bash -c "rm -f tmp/pids/server.pid && bundle exec rdebug-ide --host 0.0.0.0 --port 1234 -- bin/rails s -p 3000 -b 0.0.0.0"
    command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p 3000 -b 0.0.0.0"
    volumes:
      - .:/redmine
    ports:
      - "3003:3000"
      # - "1234:1234"
      # - "26162:26162"
    stdin_open: true
    tty: true
    depends_on:
      - db

デバッグ用のGemの追記

などの記事を参考にGemfile.に以下を追加します。

  • better_errors
  • binding_of_caller
  • pry-rails
  • pry-byebug
Gemfile
group :development do
  gem "yard" #この下に追記する
  gem "better_errors"
  gem "binding_of_caller"
  gem "pry-rails"
  gem "pry-byebug"
  # 安定動作しなかったのでコメントアウト
  # gem "ruby-debug-ide"
  # gem "debase"      
end

また、better_errosをDocker上の環境で使えるようにするため、development.rbに以下を追記します。

config/enviroments/development.rb
BetterErrors::Middleware.allow_ip! "0.0.0.0/0"

database.ymlの設定

config/database.ymlを作成し以下の内容にします。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: postgres
  password: password

development:
  <<: *default
  database: redmine_development

test:
  <<: *default
  database: redmine_test

production:
  <<: *default
  database: redmine

環境の立ち上げ

ファイルの用意が完了したのでコンテナを起動し、アプリケーションを動かします。

# 空のGemfile.lockを作る
$ touch Gemfile.lock
# コンテナイメージのビルド
$ docker-compose build
# コンテナの起動
$ docker-compose up -d
# DBの作成
$ docker-compose run --rm web rake db:create
# マイグレーションの実行
$ docker-compose run --rm web bin/bundle exec rake db:migrate
# Redmineのデフォルトデータ投入タスク
$ docker-compose run --rm web bin/bundle exec rake redmine:load_default_data

起動が成功していればdocker-compse psで以下のようにStatusがUpになります。

$ dcom ps
    Name                   Command               State    Ports                                  
--------------------------------------------------------------------------------
redmine_db_1    docker-entrypoint.sh postgres    Up      0.0.0.0:5433->5432/tcp                                                  
redmine_web_1   entrypoint.sh bash -c rm - ...   Up      0.0.0.0:3003->3000/tcp

この後にhttp://localhost:3003にアクセスするとRedmineの画面が表示されます。
また、最初はIDとパスワード共にadminでログインすることができます。
これで環境構築は完了です。

pry-byebugによるデバッグ

docker-compose upでコンテナが起動している状態でコンテナにアタッチします。

$ docker attach redmine_web_1

確認したいコードの該当箇所にbinding.pryを追記します。以下の例はRedmineのルートパスにアクセスした時に実行されるwellcome#indexに追加しています。

app/controllers/welcome_controller.rb
  def index
    binding.pry #調べたいところに追加する
    @news = News.latest User.current
  end

この状態でhttp://localhost:3003にアクセスするとターミナルに以下の内容が表示されステップ実行などができるようになります。

From: /redmine/app/controllers/welcome_controller.rb:25 WelcomeController#index:

    23: def index
    24:   binding.pry
 => 25:   @news = News.latest User.current
    26: end

[1] pry(#<WelcomeController>)> Started GET "/" for 172.18.0.1 at 2020-09-27 07:41:34 +0000
[1] pry(#<WelcomeController>)> 

[1] pry(#<WelcomeController>)>の箇所にコマンドを打つことでステップ実行などができます。

  • next
    • ステップイン
  • step
    • ステップオーバー
  • continue
    • プログラムの実行を続行しpryを終了

以上

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

RailsアプリケーションにTailwindを入れてみる

https://github.com/fukadashigeru/tailwind_sample_app/pull/1

環境

ruby '2.7.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.3'

gem 'webpacker', '~> 4.0'

準備

Slim

Gemfileに下記追加

gem 'slim-rails'
gem 'html2slim'

ターミナルで

bundle install

先にビュー用のerbファイルを作成していたら、下記コマンドをターミナルで打つ

bundle exec erb2slim app/views app/views -d

Scaffold

ターミナルで下記打つ

bin/rails g scaffold blog content:text

config/routes.rbに下記追加

root 'blogs#index'

DB作成

bin/rails db:create
bin/rails db:migrate

Tailwindを入れてみる

ref: tailwindcss Documentation

Tailwind用のスタイルを当てる

image

1.Install Tailwind via npm

# Using npm
npm install tailwindcss

# Using Yarn
yarn add tailwindcss

2.Add Tailwind to your CSS

app/javascript/src/scss/application.scssを追加

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

app/javascript/packs/application.jsに下記追加

import '../src/scss/application.scss'

3.Create your Tailwind config file (optional)

npx tailwindcss init

4.Process your CSS with Tailwind

postcss.config.jsに下記追加

module.exports = {
  plugins: [
    // ...
    require('tailwindcss'),
    require('autoprefixer'),
    // ...
  ]
}

確認

ターミナルでアプリケーションを立ち上げる

bin/rails s

別のターミナルでwebpackerを立ち上げる(これは不要?)

bin/webpack-dev-server

ローカルホストにアクセス
http://localhost:3000/

tailwindのスタイルがあたるようになっている
image

Taiwind

https://tailwindcss.com/

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

アクションとインスタンスメソッドって一緒じゃないの?何が違うの?

コントローラーに記述されるこれ

XXX.controller.rb
def アクション名
  XXXXXX
end

これってアクションとかインスタンスメソッドって言ったり、どっちが正しいの?何が違うの?

アクション

=>ルーティングで指定されるときの名前

つまり、これはアクション

config/routes.rb
Rails.application.routes.draw do
  get 'index' to: "tweets#index"

インスタンスメソッド

=>アクションの指定によって実際に実行される、コントローラに実装された「def XXXX ~ end」という形式のコード

つまり上にも書いたこのコードはアクションになる。なるほど。。。

XXX.controller.rb
def アクション名
  XXXXXX
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ActiveAdmin】セレクトボックス内の項目を好きな順でソートする方法

はじめに

rails_adminを使っていたのですが、セレクトボックス内の項目順序を変えるのすら出来なくて泣き叫んでいました。
調べても調べても分からんかったので、これもしかして出来ないんちゃう……?と。

そこでactive_adminに移行してしまいました。
こちらでは出来ました。

構成

Item belongs_to Kind

active_adminで生成したファイルの設定

app/admin/items.rb
ActiveAdmin.register Item do
  # 変更許可するカラム
  permit_params :name, :kind_id

  # (中略)

  form do |f|
    inputs do
      input :name 
      input :kind, as: :select, collection: Kind.all.order(:name)
    end

    actions
  end

end

.orderで逆順にするなら.order(name: "DESC")

まとめ

うーん美しい。

地味に苦労したのですが、目標を分割したら割とすぐ出来ました。

1.カスタムした配列をセレクトボックスで選ばせる
2.Modelからデータを引っ張ってくる

他の所で配列作ったりしてもいいのですが、最終的にこれで通りました。

もっと言うと、Modelの関連付けがちゃんとしてたらas: :selectもこれ無くて大丈夫です。

それにしても表示カラムの制限とか好き放題やれて、Modelごとに綺麗に分離してるの……気持ちいい……?

active_admin大好きになってきてます

終わり

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

railsのserver起動時エラー『 Webpacker configuration file not found』対処

本稿では、表題の
『Webpacker configuration file not found』
エラーが発生してサーバーが起動できない時の対処方法について、実際に行った手順を共有します。1

(実行環境について)

当時の環境について(ここをクリック)
(Windowsの情報)
Microsoft Windows [Version 10.0.18363.1110]
(c) 2019 Microsoft Corporation. All rights reserved.

(Nodeのバージョン)
C:\WINDOWS\system32>node -v
v14.12.0

※Nodeがインストールされていない場合は次のコマンドを実行します。
brew install node


(rubyのバージョン)
C:\WINDOWS\system32>ruby -v
ruby 2.6.6p146 (2020-03-31 revision 67876) [x64-mingw32]
(参考:)
(railsのインストールの様子です。)

C:\WINDOWS\system32>gem install rails
Fetching tzinfo-1.2.7.gem
Fetching thread_safe-0.3.6.gem
・・・
(中略)
・・・
driver, actioncable, mimemagic, marcel, activestorage, actionmailbox, actiontext, thor, 
method_source, railties, sprockets, sprockets-rails, rails after 88 seconds
40 gems installed

C:\WINDOWS\system32>rails -v
Rails 6.0.3.3
(railsプロジェクトの作成。 プロジェクト名:kita_rb)

C:\WINDOWS\system32>cd \xampp\htdocs  (作成ディレクトリに移動)

C:\xampp\htdocs>rails new kita_rb -G
      create
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  Gemfile
・・・
(中略)
・・・      
Installing webdrivers 4.4.1
Fetching webpacker 4.3.0
Installing webpacker 4.3.0
Bundle complete! 14 Gemfile dependencies, 70 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle binstubs bundler
       rails  webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/ (←エラーが出ている!!)

C:\xampp\htdocs>cd kita_rb   (プロジェクトディレクトリに移動しておく)



1.サーバーの起動(エラー発生時)

C:\xampp\htdocs\kita_rb>rails s
=> Booting Puma
=> Rails 6.0.3.3 application starting in development
=> Run `rails server --help` for more startup options
Exiting
Traceback (most recent call last):
        49: from bin/rails:4:in `<main>'
        48: from bin/rails:4:in `require'

・・・
(中略)
・・・

         2: from C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/webpacker-4.3.0/lib/webpacker/configuration.rb:88:in `data'
         1: from C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/webpacker-4.3.0/lib/webpacker/configuration.rb:91:in `load'
C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/webpacker-4.3.0/lib/webpacker/configuration.rb:95:in `rescue in load': Webpacker configuration file not found C:/xampp/htdocs/kita_rb/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - C:/xampp/htdocs/kita_rb/config/webpacker.yml (RuntimeError)

↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
(エラー発生!!)

本稿では、この
『Webpacker configuration file not found』
エラーが発生してサーバーが起動できない時の対処方法について、解決のため実際に行った手順を共有しようと思います。1


エラーメッセージ中に
『Please run rails webpacker:install』(webpackerをインストールしなさい)
とありましたのでこれを実行します。

2.webpackerのインストール

プロジェクトのルートにあるGemfileの中に
gem 'webpacker', '~> 4.0'
の記述があることを確認します。参照

Gemfile
gem 'webpacker', '~> 4.0'

bundle installを実行します。

C:\xampp\htdocs\kita_rb>rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

『Yarnがインストールされていない』ということですので
インストールします。

3.Yarnをインストール

C:\xampp\htdocs\kita_rb>npm install -g yarn
C:\Users\tatsu\AppData\Roaming\npm\yarn -> C:\Users\tatsu\AppData\Roaming\npm\node_modules\yarn\bin\yarn.js
C:\Users\tatsu\AppData\Roaming\npm\yarnpkg -> C:\Users\tatsu\AppData\Roaming\npm\node_modules\yarn\bin\yarn.js
+ yarn@1.22.5
added 1 package in 0.786s



再度、bundle installを実行します。

C:\xampp\htdocs\kita_rb>bundle install
Using rake 13.0.1
Using concurrent-ruby 1.1.7
Using i18n 1.8.5
Using minitest 5.14.2
Using thread_safe 0.3.6
Using tzinfo 1.2.7
Using zeitwerk 2.4.0
・・・
(中略)
・・・
Using turbolinks 5.2.1
Using tzinfo-data 1.2020.1
Using web-console 4.0.4
Using webdrivers 4.4.1
Using webpacker 4.3.0
Bundle complete! 14 Gemfile dependencies, 70 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.



ここでやっとwebpackerのインストールができそうです。

C:\xampp\htdocs\kita_rb>rails webpacker:install
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
・・・
(中略)
・・・      
├─ webpack-dev-middleware@3.7.2
├─ webpack-dev-server@3.11.0
└─ ws@6.2.1
Done in 9.80s.
Webpacker successfully installed ? ?



再度、サーバーを起動します。

C:\xampp\htdocs\kita_rb>rails s
=> Booting Puma
=> Rails 6.0.3.3 application starting in development
=> Run `rails server --help` for more startup options
*** SIGUSR2 not implemented, signal based restart unavailable!
*** SIGUSR1 not implemented, signal based restart unavailable!
*** SIGHUP not implemented, signal based logs reopening unavailable!
Puma starting in single mode...
* Version 4.3.6 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://[::1]:3000
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop
Started GET "/" for ::1 at 2020-09-26 16:23:01 +0900
   (10.1ms)  SELECT sqlite_version(*)
Processing by Rails::WelcomeController#index as HTML
  Rendering C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/railties-6.0.3.3/lib/rails/templates/rails/welcome/index.html.erb
  Rendered C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/railties-6.0.3.3/lib/rails/templates/rails/welcome/index.html.erb (Duration: 5.9ms | Allocations: 416)
Completed 200 OK in 24ms (Views: 13.1ms | ActiveRecord: 0.0ms | Allocations: 2331)



http://localhost:3000 をブラウザで開いて確認します。localhost3000.jpg

起動成功しました!!

以上、よろしければ参考になさってください。


  1. 同エラーの解決策としてはすでに他にいくつかのサイトがその方法を紹介しています。私の場合それらを試したときになぜかいろいろエラーが発生し、その都度解決策を探す必要がありました。その経験を踏まえ結果「こうすればよかったんじゃないか」という手順をまとめました。
    参照させていただいたサイト:「rails serverでサーバーを起動しようとしたら、Webpacker configuration file not foundエラーが発生した時の対処方法」参照  

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

Basic認証を交えたテストコードの書き方

【概要】

1.結論

2.○○になるのはどういう時か

補足:開発環境

1.結論

環境変数を変数に埋め込み、visitでその環境変数を埋め込んだURLに飛ぶようにする!

2.どのように記載するのか

def basic_pass(path) #---❶
  username = ENV["STUDY"] 
  password = ENV["STUDY_password"]
  visit "http://#{username}:#{password}@#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}#{path}"
end

RSpec.describe 'コメント投稿', type: :system do
  before do
    @time = FactoryBot.create(:time)
    @comment = Faker::Lorem.sentence
  end

  it 'ログインしたユーザーは自己学習投稿の詳細ページでコメントできる' do
    # ログインする
    basic_pass new_user_session_path #---❷
    fill_in 'Email', with: @time_report.user.email
    fill_in 'Password', with: @time_report.user.password
    find('input[name="commit"]').click
    expect(current_path).to eq root_path
  end
end

上記のように記載しました!

❶下記のURLの具体例は環境変数になっていないので、変数に環境変数を代入する形にしました。あとは下記のURLを真似させていただきました。
❷basic_passメソッドを結合テストコードが読み込まれる前に記載しないとBasic認証テストのID,パスワードを通過できません。なので、結合テストコードの内容の一番最初にコーディングし、新規登録画面(devise gem使用)に遷移するようにしています。

かなり参考にしたURL:
Capybara + Headless Chrome (System Spec) で Basic認証 を通過する方法

補足:開発環境

Ruby 2.6.5
Rails 6.0.3.3
MySQL
Visual Studio Code
(Caprybara,Rspec,GoogleChrome)

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

【rails】HTTPClientを用いて外部APIに接続する方法(QiitaAPIに接続してみた)

はじめに

railsで外部APIに接続する際にはgemのHTTPClientを使用します。
今回は例としてQiitaのAPIに接続して、記事一覧を取得(get)してみます。

HTTPClientをインストール

Gemfile
gem 'httpclient'
ターミナル
$ bundle install

route.rbに追加

エンドポイントは /api/qiitaとします。

routes.rb
Rails.application.routes.draw do 
  namespace :api do 
    get '/qiita' to: 'qiita#index'
  end
end

QiitaAPIに接続する(Controller)

まず、最も基本的な形です。(headerやqueryを指定しないgetリクエスト)

controllers/api/qiita_controller.rb
class Api::QiitaController < ApplicationController
  # HTTPClientを呼び出す
  require 'httpclient'

  def index
    url = "https://qiita.com/api/v2/items"  # URLを設定
    client = HTTPClient.new                 # インスタンスを生成
    response = client.get(url)              # Getリクエスト
    render json: JSON.parse(response.body)  # 結果をjsonにパースして表示
  end
end

http://localhost:3000/api/qiita にアクセスするとこんな感じでデータの一覧がjsonで返ってきます。
整形していないので非常に見づらいですね。

スクリーンショット 2020-09-27 13.39.51.png

次にheaderやqueryを指定する場合です。

controllers/api/qiita_controller.rb
class Api::QiitaController < ApplicationController
  # HTTPClientを呼び出す
  require 'httpclient'

  def index
    url = "https://qiita.com/api/v2/items"
    header = { Authorization: "Bearer xxxxx" } # 例) ヘッダーに"Bearer xxxxx"を付与
    query = { page: 1, per_page: 20 }          # 例) 1ページ目、1ページごとのデータ取得数を20件にするquery       
    client = HTTPClient.new
    response = client.get(url, header: header, query: query) #headerとqueryを指定
    render json: JSON.parse(response.body)
  end
end

終わりに

今回はgetリクエストのみだけの説明にしましたが、HTTPClientではPostなど他のリクエストも送れるので調べてみてください。

QiitaのAPIの詳細は下記を参照しました。
Qiita API v2 documentation - Qiita:Developer
Qiita API v2 の概要(非公式)

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

しがないRailsエンジニア2年生が2年間の振り返り

初めまして。
あと1ヶ月ほどでエンジニアになって2年経ちます。
振り返りついでにその時感じた課題の変遷をまとめようと思いました。
(一旦Railsに関することだけ抜き出しています)

なんとか生き残ってるエンジニアの課題を共有できれば幸いです。


エンジニア歴

  • 1ヶ月目
    • Railsは半年ほど独学でやってきたものの全体的にあまり理解できていなかった
    • Rails以外にもエンジニアの仕事の仕方やITの概念が必要で覚えることが多かった
  • 3ヶ月目
    • 何となくRailsが使えてるだけで天狗になっていた時期
    • 動けば良いや精神
  • 6ヶ月目
    • 動くものは作れるようになったけどこの時は適当に書いて動くからヨシとしていた
  • 1年目
    • Railsのコードを俯瞰して追えるようになり全くわからなくなった
    • 過去の自分が恥ずかしくなった
    • Rails以外にReactやサーバ知識など、覚えることが多くRailsは後回しにしていた
  • 1年2ヶ月目
    • Railsそっちのけでインフラ周りに注力していた
    • Railsはまだ動けば良いや精神
  • 1年7ヶ月目
    • API初挑戦
    • Rails全然理解していないと自覚して焦りだす
    • N+1を気をつけるようになる
    • やっとクラスの使い方やRailsの動きや設定周りを理解(した気がする)
    • やっとmapやAR(ActiveRecord)の動きを追えるようになる
    • さらに焦る
  • 1年10ヶ月目
    • ModuleやClassが少し分かるようになり調子に乗る
    • include, expendの違いが分かり調子に乗る
    • OOD(オブジェクト指向設計)ができなくプログラミング自体分からない時期突入
    • Rails wayが分からなくて泣く
    • 共通化ができなくて泣く
  • 3年目(これから)
    • Rails wayを身に付けたい
    • 処理を見て非効率な部分を改善できるようになりたい
    • セキュアな書き方も身に付けたい
    • バックエンドに絞って頑張っていきたい


ざっと書いてみました。
最近感じたことしか覚えてないのでメモ書きです。

思い出した時に追記していきます。
自分用のメモ書きにはなりますが誰かのモチベや目標に繋がれば嬉しいです。

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

[Ruby on Rails]データ登録時に重複したレコードがあった場合、登録させない

背景

オリジナルアプリをデプロイ後、重複したデータを登録されていることに気づきます。
スクリーンショット 0002-09-27 午前9.43.35.png

アプリの仕様としては、登録するレコードは重複させたくないのです。

データベースを確認すると、
登録するユーザが異なっていれば、登録できてしまうことが判明。
スクリーンショット 0002-09-27 午前9.44.59.png

解決にそこそこ時間がかかったので、備忘として記録します。

問題箇所

controllerにデータ登録時の条件を設けていました。

require 'rubygems'
require 'mechanize'

def create
    f = (params[:wiki_url])

    unless 
      f.start_with?("https://ja.wikipedia.org/wiki/")
      flash[:notice] = "無効なURLが入力されたため保存できませんでした。"
      redirect_to action: 'new'
      return
    else
      agent = Mechanize.new
      page = agent.get(f) 
      page.encoding='utf-8'

以下、省略...

unless文には、入力したデータ(URL)に「https://ja.wikipedia.org/wiki/」
が含まれていない場合は、登録させないという条件を組んでいました。

しかし、重複したレコードがある場合、登録させないという条件が含まれていなかったため、
写真のような同じデータが登録されてしまったわけです。

解決(結論)

以下のように書き換えて、解決しました。

require 'rubygems'
require 'mechanize'

  def create
    f = (params[:wiki_url])

    if not 
      f.start_with?("https://ja.wikipedia.org/wiki/")
      flash[:notice] = "無効なURLが入力されたため登録できませんでした。"
      redirect_to action: 'new'
      return
    elsif
      Company.where(page_url: "#{f}").count >= 1
      flash[:notice] = "登録済みのURLが入力されたため登録できませんでした。"
      redirect_to actiont: 'new'
      return
    else
      agent = Mechanize.new
      page = agent.get(f) 
      page.encoding = 'utf-8'

以下、省略...

teratailの記事が参考になり、解決に至りました。
:point_right:同じ名前のデータが2件以上登録されているレコードをActiveRecordを用いて取得したいです。

変更点は大きく2つです。

  • unless文からif not文に変更
  • whereメソッド追加(ココがポイント!)

unless文からif not文に変更

ruby unless文にelsifはないよ。。。
だそうです。

不正データの入力防止の条件に加えて、重複防止の条件を追加したい。
しかし、unless文にelsifが使えないので、
if not文に変えてelsifを追加しました。

whereメソッド追加(ココがポイント!)

条件(処理)の流れは以下の通りです。

  • whereメソッドで、テーブル内の条件に一致したレコードの数を取得する
  • 取得したデータの数を条件にかけて1以上(データがあるか)判定する
  • 1以上(データがあり)ならば、受付けない

whereメソッドで、テーブル内の条件に一致したレコードの数を取得する

.where("条件")で条件にあうレコードを取得できます。

モデル名.where("条件")

.
今回、取得するのは Companyモデル の page_urlカラムです。

Company.where(page_url)

.
さらに、page_urlカラム内に入力したデータと一致するものを検索にかけたいので、
下記のように付け加えます。

Company.where(page_url: "#{f}")

.
 #{f}は変数内の文字列を表しています。
変数fは以下の場所で定義しており、中身はこのようになっています。

require 'rubygems'
require 'mechanize'

  def create
    f = (params[:wiki_url]) <= ココです。

以下、省略...

image.png

.
page_urlカラム内で検索をかけた結果、いくつあったか判定するために
countメソッドを追加します。

Company.where(page_url: "#{f}").count

取得したデータの数を条件にかけて1以上(データがあるか)判定する

1以上、つまり入力したデータと同じURLの数を数えて1以上あるか確認します。

Company.where(page_url: "#{f}").count >= 1

1以上(データがあり)ならば、受付けない

image.png

おまけ

アプリに反映させているので、是非遊んでみてください。
Unsung:hero
image.png

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

Rails Tutorial 第14章 完了、全部で 155.5時間かかりました。

2020/9/13 0.5時間

14.1.1完了です。

2020/9/14 0.5時間

14.1.2.1途中です。

2020/9/15 0.5時間

14.1.2完了です。

2020/9/16 0.5時間

14.8途中です。

2020/9/17 0.5時間

14.1.4完了です。

2020/9/18 0.5時間

14.1.5完了です。

2020/9/19 2.0時間

14.2.1完了し、演習 14.2.2.2の途中です。

2020/9/20 2.0時間

14.2.2完了し、14.27の途中です。

2020/9/21 2.0時間

14.2.3-4完了し、演習16.2.5.1の途中です。

2020/9/22 2.0時間

14.2.5-14.3.1完了です。

2020/9/24 0.5時間

14.44の途中です。
9/23は朝早く出社だったのでできませんでした。夜帰宅した後は疲れていてやる気がでないです。朝にやるのがよいと改めて感じました。

2020/9/25 0.5時間

演習14.3.2.3の途中です。
INNER JOINは意味は分かりますが、SQL文で書かれているのは初めて見ました。データベースの知識が昔のままなのだからかと思います。

2020/9/26 2.0時間

14.3完了です。

2020/9/27 0.5時間

14章を完了です。
所要時間は14.5時間です。

所要時間まとめ

1 章 22.5 時間
2 章 3.5 時間
3 章 5.5 時間
4 章 7.5 時間
5 章 7 時間
6 章 8 時間
7 章 10 時間
8 章 6 時間
9 章 11.5 時間
10 章 13 時間
11 章 16.5 時間
12 章 11 時間
13 章 19 時間
14 章 14.5 時間
計 155.5 時間 でした。

1章は環境設定で大変時間がかかりました。

今後

14.4.1に、拡張機能の課題がありましたので、これからやってみたいと思います。

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

関連付しているモデルのレコードが消せずハマった

作業環境

Rails '5.2.3'
Ruby '2.7.1'
PostgreSQL

何にハマったか

レビュー共有アプリ作成中にローカル環境で動作確認中、登録されているitemを削除しようとしましが

ActiveRecord::InvalidForeignKey in ItemsController#destroy
PG::ForeignKeyViolation: ERROR: update or delete on table "reviews" violates foreign key constraint "fk_rails_5350d1b47c" on table "comments" DETAIL: Key (id)=(6) is still referenced from table "comments". : DELETE FROM "reviews" WHERE "reviews"."id" = $1

というエラー、、。on table "comments"ってコメントテーブルなんてないぞ。と頭を悩ませていましたが、アプリ作成当初にreviewに対してコメント機能を作ろうと考えていたがreviewに対して一方的なコメント機能なんて必要ないのでは?と考え実装をやめたという過去があったのです(忘れていた)。
ですが、commentsテーブルは消したものと思いこんでおりハマってしまったのですね

ハマった背景

レビュー共有アプリをRailsで作成中、itemが複数のreviewを持っているという関係です。

item.rb
has_many :category_items, dependent: :destroy
has_many :categories, through: :category_items
has_many :reviews, dependent: :destroy
has_many :favorites, dependent: :destroy
accepts_nested_attributes_for :category_items
review.rb
belongs_to :user
belongs_to :item
has_many  :notifications, dependent: :destroy

エラーの原因は何か考える

ここからは初学者の考えた考察です。違っていたらご指摘いただけると幸いです。
itemを削除しようとすると同時にitemに関連したreviewも削除されます。
has_many :reviews, dependent: :destroy これ。
で今回は、item削除→review消える→commentテーブルが残っている上に

review.rb
(省略)
has_many  :comments, dependent: :destroy

という記述がないからreview消せない、よってitemも消せないという現象かと考えました。

ではこのエラーにどう向き合うか

まずはcommentsテーブル本当に残っているのかという確認からですよね。
rails db:schema:dumpでdb/schema.rbを更新。
その後schema.rbを見てみると

schema.rb
 create_table "comments", force: :cascade do |t|
   (省略) 
  end

commentsいた、、。不要なテーブルは消しましょう。

commentsテーブル削除

migrattionファイルを作成し、以下を記述して

def change
 drop_table :comments
end

rails db:migrateでさようなら。これでitem共にreviewの削除ができるようになりました!!

まとめ

たったこれだけで長々と書きましたが今回の件で得た教訓としては、機械は裏切らない、疑うのはまず自分であるということですね。思い込みはよくないです。
初投稿なので拙いところが多いですが、誰かの助けになればと思います。

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

【地図表示】Google Maps JavaScript APIとGeocoding APIを用いてユーザーが登録した住所から地図を表示する!

概要

Google Maps JavaScript APIとGeocoding APIを用いてユーザーが登録した住所から投稿詳細ページに地図を表示した時のことを備忘録として記録します。

環境

・ruby '2.5.7'
・rails '5.2.3'

前提

・Google MapsのAPIキーを取得済であること
・投稿モデル(ここではDatespotモデル)に住所(adress)カラムがあること

【参考】
Google MapsのAPIキーを取得する

過程

1.投稿詳細ページの作成

投稿詳細ページは、各自の仕様に合わせて作成してください。

views/show.html.erb
<div class="container">
  <div class="row">
  (省略)
    <div class="col-md-8">
      <h2 class="datespot-name"><%= @datespot.name %></h2>
      <div class="datespot-info">
    (省略)
        <h4 id="address">【住所】<%= @datespot.address %></h4>
    (省略)
      </div>
    </div>
  </div>
</div>
<%= render "map-show" %>

2.地図を表示するビューを作成

地図を表示するビューを作成します。

views/_map-show.html.erb
<div class="map-container">
  <div class="map_wrapper">
    <div id="map" class="map"></div>
  </div>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API_KEY']%>&callback=initMap"></script>

取得したAPIキーは、環境変数に入れておきましょう。

stylesheets/custom.scss
#map{
  height: 310px;
  width: 550px;
}

地図の大きさを明示的に指定しないと表示されないので、必ず指定しましょう。

3.コールバック関数を定義する

2.で記載したコールバック関数を定義します。

javascripts/map-show.js
function initMap() {
  //地図を表示する領域の div 要素のオブジェクトを変数に代入
  var target = document.getElementById('map');
  //マーカーのタイトル
  var title = $('.datespot-name').text();
  //HTMLに記載されている住所の取得
  var address = document.getElementById('address').textContent;
  //ジオコーディングのインスタンスの生成
  var geocoder = new google.maps.Geocoder();

  //geocoder.geocode() にアドレスを渡して、コールバック関数を記述して処理
  geocoder.geocode({ address: address }, function(results, status){
  //ステータスが OK で results[0] が存在すれば、地図を生成
    if (status === 'OK' && results[0]){
      //マップのインスタンスを変数に代入
      var map = new google.maps.Map(target, {
      //results[0].geometry.location に緯度・経度のオブジェクトが入っている
        center: results[0].geometry.location,
        zoom: 15
      });
      //マーカーの生成
      var marker = new google.maps.Marker({
        position: results[0].geometry.location,
        map: map,
        animation: google.maps.Animation.DROP
      });
      //取得した座標の生成
      var latlng = new google.maps.LatLng(results[0].geometry.location.lat(), results[0].geometry.location.lng());
      //情報ウィンドウに表示するコンテンツを作成
      var content = '<div id="map_content"><p>' + title + '<br/>' + address + '<br/><a href="https://maps.google.co.jp/maps?q=' + latlng + '&iwloc=J" target="_blank" rel="noopener noreferrer">Googleマップで見る</a></p></div>';
      //情報ウィンドウのインスタンスを生成
      var infowindow = new google.maps.InfoWindow({
        content: content,
      });
      //marker をクリックすると情報ウィンドウを表示(リスナーの登録)
      google.maps.event.addListener(marker, 'click', function() {
        //第2引数にマーカーを指定して紐付け
        infowindow.open(map, marker);
      });
    }else{
    //ステータスが OK 以外の場合や results[0] が存在しなければ、アラートを表示して処理を中断
      alert("住所から位置の取得ができませんでした。: " + status);
      return;
    }
  });
}

var target = document.getElementById('map');は、
views/map-show.html.erbの<div id="map" class="map"></div>を参照しています。

var title = $('.datespot-name').text();は、
views/show.html.erbの<h2 class="datespot-name"><%= @datespot.name %></h2>を参照しています。

var address = document.getElementById('address').textContent;は、
views/show.html.erbの<h4 id="address">【住所】<%= @datespot.address %></h4>を参照しています。

結果

これで、ユーザーが登録した住所から投稿詳細ページに地図を表示できました!
200927_地図の表示.png

参考

Google Maps API の使い方・利用方法
Google Mapsを使ってみよう

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

VPSのRailsに独自ドメインを適用し、SSL(https)化する(CentOS 8.2 / Nginx)

*この記事は自分のブログに投稿していた内容からの転載です。日頃、Qiitaの記事には大変お世話になっているため、私のブログ記事の中でもよくアクセスされる(=つまり参考にされていると思われる)ページをQiitaに投稿してわずかばかりでも恩返しとなればと思っています。

なお、前回投稿した以下の記事から連続して作業している状態です。ただし、前回の記事と直接的な繋がりはないため、本記事の作業だけを独立して実施しても大丈夫です。

「ConoHa VPS(CentOS 8.2)に Rails 6 + PostgreSQL + Nginx + Unicorn + Capistrano でデプロイする」

以下、早速スタートです。


CentOS(8.2)とNginxの環境を前提としています。作業開始時点はhttp://IPアドレスを入力するとRailsのアプリが表示されるようになっている状態が前提です。

また、お名前ドットコムですでに独自ドメインを取得している想定で書いています(どこのレジストラから取得していてもあまり関係はないですが)。

ではさっそく、以下手順です。

1.独自ドメインを適用する

1−1.お名前ドットコムのDNS設定を変更する

お名前ドットコムのDNSレコード設定のページで以下2つを追加。例としてドメインをexample.comにしている。

ホスト名 TYPE TTL VALUE
example.com A 3600 サーバのIPアドレス(xxx.xxx.xxx.xxx)
www.example.com CNAME 3600 example.com

上記実施後、レコード設定は下記となっているはず。

ホスト名 TYPE TTL VALUE
example.com NS 86400 01.dnsv.jp
example.com NS 86400 02.dnsv.jp
example.com NS 86400 03.dnsv.jp
example.com NS 86400 04.dnsv.jp
example.com A 3600 サーバのIPアドレス(xxx.xxx.xxx.xxx)
www.example.com CNAME 3600 example.com

設定が反映され次第、example.comおよびwww.example.comにアクセスすると、サーバにアクセスしに行くように変わっている。

しかし、この状態でexample.comにアクセスしてみると、Nginxのページが表示されてしまう。Nignxの設定も変更する必要がある。

*メモ:DNS設定について

ホスト名 TYPE TTL VALUE
example.com A 3600 サーバのIPアドレス(xxx.xxx.xxx.xxx)
www.example.com CNAME 3600 example.com

上記が意味しているのは、

  • example.comのIPアドレスはxxx.xxx.xxx.xxx
  • www.example.comexample.comの別名

ということで、つまりどちらを入力してもxxx.xxx.xxx.xxxにアクセスするようになる。

ここで、CNAME(別名)の設定を使わずに、

ホスト名 TYPE TTL VALUE
example.com A 3600 サーバのIPアドレス(xxx.xxx.xxx.xxx)
www.example.com A 3600 サーバのIPアドレス(xxx.xxx.xxx.xxx)

上記としても動きは全く同じになるが、意味合い上、CNAMEを使って設定するのが良い。

1−2.Nginxの設定を変更する

サーバに接続しrootユーザにスイッチした後、以下で設定ファイルを開く。

vim /etc/nginx/conf.d/アプリケーション名.conf

下記のような感じになっているはず。

/etc/nginx/conf.d/アプリケーション名.conf
upstream unicorn_アプリケーション名 {
  server unix:/var/www/アプリケーション名/current/tmp/sockets/unicorn.sock;
}
server {
  listen 80;
  server_name サーバのIPアドレス;
  root /var/www/アプリケーション名/current/public;
  access_log /var/log/nginx/アプリケーション名_access.log;
  error_log /var/log/nginx/アプリケーション名_error.log;
  location / {
    try_files $uri @unicorn;
  }
  location @unicorn {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://unicorn_アプリケーション名;
  }
}

上記のserver_nameのIPアドレスを、下記の通りexample.comに変更する。

/etc/nginx/conf.d/アプリケーション名.conf
server_name example.com;

変更後、設定ファイルに問題ないかをテスト。

nginx -t

問題なければ、Nginxを再起動する。

systemctl restart nginx

その後、http://example.comにアクセスすると、アプリのページが表示されるようになっている。

ここで、http://IPアドレスでアクセスすると、先ほどまではアプリが表示されていたのが、今度はNginxのページが表示されるようになっている。これは、先の設定ファイルを書き換えたことで、IPアドレスの記載がなくなった結果。

もう一度、Nginxの設定ファイルに手を加え、下記の動きを実現するように変更する。

  • example.comにアクセスするとexample.comのページが開く
  • www.example.comにアクセスすると(301リダイレクトで)example.comのページが開く
  • IPアドレスにアクセスすると(301リダイレクトで)example.comのページが開く

つまり、(www無しの)example.comにすべて片寄せするという動き。

再度ファイルを開き、

vim /etc/nginx/conf.d/アプリケーション名.conf

下記の通り設定ファイルのserver部分を更新する。

/etc/nginx/conf.d/アプリケーション名.conf
server {
  listen 80;
  server_name example.com;
  root /var/www/アプリケーション名/current/public;
  access_log /var/log/nginx/アプリケーション名_access.log;
  error_log /var/log/nginx/アプリケーション名_error.log;
  location / {
    try_files $uri @unicorn;
  }
  location @unicorn {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://unicorn_アプリケーション名;
  }
}
server {
  listen 80;
  server_name www.example.com サーバのIPアドレス;
  return 301 http://example.com$request_uri;
}

serverの記述をもう一つ追加し、server_nameには、www.example.comおよびサーバのIPアドレスを併記(両者の間には半角スペースを入れること)、そして301リダイレクトでexample.comに向けている。

$request_uriというのはその時のページを指している変数で、例えば、www.example.com/blogsにアクセスしたら、example.com/blogsにリダイレクトしている。

変更後、忘れずに設定内容テスト、問題なければNginxを再起動。

nginx -t
systemctl restart nginx

これでドメインでアクセスできるようにする設定は終わり。

2.SSL(https)化する

2−1.https接続を許可する(443番ポートを開放)

httpsを許可するために、443番ポートを開放する。

まず、firewalldの設定状況を確認してみる。

firewall-cmd --list-all

services: cockpit dhcpv6-client http ssh となっていることを確認。httpとsshが許可されている。

下記を実行。

firewall-cmd --add-service=https --permanent

firewalldをリロードして設定反映。

firewall-cmd --reload

変更確認のため再び実行。

firewall-cmd --list-all

services: cockpit dhcpv6-client http https ssh となっており、httpsが追加されている。

2−2.Certbotをインストールする

Certbotは、Let’s EncryptでのSSL証明書から設定までを自動で行ってくれるツール。

Certbotと、NginxでCertbotを使う時に必要なプラグインをインストール。

dnf -y install certbot python3-certbot-nginx

インストールできたか確認。

certbot --version

certbot 1.7.0などと表示されればOK.

2−3.SSL証明書の取得と設定を行う

Certbotを使ってSSL証明書の取得と設定を行う。

certbot --nginx

対話形式で設定内容に関する質問をされるので回答する。下記に質問内容と回答例をすべて載せておく。

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): xxxxxxxx@gmail.com #メールアドレスを入力

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: (A)gree/(C)ancel: A #A(同意)を選択

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N # N(いいえ)を選択

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: example.com
2: www.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1 # exmaple.jpをHTTPS化
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for example.com
Waiting for verification...
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/conf.d/アプリケーション名.conf
Redirecting all traffic on port 80 to ssl in /etc/nginx/conf.d/アプリケーション名.conf

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/example.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/example.com/privkey.pem
   Your cert will expire on 2020-XX-XX. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot again
   with the "certonly" option. To non-interactively renew *all* of
   your certificates, run "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

証明書を取得、設定できていることを確認する。以下を実行。

certbot certificates

example.comドメインの証明書情報が表示されればOK.

これでHTTPSで接続できるようになった。https://example.comでアクセスできることを確認する。

また証明書の期限は3ヶ月間のため、そのままだと3ヶ月後に失効する。クーロンのジョブを追加し、定期的に証明書を自動更新させることで実質的に永続利用できるようにする。以下を実行。

echo "0 0,12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

クーロンが設定されたことを確認するため、ファイルの内容を出力。

cat /etc/crontab

出力結果の最後に、「0 0,12 * * * root python3 -c ‘import random; import time; time.sleep(random.random() * 3600)’ && certbot renew -q」があることを確認。

クーロンのデーモンを再起動して設定を反映しておく。

systemctl restart crond

最後に、確認のためドライラン(=テスト)を実施してみる。

certbot renew --dry-run

結果の中に、「Congratulations, all renewals succeeded.」と書いてあればOK.

2−4.Nginxの設定を変更する

ここで、Nginxの設定ファイルを確認する。

vim /etc/nginx/conf.d/アプリケーション名.conf

そうすると、内容のうちserver部分が自動的に変わっていることが確認できる。これはCertbotが更新したため。以下は更新された後の例。

/etc/nginx/conf.d/アプリケーション名.conf
server {
  server_name example.com;
  root /var/www/アプリケーション名/current/public;
  access_log /var/log/nginx/アプリケーション名_access.log;
  error_log /var/log/nginx/アプリケーション名_error.log;
  location / {
    try_files $uri @unicorn;
  }
  location @unicorn {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://unicorn_アプリケーション名;
  }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
  listen 80;
  server_name www.example.com サーバのIPアドレス;
  return 301 https://example.com$request_uri;
}

server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


  listen 80;
  server_name example.com;
    return 404; # managed by Certbot


}

ただし、上記のままだと、https://www.example.comや、https://IPアドレス、でアクセスされた時にリダイレクトできていなかったり、インデントや記述位置をきれいに整えたかったりするので、以下の形に再整備した。

なお、「listen 443 ssl;」と1行で書くのと、「listen 443;」「ssl on;」と2行で書くのは同じ意味。

/etc/nginx/conf.d/アプリケーション名.conf
server {
  listen 443;
  ssl on;
  server_name example.com;
  root /var/www/アプリケーション名/current/public;
  access_log /var/log/nginx/アプリケーション名_access.log;
  error_log /var/log/nginx/アプリケーション名_error.log;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
  location / {
    try_files $uri @unicorn;
  }
  location @unicorn {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://unicorn_アプリケーション名;
  }
}
server {
  listen 443;
  server_name www.example.com サーバのIPアドレス;
  return 301 https://example.com$request_uri;
}
server {
  listen 80;
  server_name example.com www.example.com サーバのIPアドレス;
  return 301 https://example.com$request_uri;
}

これで、以下の場合は全て、https://example.comに301リダイレクトされるようになった。

  • https://www.example.com
  • https://IPアドレス
  • http://example.com
  • http://www.example.com
  • http://IPアドレス

設定を反映させるため、忘れずにテスト&Nginxを再起動させておく。

nginx -t
systemctl restart nginx

3.参考

Railsのconfig/environments/production.rbには以下のオプションがある。

config/environments/production.rb
# config.force_ssl = true

デフォルトでは、上記の通りコメントアウトされているはず。

このオプションのコメントアウトを解除して有効にすると、アプリにhttpでアクセスされた時にはhttpsにリダイレクトさせることができるようになる。つまり、Nginxで設定した内容と同じことが実現できるということ。

ただし、本来こういった処理はWebサーバ(Nginx)にて捌くものだと思うので、今回のようにWebサーバを使っている場合は、上記のRails機能は利用せず、Webサーバ側で対処しておく。

以上

無事にこれで終わり。

補足

記事をお読みいただきありがとうございました。

元のブログ記事へのリンクを以下に貼っておきますが、記載内容は本記事と同じため、特に参照いただく必要はないですし、むしろ、Qiitaのシンタックスハイライト機能があるため、Qiitaで記事を読んだ方がわかりやすいはずです。

knmts.com | VPSのRailsに独自ドメインを適用し、SSL(https)化する(CentOS 8.2 / Nginx)

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

【Mysql2】Mysql2::Error: Specified key was too long; max key length is 767 bytes→文字コードを適切にすることで解決する

ざっくり概要について

このエラーは「Mysql2に、格納できる文字データは、767バイトまでだよ!今のままでは、Mysqlにマイグレーションできないよ!」という意味で理解しました。

そのエラーを踏まえて、この記事では、以下2つの方法を実践しました。

その上で一番大事だなと思ったことは、 MySQLを使用する際、
「絵文字を使えるかどうか、要件定義の際にしっかり決める」ということです。

では、詳細にいきましょう!!!

エラー内容について

「Mysql2::Error: Specified key was too long; max key length is 767 bytes」というエラーが発生。

【ターミナル】

〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:reset                          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0174s
-- add_index(:users, :email, {:unique=>true})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate:reset => db:migrate
(See full trace by running task with --trace)

エラーが発生した状況について

環境
  • Rails 6.0.0を使用
  • データベースはMysql2を使用(アプリケーションはSequel Pro)
  • deviseというgemを使用(そのためモデルは、rails g devise userで作成していた)
  • deviseでは、マイグレーションファイルにおいて、string型(例:t.string :email)にあたるカラムを作成しようとしていた
  • マイグレーションファイルを作成し、rails db:migrate を実行したところ、エラー発生
初めは状況を理解できず、下記のことをさらに行ってしまった
  • rails db:migrate:statusで確認 → downになっている
  • 再度、rails db:migrate → エラー
  • rails db:migrate:reset → エラー ※上記はこの画面!

解決方法に行く前に、そもそも文字コードって何?「utf8」と「utf8mb4」って何?byteって何?

その概念が分からず、自分なりにですが、以下のように理解しました。

  • 文字コードとは、「あ」だったら1番、「い」だったら2番というように、文字に対して割り振っているコードのこと。(実際には進数など使って、もっと複雑そうですが・・・)

  • 「utf8」とは文字コードの中でも、世界で最も普及している文字コード。「utf8」は1~4バイトで文字を表現するが、MySQLでは3バイトの文字までしか扱えない。

  • 「utf8mb4」とは、データベースMySQLで扱うための文字コード。Mysqlにおいて、絵文字などは「utf8」の4バイトに当たるので、「utf8mb4」でないと絵文字が扱えない。

  • MySQLにおいて、保存できる文字数は「255文字」まで。

こちらから引用させていただきました

上記を組み合わせて、理解したこと

つまり、MySQLにおいて、

  • utf8は文字を扱う文字コード、utf8bm4は絵文字を扱う文字コード

可能な文字数は、

  • 「utf8」は「3バイト」使うので、「767バイト➗3バイト=255文字まで」

  • 「utf8mb4」は「4バイト」使うので、「767バイト➗4バイト=191文字まで」

stringにおけるデフォルト値が255文字なので、

  • 「3バイト❌255文字=765バイト」(utf8)

  • 「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

ということが、わかりました。

その上で、エラー解決方法へ

さて、エラー内容が起きた原因がすっきりしたところで、肝心の解決方法についてです。

冒頭で触れた通り、

まずは、上記方法で、解決しました!!!(ありがとうございます)

解決方法について、概要を話すと、

【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、767byte超えないように、(「4バイト❌191文字=764バイト」)文字数の上限を191文字に設定しようよ!という方法です。(絵文字が使えるように、utf8bm4はそのまま活かす方法)

以下のようなmysql.rbをconfig/initializer配下に新規作成します。(以下のコード含め、@terufumi1122さんの記事から引用しています。)

config/initializer/mysql.rb
require 'active_record/connection_adapters/abstract_mysql_adapter'

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter
      NATIVE_DATABASE_TYPES[:string] = { :name => "varchar", :limit => 191 }
    end
  end
end

上記ファイルを入力した後に、

【ターミナル】
rails db:migrate

をしたところ、無事にマイグレートされました!!!
(MySQLであるSequelProを見たところ、カラムができていました。)

そこから、考察したこと

さて、無事に解決したのですが、問題点に対して、767byteを超えない方法について、下記のような考え方もできないかなと思いました。

【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、もし、そのアプリケーションで絵文字を使わないのであれば、「3バイト❌255文字=765バイト」(utf8)に変更するのはどうか。つまり、191文字以下にするファイルを作成するのではなく、「config>database.yml」に記載している、文字コードを、「utf8bm4」から「utf8」に書き換えて、マイグレーションすればいいのでは!?

と思いました。

ここから実践する訳ですが、結論から言うと、「モデルから作り直しになる(マイグレーションの書き換えでは済まない)」ということを学びました。(つまり、面倒で大変です。)

「マイグレーションの書き換えで済まない」とは、「rails db:migrate」と共に、(指示をしていない)謎のマイグレーションファイルが出現し、そのマイグレーションファイルは不要なので、架空ファイルに変更して、ドロップやリセットを行いますが消そうとしても消えません。

「rails d devise user」でいったんモデルを消して、「rails g devise user」で作り直してやっと、エラーの出ない「utf8」文字コードのアプリケーションができました。

いざ、実践!(先ほどのエラー解決した状況から、上記考察を実践しています)

①いったんロールバック
〇〇@〇〇noMacBook-Air devise_app % rails db:rollback       
== 20200925212154 DeviseCreateUsers: reverting ================================
-- remove_index(:users, {:column=>:reset_password_token})
   -> 0.0099s
-- remove_index(:users, {:column=>:email})
   -> 0.0074s
-- drop_table(:users)
   -> 0.0040s
== 20200925212154 DeviseCreateUsers: reverted (0.0250s) =======================
②マイグレーションの状況を確認 (downになっているので、よし!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200925212154  Devise create users

③マイグレーションを実行(エラーを再現できたため、よし!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
④config>database.ymlをutf8に書き直す
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock
⑤rails db:resetを実行(良い感じ!)
〇〇@〇〇noMacBook-Air devise_app % rails db:reset  
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
⑥rails db:migrateを実行(良い感じ!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate
== 20200925212154 DeviseCreateUsers: migrarails db:migrateting ================================
-- create_table(:users)
   -> 0.0109s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0087s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0085s
== 20200925212154 DeviseCreateUsers: migrated (0.0283s) =======================
⑥rails db:migrate:status(おや? 000のファイルってなんだ?)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
   up     20200925212154  Devise create users
⑦「**NO FILE*」になってしまっているため、架空のファイル(Sample)にマイグレーションを修正して、ロールバックするも、000ファイルがdownにならない
〇〇@〇〇noMacBook-Air devise_app % rails db:rollback STEP=2
〇〇@〇〇noMacBook-Air devise_app % 
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             Sample
  down    20200925212154  Devise create users
⑦resetしてステータスを確認するも、000ファイルがdownにならない
〇〇@〇〇noMacBook-Air devise_app % rails db:reset          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
  down    20200925212154  Devise create users

⑧モデルを削除する
〇〇noMacBook-Air devise_app % rails d devise user   
Running via Spring preloader in process 10337
      invoke  active_record
      remove    db/migrate/20200925212154_devise_create_users.rb
      remove    app/models/user.rb
      invoke    test_unit
      remove      test/models/user_test.rb
      remove      test/fixtures/users.yml
       route  devise_for :users
⑨モデルを作り直す
〇〇noMacBook-Air devise_app % rails g devise user    
Running via Spring preloader in process 10485
      invoke  active_record
      create    db/migrate/20200926073222_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users
⑩マイグレーションを行う
〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200926073222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0275s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0434s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0108s
== 20200926073222 DeviseCreateUsers: migrated (0.0821s) =======================
11ステータスを確認(やっとできた!!!!!)
〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200926073222  Devise create users

以上です。

終わりに

考察を実践してみましたが、
database.ymlの記載情報を変更するには、ロールバッグでは済まず、モデル作り直しになることがわかりました。

今回はモデルに大した内容を書いていなかったため、影響なしでしたが、
もしこれが、モデル含めたテーブル・カラム・レコード・マイグレーションなどの作り直しとなると手間がかかり大変ですよね。

utf8含めて、database.ymlの編集は、(一番初めに「rails db:create」を行う前に)しっかり要件定義をした上で、設定することが大事だと学びました。

見ていただいて、有り難うございました。
もし謎ファイルの原因わかる方や、誤っていることを載せていたら、ぜひ教えてください。

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

【Mysql2】Mysql2::Error: Specified key was too long; max key length is 767 bytes→文字制限をかけるファイルを新規作成することで解決する

ざっくり概要について

このエラーは「Mysql2に、格納できる文字データは、767バイトまでだよ!今のままでは、Mysqlにマイグレーションできないよ!」という意味で理解しました。

そのエラーを踏まえて、この記事では、以下2つの方法を実践しました。

その上で一番大事だなと思ったことは、 MySQLを使用する際、
「絵文字を使えるかどうか、要件定義の際にしっかり決める」ということです。

では、詳細にいきましょう!!!

エラー内容について

「Mysql2::Error: Specified key was too long; max key length is 767 bytes」というエラーが発生。

【ターミナル】

〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:reset                          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0174s
-- add_index(:users, :email, {:unique=>true})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate:reset => db:migrate
(See full trace by running task with --trace)

エラーが発生した状況について

環境
  • Rails 6.0.0を使用
  • データベースはMysql2を使用(アプリケーションはSequel Pro)
  • deviseというgemを使用(そのためモデルは、rails g devise userで作成していた)
  • deviseでは、マイグレーションファイルにおいて、string型(例:t.string :email)にあたるカラムを作成しようとしていた
  • マイグレーションファイルを作成し、rails db:migrate を実行したところ、エラー発生
初めは状況を理解できず、下記のことをさらに行ってしまった
  • rails db:migrate:statusで確認 → downになっている
  • 再度、rails db:migrate → エラー
  • rails db:migrate:reset → エラー ※上記はこの画面!

解決方法に行く前に、そもそも文字コードって何?「utf8」と「utf8mb4」って何?byteって何?

その概念が分からず、自分なりにですが、以下のように理解しました。

  • 文字コードとは、「あ」だったら1番、「い」だったら2番というように、文字に対して割り振っているコードのこと。(実際には進数など使って、もっと複雑そうですが・・・)

  • 「utf8」とは文字コードの中でも、世界で最も普及している文字コード。「utf8」は1~4バイトで文字を表現するが、MySQLでは3バイトの文字までしか扱えない。

  • 「utf8mb4」とは、データベースMySQLで扱うための文字コード。Mysqlにおいて、絵文字などは「utf8」の4バイトに当たるので、「utf8mb4」でないと絵文字が扱えない。

  • MySQLにおいて、保存できる文字数は「255文字」まで。

こちらから引用させていただきました

上記を組み合わせて、理解したこと

つまり、MySQLにおいて、

  • utf8は文字を扱う文字コード、utf8bm4は絵文字を扱う文字コード

可能な文字数は、

  • 「utf8」は「3バイト」使うので、「767バイト➗3バイト=255文字まで」

  • 「utf8mb4」は「4バイト」使うので、「767バイト➗4バイト=191文字まで」

stringにおけるデフォルト値が255文字なので、

  • 「3バイト❌255文字=765バイト」(utf8)

  • 「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

ということが、わかりました。

その上で、エラー解決方法へ

さて、エラー内容が起きた原因がすっきりしたところで、肝心の解決方法についてです。

冒頭で触れた通り、

まずは、上記方法で、解決しました!!!(ありがとうございます)

解決方法について、概要を話すと、

【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、767byte超えないように、(「4バイト❌191文字=764バイト」)文字数の上限を191文字に設定しようよ!という方法です。(絵文字が使えるように、utf8bm4はそのまま活かす方法)

以下のようなmysql.rbをconfig/initializer配下に新規作成します。(以下のコード含め、@terufumi1122さんの記事から引用しています。)

config/initializer/mysql.rb
require 'active_record/connection_adapters/abstract_mysql_adapter'

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter
      NATIVE_DATABASE_TYPES[:string] = { :name => "varchar", :limit => 191 }
    end
  end
end

上記ファイルを入力した後に、

【ターミナル】
rails db:migrate

をしたところ、無事にマイグレートされました!!!
(MySQLであるSequelProを見たところ、カラムができていました。)

そこから、考察したこと

さて、無事に解決したのですが、問題点に対して、767byteを超えない方法について、下記のような考え方もできないかなと思いました。

【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ!!:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、もし、そのアプリケーションで絵文字を使わないのであれば、「3バイト❌255文字=765バイト」(utf8)に変更するのはどうか。つまり、191文字以下にするファイルを作成するのではなく、「config>database.yml」に記載している、文字コードを、「utf8bm4」から「utf8」に書き換えて、マイグレーションすればいいのでは!?

と思いました。

ここから実践する訳ですが、結論から言うと、「モデルから作り直しになる(マイグレーションの書き換えでは済まない)」ということを学びました。(つまり、面倒で大変です。)

「マイグレーションの書き換えで済まない」とは、「rails db:migrate」と共に、(指示をしていない)謎のマイグレーションファイルが出現し、そのマイグレーションファイルは不要なので、架空ファイルに変更して、ドロップやリセットを行いますが消そうとしても消えません。

「rails d devise user」でいったんモデルを消して、「rails g devise user」で作り直してやっと、エラーの出ない「utf8」文字コードのアプリケーションができました。

いざ、実践!(先ほどのエラー解決した状況から、上記考察を実践しています)

①いったんロールバック
〇〇@〇〇noMacBook-Air devise_app % rails db:rollback       
== 20200925212154 DeviseCreateUsers: reverting ================================
-- remove_index(:users, {:column=>:reset_password_token})
   -> 0.0099s
-- remove_index(:users, {:column=>:email})
   -> 0.0074s
-- drop_table(:users)
   -> 0.0040s
== 20200925212154 DeviseCreateUsers: reverted (0.0250s) =======================
②マイグレーションの状況を確認 (downになっているので、よし!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200925212154  Devise create users

③マイグレーションを実行(エラーを再現できたため、よし!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
④config>database.ymlをutf8に書き直す
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock
⑤rails db:resetを実行(良い感じ!)
〇〇@〇〇noMacBook-Air devise_app % rails db:reset  
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
⑥rails db:migrateを実行(良い感じ!)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate
== 20200925212154 DeviseCreateUsers: migrarails db:migrateting ================================
-- create_table(:users)
   -> 0.0109s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0087s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0085s
== 20200925212154 DeviseCreateUsers: migrated (0.0283s) =======================
⑥rails db:migrate:status(おや? 000のファイルってなんだ?)
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
   up     20200925212154  Devise create users
⑦「**NO FILE*」になってしまっているため、架空のファイル(Sample)にマイグレーションを修正して、ロールバックするも、000ファイルがdownにならない
〇〇@〇〇noMacBook-Air devise_app % rails db:rollback STEP=2
〇〇@〇〇noMacBook-Air devise_app % 
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             Sample
  down    20200925212154  Devise create users
⑦resetしてステータスを確認するも、000ファイルがdownにならない
〇〇@〇〇noMacBook-Air devise_app % rails db:reset          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
  down    20200925212154  Devise create users

⑧モデルを削除する
〇〇noMacBook-Air devise_app % rails d devise user   
Running via Spring preloader in process 10337
      invoke  active_record
      remove    db/migrate/20200925212154_devise_create_users.rb
      remove    app/models/user.rb
      invoke    test_unit
      remove      test/models/user_test.rb
      remove      test/fixtures/users.yml
       route  devise_for :users
⑨モデルを作り直す
〇〇noMacBook-Air devise_app % rails g devise user    
Running via Spring preloader in process 10485
      invoke  active_record
      create    db/migrate/20200926073222_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users
⑩マイグレーションを行う
〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200926073222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0275s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0434s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0108s
== 20200926073222 DeviseCreateUsers: migrated (0.0821s) =======================
11ステータスを確認(やっとできた!!!!!)
〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200926073222  Devise create users

以上です。

終わりに

考察を実践してみましたが、
database.ymlの記載情報を変更するには、ロールバッグでは済まず、モデル作り直しになることがわかりました。

今回はモデルに大した内容を書いていなかったため、影響なしでしたが、
もしこれが、モデル含めたテーブル・カラム・レコード・マイグレーションなどの作り直しとなると手間がかかり大変ですよね。

utf8含めて、database.ymlの編集は、(一番初めに「rails db:create」を行う前に)しっかり要件定義をした上で、設定することが大事だと学びました。

あとは、今回「767byte」を超えない方法を考察しましたが、@terufumi1122さんの記事には「767byte」を超えてもいいようにするともありました。

エラーを解決するには、アプリケーションに目的に沿って、いろんな方面から柔軟に見る必要があることと、その引き出しを増やすために「WHY」の視点が大事だと改めて感じました。

見ていただいて、有り難うございました。
もし謎ファイルの原因わかる方や、誤っていることを載せていたら、ぜひ教えてください。

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

Rails APIサーバー開発時に入れたGemの使い方 (rack-cors, rspec-rails, factory_bot_rails, rubocop)

前提

docker-composeを用いたRailsのAPIサーバー環境構築
この記事でGemについてはあまり触れなかったので続き

Gemfileの編集

下記のGemをインストールする。

...
gem 'rack-cors'

group :development, :test do
...
  gem 'rspec-rails', '~> 3.9'
  gem 'factory_bot_rails'
end

group :development do
...
  gem 'rubocop', require: false
end

rack-cors

postman使う時に必要。
やっておかないとCORS問題発生してエラー出る。

config > initializers > cors.rb
を以下のように書くと、どこからでもAPI叩けるようになる。

cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

rspec-rails, factory_bot_rails

モデルやコントローラーをgenerateするときに自動生成するファイルがあるのであらかじめ入れておいた方が良い。

rspec-rails GitHub
Rails 5ではrspec-railsの3系を使用する。(Rails 6では4~)

bundler installしたら、以下で初期ファイル?インストール

rails generate rspec:install
…
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

また、.rspecとapplication.rbに以下を追加

参照 RailsアプリへのRspecとFactory_botの導入手順

.rspec
--color
--require rails_helper
--format documentation
application.rb
    config.generators do |g|
      g.test_framework :rspec,
                       view_specs: false,
                       helper_specs: false,
                       controller_specs: false,
                       routing_specs: false

モデルのテストは自動生成されたが、
結合テストはRequest specで書くために手動でファイルを作った。

spec > requests > hoge_api_spec.rb
のように名付けた。

リクエストスペックは以下のような構成で書いた

hoge_spec.rb
RSpec.describe 'HogeAPI' do
  describe 'POST #create' do # アクション名を示す
    context 'xxxな場合' do # 条件
      before do
        # ダミーのデータを用意
        FactoryBot.create(:hoge)
        ...
      end

      it 'yyする' do
        expect do # DBの変更を検知するときはくくる
          post '/hoge/create', params: { name: "Hoge" }
        end.to change(Hoge, :count).by(+1) and change(Table2, :count).by(0)
        expect(response.status).to eq(201) # ステータスコードの確認
      end
    end

参考

【rspec】Railsモデルテストの基本
【Rails】APIテストの書き方
RSpecを使ってAPIのテストを行う
Railsプロジェクトで、FactoryBotを用いたテストデータを作成する方法

ダメだったapplication.rbの書き方

application.rbのgeneratorsはcontrollerを生成するためにcontrollerのテストファイルを生成しないようにしたいだけなので、
Everyday Rails - RSpecによるRailsテスト入門を参考に

application.rb
# NG
    config.generators do |g|
      g.test_framework :rspec,
                       controller_specs: false

と最初は書いていたが、コントローラー生成は期待通りだったもののなぜかモデルのテストファイルがエラーで生成できなくなった。

rubocop

rubocopはVSCodeでFormatする時にも使うのでローカルにもインストールした。

ファイルの書き方が正しいかどうかを確認してくれる。

// 確認したい時
rubocop

// 確認して直せるところは自動でなおすとき
rubocop -a

めっちゃ警告でるので、警告の内容をみて不要であればスキップするように設定する。

.rubocop.yml
AllCops:
    Exclude: # 除外したいファイルは Exclude に指定する。
      - 'spec/*.rb'
      - 'db/schema.rb'
      - 'test/*'
      - 'config/**/*'
      - 'Gemfile'
      - 'bin/*'


Style/FrozenStringLiteralComment:
    Enabled: false

Style/Documentation:
    Enabled: false

Style/StringLiterals:
    Enabled: false

Metrics/BlockLength:
    Exclude:
        - 'spec/**/*'

Metrics/MethodLength:
    Max: 30

Metrics/AbcSize:
    Max: 30

Style/AsciiComments:
    Enabled: false

AllCopsは全ての警告に共通する設定

参考

RailsでAPIにCORSを設定する
RuboCopをRailsオプションやLintオプションで使ってみよう

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