20200928のRailsに関する記事は17件です。

【Rails】ビューファイルは使用できない、パラメータIDも存在しない。そんな中でアソシエーションのデータを取得する。【APIモードで使用するやつ】

はじめに

備忘録です。

ここでは、ホーム画面("/")で

  • 「データ全件」
  • 「1つのデータに関連付けされているデータの総数」
  • 「1つのデータに関連付けされているハッシュのデータ」

を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、

  • 「記事全件」
  • 「1つの記事に関連付けされているコメントの総数」
  • 「1つの記事に関連付けされているタグのハッシュデータ」

を取得していくようなイメージです。

全てのデータはハッシュとして取得します。

環境

Ruby: 2.6
Rails: 5.2

モデルの例(作成の過程は省略)

スクリーンショット 2020-09-28 23.12.40.png

コード

1.まずは記事全件を取得する

def index
  @all_article = Article.all
end

2.1つの記事に関連付けされているコメントの総数を取得する

def index
  @all_article = Article.all

  # [ { 1: "コメント数" }, { 2: "コメント数"  }, ... ]という形のデータを作成する。
  # article.commentsでループ処理した記事データからコメントを呼び出している。
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]  #もしコメント数を表示させるならto_sで文字列化する。
  }.to_h 

  # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる
end

3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)

def index
  # 記事全件
  @all_article = Article.all
  # コメント数
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]
  }.to_h 
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # 1つの記事が所有するタグをハッシュで取得
  @all_article.map{ |article|
    [article.id, article.article_tags.all]
  }.to_h

  # => { 
  #      "1": [
  #         { "id": 1, "tag_name": "Rails" }, 
  #         { "id": 2, "tag_name": "React" }, 
  #          ... ,
  #      ], 
  #      "2": [ 
  #         { "id": 1, "tag_name": "JavaScript" }, 
  #         { "id": 2, "tag_name": "Qiita" }, 
  #          ... ,
  #      ]
  #     }
end

以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。


最後に

バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?‍♂️

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

【Rails】ビューファイルは使わず、パラメータIDも存在しない中で、アソシエーションのデータを取得する。【APIモードで使う】

はじめに

備忘録です。

ここでは、ホーム画面("/")で

  • 「データ全件」
  • 「1つのデータに関連付けされているデータの総数」
  • 「1つのデータに関連付けされているハッシュのデータ」

を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、

  • 「記事全件」
  • 「1つの記事に関連付けされているコメントの総数」
  • 「1つの記事に関連付けされているタグのハッシュデータ」

を取得していくようなイメージです。

全てのデータはハッシュとして取得します。

環境

Ruby: 2.6
Rails: 5.2

モデルの例(作成の過程は省略)

スクリーンショット 2020-09-28 23.12.40.png

コード

1.まずは記事全件を取得する

def index
  @all_article = Article.all
end

2.1つの記事に関連付けされているコメントの総数を取得する

def index
  @all_article = Article.all

  # [ { 1: "コメント数" }, { 2: "コメント数"  }, ... ]という形のデータを作成する。
  # article.commentsでループ処理した記事データからコメントを呼び出している。
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]  #もしコメント数を表示させるならto_sで文字列化する。
  }.to_h 

  # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる
end

3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)

def index
  # 記事全件
  @all_article = Article.all
  # コメント数
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]
  }.to_h 
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # 1つの記事が所有するタグをハッシュで取得
  @all_article.map{ |article|
    [article.id, article.article_tags.all]
  }.to_h

  # => { 
  #      "1": [
  #         { "id": 1, "tag_name": "Rails" }, 
  #         { "id": 2, "tag_name": "React" }, 
  #          ... ,
  #      ], 
  #      "2": [ 
  #         { "id": 1, "tag_name": "JavaScript" }, 
  #         { "id": 2, "tag_name": "Qiita" }, 
  #          ... ,
  #      ]
  #     }
end

以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。


最後に

バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?‍♂️

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

RailsアプリにElasticsearchを組み込む

Elasticsearchとは

ElasticsearchとはElastic社が開発しているOSSの全文検索エンジンです。
大量のドキュメントから目的の単語を含むドキュメントを高速で抽出することができます。

RailsアプリでElasticsearchを扱う考え方

1.全文検索エンジンに検索対象のデータが入っている
2.アプリケーション側で検索すると、検索エンジンに対してクエリを発行し、結果が返却される
3.アプリケーション側で検索対象のデータが更新されると、連携して検索エンジンのデータも更新される

RailsアプリでElasticsearchを扱う

インデックスを作成する

Elasticsearchでは、データの保存場所としてインデックスを作成します。
リレーショナル・データベースでいうところのテーブルのようなものです。

まずは、RailsのモデルをElasticsearchでも扱うために専用のgemをインストールします。
以下をGemfileに記述し、bundle installします。

gem 'elasticsearch-model', github: 'elastic/elasticsearch-rails'
gem 'elasticsearch-rails', github: 'elastic/elasticsearch-rails'

bundle installが終わったら、Elasticsearchにインデックスを作成します。
検索対象としたいモデルの中で、Elasticsearch::Modelをincludeします。

class Article < ActiveRecord::Base
  include Elasticsearch::Model
end

これでモデルでElasticsearchを扱う準備ができました。
インデックスは以下のようなコードで作成できます。

Article.__elasticsearch__.create_index! force:true

インデックスにドキュメントを入れる

Elasticsearchでは、インデックスに入っているデータのことをドキュメントと呼びます。
インデックスには検索対象にしたいデータを入れます。

以下のコードでElasticsearchにドキュメントをインポートします。

Article.import

これでElasticsearchのインデックスの中に、ドキュメントが登録されます。

ドキュメントを検索する

ドキュメントを検索するには、Elasticsearchにクエリを投げます。
以下のように書くことで、RailsからElasticsearchにクエリを投げることができます。

response = Article.search 'hoge'

引数で検索文字列を指定することで、ドキュメントを検索することができます。

フロントからパラメータを受け取って検索すると以下のように書けます。

def index
  @articles = Article.search(params)
end

Rails側で検索対象のレコードが更新されると、それに伴ってElasticsearchのドキュメントも更新する

実際にサービスを運用するとなると、Rails側でレコードが更新されるとElasticsearchのドキュメントを更新する必要があります。

まず単にElasticsearchのドキュメントを更新するには、以下のように実装します。

Article.first.__elasticsearch__.update_document

他にも、delete_documentというメソッドがあるので、これを使えばドキュメントの削除もできます。

上記のように明示的に書かなくても、レコードを更新した際に自動的にドキュメントを更新することもできます。
gemであるelasticsearch-modelでは、Elasticsearch::Model::CallbacksをModelにincludeしておくと、レコードを更新した際にElasticsearchのドキュメントを更新するクエリを投げてくれます。

class Article
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end

RailsアプリにElasticsearchを組み込む

実際にArticleモデルの検索周りの処理を作ります。

article-m/app/models/concerns/article/searchable.rb
require 'active_support/concern'
module Article::Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    index_name "article"

    settings index: {
      number_of_shards: 1,
      number_of_replicas: 0
    } do
      mapping _source: { enabled: true } do
        indexes :id, type: 'integer', index: 'not_analyzed'
        indexes :title, type: 'string'
        indexes :content, type: 'text'
      end
    end
  end

  module ClassMethods
    def create_index!(options={})
      client = __elasticsearch__.client
      client.indices.delete index: "article" rescue nil if options[:force]
      client.indices.create index: "article",
      body: {
        settings: settings.to_hash,
        mappings: mappings.to_hash
      }
    end
  end
end

モジュールの中で、include Elasticsearch::Modelして便利なメソッド群を使えるようにします。

index_nameはインデックス名、settingsにはインデックスの設定を書きます。
number_of_shardsやnumber_of_replicasはシャードやレプリカの設定で、耐障害性や性能に関連します。

mappingはインデックスをどのように定義するか決めます。RDBでいうテーブルスキーマのようなものです。

create_index!は実際にインデックスを作成するヘルパーです。
elasticsearch.clientでElasticsearchのクライアントのオブジェクトが取れるので、
このクライアント経由でいろいろ操作できます。

作ったモジュールをモデルにincludeします。

article-m/app/models/article.rb
class Article < ActiveRecord::Base
  include Article::Searchable
  def self.search_message(keyword)
    if keyword.present?
      query = {
        "query": {
          "match": {
            "message": keyword
          }
        }
      }
      Article.__elasticsearch__.search(query)
    else
      Article.none
    end
  end
end

もらったキーワードから検索のクエリを組み立てて、Article.elasticsearch.searchに渡します。
Articleモデルに対してelasticsearch.searchを呼び出すことで、elasticsearch-railsとelasticsearch-modelがクエリを投げてくれます。

コントローラーは以下のようになります。

article-m/app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def search
    @keyword = params[:keyword]
    @articles = Article.search_message(@keyword).paginate(page: params[:page])
  end

以上がRailsアプリにElasticsearchを組み込む一例です。

参考

elastisearch-railsを使ってRailsでElasticsearchを動かす【初心者向け】
Railsアプリの検索処理にElasticsearchを組み込むのにやったことまとめ

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

[rails6.0.0]ウィザード形式でActiveStorageを使用して画像を保存する方法

概要

ユーザー登録とプロフィールをさせる時に、ウィザード形式でフォームを作成。
プロフィールに画像を保存したかったが上手くいかなかったため備忘録として記載。
※検索しても同じ状況の方がいなかったので役に立てばと思います。

ウィザード形式とは何か?という方はこちらを参考にしてください

内容

開発環境

MacOS Catalina 10.15.6
Rails 6.0.0
Ruby 2.6.5

テーブル構成

deviseを使ってユーザー登録をさせようとしています。
userテーブルはnicknameのみ追加。
profileテーブルにはtwitterのリンクなど記載。
profileテーブルにactive_strageで画像を保存したい。

ウィザード形式を実装

ここらへんの機能の実装に関しては
こちらの記事がかなり近いので参考にしました。

問題点

ウィザード形式で、画像以外はしっかり保存できたけど、画像は保存されない。

registrations_controller
class Users::RegistrationsController < Devise::RegistrationsController

  def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @profile = @user.build_profile
    render :new_profile
  end

  def create_profile
    @user = User.new(session["devise.regist_data"]["user"])
    @profile = Profile.new(profile_params)
    unless @profile.valid?
      render :new_profile
    end
    @user.build_profile(@profile.attributes)
    @user.save
    session["devise.regist_data"]["user"].clear
    sign_in(:user, @user)
    redirect_to root_path
  end

  private

  def profile_params
    params.require(:profile).permit(:avatar, :favorite_beer, :twitter_link, :info)
  end

この中の以下の記述が悪さをしていた様子。

registrations_controller
@user.build_profile(@profile.attributes)
@user.save

ここでは、@user@profileをbuildで関連付けさせて@user.saveでまとめて保存をしているのですが、ここでの保存が原因で上手く行ってない様子。
この記事を見つけて

原因
ActiveStorageに実際にファイルが保存されるタイミングはmodelをsaveして処理がコミットされた時なので、保存される前のmodelから画像をattachしてもファイルが未完成の状態になるっぽいです。

どうもActiveStrageはモデルをちゃんとsaveしないと行けなさそうなので以下のように記述を変更したら解決しました。

registrations_controller
# それぞれのモデルで保存させた    
# @user.build_profile(@profile.attributes)
@user.save
@profile.user_id = @user.id
@profile.save

まとめ

正直buildを使っての保存の仕組みがよくわかってなかったのに使用してしまったのがエラーの原因かもしれないです。
少しでも参考になれば幸いです。

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

Markdown記法について

今回がQiita初投稿になります。よろしくおねがいします。

「プログラミング知識をMarkdown記法で書いて共有しよう」

と、Qiitaの投稿画面の一番初めにもデフォルトである通りQiitaにしろREADMEにしろGitHubのプルリクエストにしろプログラミング業界ではMarkdown記法というのがちょいちょい出てくるんですね。
自分も初めの頃は参考のサイトを見て少しやろうと思いましたが、案外やり方が多くて面倒になり別にまだ覚えなくてもいいかと思っていたのですが最近になり少しずつやり方が分かってきたので備忘録的な意味と自分の練習もかねて投稿しようと思いました。

まず初めにMarkdownについてですが

Markdownとは

Markdown(マークダウン)は文章を記述するための記法(マークアップ言語)の1つです。
Markdownとは、メールを記述する時のように書きやすくて読みやすいプレーンテキストをある程度見栄えのするHTML文書へ変換出来るフォーマットとしてジョン・グルーバーによって開発されました。
以下の特徴があります。

  • 簡単で覚えやすい記述
  • 文章の構造を明示出来る
  • Markdownそのままでも理解出来る
  • 対応アプリを使うことでより快適に読み書き出来る
  • 拡張子は「.md」

とのことです。

例として以下にRailsのアプリケーションのREADMEの一例を載せます。

README

users テーブル

Column Type Options
user_name string null: false
email string null: false
password string null: false

Association

  • has_many :friends
  • has_many :comments

というようにアプリケーションにはREADMEが必須でありその中の記述はMarkdown記法のためMarkdown記法を最低限は覚える必要があるということですね。

では自分がよく使うものを抜粋して紹介します。

  • 「#」を先頭に入れて半角スペース1つ分空けると見出しの意味になり大文字になり強調されます。(h1タグと同じ意味)
    「#」が一番大きくて以下1つずつ増やすごとに大文字がだんだんと小さくなり「######」最大6つまで連続で記述出来ます。

  • 「-」を先頭に入れて半角スペース1つ分空けると「・」が付与されます。

  • 「---」と「-」を3つ以上連続で入れると水平線が出来ます。

と上記が自分がよく使用している記述でした。上記の記述方法は本当にほんの一部なので興味のある人はちゃんとしたサイトから学習頂ければと思います。(笑)
と言ってもQiitaにいる人達はみんなMarkdown記法で記事を投稿しているのだから恐らくQiitaに投稿した人の中で自分が一番Markdown記法を知らない(使いこなしていない)と思いますのでこれから精進して参りたいと思います。ここまでお付き合い頂きありがとうございました。

注:Markdown記法の「#」や「-」などは全て半角で記述しないと正しく反映されません。さらには半角スペースも1つ分空けてください。全角では反映されませんので注意!

今回参考にさせて頂いたサイト

株式会社アーティス ビジネスとIT活用に役立つブログ

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

データベースのカラムなどを修正したい場合

db:migrateしたデータベースを修正したい場合

プログラミングスクールでのチーム開発学習中にデータベースの修正を行いたかったのですが、
細かい部分の知識が抜け落ちていたので、備忘録用にまとめてます。

データベースの操作に慣れていない方や、これからデータベースを学習する方の参考にもなれば幸いです。

開発環境

DB: MySQL
Rails: 5.2.4.3

まず自分が修正したいデータベース(テーブル)の確認を行いましょう

ターミナル
% rails db:migrate:status

すると、テーブルがこの様に出てくると思います(出てくるテーブルの数や名前はそれぞれ違ってきます)

ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 今回はこのテーブルを修正したい
   up     20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

ここでupとdownに注目です。

マイグレーションの修正を行うには、statusをdownの状態にしておく必要があります。

次に自分が修正したいデータベース(テーブル)をdownにしましょう

downの状態にするにはターミナルでこの様なコマンドを実行しましょう

ターミナル
% rails db:rollback

もう一度statusを確認してみましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 今回はこのテーブルを修正したい
  down    20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

あれ?

一つ下しかdownに変わってません。

というのもrollbackコマンドは一つずつしかdownに変えられないのです。

なので、もう一度やってみましょう。

ターミナル
% rails db:rollback

もう一度statusを確認してみましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
  down    20200906141656  Create -----s 今回はこのテーブルを修正したい
  down    20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

今度は無事に目的のテーブルをdownに出来ました。

修正が終わった後

今回はカラム名の修正を行いたかったので、この後にマイグレーションファイルのカラム名の変更を行いました。

最後に

ターミナル
% rails db:migrate

もう一度statusを確認しておきましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 修正したテーブル
   up     20200907114227  Create -----s
   up     20200927061950  Create -----s
   up    20200927065357  Create -----s
※----は自分で作成したテーブル名

rails db:migrateコマンドの場合は、downのテーブルを全てupに変更します。

db:migrateは1度で全てupにするけど
db:rollbackは1つずつしかdownに出来ないんですね。

一度にrollbackをまとめて行いたい場合

今回の様に複数回rollbackを行わないといけない場合にまとめて行える方法も紹介します

ターミナル
% rails db:rollback STEP=2

※STEP=2を入力する事でrollbackを2回分まとめて実行してくれます。

rollbackコマンドに慣れてきたら、STEPオプションも積極的に使って、作業性をあげていきましょう。

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

form_objectで親子関係のあるフォームを作成する(テストも書いてます)

背景

@shopに紐づく@comment@employeeなど、様々な子要素があるテーブルがあります。初回情報登録時には、@shop@comment@employeeなど、親要素と一緒に全ての子要素も一緒に保存できるようにしていたのですが、

下記の記事のような形で、accepts_nested_attributes_forを用いてこれらを実現していたものの、

▼こんなふうに実現していました
fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]

そのうち、@shopだけの編集フォームや、@comment@employeeなどの投稿・編集フォームも必要になってきたため、だんだんmodelが様々な記述で肥大化してきました。

form_objectとは?

↑上記のような状態の時に、特定のフォームに関するバリデーションやデフォルト値の設定などを一箇所に集め、モデルの記述を簡素化できるのが、form_objectです。個人的には、導入にかなりつまづいてしまったので、記事を書いて記録を残しておこうと思います。

なお、実行環境は下記の通りです。

  • Rails 5.2.4.2
  • rspec-rails 4.0.1

導入方法

form_object, controller, viewの基本の書き方は下記の通りです。なお、今回は@shopの初回登録時に@commentも1件登録できるようなフォームを例にしたいと思います。

実装にあたって、一番参考にさせていただいたのは、こちらの記事です。

accepts_nested_attributes_forを使わず、複数の子レコードを保存する

DB構造

shops
name string
category integer

↑ categoryはショップ種別。enumのカラム。

comments
content text
shop_id integer

作成したファイル

form_object

/forms/shop_entry_form.rb
class ShopEntryForm
  include ActiveModel::Model

  # @shopに関する記述 -----------------------------
  concerning :ShopBuilder do
    def initialize(params = {})
      super(params)
      @category = params[:category]
    end

    def facility
      @shop ||= Shop.new
    end
  end

  attr_accessor :name, :category
  validates :name, presence: true
  validates :category, presence: true

  # @commentに関する記述 -----------------------------
  concerning :CommentBuilder do
    attr_reader :comments_attributes

    def comments
      @comments_attributes ||= Comment.new
    end

    def comments_attributes=(attributes)
      @comments_attributes = Comment.new(attributes)
    end
  end

  attr_accessor :content

  # 実装のロジック ------------------------------------
  def save
    # バリデーションエラーならfalseを返して以下の処理は行わない
    return false if invalid?

    shop.assign_attributes(shop_params)
    build_asscociation

    shop.save ? true : false
  end

  private

  def shop_params
    {
      name: name,
      category: @category,
    }
  end

  def build_asscociations
    # shopの子要素にcommentを追加する。ただし、中身が空なら追加しない。
    shop.comments << comments if comments[:content].present?
  end


end

これだけでつまづきどころがかなりありました。。。。
まず、concerning :ShopBuilder do ... endの部分ですが、以下のような意味を持ちます。

# この記述は...
concern :ShopBuilder do
  ...
end

# 下記と同じ
module ShopBuilder
  extend ActiveSupport::Concern
  ...
end

詳しくは、実装にあたって参考にした、こちらの記事をご覧ください。

次に、initialize(params = {}) ... endの部分なのですが、以下のような意味を持ちます。

def initialize(params = {})
  # @shopのparamsにアクセスできるようにする
  super(params)

  # DBでデフォルト値が設定されているカラム用の記述
  @category = params[:category]
end

まず、super(params)については、こちらも実装にあたって大変参考にさせていただいた記事である以下の記事によると

フォームクラスを使う

super(params)でパラメーターを格納する記述で、以下の記述と同じ意味を持ちます。

@attributes = self.class._default_attributes.deep_dup
assign_attributes(params)

また、db側でデフォルト値が設定されているカラムは、以下のように明示的にparamsにアクセスすることを書かないとparamsにアクセスできず、値を入力してもDBのデフォルト値になってしまいました...。

@category = params[:category]

この謎は解けず。今後の課題としたいです。。。
enumを使ったカラムにdb側でデフォルト値が必要な理由は、こちらの記事をご覧ください。

そして def comments_attributes=(attributes) ... end の部分なのですが、

def comments_attributes=(attributes)
  @comments_attributes = Comment.new(attributes)
end

こちらはRailsばかりやっているとなかなか目にしない、セッターメソッド という書き方で、=でおわるメソッド(引数)の形で、引数によって@のつく要素を変更することができます。
個人的には、こんなことをやっているイメージに近いのではないかなと思いました。

def comments_attributes=(attributes) # ... 以下略

# こんなイメージ
comments_attributes = attributes

# なので、こんな感じに呼び出せる
self.comments_attributes
# => attributesの中身

Rubyのゲッターとセッターを正しく理解していなかったせいですね。。。。トホホ。。。頑張ります。。。
なお、=でおわるメソッドについては、『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』p215を15回ぐらい読み直しました。

controller

次は、コントローラーの記述です。コントローラーはこのような形になりました。

app/controllers/shops_controller.rb
class ShopsController < ApplicationController

  def new
    @shop = ShopEntryForm.new
  end

  def create
    @shop = ShopEntryForm.new(shop_entry_params)
    if @shop.save
      # 成功したときの処理
    else
      # 失敗したときの処理
    end
  end

  private

  def shop_entry_params
    params.require(:shop_entry_form).permit(:caregory,
                                            :name,
                                            comments_attributes: [:content])
  end
end

こちらは、意外に記述が減らなかった印象があります。当初shop_entry_paramsがcontrollerから減ってくれればいいなーと期待したものの、結局controllerからは消せず。アソシエーションを作るメソッドだけはcontrollerから削除することができました。

なお、Modelに関しては、バリデーションとデフォルト値設定のメソッド、アソシエーションなども全て消すことができました!増えた記述は、なし!!やはり、form_objectはモデルをスリム化するために便利な書き方なのですね!!

View

最後に、Viewはこのようになっています。

app/views/shops/new.html.haml
= form_with model: @shop, url: shops_path, local: true do |f|
    = f.text_field :name

    = f.fields_for :shop_comments, local: true do |comment_form|
      = comment_form.text_field

    = f.submit "送信"

fields_forを使うあたりは、accept_nested_attributes_forを使った実装と変わらないのですね^^

テスト

テストも至ってシンプルでした!

spec/forms/shop_entry_form_spec.rb
require 'rails_helper'

RSpec.describe ShopEntryForm, type: :model do
  before do
    @shop_form = ShopEntryForm.new(category: "category1", name: "テストのお店")
  end

  describe "バリデーションのテスト" do
    it "名前とカテゴリーがあればバリデーションを通過すること" do
      @shop_form.valid?
      expect(@shop_form).to be_valid
    end

    # 以下略
  end
end

ファイルの置き場所と、RSpec.describe ShopEntryForm ...の部分, テスト用のインスタンス生成時の記述に注意すれば良いだけでした^^

これは、少し古いのですがこちらの記事を参考に作成しました。

フォームオブジェクトのテストをRSpecで書く

感想・参考資料など

さて、、、、本当に長い時間が実装にかかりました。実際のフォームはネストした子要素が3種類もあったり、形もかなり複雑だったのもあるのですが、何よりも素のRubyの書き方に慣れていなかったのが大きかったと思います。。。落ち着いたら、またRubyを復習したいです。

今回、参考にした記事や資料まとめです。

▼全体的な書き方
accepts_nested_attributes_forを使わず、複数の子レコードを保存する

▼paramsへのアクセス方法
フォームクラスを使う
『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』(p.215)

▼Concerningについて
Bite-sized separation of concerns

▼テストの書き方
フォームオブジェクトのテストをRSpecで書く

この後、editとupdateのフォームも残っているので、次はそちらを取り組みたいです^^

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

【Rails】 binding.pryの活用方法

この記事では、binding.pryの使い方を解説しています。
 
binding.pryを活用することで、
・一次ソースに触れる機会が増え、学習効率が上がる
・binding.pryを複数設置して、paramsの流れが理解しやすくなる

など、たくさんメリットがあります。
 
自分みたいな、初学者方の参考になればと思い、記事にしてみました。

前提

チャットアプリを題材にbinding,pryの使い方を学びます。
(注意:この記事ではチャットアプリは完成しません!チャットアプリ作成の記事ではありません)

開発環境
・ruby 2.6.5
・Rails 6.0.3.3

完成イメージ

chat_demo.gif

ER図

名称未設定ファイル (1).png

必要なテーブル
・usersテーブル
・roomsテーブル
・entriesテーブル (中間テーブルです!)

流れ

①user, room, entryモデル、テーブルを作成(下準備)

②アソシエーションを書く(下準備)

③Gemfileにpry-railsを追加してbundle installを実行(下準備)

④ビューにform_withを用意

⑤roomsコントローラーにcreateアクションを書く

①user, room, entryモデル、テーブルを作成(下準備)

userモデルのマイグレーションを編集
userモデルは、deviseを使って作成しているものとします!

db/migrate/xxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :nickname,  null: false
      t.string :email,     null: false, default: ""
      #省略
    end
    #省略
  end
end

usersテーブルには、nicknameとemailのカラムを用意しています。

roomモデルのマイグレーションを編集

db/migrate/xxxx_create_rooms.rb
class CreateRooms < ActiveRecord::Migration[6.0]
  def change
    create_table :rooms do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end

roomsテーブルには、nameのカラムを用意しています。

entryモデルのマイグレーションを編集(下準備)

db/migrate/xxxx_create_entries.rb
class CreateEntries < ActiveRecord::Migration[6.0]
  def change
    create_table :entries do |t|
      t.references :room, foreign_key: true
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

entriesテーブルは、usersテーブルとroomsテーブルを繋ぐ中間テーブルなので、
user, roomそれぞれを外部キーとして、references型で保存するようにしています。

②アソシエーションを書く(下準備)

一人のuserは複数のroomに入れて、
一つのroomは複数人のuserが入るので、
usersテーブルとroomsテーブルは「多対多」の関係になります。
以下のように、アソシエーションを記述します。

userモデル

app/models/user.rb
class User < ApplicationRecord
  #省略
  has_many :entries
  has_many :rooms, through: :entries
end

roomモデル

app/models/room.rb
class Room < ApplicationRecord
  has_many :entries
  has_many :users, through: :entries
end

entryモデル

app/models/entry.rb
class Entry < ApplicationRecord
  belongs_to :room
  belongs_to :user
end

③gem 'pry-rails' をインストール(下準備)

pry-railsをインストールできるようGemfileに記述して、bundle installを実行します。
参考 : rweng/pry-rails: Rails >= 3 pry initializer - GitHub

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

④ビューにform_withを用意

新しくルームを作成するために、roomsコントローラーにnewアクションを定義します。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new #newメソッドでインスタンスを作成
    @users = User.all #全ユーザーのレコードを取得
  end
end

 
ビューには、フォームを設置します。
チャットルーム名(name)を入力し、チャットしたい相手(user_ids)を選べるようにします。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">        <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user %></option>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

ここで、user_idsと複数形になっているのは、自分と相手の2人分保存するからです!
userモデルにて、has_manyを定義したことで、_idsメソッドが使えるようになりました!
参考 : Active Record の関連付け - Railsガイド

実際にフォームへ入力してみます。
すると下記のように、ユーザーが誰が誰だか分からないではありませんか!

ユーザー一覧表示_失敗

では、何が原因でこの出力が得られたのか推測します。

考えやすくするために、ユーザー選択の記述を、rubyの文法で書き直してみます。

@users.each do |user| 
 user 
end 

@usersは、roomsコントローラーのnewアクションで定義しているインスタンス変数で、
@users = User.allと定義しています。
ビューの中では、each文による繰り返し処理によって、@usersから一人ずつ取り出しています。

では、binding.pryを使って、出力される値を確認してみましょう。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">      <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <% binding.pry %>                  <%# ?each文の中にbinding.pryを設置!! %>
      <option value=<%= user.id %>><%= user %></option>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

 
ブラウザをリロードすると、ターミナルに以下のような出力が表示されます。

ターミナル
     3:   <%= f.text_field :name%>
     4:   <p><label>チャットしたい相手</label></p>
     5:   <select name="room[user_ids][]">
     6:     <option value="">未選択</option>
     7:     <% @users.each do |user| %>
 =>  8:     <% binding.pry %>
     9:       <option value=<%=user.id%>><%= user %></option>
    10:     <% end %>
    11:   </select>
    13:   <p><%= f.submit%></p>

[1] pry(#<#<Class:xxxx>>)> 

=>で、ビューの8行目で処理を止めてるよ!とターミナルが教えてくれています。
 
7〜10行間は、each文で繰り返し処理していることから、
繰り返し処理の1回目で、一時的に処理を止めてくれています。

したがって、userには一人目のデータが格納されていると考えられます。
では、実際にuserの値を確認してみましょう。

ターミナル
[1] pry(#<#<Class:xxxx>>)> user
=> #<User id: 1, nickname: "user_1", email: "test@1">
[2] pry(#<#<Class:xxxx>>)> user.nickname
=> "user_1"

[1]pry> の後に、を入力することで、
=>後に、を出力してくれます。

userには、idが1であるユーザーのレコードが格納されていることが確認できました。
 
今回はユーザー名を一覧表示させたいので、nicknameの値だけを取り出すことにします。
2回目のpryで、user.nicknameと記述すると、ユーザー名を取り出せると確認できました。
したがって、ビューファイルを下記のように書き換えます。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">      <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user.nickname %></option> <%# ?user.nicknameを表示するように変更!! %>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

ユーザー一覧表示_成功

無事にユーザー名を一覧表示することができました!
 
しかし、この一覧表示には、一つだけ問題があります。
それは、自分自身も表示されていることです。
 
このままでは、自分しかいない孤独なチャットルームが作成されてしまいます...。
このような事態を防ぐために、自分以外のユーザーを一覧表示するようにします。

どんな式が必要か、binding.pryを活用して探していきます。

ターミナル
     3:   <%= f.text_field :name%>
     4:   <p><label>チャットしたい相手</label></p>
     5:   <select name="room[user_ids][]">
     6:     <option value="">未選択</option>
     7:     <% @users.each do |user| %>
 =>  8:     <% binding.pry %>
     9:       <option value=<%= user.id %>><%= user.nickname %></option>
    10:     <% end %>
    11:   </select>
    12:   <p><%= f.submit%></p>

[1] pry(#<#<Class:xxxx>>)>

まず初めに、@usersの中身を確認しましょう。

ターミナル
[1] pry(#<#<Class:xxxx>>)> @users
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">,
 #<User id: 5, nickname: "user_5", email: "test@5">] # ?現在のユーザー(current_user)

@usersには、全ユーザーのデータが、一人ずつ配列で格納されていることが確認できます。
ではそもそも、インスタンス変数@usersとは何と定義していたかというと、
User.allと等しいよ!と定義していましたね。
 
では、User.allの内容を確認しましょう。

ターミナル
[2] pry(#<#<Class:xxxx>>)> User.all
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">,
 #<User id: 5, nickname: "user_5", email: "test@5">]

@usersと全く同じデータが出力されることが確認できました。

続いて、現在のユーザーのデータを取り出せないか試してみます。

ターミナル
[3] pry(#<#<Class:xxxx>>)> User.all.where(id: current_user)
=> [#<User id: 5, nickname: "user_5", email: "test@5">]

.whereはActive Recordのメソッドの一つで、条件に該当するレコードを配列に格納して出力してくれます。
超便利なのでどんどん使っていきましょう。
参考 : Active Record の基礎 - Railsガイド

さて、現在のユーザーのデータを取り出すことができました。

ということは、現在のユーザー以外のデータも取り出せるのでは?と思いつきます。
.where.notメソッドを使うと良さそうです。
.where.notは、条件に該当しないレコードを配列に格納して出力してくれる、.whereと対をなすメソッドです。

ターミナル
[4] pry(#<#<Class:xxxx>>)> User.all.where.not(id: current_user)
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">]

現在のユーザーはuser_5なので、現在のユーザー以外のデータが出力されています。
 
これで、現在のユーザー以外を一覧表示させる式を見つけることができました!
したがって、@usersの定義を変更しましょう。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new #newメソッドでインスタンスを作成
    @users = User.all.where.not(id: current_user) #現在のユーザー以外のレコードを取得
  end
end

ユーザー一覧表示_自分以外_成功
これで自分だけのチャットルームを作らないよう設定できました。
 
めでたしめでたし...と言いたいところですが、
roomを保存できるか確認してみましょう。

⑤roomsコントローラーにcreateアクションを書く

roomsコントローラーにcreateアクションを定義します。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new
    @users = User.all.where.not(id: current_user)
  end

  def create      #createアクションを定義
    binding.pry
  end
end

フォームで入力した情報(リクエスト)を確認したいので、
この段階では、createアクションには何も処理は定義せず、
binding.pryを設置しておきます。
 
こうすることで、
「フォームで入力された情報が届いたよー!」
と、ルーティングを介して、roomsコントローラーのcreateアクションを実行する瞬間に、
処理を止めることができます。
 
では、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。
すると下記のようにターミナル上で、createアクション内で定義したbinding.pryで処理を止めてるよと教えてくれます。

ターミナル
    7: def create
 => 8:   binding.pry
    9: end

[1] pry(#<RoomsController>)> 

 
では、リクエストのパラメータを確認してみましょう。

ターミナル
[1] pry(#<RoomsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false>

params(パラムス)はパラメーターズの略です。
authenticity_tokenは、セキュリティのために生成されるトークンなので、今回は無視します。
 
roomの中に、form_withで入力したパラメータが、配列としてハッシュで管理されています。
このroomとは、form_withで用意した、model: @roomと対応しています。

では、paramsの中の、roomの情報だけ見てみます。

ターミナル
[2] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2"]} permitted: false>

params[:xxxx]とすることで、見たいパラメータxxxxだけを確認できます。
上記の結果から、チャットルームroom1user_idが2のユーザーが入ったことが確認できます。
ちゃんとルームに人を呼べていることが確認できましたね。
 
めでたしめでたし.....
 
って、自分自身がルームに入ってないじゃん!!!
 
ここから、自分もルームに入れるよう、ビューを書き換えきます!

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
<select name="room[user_ids][]">        <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user.nickname %></option>
    <% end %>
  </select>
  <input name="room[user_ids][]" type="hidden" value=<%=current_user.id%>>  <%# ?現在のユーザーもroomに追加するように変更!! %>
  <%= f.submit %>
<% end %>

inputは、formにおけるテキストフィールドの種類を指定します。
hidden属性を指定することで、ブラウザには表示せずにパラメータとしてデータを受け渡すことができます。
この記述では、user_idscurrent_user(現在のユーザー)も含まれるよう記述しています!
 
ではもう一度、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。

ターミナル
[1] pry(#<RoomsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2", "5"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false>

[2] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

無事に現在のユーザー(user_5)user_idsに含まれていることが確認できました!
 
続いて、createアクションを定義し直して、テーブルにデータが保存できるようにしましょう。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  #省略
  def create
    @room = Room.new(room_strong_params)
    if @room.save
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def room_strong_params
    params.require(:room).permit(:name, user_ids: [])
  end
end

ストロングパラメータは、roomモデルの、nameuser_idsのパラメータだけ許可するとしています。
 
createアクションの内部で、どのようにパラメータの受け渡しがされているか、binding.pryを使って確認しましょう。

app/controllers/rooms_controller.rb
  #省略
  def create
    @room = Room.new(room_strong_params)
    binding.pry      # ?binding.pryを設置!!
    if @room.save
      binding.pry    # ?binding.pryを設置!!
      redirect_to root_path
    else
      render :new
    end
  end

インスタンス変数@roomが、保存される前、後でパラメータをそれぞれ確認してみます。
 
では、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。

ターミナル
     7: def create
     8:   @room = Room.new(room_strong_params)
 =>  9:   binding.pry
    10:   if @room.save
    11:     binding.pry
    12:     redirect_to root_path
    13:   else
    14:     render :new
    15:   end
    16: end

[1] pry(#<RoomsController>)>

@room保存前の各パラメータを確認します。

ターミナル
[1] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

[2] pry(#<RoomsController>)> @room
=> #<Room:xxxx id: nil, name: "room1", created_at: nil, updated_at: nil>

[3] pry(#<RoomsController>)> room_strong_params
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>

2回目のpryに注目してください。
@roomは、id: nilであることから、この時点では、レコードは作成されていないと分かります。
 
3回目のpryでは、ストロングパラメータを確認していますが、nameuser_idsに値が正しく格納されていることが確認できます。
 
リクエストしたパラメータが、コントローラーのcreateアクションに正しく受け渡されているのに、まだレコードが作成されていない理由は、
.newメソッドでインスタンスを作成する場合、
.saveメソッドを実行して初めてデータベースにレコードとしてコミットされるからです。

では、@room.save後を確認してみます。

ターミナル
     7: def create
     8:   @room = Room.new(room_strong_params)
     9:   binding.pry
    10:   if @room.save
 => 11:     binding.pry
    12:     redirect_to root_path
    13:   else
    14:     render :new
    15:   end
    16: end

[1] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

[2] pry(#<RoomsController>)> @room
=> #<Room:xxxx id: 1, name: "room1">

[3] pry(#<RoomsController>)> room_strong_params
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>

2回目のpryに注目してください。
@roomは、id: 1であることから、レコードは正常に保存されました!

今回は無事に保存できたのですが、
レコードを保存できなかった時に使える便利なメソッドも、合わせて紹介します!

ターミナル
[4] pry(#<RoomsController>)> @room.valid?
=> true

[5] pry(#<RoomsController>)> @room.errors
=> #<ActiveModel::Errors:xxxx @base=#<Room:xxxx id: 1, name: "room1">, @details={}, @messages={}>

[4] pryの@room.valid?では、「@roomのバリデーションはOK?」みたいな感じで、
バリデーションを実行してエラーがあるかを判別します。
エラーが無ければtrueを,
エラーが有ればfalseを返します。

[5] pryの@room.errorsでは、@room.valid?でfalseが返された時に、エラーメッセージを出力してくれます。
今回はエラーはないので、エラーメッセージは出力されていません。
エラーがある時はmessages{}の中にエラーメッセージが格納されます。

最後に、保存されたデータをコンソールで確認してみましょう。

ターミナル
% rails c
[1] pry(main)> Room.all
=> [#<Room:xxxx id: 1, name: "room1">]

[2] pry(main)> Entry.all
=> [#<Entry:xxxx id: 1, room_id: 1, user_id: 2>,
 #<Entry:xxxx id: 2, room_id: 1, user_id: 5>]

コンソールでも、Active Recordのメソッドを使うことができます。
 
Room.allで全てのルームを表示させると、
room1が保存されていることが確認できます。

Entry.allで全てのレコードを表示させると、
2つのレコードが保存されていることが確認できます。
「ルーム1にユーザー2と5がいるよーっ!」と教えてくれています。

コンソールで確認した内容は、以下の表と同じ内容です!

roomsテーブル

id name
1 room1

entriesテーブル

id room_id user_id
1 1 2
2 1 5

最後までお付き合いいただきありがとうございました!:smile:

参考資料

【Rails】find・find_by・whereについてまとめてみた

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

【Rails】検索フォームで、ひらがな・カタカナ・漢字の区別なく検索(精度は100%ではないよ)

自分がPFとして作っているアプリに検索フォームを実装しました。
ただ、【rails 検索 フォーム】 とかで検索すると、部分一致や完全一致が多く出てきます。
なんとかして漢字の部分をひらがなやカタカナで検索できないかな、と思いやってみました。

流れとしては、動画のタイトルを保存する時に、そのタイトルをローマ字に変換して専用のカラムに保存し、検索するときも検索ワードをローマ字に変換して専用のカラムと参照する、という感じです。
最初は検索する時にタイトルを全部変換しようかと思いましたが、動画が増えると時間がかかりそうだな・・・と思ったので上記の方法にしました。

如何せん初学者なので、そんな冗長なことしなくてもみたいな部分はあるのと思いますが、忘備録の意味合いも込めて書くので大目に見てやって下さい。

参考にしたサイト

検索フォームを作る

検索フォーム自体は色々と記事があるので簡単に作れると思います。
自分の場合は動画の投稿サイトです。検索で、検索ワードが動画のタイトルに一致する、という検索フォームを作ります。

/application.html.erb
      <div id="search-box">
        <%= form_tag(search_path, :method => 'get') do %>
          <div class="input-tag">
            <%= text_field_tag :search, '', placeholder: '検索', value: params[:title] %>
          </div>
          <div class="submit tag">
            <%= button_tag type: 'submit', class: 'btn btn-default' do %>
              <i class="fas fa-search"></i>
            <% end %>
          </div>
        <% end %>
      </div>

ルーティングを、videosコントローラーのサーチアクションに飛ばします。

routes.rb
  # 検索機能
  get "search" => "videos#search"

コントローラーに追記していく

gemを導入します。
今回、 miyabi というgemを使いました。ひらがな〜カタカナ〜ローマ字に変換したり判定したりできるgemです。
今回使ったメソッド

.to_roman   #文字列をローマ字に変換
.to_kanhira #漢字が含まれた文字列をひらがなに変換
.is_hira?   #文字列がひらがなか判定
.is_kana?   #文字列がカタカナか判定

gemを導入したらコントローラーのcreateとsearchを書いていきます。
Videoというモデルには
user_id title introduction
のカラムがあり、そこにタイトルをローマ字に変換した物を保存する conversion_title というカラムを追加しました。

schema.rb
  create_table "videos", force: :cascade do |t|
    t.integer "user_id"
    t.string "title"
    t.text "introduction"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "conversion_title"
  end

videos.controllerのcreateを作るのですが、問題が発生しました。

gem 'miyabi' では、【漢字が含まれている文字列を変換】はできるのですが、【漢字が文字列に含まれているか】は判定することができません。

rubyの持つ、正規表現で漢字が含まれているかどうかを判断します。
今回は漢字がタイトルに含まれているかどうかを調べたいので、ピンポイントで漢字だけを判定させます。

@video.title.match(/[一-龠々]/)

これで漢字が含まれているかどうか判定できます。

createで投稿された動画を保存します。

/videos_controller.rb
  def create
    @video = Video.new(video_params)
    @video.user_id = current_user.id
    if @video.title.match(/[一-龠々]/)
      @video.conversion_title = @video.title.to_kanhira.to_roman
    elsif @video.title.is_hira? || @video.title.is_kana?
      @video.conversion_title = @video.title.to_roman
    else
      @video.conversion_title = @video.title
    end
    if @video.save
      redirect_to video_path(@video)
    else
      render :new
    end
  end

  private

  def video_params
    params.require(:video).permit(:title, :introduction, :video)
  end

上から順に、まず、タイトルに漢字が含まれるか判定します。
含まれていれば、タイトルをひらがなに変換した後さらにローマ字に変換して保存します。

漢字が含まれておらず、全てひらがな、カタカナの場合はローマ字に変換し保存します。

どちらにも当てはまらない場合は、ローマ字で投稿されていると判断してそのまま保存します。

searchアクションも同様に書いていきます。

/videos_controller.rb
  def search
    word = params[:search]
    unless word.blank?
      if word.match(/[一-龠々]/)
        conversion_word = word.to_kanhira.to_roman
      elsif word.is_hira? || word.is_kana?
        conversion_word = word.to_roman
      else
        conversion_word = word
      end
    end
    @search_video = Video.search(conversion_word)
  end

フォームで検索されたワードを、wordに代入して、wordが入っていればローマ字に変換します。
createと同様に、上から順番に条件にあった変換をします。
もし検索ワードが何も無しで検索された場合は動画を全て返しています。

検索結果のviewはこんな感じ。

/videos/search.html.erb
<h2>検索結果</h2>

      <% unless @search_video.blank? %>
        <div class="row">
        <% @search_video.each do |video| %>

        ===== 省略 =====    

        <% end %>
        </div>
      <% else %>
        <p>検索結果はありません</p>
      <% end %>
  </div>

検索したワードに一致するものがなければ、その旨を表示するようにしてあります。
これで一通りできました。

完成!

実際にやってみます。

スクリーンショット 2020-09-28 13.30.22.png

ひらがなで "うみがめ" と入力

スクリーンショット 2020-09-28 13.31.12.png

"umigame" という conversion_title を持っている動画を返してくれました。
(海亀のタイトルを持っている動画がたくさんありますが、これは conversion_title を追加する前の動画です。ご愛敬。)

ローマ字でも検索してみます。

スクリーンショット 2020-09-28 14.38.18.png

スクリーンショット 2020-09-28 14.38.33.png

表示されました。

その他

テストをしながら、ブラウザバック等が入るとパラメーターの動きが変わるのか、全ての動画が読まれたりというとが発生します。
多分キャッシュとかなんだろうな...JSも勉強しないとなぁ...と思うところであります。

漢字に関しては、タイトルにもありますが100%完璧に変換してくれる訳ではないようです。
(実際、 "最強" という文字が "saikiu" と変換されていました)
ちょっとした検索を作りたい時などに利用できるかと思います。

もっといい方法があれば、ぜひお願いします。

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

active recordで特定カラムを空で保存できるように修正

解決策

下記のようにallow_blankを追加したら解決できました

validates :something, allow_blank: true

参考記事

Validate attribute only if it present (only if user fill in it)

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

【ActiveAdmin】コピペ新規作成するcloneアクションを追加する

はじめに

rails_adminの方ではclone機能をgemで追加して実装出来たのですが……
→ https://qiita.com/MATO/items/116bda1f3629ece0812c

active_adminの方ではgemではなく設定ファイルをいじって実装出来たのでメモです。
かなり汎用性あるコード書けたと思います。

ソースコード

関係ない所もありますが、見返す用に全部載せておきます。

app/admin/items.rb
ActiveAdmin.register Item do

  permit_params :kind_id, :name, :memo

  # 一覧ページで検索フィルター要らないので消しておく
  config.filters = false

  # 一覧ページでのデフォルトソート
  config.sort_order = 'updated_at_desc'

  # 間違って削除しないように処理自体を消しておく
  actions :all, :except => [:destroy]

  # カスタムアクション、clone
  member_action :clone, method: :get do
    from_item = Item.find(params[:id])
    @item     = Item.new

    # ここでコピペしておきたいカラムをもう入れてしまう
    @item[:kind_id]     = from_item.kind_id
    @item[:name]        = from_item.name

    render :new, layout: false
  end

  # 詳細ページにもCloneボタンを追加
  # indexページでエラーが出る、、、のでIF文入れておく
  action_item :only => :show do
    if params[:id].present?
      link_to "Clone Item", clone_admin_item_path(id: params[:id])
    end
  end

  index do
      # selectable_column
      # id_column

      column :kind
      column :name
      column :memo

      actions defaults: false do |item|
        item 'View', admin_item_path(item), class: 'view_link member_link'
        item 'Edit', edit_admin_item_path(item), class: 'edit_link member_link'
        item 'Clone', clone_admin_item_path(id: item.id), class: 'clone_link member_link'
      end

  end

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

    actions
  end


end

これで一覧ページでレコードごとにCloneリンクも追加されて、機能します。
/adminから/manageとかに名前を変更した場合でも、rails routesで表示されるパスリンク?を使えばいけます。

カスタムアクションとして

今回は「新規作成ページ」というnewと同じ処理する時にちょっとデータを入れただけ、ですね。
ただこれを元にしたら、そのレコードに対しての処理を好き放題にControllerに書くように書けたので、かなり楽しくカスタムできそうです。

参考ページ

GitHubにドンピシャの質問あったのですが、色々と方法ありすぎたり、2013年とちと古かったり、英語だし……でちょっと違いますね。
https://github.com/activeadmin/activeadmin/issues/972

俺のコードの方がキレイ?

終わり

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

クラスやメソッド、変数における名前の単語の区切り方

疑問
 クラスやメソッド、変数の名前をつける時に、単語の区切り方の違いが分からない

結論
 クラスにはアッパーキャメルケースを使い、メソッドや変数にはスネークケースを用いる。

説明
(ex)
「 user tweet creator」という名前を付けたい時を例にする

・クラスに命名する時
UserTweetCreator
というように命名する

・メソッドや変数に命名する時
user_tweet_creator
というように命名する

つまり、
アッパーキャメルケース
 初めの文字を大文字にし、単語の区切りを大文字で表す

スネークケース
 単語の区切りを_で表す

となります。メソッドや変数はスネークケースを使用かつ、全て小文字となります。

まとめ
命名規則を守ることで、チームの方達との、コードの意味の伝達が円滑になると思いますので、
参考にして、いただけると幸いです。

*ちなみにカラム名もスネークケースを用います。

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

とりあえずのdocker-compose upから入って、Web server(Nginx)の基礎設計を学びながら、Dockerを学ぶ①

①環境構築〜最もシンプルなWeb server構築編です

分かりづらい点、不正確な点はコメントいただけましたらモチベーションに繋がります
今回こちらに先立ってZennというサービスで記事を書いて見ました
元記事
とりあえずのdocker-compose upから入って、Web serverの基礎設計を学びながらDockerを学ぶ① | Zenn

はじめに

以下のような方とって有益な内容になればと思っています

  1. これから初めてのwebアプリを作成する
  2. webアプリを作成し、これからデプロイする
  3. 初めてデプロイまで到達したが、Nginxが何をしているかとか、設定内容はコピペでよくわかっていない

私は独学プログラミング5ヶ月目で3の状態に近いかと思います
個人的なメッセージとしては是非1.の状態の方により多く、この記事の内容が届くといいなと思っています
Dockerがなんとなくわかる、便利な気がする!webアプリが動く仕組みに関心を広げる、そんな気づきを共有できたらいいなと思っています

この記事と同じ内容は、AWS上のEC2を利用したり、契約したVPSを利用するよることで再現可能ですが、dockerを利用すれば完全無料で挑戦可能です!より手軽で、予定外の課金に怯える必要はありません


この記事で知ることができる内容

ネットワークに視野を広げる

  • webアプリが最低限動作するために必要な構成を知る
  • Nginx(エンジンエックスと読みます)の3つの重要な役割, webサーバー, ロードバランサー, リバースプロキシの役割に触れる
  • Nginxの基本的な設定を知る

Dockerの基本を知る

  • 既存の開発環境を簡単に再現できることを知る
  • Docker上で開発を行うために最低限必要なコマンドを試すことができる
  • docker-compose.ymlに記述された内容や、volumeの仕組みを手を動かして知ることができる
  • 複数のdocker-compose.ymlを用意して、異なる環境をシミュレートする (development -> production)

webアプリの開発環境 - 本番環境での違いを知る

  • 本番環境でアセットコンパイルが必要な理由を知る
  • 開発環境でアセットコンパイルが必要でない理由を知る

よって、この記事の最後ではDocker上で、仮想の本番環境で開発環境との違いに触れながら、アプリをデプロイしてみます

Appendix

appendixは補足的内容となっています
その項で知ることのできる内容を初めに書いておきましたので、
改めて知る必要のない内容でしたら読み飛ばして頂いて結構です
もし知らない内容でしたら、実際に手を動かして頭の片隅に留めておくことで、後々役に立つ物があるかもしれません


必要なもの、スキル

【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その1:コンテナってなに? 〜 | SIOS Tech. Lab
概念をさらっと理解していただき、ここでは手を動かしてみるというのがおすすめです

アプリの部分はFW(フレームワーク)にRailsを使用しておりますが、
Railsの知識はなくても大丈夫です

(私自身Rails以外の開発経験がないため、他のFWにおいて不適切な内容があるかもしれません)

検証環境

macOS Catalina
docker desktop: 2.3.05
(docker engin: 19.03.12, docker-compose: 1.27.2)
Nginx: 1.18
Rails: 6.03
PostgreSQL: 11.0

アーキテクチャ(設計)概要

これからDockerで構築する環境では
Nginxはリバースプロキシとして機能していて、静的コンテンツをapp: Railsに代わって代理(=プロキシ)配信しており、動的コンテンツへのリクエストのみapp: Railsに転送するようになっています

というのを少しずつ理解していきたいと思います

スクリーンショット 2020-09-28 2.53.04.png

よく見る構成です(Databaseほか一部省略)
docker上でweb(Nginx), app(rails)というサービスがそれぞれ独立したコンテナで動いていて
docker-composeによってそれぞれの依存関係等が定義されているような理解です


目標5分、DockerでRailsの環境構築

Nginx - Railsの環境を構築します
以下の素晴らしい記事を参考にします(笑)
Nginx, Rails 6, PostgreSQL環境(おまけにBootstrapまで)がすぐに構築できます!
少しづつ改善していますので、改善コメントもお待ちしております。

コマンドひとつ、5分でRails6の開発環境構築 on Docker - Rails6 + Nginx + PostgreSQL + Webpack (Bootstrap install済) - Qiita

上記をベースに今回の記事のために用意したソースコード
https://github.com/naokit-dev/try_nginx_on_docker.git

ソースコードをgit clone

#アプリを配置するディレクトリを作成(アプリケーションルート)
mkdir try_nginx_on_docker

#アプリケーションルートへ移動
cd $_

#ソースコード取得
git clone https://github.com/naokit-dev/try_nginx_on_docker.git

#アプリケーションルートにソースコードを移動
cp -a try_nginx_on_docker/. .
rm -rf try_nginx_on_docker

以下のような構成になるかと思います

.(try_nginx_on_docker)
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── docker
│   └── nginx
│       ├── default.conf
│       ├── load_balancer.conf
│       └── static.conf
├── docker-compose.prod.yml
├── docker-compose.yml
├── entrypoint.sh
├── setup.sh
└── temp_files
    ├── copy_application.html.erb
    ├── copy_database.yml
    └── copy_environment.js

ソースコードの一部
docker-compose.yml
4つのコンテナが定義されています

version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
      - /var/www/html
    depends_on:
      - app

  db:
    image: postgres:11.0-alpine
    volumes:
      - postgres:/var/lib/postgresql/data:cached
    ports:
      - "5432:5432"
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
      TZ: Asia/Tokyo

  app:
    build:
      context: .
    image: rails_app
    tty: true
    stdin_open: true
    command: bash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp:cached
      - rails_cache:/myapp/tmp/cache:cached
      - node_modules:/myapp/node_modules:cached
      - yarn_cache:/usr/local/share/.cache/yarn/v6:cached
      - bundle:/bundle:cached
      - public:/myapp/public
      - log:/myapp/log
      - /myapp/tmp/pids
    tmpfs:
      - /tmp
    ports:
      - "3000-3001:3000"
    environment:
      RAILS_ENV: ${RAILS_ENV:-development}
      NODE_ENV: ${NODE_ENV:-development}
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      WEBPACKER_DEV_SERVER_HOST: webpacker
    depends_on:
      - db
      - webpacker

  webpacker:
    image: rails_app
    command: ./bin/webpack-dev-server
    volumes:
      - .:/myapp:cached
      - public:/myapp/public
      - node_modules:/myapp/node_modules:cached
    environment:
      RAILS_ENV: ${RAILS_ENV:-development}
      NODE_ENV: ${NODE_ENV:-development}
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    tty: false
    stdin_open: false
    ports:
      - "3035:3035"

volumes:
  rails_cache:
  node_modules:
  yarn_cache:
  postgres:
  bundle:
  public:
  log:
  html:

環境構築

source setup.sh

正常にセットアップが終われば
アプリケーションルートディレクトリで以下のコマンドでコンテナ立ち上げた後

docker-compose up

# バックグラウンドで起動させる場合 -dオプション
docker-compose up -d

ブラウザからlocalhostもしくはlocalhost:80へアクセスすると

Yay! You’re on Rails!が確認できるかと思います
誰でも簡単に開発環境を構築できます!Dockerのメリット1つ目

ここで起動しているコンテナを確認してみます
(-dオプションを付けずにdocker-compose upした場合には新しくターミナルを開きます。VS Codeならcontrol+@ *mac環境)

docker ps

web (Nginx), app(Rails), webpacker(webpack-dev-server), db(PostgreSQL)の4つのコンテナが起動していることだけ確認してください

確認できたら一旦コンテナを終了させておきます

docker-compose down

Nginxで静的コンテンツを配信してみる

まだRailsアプリは使用しません
ここでは以下に挑戦します

  • Nginxの最小の設定を確認する

  • Docker-composeを利用しつつ、コンテナを単独 (nginxのみ) で起動してみる

  • Nginx単独で単純な静的コンテンツ(HTML)を配信してみる

最もシンプルなNginxの設定

Nginxの設定を変更するためdocker-compose.ymlを編集します

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
    #ここを書き換える./docker/nginx/default.conf... -> ./docker/nginx/static.conf...
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
    depends_on:
      - app
...

Dockerのvolumeについて少し
volumes:./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf<host側path>:<container側path>になっていて
これによってホスト(ローカル)側のstatic.confをボリュームとしてマウントし、コンテナ内のdefault.confとして扱えるようにしています
ここでは、ホスト側とコンテナ内では、ストレージが独立して存在するように振る舞われるため、このようなvolumeマウントが必要ということだけ心に留めてください

docker/nginx/static.confにはNginxの設定が記述されており、中身は以下のようになっています

server { #ココから
      listen 80; # must
      server_name _; # must
      root /var/www/html; # must
      index index.html;
      access_log /var/log/nginx/access.log;
      error_log  /var/log/nginx/error.log;
} #ココまで、一つのserverブロック = Nginxが扱う一つの仮想サーバーの仕様

server: "{}"で囲われた内容(serverブロック)をもとに仮想サーバーを定義します

ここでは以下3項目が設定必須です

listen: 待ち受けるIP, portを指定(xxx.xxx.xxx.xxx:80, localhost:80, 80)
server_name: 仮想サーバに割り当てる名前。Nginxはリクエストに含まれるホスト名(example.com)やIP(xxx.xxx.xxx.xxx)に一致する仮想サーバを検索します。("_"はすべての条件で一致させるの意味です。その他、ワイルドカードや正規表現が利用可能)
root: ドキュメントルート、コンテンツが配置されたディレクトリを指定します

ちなみにlogについては、etc/nginx/nginx.confというファイルで上記と同じパスが定義されているので、
ここで記述がなくても、error_logおよびaccess_logともに/var/log/nginx/以下に記録されるはずです
例えばaccess_log /var/log/nginx/static.access.log;とすることで、当該の仮想サーバ(serverブロック)固有のログを記録することもできるようです

Nginxコンテナ単独で起動

先程のdocker-compose upではnginx, rails, webpack-dev-server, dbのすべてのコンテナが起動していますが、docker-composeのオプションを使用することで特定のコンテナだけを起動することも可能です

--no-deps: コンテナ間の依存関係を無視して起動 (ここではweb: nginxのみ)
-d: バックグラウンドでコンテナを起動、シェルは入力を継続できます
-p: ポートマッピング<host>:<container>
web: composeで定義されたnginxコンテナです

以下のコマンドでNginxコンテナを起動します

docker-compose run --no-deps -d -p 80:80 web

(ポートマッピングについてはcomposeでも指定しているのですが、改めて指定する必要があり、ホスト側のport 80をwebコンテナのport 80にマッピングしています)

docker-composeをオプション無しで実行したときとの違いを確認します

docker ps

先ほどと異なり、nginxのコンテナのみが起動していると思います

HTMLコンテンツを作成

コンテナの中でシェルを呼び出します

docker-compose run --no-deps web bash

以下webコンテナ内での作業です

# index.htmlを作成
touch /var/www/html/index.html

# index.htmlの中身を追加
echo "<h1>I am Nginx</h1>" > /var/www/html/index.html

# index.htmlを確認
cat /var/www/html/index.html
<h1>I am Nginx</h1>

これでコンテナ内のドキュメントルート直下にindex.htmlが作成できたのでexitでシェルを閉じましょう

動作確認

ブラウザからlocalhostにアクセスすると、
以下のようにHTMLとして配信されているのが確認できていると思います

スクリーンショット 2020-09-22 11.25.12.png

ここでのNginxはリクエストに一致するコンテンツをドキュメントルートから探して、一致するものを返すというシンプルな挙動をしています

確認できたら一旦コンテナを終了させておきます

docker-compose down

Appendix - リクエストに一致する仮想サーバがない場合のNginxの挙動

  • Nginxのデフォルトサーバーの概念を知る

Nginxはクライアントからのリクエストに含まれるHostフィールドの情報をもとに、どの仮想サーバーにルーティングするかを定義しています

では、いずれの仮想サーバもリクエストと一致しない場合はどのような挙動をするのでしょうか?
設計を考える上で重要そうだったので、ここではそれを確認してみます

先の設定ファイルのserverブロックで、いずれのリクエストに対しても該当するようにserver nameを定義しましたが、これをリクエストと一致しないデタラメな名前に書き換えてみます

server_name undefined_server;

再びNginxコンテナを起動します

docker-compose run --no-deps -p 80:80 web

ブラウザからlocalhostにアクセスすると、リクエストに一致する仮想サーバが存在しないにもかかわらず
予想に反して先ほどと同じ"I am Nginx"が表示されると思います

default server

Nginxはリクエストがいずれの仮想サーバにも該当しなかった場合、default serverで処理する使用になっており、一番最初、一番上に記述された仮想サーバをdefault serverとして扱う仕様になっています

In the configuration above, the default server is the first one — which is nginx’s standard default behaviour. It can also be set explicitly which server should be default, with the default_server parameter in the listen directive:
How nginx processes a request

またはlistenディレクティブに明示的にdefault_serverを指定することも可能です

listen      80 default_server;

今回の実験では"undefined_server"はリクエストに一致しないが、他に一致するものがないので
default serverとしてルーティングされたと考えられます

いずれの仮想サーバもリクエストと一致しない場合 => default serverにルーティングされる

うまくバックエンドのサーバーに接続されない場合など、エラーを切り分けるのに役立つ気がします

一旦コンテナも終了させておきましょう

docker-compose down

appendix - Dockerのvolumeを少し理解する

  • コンテナの独立性について知る
  • コンテナ - コンテナ間でストレージを共有する(永続化して共有する)仕組みとしてvolume、ここでは特にnamed volume, anonymous volumeの違いについて知る

そもそもvolumeが必要(= 永続化が必要)な意義について
Dockerではコンテナ内のデータを永続化するためにvolumeを作成し管理します

よくわからないので確認してみます

webコンテナの中でシェルを呼び出します

docker-compose run --no-deps web bash

以下webコンテナ内での作業です

# 検証用のディレクトリを作成
mkdir /var/www/test

# 検証用のファイルを作成します
touch /var/www/test/index.html

# 存在確認
ls /var/www/test/

これで/var/www/testdocker-compose.ymlの中でボリュームとして管理されていないパスであることがポイントです

一旦exitでシェルを閉じましょう(コンテナも終了します)

再度webコンテナを起動しシェルを呼び出します

docker-compose run --no-deps web bash

先程のファイルを探してみます

cat /var/www/test/index.html
ls /var/www

いかがでしょうか、
ディレクトリ/var/www/test、ファイル/var/www/test/index.htmlともに見つからないと思います

コンテナを終了すると、コンテナ内のデータは保持されないこれが原則であり
ボリュームはこの仕組を回避するために利用可能です

exitでターミナルを閉じます

すべてのコンテナを停止します

docker-compose down

volumeの種類

Dockerにおけるボリュームには以下のタイプがありますが、コンテナ内のデータを永続化するという点では同じです

  1. host volume ?(ちょっと名前がわからないです)
  2. anonymous volume (匿名ボリューム?anonymous volumeで通っている気がします)
  3. named volume (名前付きボリューム)

docker-compose.ymlを見みてみます

version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf #host volume
      - public:/myapp/public # named volume
      - log:/var/log/nginx # named volume
      - html:/var/www/html # named volume

...

volumes: # ここで異なるコンテナ間での共有を定義
  public:
  log:
  html:

host volume
nginxの設定のパートで触れました
./docker/nginx/static.conf:/etc/nginx/conf.d/default.confの部分でホスト側のパスをボリュームとしてマウントします
ホスト内のファイルをコンテナ側にコピーしているイメージです

named volume
html:/var/www/htmlの部分
"html"という名前をつけてボリュームをマウントしています
さらに、"services"ブロックと同列の"volumes"ブロックでこの名前をもって定義することで
複数のコンテナ間でボリュームをシェアすることを可能にしています

そして、このボリュームはホスト側からは独立して永続化されます

最後にanonymous volume
公式docではnamed volumeとの違いは名前があるかないかのみとありますが
実際に名前がないというより、named volumeの名前に相当する部分がコンテナごとにハッシュで与えられているそうです
ちょっとわかりにくいですが、ホスト側をマウントする必要がないが、永続化の必要がある、かつ複数のコンテナでの共有を想定しない場合に利用するケースが考えられます
(まだイメージし難いですが、この後のコンテンツでanonymous volumeでないといけない場面に遭遇します)

ここでは少し理解を深めるために検証してみます
もともとnamed volumeとして定義している/var/www/htmlをanonymous volumeに変更して
本項で実施したHTMLファイル作成の手順を繰り返してみます

docker-compose.yml

version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
      - /var/www/html # コンテナ側のpathのみ指定しanonymous volumeに変更

...

volumes:
  public:
  log:
  # html: ここをコメントアウト

Nginxをweb serverとして起動

docker-compose run --no-deps -d -p 80:80 web

シェルを呼び出します

docker-compose run --no-deps web bash

ここが重要なのですが、別のターミナルでいま起動しているコンテナを確認すると

docker ps

2つのコンテナが起動しており、シェルが動いているコンテナは、80:80でポートマッピングしているコンテナとは別であることがわかります

このままコンテナ内でHTMLを作成

# index.htmlを作成
touch /var/www/html/index.html

# index.htmlの存在を確認
ls /var/www/html

さきほどと同様にブラウザからlocalhostにアクセスしてみましょう

するとブラウザは403エラーを示し
Nginxのエラーログを確認すると

tail -f 20 /var/log/nginx/error.log
...directory index of "/var/www/html/" is forbidden...

ディレクトリを見つけられないとエラーが記録されています

named volume -> anonymous volumeに変更したことで
2つのコンテナ間で/var/www/html/以下の内容が共有されなくなり
ローカルからport 80でリクエストを受けたコンテナからはindex.htmlを参照することができなくなったことで
このようなエラーが生じていると考えられます

永続化はするが、他のコンテナとボリュームを共有しない、その特性に触れることができたかと思います

確認できたらexitでシェルを閉じ

毎度ですがコンテナを終了させておきましょう

docker-compose down

(変更したdocker-compose.ymlの内容はこのままでも構いません)

...

appendixの内容に思ったよりも熱が入ってしまい長くなったので、(私のモチベーション維持のために)一旦ここで区切ります

②に続く


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

最後に任意の文字があるかのメソッドを作る

【概要】

1.結論

2.どのようにコーディングするか

3.開発環境

1.結論

downcaseメソッド、lengthメソッドと、sliceメソッド3種類を使う!

2.どのようにコーディングするか

def str_discrimination(str_a, str_b)
  a_down_str = str_a.downcase #---❶
  b_down_str = str_b.downcase
  a_len_str = a_down_str.length #---❷
  b_len_str = b_down_str.length
  if b_down_str.slice(-(a_len_str)..- 1) == a_down_str || a_down_str.slice(-(b_len_str)..- 1) == b_len_str #---❸
    puts "True"
  else
    puts "False"
  end
end

❶:大文字小文字で区別しないためです。任意の2種類の文字列(str_a,str_b)をdowncaseメソッドで全てを小文字にしました。

変数(文字列).downcase

❷:❶で小文字に変換した変数をlengthメソッドで文字数を返しています。これは❸で使うためにコーディングしています。

❸:ここで最後に任意の文字があるかを判別しています。str_aやstr_bのどちらに探されたい文字列、探したい文字が来てもいいようにOR条件にしています。またsliceメソッドを使用し、❷で使用したlengthメソッドで代入された変数を入れることで最後から任意の文字を探しています。(-(a_len_str)..- 1)は、最後から(-1)から-(a_len_str)の文字数までを示しています。

参考にしたURL:
はじめてのRuby!文字列を大文字⇔小文字に変換する方法まとめ
length、size、count メソッドの違いまとめ【Ruby】

3.開発環境

Ruby 2.6.5
Rails 6.0.3.3
Visual Studio Code

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

[Rails]データベースに保存された情報をviewに表示する方法

投稿の内容

今回はデータベースに保存された情報をviewに表示する方法について投稿します。
なお、既にデータベースに情報は保存されている、として投稿します。

実装の流れは以下の通り。

① コントローラーアクションの記述(showアクション)
② 表示したい情報が文字列の場合
③ 表示したい情報が複数枚画像の場合

環境

Rails 5.2.4.3
ruby 2.5.1
mysql 14.14
viewはhamlで実装

① コントローラーアクションの記述(showアクション)

今回は商品(item)詳細ページに情報を表示したいので、itemsコントローラーのshowアクションを使います。

items.controller.rb
class ItemsController < ApplicationController
#省略

  def show
    @item = Item.find(params[:id])
  end

#省略

解説) まずItemモデルのpathのidをfindメソッドで検索し、該当するidに属した商品(item)のインスタンスを作成します。← 分かりにくければすいません...。

ここで作成したインスタンスをもとに、データベースから情報を引き出します。

② 表示したい情報がテキストの場合

image.png
データベースに保存されている情報が上記画像のような場合、= @item.カラム名と指定してあげるとviewに表示できます。

例えば、nameカラムの情報を表示したい場合は以下のようにすればOKです。

show.html.haml
%h2.item-show-page__item-name
  = @item.name

③ 表示したい情報が複数枚画像の場合

今回はitemsテーブルとアソシエーションを組んでいるitem_imagesテーブルに保存されている複数枚画像を表示したいとします。

show.html.haml
- @item.item_images.each do |image|
  = image_tag image.image.url

解説) まず、itemsテーブル(@item)とアソシエーションを組んでいるitem_imagesテーブルをブロック変数imageに変換します。次に、image_tagを使い、先ほど変換したブロック変数image(item_imagesテーブル)のimageを呼び出し、.urlを付けます。

この.urlはデータベースから情報を選択し、viewに表示させる場合必須となります。.urlがなければviewにはデータベースに保存されている画像URLが表示されてしまします。

あとはeach文で繰り返し処理ですね。

最後に

今回はデータベースに保存された情報をviewに表示する方法について投稿しました。
Railsでアプリケーションを開発する際に、必ず用いる方法と言っても過言ではないと思いますので、是非参考にしていただければと思います。

最後まで目を通していただきありがとうございました!

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

Rails6 OmniAuth activestorage ユーザー画像を取得する

永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。

この記事の目的

active storageにユーザーのプロフィール画像を保存する。
ググってみてもcarrierwaveばかりでactive storageの記事をあまり見かけなかったので、同じ境遇の人がいればと思い投稿ました。

環境

Ruby 2.7.1p83
Rails 6.0.3.3

前提条件


deviseによるログイン機能実装済み。


OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。


私は、以下の記事を参考にさせて頂きました


・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240

参考

・active storageに画像URLを保存する方法
https://qiita.com/gomasio1010/items/09c6ee58ed4c95f109ff

とても助かりました!ありがとうございます。

active storageに画像urlを保存する

機能実装できている前提で、

app/models/user.rb

require "open-uri"  #ここ
class User < ApplicationRecord

  #省略

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.name = auth.info.name
      user.password = Devise.friendly_token[0, 20]
      user.email = auth.info.email    
      user.email = User.dummy_email(auth) if user.provider == "twitter" 
      avatar = open("#{auth.info.image}") #ここ
      user.image.attach(io: avatar, filename: "user_avatar.jpg") #ここ
    end
  end

 #省略

end

記事を参考に画像を取得する事ができました。
active storageでの記事はあまり見かけなかったので、とても助かりました。

画像が小さくて、荒くなるとの噂でしたので、サイズが大きくなりそうな感じにしてみました。
色々見ているとモデル側で指定している方もいらっしゃいました。

config/initializers/devise.rb

  config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET_KEY'], :image_size => 'large'#これ
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
  config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY'], callback_url: "http://localhost:3000/users/auth/twitter/callback", :image_size => 'original'#これ

ログイン認証関連の実装は、時間帯によってうまくいかなかったり、ブラウザにcookieが残っていると正しく動かない時がありましたので気長にやると良さそうです。

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

Rails6 OmniAuth twitter認証 emailの条件分岐

永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。

この記事の目的

twitter認証はメールアドレスを取得しないので、ランダムメールアドレスを生成する。
google、facebook認証は、正規のメールアドレスを取得する。
twitterはランダムアドレス。他は、正規のアドレスで条件分岐させる。

環境

Ruby 2.7.1p83
Rails 6.0.3.3

前提条件


deviseによるログイン機能実装済み。


OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。


私は、以下の記事を参考にさせて頂きました


・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240

条件分岐 : TwitterかTwitter以外

機能実装できている前提で、

app/models/user.rb

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name
    user.password = Devise.friendly_token[0, 20]
    user.email = auth.info.email    #google,facebookの時
    user.email = User.dummy_email(auth) if user.provider == "twitter"   #twitterの時
    avatar = open("#{auth.info.image}")
    user.image.attach(io: avatar, filename: "user_avatar.jpg")
  end
end

としたら、twitterではランダムアドレス。
google、facebookでは正規のアドレスが取得する事ができました。

条件分岐がうまくいって嬉しかったです。

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