20201216のRailsに関する記事は30件です。

[Ruby on Rails] macで環境構築をしよう

環境

macOS Catalina バージョン 10.15.7

今回インストールするバージョン
Ruby 2.6.6
Ruby on Rails 6.1.0

手順

Homebrewのインストール

Homebrewは、macOSオペレーティングシステム上のパッケージ管理システムのひとつです。
Railsで必要なものをインストールするためにHomebrewを使うので入っていない場合はインストールしましょう。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールされたか確認するコマンド

$ brew help

Homebrewで使えるコマンド一覧が表示されればインストール成功です。

rbenvのインストール

rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使うことを可能としてくれるツールです。

$ brew install rbenv ruby-build

インストールされたか確認するコマンド

$ rbenv --version

rbenv 1.1.2 などと表示されればインストール成功です。(数字はバージョンによって異なります)

次にターミナルを起動した際にrbenvの初期化を自動で行うように設定をします。

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi' >> ~/.bash_profile
$ source ~/.bash_profile

Rubyのインストール

インストール可能なRubyのバージョン確認をします。

$ rbenv install -l

インストールしたいバージョン(今回は2.6.6)が一覧に表示されていたらインストールを続けていきます。
他のバージョンをインストールする場合は2.6.6の部分を置き換えればそのバージョンがインストールされます。

rubyのインストールは少し時間がかかるので注意!

$ rbenv install 2.6.6

インストールされたか確認するコマンド

$ rbenv versions

system
* 2.6.6

このように表示されればインストール成功です。

次にrubyのバージョンを確認してみます。

$ ruby -v

rubyのバージョンを確認してみて2.6.6になっていない場合は、先程インストールした2.6.6に切り替える作業をしていきます。

$ rbenv global 2.6.6
$ rbenv rehash

使用するバージョンを切り替えることができたのかを確認するコマンドを実行してみましょう。

$ ruby -v

ruby 2.6.6p146

このように表示されれば成功です。

bundlerをインストール

Gemとは?

rubyのパッケージ管理ツールです。gemを容易に管理でき、gemを配布するサーバの機能も持ちます。

例えばWebアプリケーションにユーザー登録やログイン機能などのユーザー認証機能を実装したい時、これらを一からコードを書くのはとても時間がかかってしまいますが、「devise」と呼ばれるgemをインストールするだけで簡単にユーザー認証で必要な機能が使えます。

bundler とは

bundlerとは、gemのバージョンやgemの依存関係を管理してくれるgemの一つです。 bundlerを使うことで、複数人での開発やgemのバージョンが上がってもエラーを起こさずに開発できます。

以下を実行することによって、bundlerをインストールできます。
インストールしていない場合はインストールしましょう。

$ gem install bundler

インストールされたか確認するコマンド

$ bundler -v

Railsのインストール

次に、 Rails のインストールです。まだしていない場合はインストールしましょう。

$ gem install rails

インストールされたか確認するコマンド

$ rails -v

Railsアプリケーションの開発

作成するアプリケーション用のディレクトリを作成します。今回はsampleという名前で作成します。
作成したらそのディレクトリの中に移動しましょう。

$ mkdir sample
$ cd sample

Railsで開発を始めるには、rails new アプリケーション名 というコマンドを実行します。
このコマンドを実行することで、入力したアプリケーション名と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意されます
今回はsample_appという名前のアプリケーションを作成していきます。

$ rails new sample_app

開発中のアプリケーションをブラウザで表示するためには、サーバーを起動する必要があります。サーバーの起動は、以下のコマンドを実行するだけで完了です。
(サーバーを起動する場所に注意!! cd sample_app  でカレントディレクトリを変更して起動)

$ rails s

サーバーを起動した後、ブラウザでlocalhost:3000というURLにアクセスすると、Railsの初期画面が表示されるようになります。

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

[Rails] renderとredirect_toの違い

render

・部分テンプレートファイル
_form.html.erbのように_から始まります。

・ビューファイル内で部分テンプレートを呼び出すとき
<%= render partial: "form" %> _と拡張子を除いた形で呼び出します。
partialは部分テンプレートを指定するオプションです。

・部分テンプレート内で変数を使いたい時
<%= render partial: ‘form’, locals: { form: @form } %>
のように、localsオプションを用いて、
locals: { 部分テンプレート内で使いたい変数: 持っていきたい値 }
と記述します。

・部分テンプレート内で繰り返し処理を行いたい時
<%= render partial: ‘form’, collection: @forms %>
collectionオプションに@formsと複数形の変数を渡すと、部分テンプレートでformが個別のインスタンスとして呼ばれる変数となり、さらにeach文を使用せずに繰り返し処理も行ってくれます。

<%= render @forms %>
と略して書くことも可能です。

redirect_to

redirect_to root_pathのように使います。

違い

render   →  ビュー
redirect_to  →  ルーティング → コントローラー →  ビュー

表示されるまでのプロセスが違います。

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

FormObjectをうまく使いこなしたい

はじめに

Rails Advent Calendar 2020 16日目の記事です!

業務で毎日、Railsを触れていいコードが書けるように精進しています。

そんな中、今年一扱いに苦戦した(今も苦戦している)FormObjectについて、自分が考えることを共有しておきたいと思います!!

FormObjectとは

そもそもFormObjectについて、「なにそれ????」と思う方もいると思うでの簡単に紹介します!

簡単に言うと、FormObjectは、Railsのデザインパターン設計手法の1つです。

MVC

Railsの基本設計は、MVC(Model, View, Controller)です。

Viewは、ユーザーからの入力を受け付けます。その情報をControllerにわたし、Controllerはその情報を整形・制御します。
そして、Controllerは整形・制御した情報をModelに対して渡してあげます。
Modelは、Controllerから受け取った情報をもとにDBにアクセスし、データの保存・変更・削除を行い、
その結果をControllerViewに返します。
返ってきた結果は、Controllerで整形・制御しViewに表示させたり、制御・整形せずそのまま表示したりします。

これが基本的なMVCの流れですが、、、以下のようなことがあると思います。

  1. 一つのViewで複数のModelの属性を入力してもらい、DBを更新したい。
  2. ModelやDBの制限とは別にView側で制限をかけたい。
  3. ControllerでのViewから受け取った情報の整形が複雑化してしまって、見辛くなった。 ...

これらの実装をMVCだけで行おうとすると、ControllerModelに責務が偏ってしまいます。
これでは、コードが追いづらくなったり、影響範囲が大きくなったりしてしまいます。

そうすると、アプリの保守性が低いことになってしまうので、

新しい機能を追加したり、、、
修正したり、、、
DBの構造を変えたり、、

するときに大変苦労な思いをすることになってしまいます。

それらを防ぐための手法の一つとして、FormObjectという概念が存在します。

メリット・デメリット

FormObjectを使用する一番の目的は、責務を切り離すことだと思います。

Controllerの肥大化の原因のひとつである、Viewからの情報を整形するロジックFormObjectでカプセル化する事で、
Controllerの責務が一つ減ることになります。
これでControllerは、肥大化せずに済みますし、単一責務の状態になるので依存関係が少なくなり、保守性は高くなります。

また、Modelの状態に関係ないロジックをModelにかくと、Modelが肥大化してしまいます。そういったロジックをFormObjectに書くことで、
Modelの責務が一つ減り、肥大化を防ぐことができます。

このように、ControllerModelに偏りがちなロジックをFormObjectにまとめることで、過剰責務にならずに済みます!!

ただ、上記のことはデメリットにもなり得ると思います。

やたら滅多に、FormObjectにロジックを集約させると、今度はFormObjectが肥大化してしまいます
これでは本末転倒です。。。
ControllerModelViewに関するロジックをFromObjectに任せて、責務を切り離すことを目的に使用しているにもかかわらず、
FormObjectの責務がでかくなってしまって、そこの保守性が低くなるのは良くありません。

そのため、使用する前はしっかりとFormObjectの責務を明確にしてから使用しましょう!

FormObjectの責務とは???

FormObjectがなんなのか大体わかってきました。

かなり便利なFromObjectですが、、、いろいろな記事をみて、実際に書いていく中で、ふと思ったことがあります。。。

「ここってFomObjectでやるべき???」
「ControllerやModelのロジックとかぶってない???」
「FormObjectの責務って、、、結局どこまで持てばいいの???」

これらを思ったきっかけを実際のコードを例に説明していきたいと思います。

FormObjectのvalidate

FormObjectを使用するとき、ActiveModel::ModelをincludeしてればModel同様にvalidateを書けることができます。
例えば、記事の登録でタグ情報も入力するフォームがあったとします。

フォームでは、記事のタイトルと内容、タグの名前が必須であった場合、以下のようにかけます。

class ArticlePostForm
  include ActiveModel::Model
  attr_accessor :article_body, :article_title, :tag_name

  validates :article_body, presence: true
  validates :article_title, presence: true
  validates :tag_name, presence: true
end

これでFormの入力に対して制限を設けることができました。ただ、記事とタグのModelにも以下のようなvalidateが設けてありました。

class Article < ApplicationRecord
  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
  validates :is_display, inclusion: [true, false]
end

class Tag < ApplicationRecord
  validates :name, presence: true, uniqueness: true
end

FormObjectでのvalidateとModelでのvalidateでは、意味は違います。
FormObjectでのvalidateでは入力された値に対する制限なのに対して、ModelでのvalidateではDBに保存する際の制限です。

ただ、今回では検証している属性がFormObjectModelでかぶってしまっているので、

FormObjectのvalidateは必要なのか?

という疑問があります。

FormObjectでのsave・update

記事を見ているとよく見かけるものです。
以下のようにFormObjectでActiveRecordの作成を行うsaveメソッドがあります。

class ConractForm
  include ActiveModel::Model
  attr_accessor :first_name, :last_name, :email, :body

  def save
    return false if invalid?

    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end
end

Controllerでは、以下のように書けるはずです。

class ContactController < ApplicationController
  def new
    @contact = ContactForm.new
  end

  def create
    @contact = ContactForm.new(set_params)
    if @contact.save
      flash.now = 'お問い合わせを承りました'
      redirect_to :contacts
    else
      flash.alert = '送信に失敗しました'
      render :new
    end
  end

  private

  def set_params
    params.require(:contact).permit(:first_name, :last_name, :email, :body)
  end
end

ここで、FormObjectModelの生成・更新の役割を持っていいのかどうかに疑問を持ちました。
FormObjectは、入力に対してロジックを持たせるのが一般的だと思います(間違っていたらすいません。。。)

ここで僕はFomObjectに対しての責務で、

「Viewの入力に対しての制御のみでModelの生成は別」と「validateをかけているからModelの生成までがいい」

の2通りの考え方できると思ったので、どちらに寄せるべきなのかを迷いました。

FormObjectのエラーハンドリング

Modelの生成において、validateによって発生したエラーをFormObjectで取り扱うかどうかがどうなのか疑問に思いました。

ModelのエラーとFormObjectのエラーは分けたほうがいいのか、一緒にすべきなのかが一番悩ましいところです。。。

(ここは僕もまだ考えがまとまっていないので、知見が増えたらまた共有します。)

def save
    # ここはFormObjectの入力のエラー
    return false if invalid?

    # ここはModel生成時のエラー
    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end

まとめ

FormObjectに対しての責務は、その都度考え、その責務以上のことをさせないように実装することが大事だと思っています。

ただ、僕自身もFormObjectを完璧に把握・理解しているわけではなく、他にも設計思想があるので、そこらへんと組み合わせた
FormObjectの考えもあるのかなと感じています。

参考サイト

Form Object という選択肢を検討してみる

【Rails】FormObjectを使ってほしい

Railsのデザインパターン: Formオブジェクト

【Rails】Form Objectを使ってModelに依存しないFormを作成する

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

rails:db:migrateを実行すると「Directly inheriting from ActiveRecord::Migration is not supported.」と出た

<この記事について>
自作のアプリ製作中にカラムを追加する流れで「rails:db:migrate」を実行すると「Directly inheriting from ActiveRecord::Migration is not supported.」と表示されエラーに。
初歩的なミスでしたが備忘録として投稿!

[環境]
・Ruby 2.6.5,
・Rails 6.0.0
・macOS

rake aborted!
StandardError: An error has occurred, all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

調べてみると、原因はマイグレーションファイルがrailsのバージョンに対応していないことが原因のようでした。。

対象のマイグレーションファイルの1行目末尾にrailsのバージョンを追加。

class AddAttachmentImageToPosts < ActiveRecord::Migration[6.0] #←[6.0]を追加
  def self.up
    change_table :posts do |t|
      t.attachment :image
    end
  end

  def self.down
    remove_attachment :posts, :image
  end
end

その後、再度以下を実行すると解決しました。

$ rails db:migarate

<最後に>
実装してエラーが出る、思うような結果が得られないことは多々あると思います。
そんな時は冷静な気持ちでエラー文と丁寧に向き合うようにしましょう!

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

エラーメッセージの日本語化(rails)

rails-i18n(Gem)

 日本語に対応できるようになるGemのこと。

localeファイル

 エラーメッセージを全て日本語にするために必要なファイルで、さまざまな言語に対応できるファイルのこと。この中に日本語化用のファイルを作成することで、英語を日本語に翻訳できる。

実装方法

①日本語の言語設定

config/application.rb
require_relative 'boot'

require 'rails/all'

Bundler.require(*Rails.groups)

module Asakatu5757
  class Application < Rails::Application
    config.load_defaults 6.0
    config.i18n.default_locale = :ja

    config.time_zone = 'Tokyo'
  end
end

 今回追加した記述は「config.i18n.default_locale = :ja」で、標準言語の日本語にすることができる。

②rails-i18nの導入

Gemfile
gem 'rails-i18n'
ターミナル
% bundle install

③locales内に日本語化用のファイルを作成し、編集する。

例)Userモデルのnicknameカラムを日本語化する場合。

config/locales/ja.yml
ja:
 activerecord:
   attributes:
     user:
       nickname: ニックネーム
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでオブジェクト配列をハッシュ化するあれこれ(injectとかindex_byとか)

例えばUserモデルがあり、nameやらemailやらageやらを各オブジェクトが持っていたとする。

ある特定のageの人たちだけに施したい処理があって、どの年齢が対象となるかは既に配列のグループがあったとする。(若干例え悪いけどそんな感じだった)

最初、以下の様な処理を書いていた。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]

targets.each do |target|
  User.all.each do |user|
    # 一致するものがあれば次の処理へ
    next if target != user.age
    user.nextaction
  end
end

これだとeachで二重に回すことになるので、もっと簡単にできないものかと考えた時に、ageをキーとするハッシュつくればいいのでは??となった。

そして最初、injectでハッシュを生成したのがこちら。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]
# ageをキーとしてUser.allをハッシュ化
users_sort_age = User.all.inject({}) {|hash,user| hash[user.age] = user; hash }
targets.each do |target|
  # targetをキーとした(=targetと同じ値のageが存在した)場合次の処理に飛ばす
  next if users_sort_age[target].blank?
  user.nextaction
end

これでもできるんだけど、index_byを使うともっとみやすくなりました。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]
# ageをキーとしてUser.allをハッシュ化をなんとこれだけで表している。
users_sort_age = User.all.index_by(&:age)
targets.each do |target|
  next if users_sort_age[target].blank?
  user.nextaction
end

非常にみやすくなった。
ただしindex_byはActiveRecordでサポートされているものなので、デフォルトのRubyではinjectionを使いましょうという話。

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

Rails + Grape + Grape SwaggerでちゃんとOpenAPI/Swaggerドキュメンテーションしようとしたらハマったこと

この記事はCBcloud Advent Calendar 2020 の12日目の記事です。

Rails + Grape + Grape Swaggerで、実際に業務でOpenAPIドキュメントを生成してメンバーに共有しようとしたときにハマったことについて紹介します。

まずはおさらい: OpenAPI、Swaggerってなんだっけ?

OpenAPI

OpenAPIのリポジトリには下記のように記述されています。
https://github.com/OAI/OpenAPI-Specification

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

つまりOpenAPIは、プログラミング言語に依存しないHTTP APIのインタフェースを記述するための仕様です。
その仕様に従い記述されたドキュメントは、人が読んでも理解できるし、それを解析していい感じのUIを提供することもできるようになっている、ということです。

Swagger

Swaggerのページには下記のように記述されています。
https://swagger.io/about/

Swagger is a powerful yet easy-to-use suite of API developer tools for teams and individuals, enabling development across the entire API lifecycle, from design and documentation, to test and deployment.
〜略〜
Swagger started out as a simple, open source specification for designing RESTful APIs in 2010. Open source tooling like the Swagger UI, Swagger Editor and the Swagger Codegen were also developed to better implement and visualize APIs defined in the specification.
〜略〜
In 2015, the Swagger project was acquired by SmartBear Software. The Swagger Specification was donated to the Linux foundation and renamed the OpenAPI

つまりSwaggerはAPI開発者のためのツール群を提供するプロジェクトということですね。そして、SwaggerのAPI定義のための仕様がLinux foundationに寄付され、OpenAPIに改名されたとのことです。

この記事が対象とする範囲

上記を踏まえ、この記事でやろうとしていること(そしてハマったこと)は、Rails + Grapeの構成で開発しているプロジェクトで、Grape SwaggerのDSLを使ってOpenAPI/Swagger仕様に沿ったJSONを生成することです。
また、その生成したJSONを読み込んで見やすくしてくれるツールで見たときに、意図した構成として表示されることを目指します(そしてハマります)

使用したgem

ドキュメント生成にあたり使用したgemは下記の通りです。

gem 'grape' # RESTful APIを開発するためのDSLを備えたフレームワーク
gem 'grape-swagger' # Grape APIからのドキュメント生成
gem 'grape-entity' # Grapeフレームワークにレスポンス整形のツールを加える
gem 'grape-swagger-entity' # grape-entityからのドキュメント生成

設定と使い方は各リポジトリをご参照のこと。
https://github.com/ruby-grape/grape
https://github.com/ruby-grape/grape-swagger
https://github.com/ruby-grape/grape-entity
https://github.com/ruby-grape/grape-swagger-entity

ハマったこと

1. descブロックのparamsと、descと同階層のparamsは別物

descと同階層のparamsではバリデーションをしてくれる

Grapeを使っているとき、下記のような書き方をすることがあると思います。
これはuser_nameは必須パラメータであることを指しており、実際にリクエストボディを空にしてリクエストを投げると、400エラーになってuser_name is missingとして返してくれます。

class Users < Grape::API
  resources :params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

descブロック内のparamsではバリデーションはしてくれない

次にGrape Swaggerでドキュメンテーションしていこうとすると、下記のような記述方法が出てきます。
これにも先程と同様にリクエストボディ無しでリクエストを投げると、今度は400エラーにはならず201が返ってきます。

class Users < Grape::API
  resources :params_whitin_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
end

ただしUIで見ると見た目はほぼ同じ

上記をSwagger UIで見たときにどのような差があるか比べてみます。
どちらもユーザ名が必須であることは表現できています。しかし2つ目のエンドポイントでは実際には必須になっていないので注意が必要です。

image.png
image.png

両方にparamsを書いたら、キーが一致していればdesc内のドキュメンテーションが優先されるが、一致していないものはそれぞれ出力される

それでは両方に書くとどうなるでしょうか。さらにそれぞれでしか定義されていない、age、blood_typeというキーを追加してみました。

class Users < Grape::API
  resources :params_in_same_layer_and_desc_block do
    desc 'descブロック内と、descの同階層の両方にparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
      optional :age, type: Integer, documentation: { desc: '年齢', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
  expose :blood_type, documentation: { desc: '血液型(エンティティで定義)', type: 'string' }
end

Swagger UIで見るとこうなります。キーが一致している場合は、ドキュメンテーションはdesc内のものが優先され、一致していない場合は、それぞれ出力されるようです。

image.png

2. 複雑な構成のリクエストをUIでちゃんと表示しようと頑張ったが、結局は使用するUIの挙動次第

複雑なパラメータのパターンでUIがどうなるか確かめてみます。またdescブロック内に記述した場合と同階層に書いたエンドポイントを用意します。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :addresses, type: Array[JSON], documentation: { desc: '届け先情報', type: 'array', collectionFormat: 'multi' } do 
        requires :name, type: String, documentation: { desc: '届け先名', type: 'string' }
        requires :address, type: String, documentation: { desc: '届け先住所', type: 'string' }
        requires :tags, type: Array[JSON], documentation: { desc: 'タグ', type: 'array', collectionFormat: 'multi' } do
          optional :name, type: String, documentation: { desc: 'タグ名', type: 'string'}
        end
      end
    end
    post do
      present hoge: 'fuga'
    end
  end

  resources :complex_params_in_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params ComplexUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class ComplexUserParamsEntity < Grape::Entity
  class TagEntity < Grape::Entity
    expose :name, documentation: { desc: 'タグ名(エンティティで定義)', type: 'string' }
  end

  class AddressEntity < Grape::Entity
    expose :name, documentation: { desc: '届け先名(エンティティで定義)', type: 'string' }
    expose :address, documentation: { desc: '届け先住所(エンティティで定義)', type: 'string' }
    expose :tags, documentation: { desc: 'タグ(エンティティで定義)', type: 'array', is_array: true }, using: TagEntity
  end

  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string' }
  expose :addresses, documentation: { desc: '届け先情報(エンティティで定義)', type: 'array', is_array: true }, using: AddressEntity
end

Swagger UIで確認してみます。一方は配列内が表示されず、もう一方は表示されるけど階層構造がわかりにくい表示です。

image.png
image.png

見やすくできないかあがいてみます。全パラメータにparam_type: 'body'を追加してみます。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string', param_type: 'body' }
# 以下略

再びSwagger UIで確認します。すると、Entityで定義した方は相変わらず階層構造が失われています。Entityの書き方が悪いのかもしれません。
そしてもう一方は、階層構造がわかりやすくなりましたが、説明が反映されていません。

image.png
image.png

そこで、ふと他のOpenAPIを解釈できるUIを試してみます。使うのはこちら。
https://github.com/Redocly/redoc

Entityの方は相変わらずですが、説明がちょっとマシになっています。
そして同階層の方はなんと完全に思い通りの表示になっています!!(配列のキーにつけた「届け先情報」という説明もちゃんと表示されています)

image.png

image.png

このことからわかるのは、実際の表示は使用するUIツールによるので使用するツールでちゃんと表示されるようになることを確認しましょう、ということです。

3. exampleを出力するにはワークアラウンドが必要

exampleを指定しても出力されないのでなぜだろうと思ったら下記のissuesがありました。
現時点でOpenなので解決されるまではissues内に投稿されているワークアラウンドを入れればできるようです。
https://github.com/ruby-grape/grape-swagger/issues/762

おわりに

リクエストパラメータのところのエンティティの使い方なんかはまだちゃんと理解できていないので間違い等あったらご指摘いただければ助かります。

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

nilガードについて理解してみた

Rubyの勉強をしているなかでnilガードというものがでてきたので勉強の記録用として記事にしてみました。

認識違いがあった場合などは是非ご指摘下さい。

nilガード

Rubyにはnilになってしまう状態を防ぐためにnilガードという書き方が存在します。簡単に説明すると仮に変数がnilだった場合には値を入れるということです。

ということで分かりやすく下記に例を記載したいと思います。

# 例1
> number = nil
> number ||= 6
> puts number
=> 6

# 例2
> number = 5
> number ||= 6
> puts number
=> 5

初めにnilガードを実装するにパイプ演算子||と=を組み合わせます。
例1ではnumberがnilなら6を代入しています。しかし例2ではnumberがnilではないので代入が行われていません。ここでnilガードというものはnilかfalseでないと代入が行われないと分かりました。

まとめ

nilガードは変数にnilが入っているかもしれない状況でnilの代わりに何らかのデフォルト値を入れておきたいという場面で、とても便利に利用できます。

参考

・ 現場で使える Ruby on rails 5 速習実践ガイド
https://www.sejuku.net/blog/19044

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

Webpacker::Manifest::MissingEntryError

概要

このようなエラーが起きた時ひとつの解決策としてお役に立てれば幸いです。

環境

Mac OS Catarina 10.15.7
rails 6.0系
ruby 2.6.5

エラー文

1. You want to set webpacker.yml value of compile to true for your environment
   unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:

image.png

試したこと

1.webpackをインストール
2.yarnが入っているかの確認
3.yarnのupgrade
4../bin/webpack-dev-serverでサーバー立ち上げ

application.html.erbjsを読み込む記述を消せばエラーは消えるのですが、jsを読み込むことができなくなってしまいます。

解決策

nodeのバージョンが異なっているためエラーが起きておりました。

ゴールはnodeのバージョンを合わせてサーバーが立ち上がることを確認することです。

解決の流れ

1.nodeのバージョン確認
2.nodeのダウングレードするために削除・再インストールする
3.nodeのバージョン確認
4.インストールしたnodeが反映されていなかったらPATHを通す
5.Webpackerをインストールする
6.yarnが必要というエラーが起きたらyarn install
7.再度Webpackerをインストール
8.サーバーを立ち上げて確認

詳細

1.nodeのバージョン確認
2.nodeのダウングレードするために削除・再インストールする
3.nodeのバージョン確認

ターミナル
% node -v
% brew uninstall --ignore-dependencies nodejs
% brew install node@14
% node -v

4.インストールしたnodeが反映されていなかったらPATHを通す

ターミナル
% vim ~/.zshrc 
% source ~/.zshrc
vim
export PATH="/usr/local/opt/node@14/bin:$PATH" 

5.Webpackerをインストールする

ターミナル
% rails webpacker:install

6.yarnが必要というエラーが起きたらyarn install

ターミナル

========================================
  Your Yarn packages are out of date!
  Please run `yarn install --check-files` to update.
========================================
ターミナル
% yarn install

7.再度Webpackerをインストールしてみる

ターミナル
% rails webpacker:install

8.サーバーを立ち上げて確認

ターミナル
% rails s

まとめ

  • Webpackerをインストールするにはyarnが必要。
  • バージョンの違いで読み込めないということも起こることがある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(解決方法)NoMethodError in Devise::SessionsController#destroy

エラー文

NoMethodError in Devise::SessionsController#destroy
undefined method `remember_created_at=' for #User:0x00007f84757e1ea8 Did you mean? remember_me=

Extracted source (around line #432):
else
        match = matched_attribute_method(method.to_s)
        match ? attribute_missing(match, *args, &block) : super
      end
    end
    ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)

エラー内容の詳細

NoMethodError in Devise::SessionsController#destroy

devise sessionsコントローラーのdestroyアクションが定義されていません。
仮説①deviseがおかしい?

undefined method `remember_created_at=' for #User:0x00007f84757e1ea8 Did you mean? remember_me=

remember_created_at=は定義されていない。remember_me=ではないか?
remember_created_at=はマイグレーションファイルのデフォルトの記載で書いてあったような、、、
仮説②マイグレーションファイルのremember_created_at=をremember_me=へ修正したら解決されるのでは?

解決方法

仮説②がかすっていました。マイグレーションファイルのデフォルトの記述を手違いで消してしまっており、今回のエラーを発生させていました。そのため、マイグレーションファイルのデフォルトの記述を書いてあげたら、エラーは解決されました。
仮説①は、無関係でした。deviseに関するエラー文が出てきても、deviseは完璧と仮定し、その他の要因から疑っていったほうが良いみたいです。

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

Railsアプリケーションに2要素認証を導入する

はじめに

Zeals Advent Calendar 2020の16日目の記事です。

Zealsでバックエンドエンジニアをやってる高久田です。本日はRailsアプリケーションに対して2要素認証を導入するあたって、どのような方法があるのか調査したことについて記事にしようと思います。
主にAWS cognito ユーザープールの内容になるので Amazon Cognito ユーザープール からがメインになります。

背景

みなさんが普段使っている、もしくは開発しているサービスには2要素認証が導入されていますか?
email, passwordのみでログインできるものや、最近だとGoogleやFacebookを利用したSNSログインで完了できるものがほとんどではないでしょうか
最近だと不正ログインにより、大きな問題になったサービスもいくつか耳にしたことがあると思います。
その中でよく2段階認証という言葉を聞くと思うのですが、今回タイトルに書いてある2要素認証とは何が違うのでしょうか?

まずは「認証要素」について説明します。
「認証要素」とは認証を行うために必要な認証方法の種類になります。

  • 知識要素

知っていることや記憶していること(パスワードや秘密の質問)

  • 所持要素

自分が持っているデバイスに送信されてくる情報
Google Authenticatorを使う方法やSMSなどで送信されてくるメッセージを利用する

  • 生体要素

指紋認証や顔認証

この3つの認証要素のうち2つ以上の認証要素を利用したものが2要素認証、多要素認証と呼ばれます。
2段階認証はemail, passwordを入力後、秘密の質問に答えて認証を完了するようなものになり、複数の段階を踏んで認証を完了させるものになります。

海外では2段階認証という言葉あまり使われないらしく、また安全性を考えるうえで必要なのは段階の数ではなく、認証要素の数になります。

2FA

それでは実際にRailsアプリケーションに対して2FAを実装していきます。

環境

  • ruby 2.6.6
  • Rails 5.2.4.4

devise

Railsアプリケーションでログイン機能を作成しているのであればほとんどの人がdeviseを利用しているのではないでしょうか?
deviseを利用しているのならば、deviseを拡張するgemを導入することで2FAを実装することができます。

devise-two-factor 簡単に2要素認証を導入することができる
rqrcode QRコードの生成を行う

または
google-authenticator Google Authenticatorと統合できる

など既にあるgemを利用する形で実装することができます。

Amazon Cognito ユーザープール

Amazon Cognito ユーザープール とはアプリケーションで必要な認証機能を提供してくれるサービスになります。

自前で認証機能を実装する必要がなく、クラウドで提供されているものを利用していきます。
sdk も提供されているので、これを利用していきます。

クライアント作成

def initialize
 cognito_client = Aws::CognitoIdentityProvider::Client.new(
   region: ENV['COGNITO_REGION'],
   access_key_id: ENV['COGNITO_ACCESS_KEY_ID'],
   secret_access_key: ENV['COGNITO_SECRET_ACCESS_KEY']
  )
end

ユーザープール作成

ユーザプールは Amazon Cognito のユーザディレクトリです。ユーザープールを使用すると、ユーザーは Amazon Cognito を通じてウェブまたはモバイルアプリにログインできます。また、ユーザーは Google、Facebook、Amazon、Apple などのソーシャル ID プロバイダー、および SAML ベースの ID プロバイダー経由でユーザープールにサインインすることもできます。ユーザーが直接またはサードパーティーを通じてサインインするかどうかにかかわらず、ユーザープールのすべてのメンバーには、Software Development Kit (SDK) を通じてアクセスできるディレクトリプロファイルがあります。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html

def create_user_pool(user_pool_name)
  res = client.create_user_pool(
    pool_name: user_pool_name,
    policies: {
      password_policy: {
        minimum_length: 8,
        require_uppercase: false,
        require_lowercase: false,
        require_numbers: false,
        require_symbols: false,
        temporary_password_validity_days: 1
      }
    }
  )
  res.user_pool.id
end

アプリクライアントの作成

パスワードの登録、サインインなどのAPI操作を呼び出すには、アプリケーションクライアントIDとクライアントシークレットが必要になります。承認されたクライアントアプリのみがこれらの未認証操作を呼び出すことができるようにします。

def create_cognito_app_client(user_pool_id)
  res = client.create_user_pool_client(
    user_pool_id: 'ap-northeast-1_XXXXXXXXX',
    client_name: 'cognito-app-client',
    explicit_auth_flows: ['ADMIN_NO_SRP_AUTH'],
    prevent_user_existence_errors: 'ENABLED'
  )
  res.user_pool_client.client_id
end

sign_up

def sign_up(email, password, app_client_id)
  res = client.sign_up(
    client_id: app_client_id,
    username: email,
    password: password
  )
  res.user_sub
end

sign_in

def sign_in(email, password, app_client_id)
  res = client.admin_initiate_auth(
    user_pool_id: 'ap-northeast-1_XXXXXXXXX',
    client_id: app_client_id,
    auth_flow: 'ADMIN_NO_SRP_AUTH',
    auth_parameters: {
      USERNAME: email,
      PASSWORD: password
    }
  )
  res.authentication_result.access_token
end

だいたいの基本的な操作をするコードをこのようなものになります。

多要素認証を有効化する場合はユーザープールの設定を変更することで有効化することができます。

Screen Shot 2020-12-16 at 18.59.10.png

省略可能にチェックするとユーザー毎にMFAを適応するかどうかを設定することができます。
第2の要素としてSMSテキストメッセージかワンタイムパスワード(Google Authenticator)を設定することができます。
これで認証要素のうちの所持要素を確認することができます。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-mfa.html

2要素認証を設定した後は、送信されてくる/Google Authenticatorで登録した確認コードを使って認証を行うことができます。

resp = client.confirm_sign_up({
  client_id: app_client_id,
  username:  "user_name",
  confirmation_code: "*******",
  force_alias_creation: false,
})

cognitoでMFAを容易に設定できるだけではなく、ログインページ自体が提供されていたり、既存の認証基盤からcognitoへ移行する仕組みなども提供されています。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-migrate-user.html

まとめ

普段Railsを触っているとどうしてもRails内でどのように問題を解決するかに意識が向いてしまっていたのですが、Cognitoなどを利用することによって認証部分などを外部サービスを利用して切り出していくことができると思います。
サービスが大きくなっていったり、複数のサービスを提供するようになった際に、それぞれのサービスで認証基盤を作成するのは工数がかかります。
その際に楽できるところは楽していきたいですね!

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

railsでグラフ機能を導入

はじめに

今回学習記録をとるアプリ開発をしていたところ学習状況をグラフ化したい!
と思い比較的簡単に実装ができたので備忘録として残します。
筆者はrails6を使っての実装となります。
初学者のため間違え等ありましたらご指摘頂けると幸いです!

手順1 gem chartkickを導入

グラフ機能を簡単に実装するにはgemのchartkickを使います。
gemファイルに以下を追加しbundleinstallします。
groupdateはDBから様々な情報をグループ化して取り出せる便利なメソッドがあるので導入してますが、chartkickを使うだけであればgem "chartkick"のみで問題ありません。

gem "chartkick"
gem "groupdate"

rails6の場合は以下コマンドを実行

yarn add chartkick chart.js

手順2 jsファイルの読み込み

chartkickではjsを使って実装するためjsファイルをapp/javascript/packs/application.jsに記述します。
rails6の場合

require("chartkick")
require("chart.js")

rails5の場合

//= require chartkick
//= require Chart.bundle

手順3 htmlにグラフを挿入

ここまで行けばあとは公式ページにあるサンプルコードをhtmlに記述するだけで簡単にグラフを作成する事ができます。
私の場合このように使いました。
<%= column_chart @tweets.group_by_day_of_week(:created_at, format: "%a").count %>
https://gyazo.com/4c24263ba93c7e3bc2ea6974bfb0143f
ここでgroup_by_day_of_weekはgemのgroupdateを導入していると使えるメソッドです。
今回使ったのは棒グラフですが他にも折れ線や円グラフも簡単に実装できるのでchartkickの公式サイト見て見てください!!

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

【ActiveAdmin】レコードが無い場合の、まだありません。「作成する」を非表示に【blank_slate content link】

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

has_manyで中間テーブルから親モデルの別のassociationを取ってきたい

概要

Railsを使用してデータを扱う際、殆どこんな機会は訪れない(別の方法などで代替できることがほとんど)ですが、どうしよもない事情で中間テーブルから関連付けを貼る必要があったので備忘録としてメモっておきます。

事前知識

class User < ApplicationRecord
   has_many :group_users
   has_many :groups, through: :group_users
   has_many :comments
end

class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
end

このようなクラスが存在しているとします。
通常であればGroupUserという中間テーブルを起点として何かすることはほとんどないですし、あったとしてもUserやGroup経由でCommentを取得することができるので、困ることはほとんどありません。

一方、GraphQLを使用していたり、何かアソシエーションのタイミングで整ったデータが欲しいなどの稀な事情でGroupUserのインスタンスからgroupとuserが絞り込まれたcommentの一覧が欲しい場合があったりしました。

普通にthroughをして取得しようとすると、例えば下記のようなコードになります。

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
  has_many :comments, through: :user #groupでもいい
end

ただ、この方法だとthroughしたどちらかのカラムの値では絞り込まれますが、もう片方のカラムでは絞り込みが行われません。

どちらでthroughしたとしても、userかgroupのどちらかでの絞り込みしか行われず、意図した値は取得できません。

解決策

じゃあどうするのかというと、socpeを使います。

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
  has_many :comments,->(group_user) { where(group_id: group_user.group_id) }, through: :user #groupならuserでwhere
end

has_manyはスコープの引数として自分自身だけは取ることができます。なので、このようにすることでgroup_userインスタンスのgroup_idとuser_idを持ったコメントの一覧を取得することができます。

どうしても中間テーブルを起点にして取得しなければいけない状況になった場合に役立てば幸いです。

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

Pinterestのように写真を表示する

はじめに

絶賛ポートフォリオ製作中の駆け出しエンジニアです

インスタグラムのような古着専用の写真投稿サイトを作っています

その時に写真の投稿一覧画面でPinterest(https://www.pinterest.jp/) というサイトのように写真を並べるために奮闘しましたので備忘録として残したいと思います

コード

before

post.html.erb
 <div class = contents-row >
    <% @items.each do |item| %>
      <ul class='item-lists'>
          <li class='list'>
              <%= link_to items_path(item.id), method: :get do %>
                <%= image_tag(item.image, class:"item-img") %>
              <% end %>
          </li>
      </ul>
    <% end %>
  </div>
post.scss
.contents-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  width: 95vw;
}

.item-lists {
  margin: 5px;
}

.list {
  width:25vw;
}

.item-img {
  width: 100%;
  height: auto;
  border-radius: 30px;
  vertical-align: bottom;
}

after

post.html.erb
 <div class = contents-row >
  <ul class='item-lists'>
    <% @items.each do |item| %>
          <li class='list'>
              <%= link_to items_path(item.id), method: :get do %>
                <%= image_tag(item.image, class:"item-img") %>
              <% end %>
          </li>
    <% end %>
  </ul>
  </div>
post.scss
.contents-row {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  width: 95vw;
}

.item-lists {
  column-count: 3;
  column-gap: 15px;
  column-fill: auto;
 }

.list {
  width:25vw;
}

.item-img {
  width: 100%;
  height: auto;
  border-radius: 30px;
  vertical-align: bottom;
  margin: 5px;
}

行ったこと

• 【html】 ulタグがeach文の中に入っていたのでeach文の外に出した
• 【css】 item-listsにcolumn-countなどを記述し、それに合わせてそれぞれ調整した

参考サイト

https://designsupply-web.com/media/suplog/1862/

https://www.pinterest.jp/

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

【Rilas】ページネーション機能を実装で勉強したこと。

一覧表示画面にページネーション機能をつけた際に勉強になった事を備忘録として残しています。

前提

ページネーション機能をつける前のビューなどです。

コントローラー

事前にソート機能をつけています。

app/controller/stocks_controller.rb
class StocksController < ApplicationController
  helper_method :sort_column, :sort_direction

  def index
    @calendar = Calendar.find(params[:calendar_id])
    @stocks = @calendar.stocks.page.order("#{sort_column} #{sort_direction}")
    @products = Product.all
  end

  private

  def sort_direction
    %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc'
  end

  def sort_column
    Stock.column_names.include?(params[:sort]) ? params[:sort] : 'id'
  end

end

ビュー

app/views/stocks/index.html.erb
    <table>
      <thead>
        <tr>
          <th scope="col"><%= sort_order "display", "陳列" %></th>
          <th scope="col"><%= sort_order "publisher", "出版社名" %></th>
          <th scope="col"><%= sort_order "magazine_name", "雑誌名" %></th>
          <th scope="col"><%= sort_order "num", "冊数" %></th>
          <th scope="col"><%= sort_order "price", "本体価格" %></th>
          <th scope="col"><%= sort_order "i_form", "発行形態" %></th>
          <th scope="col"><%= sort_order "purchased", "買切雑誌" %></th>
        </tr>
      </thead>
      <tbody>
        <% @stocks.each do |stock| %>
          <% if stock.num > 0 %>
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
          <% end %>
        <% end %>
      </tbody>
    </table>

ページネーションの実装

今回kaminariというページネーション用のgemを使って実装しました。
意外と簡単に実装できました〜。

kaminariのインストール

Gemfileにgem 'kaminari'を追加し、$ bundle installでインストールします。

Gemfile
gem 'kaminari'

gemをインストールした後は一度サーバを再起動しましょう。
Ctrl-cでサーバを止め、rails sでサーバを起動できます。
また、忘れてて右往左往しました・・・。

これでkaminariのインストールは完了です。

ページネーションを表示させる

お次はcontrollerです。
ページネーションを表示させたいデータに.page(params[:page])を追加します。

app/controllers/stocks_controller.rb
def index
  # ページネーションをつけたいデータに.page(params[:page])を追加
  @stocks = @calendar.stocks.page(params[:page]).order("#{sort_column} #{sort_direction}")
end

余談(1.ページに表示するレコード数の変更)

1ページに表示するレコード数は初期値では25件なのだそうです。
controllerに.per(表示したいレコード数)を追加すると変更できるそうです。

例えば、30件表示したい場合は.per(30)と追加。

app/controllers/stocks_controller.rb
def index
  # .per(30)を追加
  @stocks = @calendar.stocks.page(params[:page]).per(30).order("#{sort_column} #{sort_direction}")
end

って感じです。

最後に、viewでページネーションを表示させたいところへ
<%= paginate @stocks %>を追加します。

app/views/stocks/index.html.erb
    <%= paginate @stocks %>
    <table>
 〜〜 省略 〜〜
    </table>

余談(2.表示を日本語に変更する)

ページ番号の前後についている<<Firstとか<Prveは判りづらいと感じたので
こちらを日本語に変更していきます。

config/application.rb
# 〜〜省略〜〜
module Teiki29770(アプリ名です)
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    # 日本語の言語設定
    config.i18n.default_locale = :ja
    config.time_zone = 'Tokyo'

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

サーバを再起動しましょう!
config/localesに日本語変換用のymlファイルja.ymlを作成して、
以下のコードを追加してみます。

config/locales/ja.yml
ja:
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: " &rsaquo;"
      truncate: "..."

これで表示を日本語に変更できました!やった!!
と、思っていたら・・・。

表示数にズレが出てしまう。

スクリーンショット 2020-12-16 16.17.28.png
なんか少ない・・・。30件表示できるはずなのに・・・。
調べてみると他のページも表示数はまちまちでした。

ビューでの条件式が原因

コントローラーで指定している通り、30件づつデータを送って
ビューのほうで条件式に合うデータのみを表示していることが原因でした。

app/views/stocks/index.html.erb
 〜〜 省略 〜〜
      <tbody>
        <% @stocks.each do |stock| %>
          <% if stock.num > 0 %>⇦ここが原因。
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
          <% end %>
        <% end %>
      </tbody>
    </table>

なので、コントローラーの方で条件に合うデータだけを先に抽出しておいて
そちらを30件ずつビューへ送る様にします。

app/controller/stocks_controller.rb
  def index
    @calendar = Calendar.find(params[:calendar_id])
    @stocks = @calendar.stocks.where("num > ?",0).page(params[:page]).per(30).order("#{sort_column} #{sort_direction}")

  end
app/views/stocks/index.html.erb
 〜〜 省略 〜〜
      <tbody>
        <% @stocks.each do |stock| %>
          〜〜 if文を削除 〜〜
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
        <% end %>
      </tbody>
    </table>

これで、しっかりと条件に沿ったデータを表示することができました!
スクリーンショット 2020-12-16 18.33.41.png
ちゃんとソートもできます!

勉強になったこと

MVCの関係性の大事さと共に、役割分担をしっかりとすることの大切さを再確認できました。
ありがとう、ページネーション!

参考にさせていただいたサイト様

https://qiita.com/rio_threehouse/items/313824b90a31268b0074
ありがとうございます!

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

【Rilas】ページネーション機能の実装で勉強したこと。

一覧表示画面にページネーション機能をつけた際に勉強になった事を備忘録として残しています。

前提

ページネーション機能をつける前のビューなどです。

コントローラー

事前にソート機能をつけています。

app/controller/stocks_controller.rb
class StocksController < ApplicationController
  helper_method :sort_column, :sort_direction

  def index
    @calendar = Calendar.find(params[:calendar_id])
    @stocks = @calendar.stocks.page.order("#{sort_column} #{sort_direction}")
    @products = Product.all
  end

  private

  def sort_direction
    %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc'
  end

  def sort_column
    Stock.column_names.include?(params[:sort]) ? params[:sort] : 'id'
  end

end

ヘルパーメソッド(追記)

大事なヘルパーメソッドの記述を抜かっておりました・・・。

app/helper/stocks_helper.rb
module StocksHelper
  def sort_order(column, title)
    css_class = (column == sort_column) ? "current #{sort_direction}" : nil
    direction = (column == sort_column && sort_direction == 'asc') ? 'desc' : 'asc'
    link_to title, { sort: column, direction: direction }, class: "sort_header #{css_class}"
  end
end

ビュー

app/views/stocks/index.html.erb
    <table>
      <thead>
        <tr>
          <th scope="col"><%= sort_order "display", "陳列" %></th>
          <th scope="col"><%= sort_order "publisher", "出版社名" %></th>
          <th scope="col"><%= sort_order "magazine_name", "雑誌名" %></th>
          <th scope="col"><%= sort_order "num", "冊数" %></th>
          <th scope="col"><%= sort_order "price", "本体価格" %></th>
          <th scope="col"><%= sort_order "i_form", "発行形態" %></th>
          <th scope="col"><%= sort_order "purchased", "買切雑誌" %></th>
        </tr>
      </thead>
      <tbody>
        <% @stocks.each do |stock| %>
          <% if stock.num > 0 %>
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
          <% end %>
        <% end %>
      </tbody>
    </table>

ページネーションの実装

今回kaminariというページネーション用のgemを使って実装しました。
意外と簡単に実装できました〜。

kaminariのインストール

Gemfileにgem 'kaminari'を追加し、$ bundle installでインストールします。

Gemfile
gem 'kaminari'

gemをインストールした後は一度サーバを再起動しましょう。
Ctrl-cでサーバを止め、rails sでサーバを起動できます。
また、忘れてて右往左往しました・・・。

これでkaminariのインストールは完了です。

ページネーションを表示させる

お次はcontrollerです。
ページネーションを表示させたいデータに.page(params[:page])を追加します。

app/controllers/stocks_controller.rb
def index
  # ページネーションをつけたいデータに.page(params[:page])を追加
  @stocks = @calendar.stocks.page(params[:page]).order("#{sort_column} #{sort_direction}")
end

余談(1.ページに表示するレコード数の変更)

1ページに表示するレコード数は初期値では25件なのだそうです。
controllerに.per(表示したいレコード数)を追加すると変更できるそうです。

例えば、30件表示したい場合は.per(30)と追加。

app/controllers/stocks_controller.rb
def index
  # .per(30)を追加
  @stocks = @calendar.stocks.page(params[:page]).per(30).order("#{sort_column} #{sort_direction}")
end

って感じです。

最後に、viewでページネーションを表示させたいところへ
<%= paginate @stocks %>を追加します。

app/views/stocks/index.html.erb
    <%= paginate @stocks %>
    <table>
 〜〜 省略 〜〜
    </table>

余談(2.表示を日本語に変更する)

ページ番号の前後についている<<Firstとか<Prveは判りづらいと感じたので
こちらを日本語に変更していきます。

config/application.rb
# 〜〜省略〜〜
module Teiki29770(アプリ名です)
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    # 日本語の言語設定
    config.i18n.default_locale = :ja
    config.time_zone = 'Tokyo'

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

サーバを再起動しましょう!
config/localesに日本語変換用のymlファイルja.ymlを作成して、
以下のコードを追加してみます。

config/locales/ja.yml
ja:
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: " &rsaquo;"
      truncate: "..."

これで表示を日本語に変更できました!やった!!
と、思っていたら・・・。

表示数にズレが出てしまう。

スクリーンショット 2020-12-16 16.17.28.png
なんか少ない・・・。30件表示できるはずなのに・・・。
調べてみると他のページも表示数はまちまちでした。

ビューでの条件式が原因

コントローラーで指定している通り、30件づつデータを送って
ビューのほうで条件式に合うデータのみを表示していることが原因でした。

app/views/stocks/index.html.erb
 〜〜 省略 〜〜
      <tbody>
        <% @stocks.each do |stock| %>
          <% if stock.num > 0 %>⇦ここが原因。
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
          <% end %>
        <% end %>
      </tbody>
    </table>

なので、コントローラーの方で条件に合うデータだけを先に抽出しておいて
そちらを30件ずつビューへ送る様にします。

app/controller/stocks_controller.rb
  def index
    @calendar = Calendar.find(params[:calendar_id])
    @stocks = @calendar.stocks.where("num > ?",0).page(params[:page]).per(30).order("#{sort_column} #{sort_direction}")

  end
app/views/stocks/index.html.erb
 〜〜 省略 〜〜
      <tbody>
        <% @stocks.each do |stock| %>
          〜〜 if文を削除 〜〜
                <tr>
                  <td><%= stock.display %></td>
                  <td><%= stock.publisher %></td>
                  <td><%= stock.magazine_name %></td>
                  <td><%= stock.num %></td>
                  <td><%= stock.price.to_s(:delimited) %></td>
                  <td><%= stock.i_form %></td>
                  <td><%= stock.purchased %></td>
                </tr>
        <% end %>
      </tbody>
    </table>

これで、しっかりと条件に沿ったデータを表示することができました!
スクリーンショット 2020-12-16 18.33.41.png
ちゃんとソートもできます!

勉強になったこと

MVCの関係性の大事さと共に、役割分担をしっかりとすることの大切さを再確認できました。
ありがとう、ページネーション!

参考にさせていただいたサイト様

https://qiita.com/rio_threehouse/items/313824b90a31268b0074
ありがとうございます!

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

Ruby on Railsプロジェクトの開発環境をDocker化する

ACCESS Advent Calendar 2020 の16日目の記事です。

概要

現在運用中のRuby on Railsプロジェクトの開発環境をDocker化する案件があり、
その際に行った作業の手順をまとめました。

Dockerについて

Dockerとは、コンテナと呼ばれる仮想環境を構築・実行できるようにするためのプラットフォームです。

私は今回初めてDockerを触ったのですが、Dockerの理解には入門Dockerが大変参考になりました。

前提

バージョン

  • macOS Catalina 10.15.7
  • Docker 19.03.13
  • docker-compose 1.27.4
  • Ruby 2.4.5
  • mongoDB 3.0.15
  • postgres 10

システム構成

こちらのシステム構成図の通りにDocker化していきます。
(※大分簡略化しています)

system.png

初めにRailsアプリケーションのDocker Containerを作成し、
その後オーケストレーションツールであるdocker-composeによって、
RailsアプリケーションをmongoDB及びpostgreSQLに接続します。

手順

Dockerのインストール

Docker HubよりDocker Desktop for Macを導入します。

ターミナルで下記の2つのコマンドが実行できればOKです。

$ docker -v
Docker version 19.03.13, build 4484c46d9d
$ docker-compose -v
docker-compose version 1.27.4, build 40524192

必要なファイルの作成

Docker及びdocker-composeを動かすのに必要なファイルをプロジェクトのルートに作成します。

Dockerfile

ruby 2.4.5環境が予めインストールされているruby:2.4.5-slimというDockerイメージをDocker Hubから取得し、そのイメージ上にRails環境をセットアップしています。

Dockerfile
FROM ruby:2.4.5-slim

# Dockerコンテナ上におけるプロジェクトルートを指定
ENV APP_ROOT=/app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

# apt-utilsインストールの時の警告を抑制する
# https://qiita.com/haessal/items/0a83fe9fa1ac00ed5ee9
ENV DEBCONF_NOWARNINGS yes

# aptパッケージのインストール
RUN apt-get update -y -qq && \
    apt-get install -y -qq build-essential libpq-dev libmagickwand-dev

# Railsのセットアップ
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN gem install bundler -v 1.17.3 && bundle install

# プロジェクトディレクトリをDocker Imageにコピー
COPY . $APP_ROOT

docker-compose.yml

postgres, mongo, webの3つのサービスを定義し、
webpostgres及びmongoに依存させています。

docker-compose.yml
version: "3"

services:
    # postgreSQL containerの定義
    postgres: 
        image: postgres:10
        ports:
            # <Host Port>:<Container Port>
            - "5432:5432"
        environment:
            POSTGRES_USER: xxxxxx
            POSTGRES_PASSWORD: xxxxxx

    # mongoDB containerの定義
    mongo:
        image: mongo:3.0.15
        ports:
           - "27017:27017"

    # Rails app containerの定義
    web: 
        build: . 
        env_file: .env
        # pid error の回避のため、server.pidを削除したのちにrails sを実行
        # https://qiita.com/sakuraniumarete/items/ac07d9d56c876601748c
        command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
        # 依存関係の定義 (webをビルドするとpostgresとmongoが同時にビルドされる)
        depends_on:
            - postgres
            - mongo

ビルド実行

これら2つのファイルを作成すると、

$ docker-compose build

でコンテナをビルドできるようになります。

DBの永続化

現在の状態ではDBがコンテナ内部のストレージに生成されており、
コンテナを削除して再ビルドすると、DBに保存されていたデータは全て消失してしまいます。

DB上のデータを永続化するためには、Dockerが提供しているvolumeという機能を利用します。
volumeは、Docker Containerのライフサイクルからは独立して生成されるデータ保存領域です。
volume上にDBを生成することにより、コンテナを再ビルドしてもDB上のデータが残り続けるようになります。

image.png
引用元: https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/

volumeを利用するためには、docker-compose.ymlに以下の内容を追記します。

docker-compose.yml
 services:
    postgres: 
+        volumes:
+            - "postgres-data:/var/lib/postgresql/data"
    mongo:
+        volumes:
+            - "mongo-data:/data/db"
+ volumes:
+    postgres-data:
+    mongo-data:

上記を追記した上で改めてビルドすると、Docker Volumeが作成されているはずです。

$ docker volume ls
local               mongo-data
local               postgres-data

ホストとコンテナのソースコードを同期

現在の状態では、Dockerfileをビルドした時点で、ホストの全データををImageにコピーしています。

Dockerfile
COPY . $APP_ROOT

つまり、これより後にホスト側でソースコードを変更した場合、動作しているコンテナを一旦停止させ、
docker-compose build からやり直す必要があります。

開発環境において毎度ビルドからやり直しているのでは非常に効率が悪いので、
ホストのコード変更をコンテナに即時反映できるようにします。

よくある方法は、下記のように、プロジェクトのルートディレクトリを無名volumeとしてコンテナにマウントする方法です。

docker-compose.yml
services:
    # ${APP_ROOT}はDockerfileにおいてENVで定義した環境変数
    web:.:${APP_ROOT}

しかし、上記の方法を採った場合の問題として、
Docker for Mac特有のVolume I/Oの遅さが大きく影響してしまうということがあります。
この問題については、docker/for-macのGitHubリポジトリにおいて議論されています
(この問題により最も影響を受けたのは、RSpecの実行でした。上記の方法でマウントした場合、
普段5分ほどで完了していたテストが30分ほどかかりました...)

docker-sync

現状とりうる有効な解決策として、docker-syncというサードパーティライブラリが提供されています。

新たにdocker-sync.ymldocker-compose-dev.ymlを作成します。
作成にあたってはdocker-syncのドキュメントを参考にしました。

docker-sync.yml
version: "2"

syncs:
  sync-volume:
    src: "."
    sync_excludes:
      - "log"
      - "tmp"
      - ".git"

docker-compose-dev.yml
version: "3"

services:
  web:
    volumes:
      - "sync-volume:/app:nocopy"

volumes:
  sync-volume:
    external: true

DBセットアップ

以下のコマンドで、postgreSQLとmongoDBをセットアップします。

$ docker-compose run --rm -e RAILS_ENV=development -T web rake db:setup
$ docker-compose run --rm -e RAILS_ENV=devlopment -T web rake db:mongoid:create_indexes

docker-compose run --rm <container name> <command>は、
指定したコンテナサービスを起動し、任意のコマンドを実行後、そのコンテナを削除するというコマンドです。
DBはvolumeで永続化されているので、セットアップのためだけにコンテナを作成し、その後削除してしまっても問題ないということです。

Railsサーバーの実行

ここまでの手順を実施した上で、下記コマンドを実行することでRailsサーバーが起動します。

$ docker-sync-stack start

これは以下のコマンドを短縮したものです。

$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。
参考: 設定の追加と上書き - Docker-docs-ja

また、上記はフォアグラウンドで起動するためのコマンドで、
バックグラウンドで起動する場合は以下のコマンドを実行します。

# 起動
$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up
# 停止
$ docker-compose down
$ docker-sync stop

テスト実行

RSpecを実行するためには、サーバーを起動した状態で以下のコマンドを実行します。

$ docker-compose exec -e COVERAGE=true -T web bundle exec rspec

docker-compose execで、起動中のDockerコンテナに対して任意のコマンドを実行できます。

もしくは、以下のようにコンテナに入って実行することもできます。

$ docker-compose exec web bash
root@container:/app# bundle exec rspec

CI対応

CIツールとしてJenkinsを使用しています。

テストを実行するシェルスクリプト

ビルドジョブにおいて、下記のシェルを実行することで自動テストが行われるようにしました。

# 終了時の処理
# docker-composeが失敗した際でもJenkinsビルドマシンにゴミが残らないよう後処理をかける
# https://qiita.com/ryo0301/items/7bf1eaf00b037c38e2ea
function finally {
    # Clean project
    docker-compose down --rmi local --volumes --remove-orphans
}
trap finally EXIT

# 並列実行のために、プロジェクト名としてJenkinsの環境変数である$BUILD_TAGを指定
export COMPOSE_PROJECT_NAME=$BUILD_TAG

# 環境変数で予めビルドするdocker-composeファイルを指定することで、
# -fオプションによる指定を省略できる
# https://docs.docker.jp/compose/reference/envvars.html
export COMPOSE_PATH_SEPARATOR=:
export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

# Build Container
docker-compose build --no-cache
docker-compose up -d

# Setup DB
docker-compose exec -e RAILS_ENV=test -T web rake db:setup
docker-compose exec -e RAILS_ENV=test -T web rake db:mongoid:create_indexes

# Run RSpec
docker-compose exec  -e COVERAGE=true -T web bundle exec rspec

ビルドジョブを並列実行できるようにする

RailsプロジェクトをDocker化していない時の問題として、
2つ以上のビルドジョブを並列実行すると、同じマシン上でDBの取り合いが起こり、
エラーが発生する問題がありました。

Docker化したことで、それぞれのビルドが独立したコンテナの中で実行されるようになり、
並列実行してもエラーが起こらないようになります。
ただし、並列ビルドの実行時にコンテナのポート番号が重複しないよう、
ポートフォワーディングの設定を変更する必要があります。
参考: ホスト上にコンテナのポートを割り当て - Docker-docs-ja

export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

で指定している docker-compose-test.ymlの中身でそれを行っています。

docker-compose-test.yml
version: "3"

services:
    postgres:
        ports:
            - "5432"
    mongo:
        ports:
            - "27017"
    web:
        ports:
            - "3000"

また、docker-compose.ymlに記載したポート番号をdocker-compose-dev.ymlに移動する必要があります。
なぜなら、このまま docker-compose up -d を実行すると、docker-compose.ymldocker-compose-test.ymlがマージされ、
結果としてポートの指定が以下のようになってしまい、docker-compose-test.ymlでわざわざポート指定した意味がなくなってしまうためです。

services:
    postgres:
        ports:
            - "5432:5432"
            - "5432"
    mongo:
        ports:
            - "27017:27017"
            - "27017"
    web:
        ports:
            - "3000:3000"
            - "3000"

Railsサーバーの実行の項で、

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。

と述べましたが、複数指定可能なパラメータの場合は設定値は上書きされずにマージされるので注意が必要です。

docker-compose.yml
services:
    postgres:
-        ports:
-            - "5432:5432"
    mongo:
-        ports:
-            - "27017:27017"
    web:
-        ports:
-            - "3000:3000"
docker-compose-dev.yml
services:
    postgres:
+        ports:
+            - "5432:5432"
    mongo:
+        ports:
+            - "27017:27017"
    web:
+        ports:
+            - "3000:3000"

おまけ: RubyMineへの対応

JetBrainsのIDEであるRubyMineは、Docker Container上のRuby on Railsの開発環境に完全対応しており、以下の手順で設定することができます。
チュートリアル : リモートインタープリターとしての Docker Compose — RubyMine

まとめ

今回新たに作成したファイル

開発環境をDocker化するにあたり、新たに作成したファイルは以下の通りです。

.
├── Dockerfile
├── docker-compose.yml
├── docker-compose-dev.yml
├── docker-compose-test.yml
└── docker-sync.yml

その他

今回初めてコンテナ技術に触れ、Docker化にあたっては様々な試行錯誤を重ねました。
これまでに書いた中で、もっと良い対応方法がある、或いは対応方法として正しくない箇所があるかもしれませんが、その時はご指摘いただければ幸いです。

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

Sprockets::DoubleLinkError を解消した方法

やんばるエキスパートというオンラインスクールにて教材に取り組んでいた時の出来事です。

Sprockets::DoubleLinkErrorの出現

Railsのメッセージ投稿アプリを作成しており、その中でstylesheetsフォルダのcssファイルをscssファイルに拡張子を変更し、動作確認を行ったところ、「Sprockets::DoubleLinkError」が出現しました。

原因はcssファイルとscssファイルの干渉

stylesheetsフォルダ内にcssファイル、min.cssファイル、scssファイルの3つが存在しており、cssファイルとscssファイルが干渉してエラーが起こっていたようです。

cssファイルとmin.cssファイルを削除し、動作確認を行った所、エラーは解消されました。

しかし、、、

拡張機能Easy Sassにも注意

VScodeを再起動すると、再度cssファイルとmin.cssファイルが生成されていました。
拡張機能にEasy Sassを入れており、どうやらこいつが悪さをしていたようです。
Easy Sassを無効化した所、cssファイルとmin.cssファイルは自動生成されなくなりました。

そして、エラーも出ず、問題なく動作できるようになりました。

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

【Active Admin】Unable to find input class JsonInput

エラー内容


json型のDBカラムを、

active_adminの管理画面フォームで表示させようとすると、

Unable to find input class JsonInput のエラー。


  • backtraceの一部
@input_class_finder.find(as)
      rescue Formtastic::InputClassFinder::NotFoundError
        raise Formtastic::UnknownInputError, "Unable to find input #{$!.message}"
      end

      # @api private






解決方法


Formtasticのクラスを継承した、

XxxxInputというクラスを作れば良い。


今回の場合は、

エラー文の通り、(Unable to find input class JsonInput)

JsonInputというクラスを作ればOK。


# app/inputs/json_input.rb
class JsonInput < Formtastic::Inputs::StringInput; end






その後、

サーバーを再起動してエラーは出なくなりました。






参考

https://github.com/activeadmin/activeadmin/issues/4178

https://qiita.com/hirokik-0076/items/7dacbb76b1d0b84ec75a

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

【Github Actions】Rails × PostgreSQL環境作ってHerokuにデプロイするCIとその解説

Github Actionsを使ってRails×PostgreSQL環境のCIを作りました。
Github Actionsは普段触らなかったので、作ったCIについてコードの解説をここに書いていきます。

まずはCIでやっていることと、そのコードの全体像を書いていきます。そのあとにCIのコードについて説明を書いていきます。

今回のCIでやっていること

  1. Pull Requestが作られたタイミングで、RSpecによるテスト
  2. Masterブランチにマージされたタイミングで、Herokuにデプロイ
  3. テストが完了したら、Slackに通知する

書いたCI

name: Build And Test
on:
  pull_request:
  push:
    branches:
      - master
jobs:
  build_and_test:
    name: Exec RSpec rubocop brakeman
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:11.5
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      RAILS_ENV: test
      BUNDLE_PATH: ./vendor/bundle
      DATABASE_NAME: covid19_line_bot_test
      DATABASE_USER: postgres
      DATABASE_PASSWORD: ""
      DATABASE_HOST: 127.0.0.1
      DATABASE_PORT: 5432
      TZ: Asia/Tokyo
      RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Cache Gemfile.lock
        uses: actions/cache@v2
        with:
          path: ./vendor/bundle
          key: ${{ runner.os }}-rails-bundle-v1-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-rails-bundle-v1-
      - name: Ruby 2.6.5のセットアップ
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.5
      - name: pg gem の依存関係のインストール
        run: |
          sudo apt-get update; sudo apt-get install libpq-dev
      - name: node v10のセットアップ
        uses: actions/setup-node@v1
        with:
          node-version: '10.x'
      - name: yarn install
        run: |
          yarn install --check-files
      - name: gem install
        run: |
          gem install bundler -v 1.17.3
      - name: bundler install
        run: |
          bundle check || bundle install --path vendor/bundle --jobs 4 --retry 3
      - name: Setup Database
        run: |
          bundle exec rails db:create db:schema:load --trace
      - name: Exec RSpec
        run: |
          bundle exec rspec --format documentation --force-color --backtrace
      - name: Exec Brakeman
        run: |
          bundle exec brakeman -A
      - name: Exec rubocop
        run: |
          bundle exec rubocop --extra-details --display-style-guide --parallel --display-cop-names
  test_slack_notification:
    name: 【Test Result】Slack Notification by Github Actions
    runs-on: ubuntu-latest
    needs: build_and_test
    env:
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
      SLACK_USERNAME: Github Actions Result
    steps:
      - name: Slack Notification on Success
        uses: rtCamp/action-slack-notify@v2
        if: success()
        env:
          SLACK_MESSAGE: Pass rubocop, rspec, brakeman
          SLACK_TITLE: Test Complete!?✨
      - name: Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2
        if: failure()
        env:
          SLACK_MESSAGE: Fail rubocop, rspec, brakeman
          SLACK_TITLE: Test Failure!?
          SLACK_COLOR: "#dc3545"
  deploy_production:
    name: Herokuにデプロイ
    needs: build_and_test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master'
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Deploy to Heroku Production
        uses: akhileshns/heroku-deploy@v3.6.8
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}

解説

name

name: Build And Test

ワークフローの名前GitHub リポジトリの [Actions] タブに表示されるワークフローの名前
スクリーンショット 2020-12-16 14.39.36.png

on

ワークフローを実行するタイミングを指定

on:
  pull_request:
  push:
    branches:
      - master

上記の例では、以下のタイミングでワークフローが走ります。

  • Pull Requestが作られたとき
  • MasterブランチにPushされたとき

他にもデプロイがされたとき、ブランチが作られたとき、プルリクにレビューが投げられたとき、など様々なタイミングを指定することが可能。

参考:Events that trigger workflows

jobs

ワークフローは1つ以上のJobからなる。
複数のジョブはデフォルトでは、並行に実行される。

jobs.job_id

JobにはJobを一意に判別するIDが必要

jobs:
  build_and_test:
    name: Exec RSpec rubocop brakeman
    runs-on: ubuntu-latest
    services: ~~~
    [省略]
  test_slack_notification:
    name: 【Test Result】Slack Notification by Github Actions
    runs-on: ubuntu-latest
    needs: build_and_test
    env:
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
      SLACK_USERNAME: Github Actions Result
    steps: ~~
    [省略]
  deploy_production:
    name: Herokuにデプロイ
    needs: build_and_test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master'
    [省略]

上記のコードではこれらがJobのIDのキーに該当します。

  • build_and_test
  • test_slack_notification
  • deploy_production

jobs.job_name

Jobの名前

  build_and_test:
    name: Exec RSpec rubocop brakeman
  test_slack_notification:
    name: 【Test Result】Slack Notification by Github Actions
  deploy_production:
    name: Herokuにデプロイ

jobs.runs_on

Jobを実行するマシンを指定します。

  build_and_test:
    name: Exec RSpec rubocop brakeman
    runs-on: ubuntu-latest

2020年12月16日現在は、このような環境が用意されています

Virtual environment YAML workflow label
Windows Server 2019 windows-latest or windows-2019
Ubuntu 20.04 ubuntu-20.04
Ubuntu 18.04 ubuntu-latest or ubuntu-18.04
Ubuntu 16.04 ubuntu-16.04
macOS Big Sur 11.0 macos-11.0
macOS Catalina 10.15 macos-latest or macos-10.15

参考:Github Actions Supported runners and hardware resources

jobs.services

ジョブのためのサービスコンテナをホストするために使用

[サービスコンテナ]
Jobを実行するのに必要になるDBやRedisなどのサービスを提供できるDockerコンテナ

jobs:
  build_and_test:
    name: Exec RSpec rubocop brakeman
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:11.5
        ports:
          - 5432:5432
        # ↓postgresが起動するまで待つヘルスチェックの設定
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

jobs.env

Job中で使える環境変数を定義
Stepの中に書けば、そのStepでのみ利用可能な環境変数を定義できる

jobs:
  build_and_test:
    name: Exec RSpec rubocop brakeman
    [省略]
    env:
      RAILS_ENV: test
      BUNDLE_PATH: ./vendor/bundle
      DATABASE_NAME: covid19_line_bot_test
      DATABASE_USER: postgres
      DATABASE_PASSWORD: ""
      DATABASE_HOST: 127.0.0.1
      DATABASE_PORT: 5432
      TZ: Asia/Tokyo
      RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}

jobs.needs

ジョブの実行前に正常に完了する必要があるジョブを指定できる
以下の例だと、 build_and_testが正常に完了しないと、test_slack_notificationdeploy_productionも実行され

jobs:
  [省略]
  test_slack_notification:
    name: 【Test Result】Slack Notification by Github Actions
    runs-on: ubuntu-latest
    needs: build_and_test
  deploy_production:
    name: Herokuにデプロイ
    needs: build_and_test

jobs.if

ジョブを実行する条件を書くことができる

  deploy_production:
    name: Herokuにデプロイ
    needs: build_and_test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master'
github.refとは

ワークフローの実行をトリガーしたブランチまたはタグ ref。 ブランチの場合、これはrefs/heads/ の形式で、タグの場合は refs/tags/ です

参考:GitHub Actions のコンテキストおよび式の構文

steps

Jobで実行するコマンドなどを書いていく

    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Cache Gemfile.lock
        uses: actions/cache@v2
        with:
          path: ./vendor/bundle
          key: ${{ runner.os }}-rails-bundle-v1-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-rails-bundle-v1-
      - name: Ruby 2.6.5のセットアップ
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.5
      - name: pg gem の依存関係のインストール
        run: |
          sudo apt-get update; sudo apt-get install libpq-dev
      - name: node v10のセットアップ
        uses: actions/setup-node@v1
        with:
          node-version: '10.x'
      - name: yarn install
        run: |
          yarn install --check-files
      - name: gem install
        run: |
          gem install bundler -v 1.17.3
      - name: bundler install
        run: |
          bundle check || bundle install --path vendor/bundle --jobs 4 --retry 3
      - name: Setup Database
        run: |
          bundle exec rails db:create db:schema:load --trace
      - name: Exec RSpec
        run: |
          bundle exec rspec --format documentation --force-color --backtrace
      - name: Exec Brakeman
        run: |
          bundle exec brakeman -A
      - name: Exec rubocop
        run: |
          bundle exec rubocop --extra-details --display-style-guide --parallel --display-cop-names

steps.name

GitHubで表示されるステップの名前

    steps:
      - name: Check out code
        uses: actions/checkout@v2

steps.uses

ステップとして実行するアクションを選択

    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Cache Gemfile.lock
        uses: actions/cache@v2
        with:
          path: ./vendor/bundle
          key: ${{ runner.os }}-rails-bundle-v1-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-rails-bundle-v1-
      - name: Ruby 2.6.5のセットアップ
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.5

使っているusersの説明

actions/checkout@v2

リポジトリをチェックアウトする

      - name: Check out code
        uses: actions/checkout@v2

参考:https://github.com/actions/checkout

actions/cache@v2

キャッシュできる

      - name: Cache Gemfile.lock
        uses: actions/cache@v2
        with:
          path: ./vendor/bundle
          key: ${{ runner.os }}-rails-bundle-v1-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-rails-bundle-v1-

path: ~~はキャッシュする、復元するファイルやディレクトリを指定する
key: ~~はキャッシュを保存/復元するためのキー
restore-keys: ~~はキーのキャッシュヒットが発生しなかった場合にキャッシュを復元するために使用するキーの順序付きリスト

参考:https://github.com/actions/cache

ruby/setup-ruby@v1
      - name: Ruby 2.6.5のセットアップ
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.5

ruby-version:~~で使用するRubyのバージョンを指定できる
参考:https://github.com/ruby/setup-ruby

actions/setup-node@v1
      - name: node v10のセットアップ
        uses: actions/setup-node@v1
        with:
          node-version: '10.x'

node-version:~~で使用するNodeのバージョンを指定できる
参考:https://github.com/actions/setup-node

rtCamp/action-slack-notify@v2

Slackに通知を送ることができる

      - name: Slack Notification on Success
        uses: rtCamp/action-slack-notify@v2
        if: success()
        env:
          SLACK_MESSAGE: Pass rubocop, rspec, brakeman
          SLACK_TITLE: Test Complete!?✨

SLACK_WEBHOOK:$ {{secrets.SLACK_WEBHOOK}} SlackのWebHockのURL設定は必須

参考:https://github.com/rtCamp/action-slack-notify

akhileshns/heroku-deploy@v3.6.8

Herokuにデプロイできる

      - name: Deploy to Heroku Production
        uses: akhileshns/heroku-deploy@v3.6.8
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}

heroku_api_key:、heroku_app_name:、heroku_emailは必須

参考:https://github.com/AkhileshNS/heroku-deploy

steps.run

コマンドラインプログラムを実行する

      - name: yarn install
        run: |
          yarn install --check-files
      - name: gem install
        run: |
          gem install bundler -v 1.17.3
      - name: bundler install
        run: |
          bundle check || bundle install --path vendor/bundle --jobs 4 --retry 3
      - name: Exec RSpec
        run: |
          bundle exec rspec --format documentation --force-color --backtrace

runの中では、bundle installやyarn install、Rspecの実行などを行うようにしました。

終わりに

Rails×PostgreSQL環境を使うことが多くていままでCircleCIを利用していましたが今回GithubActionsを書いてみました。CIとしてそれほど大きな違いはなく感じたので、個人的には学習コストは低かったです。
ドキュメントも非常に分かりやすく日本語で書かれていてよかったです。

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

[Rails]フラッシュメッセージを一定時間で消す方法

開発環境

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

実装

Gemfile
gem 'jquery-rails'
ターミナル
bundle install
controller
  def create
    @book = Book.new(book_params)
    @book.user_id = current_user.id
    if @book.save
      redirect_to book_path(@book)
      flash[:notice] = "本が投稿されました"
    else
      @books = Book.all
      flash.now[:alart_flash] = "本の投稿に失敗しました"
      render 'index'
    end
  end

本が投稿された時にはnotice、失敗した際にはalart_flashと名付けて判別。ここの名前は自分で変えてもOK!
flash[:]にすると、次のアクションまで表示させる。
flash.now[:]にすると、次のアクションに移行した時点で消える仕組みになっているので
renderは指定したviewsを呼び出すだけなので、アクションではないから気をつける。
redirect_toは次のアクションになるので、flash.nowだと表示がされないので注意が必要

フラッシュメッセージ装飾

application.scss
.flash{
  width: 100%;
  height: 30px;
  font-size: 18px;
  text-align: center;
  padding: 0;
  z-index: 1; 
}

.notice{
  background-color: #65A2FF;
}

.alart_flash{
  color: #FFFFFF;
  background-color: #FF0000;
}

一定時間でフラッシュメッセージを消す。

application.js
// フラッシュメッセージ
$(function(){
  $('.flash').fadeOut(4000);  //4秒かけて消えていく
});

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

【保存版】Docker × React × Railsで環境構築していく方法

はじめに

本記事へのアクセスありがとうございます。
投稿主はプログラミング初心者であり、この方法が「最適解」かは分かりません。
しかし、動作は検証済みであり同様な記事も確認できたので信憑性はあると思います。

記事通りにコピペしていくだけで環境構築できますので、説明がいらない人はコードだけをコピペして行ってください。

想定読者

  • Dockerインストール済み
  • Docker初心者
  • フロントエンド側とバックエンド側の開発環境を分けて構成したい
  • 現在 ( 2020年12月 )にある同様なQiita記事でエラーで詰まってしまっている

最終ファイル構成

qiita_docker1.png

  • apiの中にRailsファイルを格納されています。
  • frontの中にReactファイルを格納されています。

さっそくスタート

初期ファイルを用意する

qiita_docker2.png
apiの中にはDockerfile , entrypoint.sh , Gemfile , Gemfile.lockの4つを作成する。
Gemfile.lockは何も記述しないファイルとする。

docker-compose.ymlの記述

docker-compose.ymlを記述していきます

docker-compose.yml
version: '3'
services:
  db:
    image: postgres:12.3
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  api:
    build:
      context: ./api/
      dockerfile: Dockerfile
    command:  /bin/sh -c "rm -f /myapp/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    image: rails:dev
    volumes:
      - ./api:/myapp
      - ./api/vendor/bundle:/myapp/vendor/bundle
      - ./api/vendor/node_modules:/myapp/vendor/node_modules
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3000:3000"
    depends_on:
      - db
  front:
    build:
      context: ./front/
      dockerfile: Dockerfile
    volumes:
      - ./front:/usr/src/app
    command: sh -c "cd react-sample && yarn start"
    ports:
      - "8000:3000"
volumes:
  postgres-data:
    driver: local
  bundle:
  node_modules:

api / Dockerfileの記述

Dockerfileを記述していきます

FROM ruby:2.7
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarnpkg
RUN ln -s /usr/bin/yarnpkg /usr/bin/yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

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

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

api / entrypoint.shの記述

entrypoint.shを記述していきます

entrypoint.sh
#!/bin/bash
set -e

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

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

api / Gemfileの記述

Gemfileを記述していきます

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

front / Dockerfileの記述

Dockerfileを記述していきます

FROM node:10-alpine
RUN mkdir /myapp
WORKDIR /usr/src/app

node:10以上出ないと後々にcreate-react-app出来ないので注意してください

コマンドを実行する

まずは以下の3つのコマンドをターミナルで入力してください

$ docker-compose run api rails new . --force --no-deps --database=postgresql --api
$ docker-compose build
$ docker-compose run --rm front sh -c "npm install -g create-react-app && create-react-app react-sample"

api/config/database.ymlを下記のように書き換えてください

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

次に以下のコマンドをターミナルで入力してください

$ docker-compose up

以下のコマンドを現在まで使用しているターミナルとは別のターミナル(新規作成)で入力してください

$ docker-compose run api rake db:create

以上で環境構築が完了です。

おわりに

この状況で...
localhost:3000にアクセスすると、Rails用のページにアクセスします。
localhost:8000にアクセスすると、React用のページにアクセスします。

お疲れ様でした。

少しでも役に立ったと思う方がいましたらLGTMをお願いします?‍♂️

おまけ

Docker内で開発するときは以下のコマンドを利用します。

docker-compose run web bundle exec rails g コマンド
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Docker】環境構築時に起きたエラー一覧

はじめに

Dockerについて学習し、既存のRailsアプリにDockerを導入しようと思い、公式のクイックスタートなどを参照しながら行いました。
その時に発生したエラーを備忘録のため、投稿しています。

環境

Ruby '2.6.5'
Rails '6.0.0'
Docker for Mac導入済み

エラー事例①

状況

Dockerfile
FROM ruby:2.6.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 build-essential libpq-dev nodejs yarn

RUN mkdir /(アプリ名)

WORKDIR /(アプリ名)
COPY Gemfile /(アプリ名)/Gemfile
COPY Gemfile.lock /(アプリ名)/Gemfile.lock
RUN bundle install
COPY . /(アプリ名)

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

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

% docker-compose build で立ち上げようとすると下記のエラーが発生

エラー文

エラー文
/usr/local/lib/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': Could not find 'bundler' (2.1.4) required by your /assist/Gemfile.lock. (Gem::GemNotFoundException)
To update to the latest version installed on your system, run `bundle update --bundler`.
To install the missing version, run `gem install bundler:2.1.4`
    from /usr/local/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path'
    from /usr/local/bin/bundle:23:in `<main>'
ERROR: Service 'web' failed to build : The command '/bin/sh -c bundle install' returned a non-zero code: 1

解決策

RUN gem install bundlerを挿入すると解決!

Dockerfile
(中略)
COPY Gemfile.lock /(アプリ名)/Gemfile.lock
RUN gem install bundler
RUN bundle install
(中略)

調べてみると、原因はlocal環境とDocker内でのbundlerのバージョンが違うため、エラーが出たそうです。gem install bundlerを入れるととりあえず解決。。。まだまだあります。。。

エラー事例②

状況

docker-compose.yml
version: "3"
services:
  db:
    image: mysql:5.6.47
    environment:
        MYSQL_ROOT_PASSWORD: password
        MYSQL_DATABASE: root
    ports:
        - "3000:3000"
    volumes:
        - ./db/mysql/volumes:/var/lib/mysql
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    stdin_open: true
    tty: true
    volumes:
      - .:/(アプリ名)
      - gem_data:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql_data:
  gem_data:

docker-compose build成功後、docker-compose up -dコマンドを実行したところ、

エラー文

ERROR: for web  Cannot start service web: driver failed programming external connectivity on endpoint myapp_web_1 (ae889e882d7c9f8b72f9c9b244159d86662f4abebef7d15fac4016573fe56de4): Bind for 0.0.0.0:3000 failed: port is already allocated
ERROR: Encountered errors while bringing up the project.

解決策

DBサーバーとWebサーバーのポート番号を3000で同じにしていたため、Webサーバーが立ち上がらないことが原因であると考えます。単純なミスでした。。

DBのポート番号を3306に変更し、修正しました。

docker-compose.yml
(中略)
    ports:
        - "3306:3306"

エラー事例③

状況

先ほどのエラーを解決後、もう一度、docker-compose upコマンド実行してみると、、下記のエラーが発生。

エラー文
warning Integrity check: System parameters don't match
error Integrity check failed
error Found 1 errors.
web_1  | 
web_1  | 
web_1  | ========================================
web_1  |   Your Yarn packages are out of date!
web_1  |   Please run `yarn install --check-files` to update.
web_1  | ========================================
web_1  | 
web_1  | 
web_1  | To disable this check, please change `check_yarn_integrity`
web_1  | to `false` in your webpacker config file (config/webpacker.yml).
web_1  | 
web_1  | 
web_1  | yarn check v1.22.5
web_1  | info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.
web_1  | 
web_1  | 
web_1  | Exiting

見たところ、yarnのupgradeを行ってくださいかのように感じたため、yarn upgradeコマンドを実行するも、変わらず。。。

解決策

エラー文をよくよく見てみると、、、

web_1  | To disable this check, please change `check_yarn_integrity`
web_1  | to `false` in your webpacker config file (config/webpacker.yml).

のような記述があったため、早速該当のディレクトリに行ってみると

config/webpacker.yml
(中略)
check_yarn_integrity: false

ありました!!defaultでtrueになっていたため、falseに書き換えると解決しました!!!最後の1個いきます。。。

エラー事例④

状況

docker-compose up -dが成功し、localhost:3000でアクセスしようとすると

ActiveRecord::NoDatabaseError

が発生。

解決策

単純でしたね。db:createコマンドを忘れていました。。。

ターミナル
% docker-compose exec web rails db:create
% docker-compose exec web rails db:migrate

終わりに

ビューファイルが思いっきり崩れていたので、原因究明してきます。。。。

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

【Rails】seeds.rbを使ったdbへの反映

本投稿の目的

・Railsについての議事録です。


学習に使った教材

Udemyの以下2つの教材を参考にまとめました。
"はじめてのRuby on Rails入門-RubyとRailsを基礎から学びWebアプリケーションをネットに公開しよう"
"フルスタックエンジニアが教える 即戦力Railsエンジニア養成講座"


○seeds.rbについて

・新規投稿view実装前に初期データを投入する際に使用
・rails db consoleに比べて,大量のデータを実装する際に便利
・(基本的には,rails console からの作成が一般的)

○seeds.rbの記述方法

・プロジェクト名/db/seeds.erbにコードを記述
・今回は,数字の繰り返しパターンのユーザーデータを50個dbへ反映させる

qiita.rb
if Rails.env == 'development'
  (1..50) each do |i|
    model.create(name: "ユーザー#{i}", title: "タイトル#{i}", body:"本文#{i}")
  end
end

【解説】
Rails.env == 'development'
・railsの実行環境が開発モードかどうかを判定
・env は"development(開発)" "テスト(test)" "production(本番)"の3つが存在

model名.cretate(column名: 値,column名: 値)
・データベースに値を保存するコード

【createメソッドについて】
以下のようにすることで縦に並べて記述が可能

qiita.rb
model.create([
  { name: '値1' },
  { name: '値2' },
  { name: '値3' },
  { name: '値4' },
  { name: '値5' },
  { name: '値6' }
])
end

【解説】
・配列の中にハッシュ形式で記述する
・まとめて複数のデータを保存が可能

○seeds.rbの実行

rails db:seed

・seeds.rbに記載した情報をdbに反映させるためのコード


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

Rails:コントローラに定義する基本的7つのアクション

最初に

カレンダー企画2020の16日目
プログラミングの勉強を始めて3ヵ月程経ったので学んだことのメモをアウトプットとして記事に残します。
これからプログラミングの世界に入る人の手助けになれたら嬉しい限りです。
間違っていたり、言葉が違っていたり、誤解されるような言葉があったら教えてください^^
言葉を長々と読みづらかったら申し訳ありません。少しずつなれてがんばります。

railsの7つのアクションについてちょっとまとめてみる

railsのControllerで使われる基本7つのアクションがある。
(例外として自作で名前をつけて作る事もある。)
これから紹介する7つの役割をしたい時は原則つける名前が定義されている。
一覧でどうぞ!

アクション名 役割
new データを新規作成する
create データを追加する
index データの一覧を表示する
show データの内容を表示する
edit データを編集する
update データを更新する
destroy データを削除する

言葉の表現の仕方は色々あると思うので自分で理解しやすいものにしてください!(意味が変わらない程度に、、、)

new

新規作成するという役割があります。

たとえば、
SNSで何か投稿しようと文字を書いたり、写真をアップロードしたりする場所
ブログなら記事を書いている場所
そこがnewアクションです!

create

データを追加、登録するという役割があります。

たとえば、
newアクションで作成したもの(上記の例の続き)
「投稿」とかのボタンありますよね?
それがcreateアクションです!

書いただけではどこにも表示されない。誰も見れない。残らない状態ですよね!
createをしてはじめてデータとして残ります。

index

一覧表示という役割があります。

たとえば、
SNSでたくさんの人の投稿内容がズラッと並びますよね?
それがindexアクションです。

show

内容、詳細を表示という役割があります。

たとえば、
indexで表示されているものをクリック、タップすると内容が全部みれたり、画面がその投稿だけになりますよね?
それはshowアクションです。

edit

編集する役割があります。

たとえば、
投稿した内容で間違えあった!
情報が更新されたので投稿内容を変えたい!
そんな時に編集しますよね?

編集するところがeditアクションです。

newではないところに新しく作るアクションで
editではある物を作り直すアクションです。(newに似ていますね。)

update

更新する役割があります。

編集した内容を保存するイメージです。
newを保存するのにcreateアクションを使います。
editを保存するのにupdateアクションを使います。

セットで覚えやすいかもしれないですね。

updateはeditを保存するだけでなく他にも変更を更新したりします。
editとだけセットという考えはやめてください。(書いといてなんですが、、、)

上書き保存的なイメージです!(私は!!)

destroy

削除の役割です。

たとえば、
投稿をを削除したり、アカウントを削除したりする時に行われるアクションです。
データベースからデータを消し去ります!(物理削除というみたいです。)

これは余談
論理削除というのもあり、画面上では消えているがデータは残っている状態です。

たとえば、
運営としては過去に注文した履歴は残して置きたい。でも、ユーザーは退会したい。こんな時に使われます。ユーザー側からはログインできない状態(退会)管理者側の画面にはデータが残るという処理もできます。
この時に使うアクションはdestroyではなく、updateなのです!こういうupdateの使い方もあるみたいです!

最後に

ザーッとまとめましたが、あくまで基本的なアクションです。
showの画面にindexを表示するようなこともあります。上記の内容が絶対ではないですが基本的にこれを使います!感じで覚えて置くといいかも!

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

メソッド定義における基礎知識

メソッドの定義

メソッドを定義するにあたっての基礎的な部分を
アウトプットを兼ねてこちらで共有していきます!!

メソッドについて

そもそも、メソッドとはプログラミングにおいて何らかの処理をまとめたもののことを言い、そのメソッドの名称を記述することによって記述したメソッドの処理を実行することができます。

例えば、lengthメソッドを記述すれば文字列の文字数を出力することができます

※ターミナルのirbにて

irb(main):001:0> "baseball".length

=> 8

メソッドの定義

メソッド定義の記述

このようなメソッドは予め数多く存在していますが、すでにあるもののみならず自分で定義することで作成することができます。
メソッドを作成する際には以下のように記述します。

def メソッド名
  # 実行する処理のコード
end

これをもとに例文を作成してみると
例文↓

def my_hobby
  puts "私の趣味はプロ野球観戦です。"
end

my_hobby
# 出力結果
私の趣味はプロ野球観戦です。

上記のように、my_hobbyというメソッドを定義したことによって
my_hobbyと記述することによって「私の趣味はプロ野球観戦です。」を出力する処理を実行することができます!

コードが読まれる順番は普通では上からとなりますが、定義されたメソッドが存在する場合は、そのメソッドの記述は読まれずに一旦スルーされます。
上記の例で言うと、my_hobbyと記述してそれが読み込まれた段階で定義されたメソッドのmy_hobbyが呼び出されて処理が実行されます。

実行したメソッドの最終的な値

メソッドを定義し、そのメソッドの処理が実行されて出力される値のことを戻り値または、返り値といい、上記の例文でいうと
「私の趣味はプロ野球観戦です。」という出力結果の部分が戻り値となります。

またlengthメソッドを用いた場合の戻り値は、文字数として出力された数字が戻り値となります。

そして定義されたメソッドに関しては、メソッド内の処理の1番最後の行の処理の結果が戻り値となって出力されます

例文↓

def week
  "Sunday"
  "Monday"
  "Tuesday"
  "Wednesday"
  "Thursday"
  "Friday"
  "Saturday"  # 1番最後の行が戻り値!
end

puts week
# => Saturday

このようにメソッド内の処理は最後の行が出力されるが
return文をメソッド内に記述すると戻り値を指定することができます!

例文↓

def week
  "Sunday"
  "Monday"

  return "Tuesday"  # これが戻り値となり、処理が終了する!

  "Wednesday"  # 呼び出されない
  "Thursday"  # 呼び出されない
  "Friday"   # 呼び出されない
  "Saturday"  # 呼び出されない  
end

puts week
# => Tuesday

上記のようにreturnを指定することによって、戻り値として処理を行う部分を指定することができ、returnを記述した部分が出力されます。

値の使用可能範囲

定義した変数を使用できる範囲は限られていて、その範囲のことをスコープと言います。
①定義したメソッド外で定義された変数をそのメソッド内で使用することは出来ず、その逆も同じように②定義したメソッド内の変数をメソッド外で使用することは出来ないためエラーとなります。

①の場合の例文↓

def introduce
  puts name
end

name = "Kinoshita"
introduce

# => エラー!

②の場合の例文↓

def introduce
  name = "Kinoshita"
end

puts name

# => エラー!

いずれも、スコープの範囲外ということで定義された変数が使えずにエラーとなってしまいます。
こういったスコープ範囲外で定義された値を使いには引数(ひきすう)という値を使用します。
引数を使用するには以下のように記述します。

例文↓

def メソッド名(仮引数)
  # 処理の記述
end

メソッド名(実引数)

仮引数はメソッドを定義する際に記述し処理の際に利用する引数で、
実引数はメソッドを呼ぶ際に受け取る値を記述する引数のことを言います。
また、引数は複数用いることができ、その場合は
メソッド名(第1引数, 第2引数)という記述をします。

仮引数と実引数の名称は必ずしも一致している必要はありませんが、引数の数は必ず一致される必要があります。

上記の例文のエラーを解決するために引数を用いてみます。
例文↓

def introduce(name)
  puts name
end

teacher = "Kinoshita"
introduce(teacher)

# => "Kinoshita"

上記のように、introduceメソッド外で定義されたteacherが実引数として記述することで、メソッド外で定義された変数teacherが、introduceメソッド内の仮引数nameとして渡され、メソッド内の処理を実行する。
これによってメソッド外で定義された変数が使用できる形になります!

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

【Rails】デバッグ用gem 'better_errors' , 'binding_of_caller' を導入

はじめに

ポートフォリオ作成で一番苦しんだのが、デバッグ!
そこで今回は、便利で高機能なエラー画面が表示されるらしいbetter_errorsbinding_of_callerを導入してみました。

前提

  • Ruby2.7.0
  • Ruby on Rails6.0.3
  • MySQL8.0

better_errorsとは

デフォルトのエラー画面をわかりやすく整形してくれるgem。

binding_of_callerとは

上記better_errorsと一緒に使うことで、ブラウザ上でirbを使えるようになるgem。

いざ、導入

Gemfile
group :development do
  gem 'better_errors'
  gem 'binding_of_caller'
end
ターミナル
bundle

※Dockerを使用している場合はもうひと手間必要とのこと。

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

エラー箇所の画面確認

下記のように、エラーメッセージやデバッグ画面が表示され、
ターミナルを表示しなくてもブラウザからのirbでデバッグ可能。
その他、トレース情報やリクエスト情報、ローカル変数なども確認できる。
image.png

終わりに

これはめちゃくちゃ便利!デバッグが捗る!
railsでアプリを作る際は、必ず導入しようと思います!

参考サイト

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

【Rails6】ActionMailerを用いたお問い合わせ機能の実装

はじめに

転職活動用ポートフォリオ作成中です。今回、ユーザーからのお問い合わせ機能を実装したため、備忘録及び復習のため記述します。

環境

Ruby on Rails'6.0.0'
Ruby'2.6.5'

①ルーティングの記述

config/routes.rb
resource :contacts, only: [:new, :create] do
 get "/thanks" => "contacts#thanks"
end

今回は、ユーザーがお問い合わせを送信後、「お問い合わせいただきありがとうございました!」のようなページを表示させる流れです。

②モデルの記述

rails g model contactコマンドでモデルを作成後、マイグレーションファイルの記述などを行っていきます。

db/migrate/20201204073627_create_contacts.rb
class CreateContacts < ActiveRecord::Migration[6.0]
  def change
    create_table :contacts do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.text :content, null: false
      t.timestamps
    end
  end
end

今回は、名前・返信用のメールアドレス・お問い合わせ内容を指定しました。その後、rails db:migrateコマンドを実行します。

app/models/contact.rb
class Contact < ApplicationRecord
  validates :name, :email, :content, presence: true
end

名前・メールアドレス・お問い合わせ内容が空では保存できないようにバリデーションを設定します。

③ActionMailerの設定

今回はユーザーからお問い合わせが送信されると、内容が管理者へメールで届くような機能にしたいと思います。そこでActionMailerを使用しました。
まず、rails g mailer ContactMailerコマンドを実行します。すると以下のファイルが生成されるためお問い合わせが来たら管理者へメールを送信する記述をしていきます。

app/mailers/contact_mailer.rb
class ContactMailer < ApplicationMailer
  def contact_mail(contact)
    @contact = contact
    mail to: '(管理者のメールアドレス)@gmail.com', subject: '(メールのタイトル)'
  end
end

続いては新たにviewファイルが生成されているため、メールの本文を記述していきます。

app/views/contact_mailer/contact_mail.html.erb
<p>ユーザーネーム:<%= @contact.name %></p>
<p>メールアドレス:<%= @contact.email %></p>
<p>お問い合わせ内容:<%= @contact.content %></p>

④コントローラーの記述

それではコントローラーの記述をしていきます。

app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(contact_params)
    if @contact.save
      ContactMailer.contact_mail(@contact).deliver
      redirect_to thanks_contacts_path
    else
      render :new
    end
  end

  def thanks
  end

  private

  def contact_params
    params.require(:contact).permit(:name, :email, :content)
  end
end

これにより、createアクションが呼び出され、お問い合わせが正常に保存されると、メール送信処理も走り出すように設定できました。

⑤viewの記述

お問い合わせのviewファイルの記述をしていきます。

app/views/contacts/new.html.erb
<div class="container">
  <div class="row">
    <div class="offset-sm-2 col-sm-8 offset-sm-2">
    <%= form_with model: @contact, local: true do |f| %>
      <h5 class='form-header-text text-center'><i class="far fa-paper-plane fa-2x my-orange"></i> お問い合わせ</h5>

  <%= render 'layouts/error_messages', model: f.object %>
      <div class="form-group">
        <div class='form-text-wrap'>
          <label class="form-text" for="name">お名前</label>
          <span class="badge badge-danger">必須</span>
        </div>
        <div class='input-name-wrap'>
          <%= f.text_field :name, class:"input-name", id:"name", placeholder:"例) 田中太郎" %>
        </div>
      </div>

      <div class="form-group">
        <div class='form-text-wrap'>
          <label class="form-text" for="email">メールアドレス</label>
          <span class="badge badge-danger">必須</span>
        </div>
        <%= f.email_field :email, class:"input-default", id:"email", placeholder:"PC・携帯どちらでも可", autofocus: true %>
      </div>

      <div class="form-group">
        <div class='form-text-wrap'>
          <label class="form-text" for="content">内容</label>
          <span class="badge badge-danger">必須</span>
        </div>
        <%= f.text_area :content, class:"article-input-default", id:"content", autofocus: true %>
      </div>

      <div class='contact-btn text-center'>
        <%= f.submit "送信する" ,class:"btn btn-outline-danger w-50" %>
      </div>
    <% end %>
    </div>
  </div>
</div>
app/views/contacts/thanks.html.erb
<div class="container">
  <div class="row">
    <div class="offset-sm-2 col-sm-8 offset-sm-2">
      <h5 class='header-text text-center'><i class="far fa-smile fa-lg my-orange"></i> お問い合わせありがとうございました</h5>
      <%= link_to 'トップページへ戻る', root_path %>
    </div>
  </div>
</div>

⑥Gmailの設定

最後に今回アドレスで使用するGmailの設定を行いました。

config/environments/development.rb
(中略)
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address: 'smtp.gmail.com',
    domain: 'gmail.com',
    port: 587,
    user_name: '(管理者のメールアドレス)@gmail.com',
    password: ENV["GMAIL_KEY"],
    authentication: 'plain',
    enable_starttls_auto: true
  }
(中略)

すでに他の設定も記述されているかと思うので、間に記述しました。
GmailのパスワードはGitHub上に上がってしまっては、大変なことになってしまうので、環境変数を設定します。設定する方法は様々あると思いますが、私はvimコマンドを使用して「.zshrc」ファイルに設定しました。

ターミナル
% vim ~/.zshrc

上記のコマンドでファイルを開き、insertモードにしてから記述していきます。

.zshrcファイル
(中略)
export GMAIL_KEY = "Gmailのパスワード" 
(中略)

こちらで設定が完了しました!

⑦テストコードの実装

おまけとしてRspecを使用してテストコードも実装しました。

FactoryBot
spec/factories/contacts.rb
FactoryBot.define do
  factory :contact do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    content { Faker::Lorem.sentence }
  end
end
単体テストコード
spec/models/contact_spec.rb
require 'rails_helper'

RSpec.describe Contact, type: :model do
  before do
    @contact = FactoryBot.build(:contact)
  end

  describe 'お問い合わせの送信' do
    context 'お問い合わせが送信できる場合' do
      it '全ての要素が存在すれば投稿できる' do
        expect(@contact).to be_valid
      end
    end
    context 'お問い合わせが送信できない場合' do
      it 'nameが空では送信できない' do
        @contact.name = nil
        @contact.valid?
        expect(@contact.errors.full_messages).to include('お名前を入力してください')
      end
      it 'emailが空では送信できない' do
        @contact.email = nil
        @contact.valid?
        expect(@contact.errors.full_messages).to include('メールアドレスを入力してください')
      end
      it 'contentが空では送信できない' do
        @contact.content = nil
        @contact.valid?
        expect(@contact.errors.full_messages).to include('お問い合わせ内容を入力してください')
      end
    end
  end
end
結合テストコード
spec/system/contacts_spec.rb
require 'rails_helper'

RSpec.describe 'お問い合わせ送信', type: :system do
  before do
    @contact = FactoryBot.build(:contact)
  end

  context 'お問い合わせの送信ができるとき' do
    it '正しい情報を入力すれば、お問い合わせを送信できる' do
      visit root_path
      expect(page).to have_content('お問い合わせ')
      visit new_contacts_path
      fill_in 'お名前', with: @contact.name
      fill_in 'メールアドレス', with: @contact.email
      fill_in '内容', with: @contact.content
      expect do
        find('input[name="commit"]').click
      end.to change { Contact.count }.by(1)
      expect(current_path).to eq thanks_contacts_path
      click_link 'トップページへ戻る'
      expect(current_path).to eq root_path
    end
  end

  context 'お問い合わせの送信ができないとき' do
    it '正しい情報を入力しなければ、お問い合わせは送信できない' do
      visit root_path
      expect(page).to have_content('お問い合わせ')
      visit new_contacts_path
      fill_in 'お名前', with: ''
      fill_in 'メールアドレス', with: ''
      fill_in '内容', with: ''
      expect do
        find('input[name="commit"]').click
      end.to change { Contact.count }.by(0)
      expect(current_path).to eq contacts_path
    end
  end
end

終わりに

https://qiita.com/mmdrdr/items/9c5dd4ca886f034fb0ef
https://qiita.com/hirotakasasaki/items/ec2ca5c611ed69b5e85e

上記の記事を参考にさせていただきました。ありがとうございました。

メール送信のテストもしてみたいと思うので、調べてみます!!
誤っている点ありましたらご指摘ください。

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

rails_admin で alias_attribute な enum を扱えるようにする

前段

enum layout: { list: 0, grid: 1 }

上記のような top_layoutinteger のような enum があったとして、それを top_layout という名前に変えたいとします。(カラムのリネームが一番良いというのは置いておいて)

アプリケーションで扱う上では、以下のように alias_attributeenum の変更で事足りました。

alias_attribute :top_layout, :layout

enum top_layout: { list: 0, grid: 1 }

一方で、 rails_admin が生成する UI の方は依然として layout が表示され、 top_layout が現れません。

rails_admin が ActiveRecord の属性をどう取得しているか

https://github.com/sferik/rails_admin/blob/f0c46f1e128b5d31d812ff3a80d15db8692c848b/lib/rails_admin/adapters/active_record.rb#L56-L65

adapters/active_recored.rb
module RailsAdmin
  module Adapters
    module ActiveRecord
      def properties
        columns = model.columns.reject do |c|
          c.type.blank? ||
            DISABLED_COLUMN_TYPES.include?(c.type.to_sym) ||
            c.try(:array)
        end
        columns.collect do |property|
          Property.new(property, model)
        end
      end

ざっと見ただけなので、他にも関連する要素はあるかもしれませんが、この properties で生成されたものをベースに扱っているように見えます。

これを確認したい場合には、console を開いて以下で確認できます。

> c = RailsAdmin.config(SampleModel.first)
> c.abstract_model.properties

当然 columns を取得されると、 alias_attribute したところでどうしようもないです。

カスタマイズする

config/initializer/rails_admin.rb
RailsAdmin.config do |config|
  config.model SampleModel do
    include_all_fields
    exclude_fields :layout

    field :top_layout, :active_record_enum
  end

Wiki に従い、無効にしたい layoutexlude_fields に入れ、新たに入れたい top_layout を追加し、 active_record_enum として登録しました。

これで、通常の enum カラムと同じように表示されるようになります。

更新時に失敗する

このままだと、更新時になぜか 0, 1 といった値が、 enum の値として認識されず、「'0' のような値は enum には存在しない」といったエラーが出てしまいます。

ここに関しては以下が関わっています。

https://github.com/sferik/rails_admin/blob/f0c46f1e128b5d31d812ff3a80d15db8692c848b/lib/rails_admin/config/fields/types/active_record_enum.rb#L48

config/fields/types/active_record_enum.rb
module RailsAdmin
  module Config
    module Fields
      module Types
        class ActiveRecordEnum < Enum
          def parse_input_value(value)
            abstract_model.model.attribute_types[name.to_s].deserialize(value)
          end

この abstract_model.model でアプリケーション側のモデル(例えば SampleModel)を参照しているので、 attribute_typesSampleModel.attribute_types などすれば確認できます。

確認するとわかりますが、alias_attribute するだけでは、この一覧に top_layout は入ってきません。よって、ここでの deserialize は何も変化がなく、リクエストパラメーターとして送信された '0'という文字列がそのまま assign_attributes に渡されてしまい、上述のエラーになってしまいます。

attribute を定義する

attribute :top_layout, :integer

このような形で top_layout も attribute として定義を追加します。ここは ActiveRecord::Enum::EnumType を直接指定できるような type を与えたいところですが、それはできないので integer にします。これは、もともとの layout の型と同じです。

ここで attribute_types を実行するとわかりますが、alias_attribute 後も layout は ActiveRecord::Enum::EnumType として存在し続け、 top_layout は integer として定義されたような形になります。top_layout の enum 定義が、そのまま alias のオリジナルの layout の方にかかる形になるようです。

これで、更新時にも値の型変換が行われるようになったため、更新も問題なくできるようになりました。

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