20201125のRailsに関する記事は25件です。

ひな祭り[Ruby]

ひな祭り[Ruby]問題

問題

ひな祭りの準備をしましょう。あなたが持っている 10 体の人形を "A", "B", "C", "D", "E", "F", "G", "H", "I", "J" で表します。ひな壇の各段 (計 3 段) に並べる人形の数が与えられるので、各段にならべる人形の記号を改行区切りで出力してください。人形は必ずもとの A, B, C, ... の順番で並べます。

例)

各段にならべる人形の数: 2, 3, 5

→ 人形の並べ方:

AB
CDE
FGHIJ

入力される値

入力は以下のフォーマットで与えられます。

h_1 h_2 h_3
ひな壇の 1 段目、2 段目、3 段目に並べる人形の数を表す整数 h_1, h_2, h_3 がこの順に半角スペース区切りで与えられます。入力は 1 行となり、末尾に改行が 1 つ入ります。

期待する出力

ひな壇への人形の並べ方を以下の形式で出力してください。

s_1
s_2
s_3
ひな壇の 1 段目、2 段目、3 段目に並べる人形を表す文字列 s_1, s_2, s_3 をこの順に改行区切りで出力してください。

s_i (1 ≦ i ≦ 3) は i 段目に並べる人形の記号をアルファベット順に結合した文字列です。

入力例1

2 3 5

出力例1

AB
CDE
FGHIJ

入力例2

1 1 8

出力例2

A
B
CDEFGHIJ

入力例3

3 4 3

出力例3

ABC
DEFG
HIJ

アドバイス頂いた結果

ns = gets.split.map(&:to_i)
str = "ABCDEFGHIJ"
i = 0
ns.each do |n|
  puts str[i, n]
  i += n
end

このほかにも Array#shift で求めるコードも!

以上!

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

seedで初期データを生成するコードで使ったメソッド

はじめに

 先日、投稿した「初期データをseeds.rbに記述して、ゲストログイン機能のエラーを解決してみた」でseeds.rbに初期データを作成したときに使ったいくつかのメソッドについて、説明する。

基本の書き方

seeds.rb
モデル名.メソッド(カラム名: )

create!

基本は、これかと…
単純に、「作って!」というやつ。
同じidや一意性のバリデーションによってエラーになることもあるので、注意が必要。

first_or_create_by!

中学英語みたいだが、firstorとつくので、
「id=1が存在しなければ、作るよ!」みないな意味。
つまり、初期データを作成する、ターミナルでrails db:seedを行う前に、データが存在すると、seed.rbで作成したデータは保存されない。

find_or_create_by!

同じく中学英語みたい。
「テーブルになければ、作るよ!」みたいな意味。

おわりに

 idのカラムを指定することができるが、その後に、生成するidが、seeds.rbで指定したidからの連番になってしまう。
ex)id=100をseeds.rbで記述していると、その次に情報を保存すると、id=101になる。

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

Active Storage の導入まとめ

はじめに

・Active Strage の導入を自分なりにまとめたもので、メモのようなものです。
・投稿者は初学者ですので誤った情報を投稿してしまうことがあります。その時はぜひ、遠慮会釈なしにご指摘いただけると幸いです。

Active Strageとは

ファイルアップロード機能を手軽に実装できるGemのこと。
元々はインストールが必要だったが、Rails5.2.0以降、初期段階から統合される事となった。このGemを用いると、ファイルアップロードのメソッドを使用できたり、画像を保存するテーブルを簡単に作成できる。

Active Strageのお供

Active Strageと一緒に導入すると画像の加工の幅が広がる物を3つ列挙します。

・ImageMagick
・MiniMagick
・ImageProcessing

ImageMagickはソフトウェアで、MiniMagickとImageProcessingはGemになります。

ImageMagick
コマンドラインから画像に処理を加えることができるツールのこと。
画像の作成やサイズ変更、保存形式の変更などが可能となる。
Homebrewよりインストールする。

MiniMagick
ImageMagickをRubyで扱えるようにしてくれる接合の役割を担うGemのこと。

ImageProcessing
ImageMagickでの画像サイズ機能を拡張してくれるGemのこと。

導入の手順

1、ImageMagickをHomebrewからインストールするために、下記のコードをターミナルに入力する。

% brew install imagemagick

2、MiniMagickとImageProcessingをインストール。

Gemfile.
gem 'mini_magick'
gem 'image_processing', '~> 1.2'
bundle install

'~> 1.2'と表記があるが、これは、1.2.0以上、1.3.0未満のバージョンを指定、という意味。

3、ローカルサーバーを再起動

rails s

4、Active Strageをインストール
導入手順1でrailsアプリの中にActive Strageを入れた。
ここでのインストールはActive Strageを用いるための周辺準備のためのインストール。

rails active_storage:install

これにより、Active Strageに関連したマイグレーションファイルが作成される。

続けて、以下のようにmigrateをすることでテーブルが作成される。

rails db:migrate
余談 ※読み飛ばし可*

導入手順1と4はどちらもインストールであるが、何が違うのかを自分なりに解説。

アパートの引越しで例えるならば、

導入手順1は「仲介業者と賃貸の契約」
導入手順4は「家具や消耗品を部屋へ搬入」

である。

仲介業者と契約を結んだだけでは(導入手順1)その部屋で快適には過ごせない。冷蔵庫に電子レンジ、シャンプーにお皿などの生活用具を部屋に運搬して(導入手順2)初めて生活ができる空間となる。

まとめ

Active Strageを調べていく中で、CarrierWaveという、同じく画像アップロード実装のGemを見つけました。
CarrierWaveは10年ほど前から利用されている古い技術ということで今はActive Strageが主流なのかと思いましたが、意外にもCarrierWaveも現役選手でした。
Active Strageはシンプルなファイルアップロード。
CarrierWaveはアップロード+細かな設定。
それぞれ別の役割がしっかりと備わっているようなので、CarrierWaveのことも調べて実装したいと思います。

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

[Rails]オーバーライド(継承)について勉強してみた![初心者]

オーバーライドとは?

親クラス(スーパークラス)で定義されたメソッドについて、小クラス(サブクラス)で同じ名前で再定義すること。

クラスの継承とは?

他のクラスをベースとして新しいクラスを作成することをクラスの継承と言います。

利用したいクラスが複数存在する場合、全てを1から作成していると骨が折れますし、共通する部分を複数のクラスに重複して定義する必要があります。
そこで、あるクラスが既に作成されている場合、そのクラスを拡張することで、新しいクラスを作成することができます。

クラスの継承は以下のように記載します。

class 子クラス名 < 親クラス名
end

継承の具体例

class Days
  def study
    puts "勉強する"
  end
end

class Holiday < Days
  def sleep
    puts "寝過ごす"
  end
end

holiday = Holiday.new
holiday.study
実行結果
勉強する

オーバーライドの具体例

class Days
  def study
    puts "勉強する"
  end
end

class Holiday < Days
  def sleep
    puts "寝過ごす"
  end

  def study
    super
    puts "休日だろうと勉強しなければ"
  end
end

holiday = Holiday.new
holiday.study
実行結果
勉強する
休日だろうと勉強しなければ

サブクラスにメソッド定義がない場合

サブクラスで定義していないメソッドが呼び出された時は、スーパークラスに同じ名前のメソッドがある場合に限り、スーパークラスのメソッドが呼び出せます。
具体例を以下に載せました。

class Days
  def study
    puts "勉強する"
  end
end

class Holiday < Days

end

holiday = Holiday.new
holiday.study
実行結果
勉強する

superclassメソッド

全てのクラスは、クラスメソッドとしてsuperclassメソッドを持っています。
また、スーパークラスのメソッドを引数なしで呼び出したい時には、super()も使用できます。

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

【Ajax】jQueryを使って一覧ページでいいね機能

はじめに

一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです!

概要

まず大前提にテーブルは、users, teams, likesの3つでlikes
usersteamsの中間テーブルになっています。
AjaxでHTTPメソッドのGET, POST, DELETEができるように実装していきます。

起きている問題・解決したいこと(仮説)

起きている問題と解決したいことを簡単に書いていきたいと思います。

起きている問題

  • 全ての記事にいいねがついてしまう。
  • 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。(デベロッパーツールで確認)

解決したいことと仮説

  • 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
  • いいね対象を判断できるようにクラスをつけないといけない。
  • クリックした対象に動的idが付与されていれば実装できるのではないか。

Ajaxで実装すること

  • いいねの表示
  • いいねされたら黄色い星を表示
  • いいねされているものをクリックしたら白い星を表示

LikesController

show, create, destroyアクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。

LikesController
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like

  def show
    like_status = current_user.has_liked?(@team)
    render json: { hasLiked: like_status }
  end


  def create
    @team.likes.create!(user_id: current_user.id)

    render json: { status: 'ok' }
  end

  def destroy
    like = @team.likes.find_by!(user_id: current_user.id)
    like.destroy!

    render json: { status: 'ok' }
  end

  private

  def set_like
    @team = Team.find(params[:team_id])
  end

end

user.rb

has_liked?user.rbで定義しています。showアクション内の処理のようにcurrent_userがteamをlikeしてる?と直感的にわかりやすいと思い、定義しております。

user.rb
def has_liked?(team)
  likes.exists?(team_id: team.id)
end

index.html.haml

HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。data-<情報>とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいのでteamidが必要です。
routesを確認するとどこにリクエストを送れば良いかよくわかります。

たとえばですが、以下index.html.hamlのように記述してデベロッパーツールで確認すると

  • data-team-id="1"
  • id="active-star1"

このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、active-starというidだけだと、active-starというidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、動的なid(今回はチームのid)を付与しております。数字の1の部分がチームのidです。

ここで重要なのは、

  • カスタムデータ
  • hiddenでいいねの表示を隠していること

hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。

index.html.haml
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
          = image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
          = image_tag 'star-white.png'
css
.hidden {
  display: none;
}

jQuery

本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのはcsrfTokenをリクエスト時に持たせることです。これをしないと422 (Unprocessable Entity)というエラーが起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、rails-ujsを使ってaxiosでリクエスト時にcsrfTokenというのを持たせるようにしております。

jQuery
import $ from 'jquery'
import axios from 'axios'

import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

document.addEventListener('DOMContentLoaded', () => {

  // ロード時にいいねされていない星を配列で取得
  $('.in-active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const inActiveStatus = response.data.hasLiked
        // falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
        if ( inActiveStatus === false ) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // ロード時にいいねされている星を配列で取得
  $('.active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const activeStatus = response.data.hasLiked
        // trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
        if ( activeStatus === true) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // #create いいねをつけたいときの処理
  $('.in-active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いPOSTリクエストを送る
    axios.post(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#in-active-star${teamId}`).addClass('hidden');
        $(`#active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })

  })

  // #destroy いいねを外したいときの処理
  $('.active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いdeleteメソッドを使う
    axios.delete(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#active-star${teamId}`).addClass('hidden');
        $(`#in-active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
  })

})

まとめ

  • Ajax処理はデベロッパーツールを使いdebuggerconsole.log()を使い値が取れているか確認をしっかりすると開発が捗る。
  • 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
  • POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。

最後に

今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います!

参考文献

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

【Rails】いいね機能をjQueryで実装

はじめに

一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです!

概要

まず大前提にテーブルは、users, teams, likesの3つでlikes
usersteamsの中間テーブルになっています。
AjaxでHTTPメソッドのGET, POST, DELETEができるように実装していきます。

起きている問題・解決したいこと(仮説)

起きている問題と解決したいことを簡単に書いていきたいと思います。

起きている問題

  • 全ての記事にいいねがついてしまう。
  • 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。(デベロッパーツールで確認)

解決したいことと仮説

  • 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
  • いいね対象を判断できるようにクラスをつけないといけない。
  • クリックした対象に動的idが付与されていれば実装できるのではないか。

Ajaxで実装すること

  • いいねの表示
  • いいねされたら黄色い星を表示
  • いいねされているものをクリックしたら白い星を表示

LikesController

show, create, destroyアクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。

LikesController
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like

  def show
    like_status = current_user.has_liked?(@team)
    render json: { hasLiked: like_status }
  end


  def create
    @team.likes.create!(user_id: current_user.id)

    render json: { status: 'ok' }
  end

  def destroy
    like = @team.likes.find_by!(user_id: current_user.id)
    like.destroy!

    render json: { status: 'ok' }
  end

  private

  def set_like
    @team = Team.find(params[:team_id])
  end

end

user.rb

has_liked?user.rbで定義しています。showアクション内の処理のようにcurrent_userがteamをlikeしてる?と直感的にわかりやすいと思い、定義しております。

user.rb
def has_liked?(team)
  likes.exists?(team_id: team.id)
end

index.html.haml

HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。data-<情報>とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいのでteamidが必要です。
routesを確認するとどこにリクエストを送れば良いかよくわかります。

たとえばですが、以下index.html.hamlのように記述してデベロッパーツールで確認すると

  • data-team-id="1"
  • id="active-star1"

このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、active-starというidだけだと、active-starというidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、動的なid(今回はチームのid)を付与しております。数字の1の部分がチームのidです。

ここで重要なのは、

  • カスタムデータ
  • hiddenでいいねの表示を隠していること

hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。

index.html.haml
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
          = image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
          = image_tag 'star-white.png'
css
.hidden {
  display: none;
}

jQuery

本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのはcsrfTokenをリクエスト時に持たせることです。これをしないと422 (Unprocessable Entity)というエラーが起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、rails-ujsを使ってaxiosでリクエスト時にcsrfTokenというのを持たせるようにしております。

jQuery
import $ from 'jquery'
import axios from 'axios'

import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

document.addEventListener('DOMContentLoaded', () => {

  // ロード時にいいねされていない星を配列で取得
  $('.in-active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const inActiveStatus = response.data.hasLiked
        // falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
        if ( inActiveStatus === false ) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // ロード時にいいねされている星を配列で取得
  $('.active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const activeStatus = response.data.hasLiked
        // trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
        if ( activeStatus === true) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // #create いいねをつけたいときの処理
  $('.in-active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いPOSTリクエストを送る
    axios.post(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#in-active-star${teamId}`).addClass('hidden');
        $(`#active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })

  })

  // #destroy いいねを外したいときの処理
  $('.active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いdeleteメソッドを使う
    axios.delete(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#active-star${teamId}`).addClass('hidden');
        $(`#in-active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
  })

})

まとめ

  • Ajax処理はデベロッパーツールを使いdebuggerconsole.log()を使い値が取れているか確認をしっかりすると開発が捗る。
  • 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
  • POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。

最後に

今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います!

参考文献

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

Rails 6用のJavaScriptをRails 5.2で使う

RailsアプリケーションでRailsをバージョンアップするときには、段階的に修正しては本番サーバーを更新、ということをやっています。

  • JavaScriptの修正、パッケージの更新。
  • 新しいRailsでwarningが出るところを修正。
  • 最後にRailsをバージョンアップ。

といった感じです。ここでは、Rails 5.2を6.0に上げる準備として、Rails 6.0で導入されたJavaScriptのパッケージを5.2アプリケーションで使ってみます。

コピペ元の6.0アプリケーションを作っておく

5.2と6.0のコードを比較したりコピペ元として使ったりするために、6.0アプリケーションを作成します。

% rails --version
Rails 6.0.3.4
% rails new foo --webpack=vue

JavaScriptパッケージの導入

Railsがデフォルトで使うJavaScriptは、6.0からはassetsからWebpackerに移されました。次の4つです。

  • @rails/actioncable
  • @rails/activestorage
  • @rails/ujs
  • turbolinks

5.2アプリケーションで、yarn add でパッケージを追加します。

% yarn add @rails/actioncable @rails/activestorage @rails/ujs turbolinks

6.0アプリケーションの app/javascript/packs/application.js から requireの 行を5.2アプリケーションの application.js にコピーします。使わないものはコピーしなくてもよいです。

app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

ActionCable を使う場合は、6.0アプリケーションの app/javascript/channels 下から consumer.js と index.js もコピーします。

assetsのJavaScriptを削除

5.2アプリケーションで、assets の application.js からWebpackerに移すものを削除します。

app/assets/javascripts/application.js
- //= require rails-ujs
- //= require activestorage
- //= require turbolinks

ActionCableも移す場合は、各チャンネル用の xxx.coffee ファイルをバックアップしておいてから、channelsディレクトリを削除します。

assetsのJavaScriptを全部削除する場合

5.2アプリケーションで、JavaScriptをすべてWebpackerに移す場合は、app/assets/javascripts ディレクトリを削除します。

また、manifest.js からJavaScriptの設定を消します。

app/assets/config/manifest.js
- //= link_directory ../javascripts .js

Gemfile から uglifier と coffee-rails を削除して bundle install します。さようならCoffeeScript。

Gemfile
- gem 'uglifier', '>= 1.3.0'
- gem 'coffee-rails', '~> 4.2'

produciton.rb から uglifier の設定を消します。

config/environments/production.rb
  # Compress JavaScripts and CSS.
  # config.assets.js_compressor = :uglifier

ActionCableを移す場合

ActionCable用のJavaScriptを移す場合は、6.0アプリケーションで、rails g を使ってチャンネル用のJavaScriptのひな形を作ります。次の例では、5.2アプリケーションにuserチャンネルがあるものとします。

% bin/rails g channel user

6.0アプリケーションの app/javascript/channels から user_channel.js をコピーして5.2に移します。あとは、保存しておいた user.coffee のコードを user_channel.js で動くように移植します。

動作確認

このやり方で、ActionCable用のJavaScriptが動くことは確認しました。Turbolinksとrails-ujsも、まあたぶん大丈夫でしょう。

ActiveStorage用のJavaScriptについては確認していません。すいません。

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

Rails API × GraphQL プロジェクトを作成する際にハマったこと

概要

以下の記事を参考にプロジェクトを作ろうとしたところ私のmac環境で次のような問題でハマったためまとめます。

環境

  • macOS Mojave v10.14.6
  • Rails 5.0.7.2

make: *** [mysql2.bundle] Error 1

これはGraphQL関係なく、mysql2の $ bundle install エラーでした。

$ bundle install
...
Fetching mysql2 0.5.3
Installing mysql2 0.5.3 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
    current directory: /Users/gremito/.gem/gems/mysql2-0.5.3/ext/mysql2
/Users/gremito/.rbenv/versions/2.7.1/bin/ruby -I /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0 -r ./siteconf20201125-31525-3obuae.rb
extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_wait_for_single_fd()... yes
-----
Using mysql_config at /usr/local/opt/mysql@5.7/bin/mysql_config
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for SSL_MODE_DISABLED in mysql.h... yes
checking for SSL_MODE_PREFERRED in mysql.h... yes
checking for SSL_MODE_REQUIRED in mysql.h... yes
checking for SSL_MODE_VERIFY_CA in mysql.h... yes
checking for SSL_MODE_VERIFY_IDENTITY in mysql.h... yes
checking for MYSQL.net.vio in mysql.h... yes
checking for MYSQL.net.pvio in mysql.h... no
checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes
checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes
checking for my_bool in mysql.h... yes
-----
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/opt/mysql@5.7/lib
-----
creating Makefile
current directory: /Users/gremito/.gem/gems/mysql2-0.5.3/ext/mysql2
make "DESTDIR=" clean
current directory: /Users/gremito/.gem/gems/mysql2-0.5.3/ext/mysql2
make "DESTDIR="
compiling client.c
client.c:787:14: warning: incompatible pointer types passing 'VALUE (void *)' (aka 'unsigned long (void *)') to parameter of type 'VALUE
(*)(VALUE)' (aka 'unsigned long (*)(unsigned long)') [-Wincompatible-pointer-types]
  rb_rescue2(do_send_query, (VALUE)&args, disconnect_and_raise, self, rb_eException, (VALUE)0);
             ^~~~~~~~~~~~~
/Users/gremito/.rbenv/versions/2.7.1/include/ruby-2.7.0/ruby/ruby.h:1988:25: note: passing argument to parameter here
VALUE rb_rescue2(VALUE(*)(VALUE),VALUE,VALUE(*)(VALUE,VALUE),VALUE,...);
                        ^
client.c:795:16: warning: incompatible pointer types passing 'VALUE (void *)' (aka 'unsigned long (void *)') to parameter of type 'VALUE
(*)(VALUE)' (aka 'unsigned long (*)(unsigned long)') [-Wincompatible-pointer-types]
    rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0);
               ^~~~~~~~
/Users/gremito/.rbenv/versions/2.7.1/include/ruby-2.7.0/ruby/ruby.h:1988:25: note: passing argument to parameter here
VALUE rb_rescue2(VALUE(*)(VALUE),VALUE,VALUE(*)(VALUE,VALUE),VALUE,...);
                        ^
2 warnings generated.
compiling infile.c
compiling mysql2_ext.c
compiling result.c
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1
make failed, exit code 2
Gem files will remain installed in /Users/gremito/.gem/gems/mysql2-0.5.3 for inspection.
Results logged to /Users/gremito/.gem/extensions/x86_64-darwin-18/2.7.0/mysql2-0.5.3/gem_make.out
An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.
In Gemfile:
  mysql2

解決方法

以下のコマンドを実行した後に再度 $ bundle install すると正常にインストールできました。
原因は、以下の参考にした記事で解説されてあります。

$ sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /

Password:
installer: Package name is macOS_SDK_headers_for_macOS_10.14
installer: Installing at base path /

installer: The install was successful.

Sprockets::Railtie::ManifestNeededError

標準ライブラリの sprocketsによって起きたエラーでした。

$ rails generate graphql:install
...
But did not, please create this file and use it to link any assets that need
to be rendered by your app:

Example:
  //= link_tree ../images
  //= link_directory ../javascripts .js
  //= link_directory ../stylesheets .css
and restart your server

For more information see: https://github.com/rails/sprockets/blob/070fc01947c111d35bb4c836e9bb71962a8e0595/UPGRADING.md#manifestjs (Sprockets::Railtie::ManifestNeededError)
        from /Users/gremito/.gem/gems/railties-5.0.7.2/lib/rails/initializable.rb:30:in `instance_exec'
        from /Users/gremito/.gem/gems/railties-5.0.7.2/lib/rails/initializable.rb:30:in `run'
        from /Users/gremito/.gem/gems/railties-5.0.7.2/lib/rails/initializable.rb:55:in `block in run_initializers'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:228:in `block in tsort_each'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:431:in `each_strongly_connected_component_from'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:349:in `block in each_strongly_connected_component'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:347:in `each'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:347:in `call'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:347:in `each_strongly_connected_component'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:226:in `tsort_each'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/tsort.rb:205:in `tsort_each'
        from /Users/gremito/.gem/gems/railties-5.0.7.2/lib/rails/initializable.rb:54:in `run_initializers'
        from /Users/gremito/.gem/gems/railties-5.0.7.2/lib/rails/application.rb:352:in `initialize!'
        from /Users/gremito/rails/graphql-sample/config/environment.rb:5:in `<top (required)>'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:106:in `require'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:106:in `preload'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:157:in `serve'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:145:in `block in run'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:139:in `loop'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application.rb:139:in `run'
        from /Users/gremito/.gem/gems/spring-2.1.1/lib/spring/application/boot.rb:19:in `<top (required)>'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'
        from /Users/gremito/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'
        from -e:1:in `<main>'

解決方法

バージョンを落とすしか方法がありませんでした・・・

$ bundle exec gem uninstall sprockets
...

Select gem to uninstall:
 1. sprockets-3.7.1
 2. sprockets-3.7.2
 3. sprockets-4.0.2
 4. All versions
> 4
Successfully uninstalled sprockets-3.7.1
Successfully uninstalled sprockets-3.7.2
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /Users/gremito/.gem/specifications/erubis-2.7.0.gemspec:17.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /Users/gremito/.gem/specifications/rack-test-0.6.3.gemspec:17.

You have requested to uninstall the gem:
        sprockets-4.0.2

sprockets-rails-3.2.2 depends on sprockets (>= 3.0.0)
If you remove this gem, these dependencies will not be met.
Continue with Uninstall? [yN]  y
Remove executables:
        sprockets

in addition to the gem? [Yn]  y
Removing sprockets
Successfully uninstalled sprockets-4.0.2

$ rm -fr Gemfile.lock

Gemfile

gem 'sprockets', '< 4'

を追記して再度 $ bundle installして正常にダウングレードできたら $ rails generate graphql:install を再度実行して終了。

 

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

ActiveRecordでは present? の代わりに exists? を使おう

小ネタです。

条件に該当するレコードが存在するかを確認したい

ActiveRecordで条件に存在するレコードが存在するかを確認するのに、つい、

Model.where(conditions).present?

と書いてしまいがちですが、これはパフォーマンス上の問題が生じる可能性があります。

.present? は ActiveSupport によってモンキーパッチされたメソッドです。
Mode.where(conditions) のような ActiveRecord_Relation クラスのオブジェクトに .present? を適用するとどうなるか?

すると条件に該当するレコードを全てDBから取得して、(Rails上のモデルの)配列として評価することになります。
配列に要素が存在すれば true、 しなければ false ですね。

なぜこれはダメなのか

一見すると問題なさそうですし、実際結果自体は正しいのですが、条件に該当するレコードが1つでも存在することを確認できれば良いわけで、全件を取得する必要はありません。
100万レコードが条件に該当した場合、その100万レコードがDBからピックアップされ、その100万レコード分のデータがRails側に転送され、100万のモデルが作成された上で捨てられる。という壮大な無駄が発生します。結果、いつまでたっても結果が返ってこない。というパフォーマンス上の障害を引き起こしてしまうのです。

こういうのはレコード数の少ない開発環境では顕在化しずらく、レコード数の多い本番環境でいきなり顕在化してパニックになることがあります。

では、どうする?

.exists? メソッドを使いましょう。

Model.where(conditions).exists?

こちらを使うと、SQLが exists 式を使うものに代わります。
exists 式は条件に該当するレコードが1件でも存在すればDBがそこで探索を打ち切ってくれるので、無駄に全件を取ってくることはありません。
DBレベルで true または false で返してくれるのでオーバヘッドが最小で済みます。

present? の逆は?

.present? の逆である .blank? に対応するメソッドは .empty? になります。
.empty? を使えばSQL上は exists 式を使います。

ただ、条件に該当するレコードが存在しないことを確認するには、結局全件のチェックが必要になるので両者に速度的な差は少ないかもしれません。

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

TableExportで好きな位置にExcelやCSV出力ボタンを配置する

はじめに

JQueryプラグイン「TableExport」ではテーブルを簡単にExcel、CSV、テキストファイルなどで出力できます。ただ、出力ボタンの位置は上下で固定っぽくて不便だったので、改善策を備忘録として残しておきます。

TableExport自体の使い方はこちらのサイトが分かりやすいので参考にされてください。

開発環境は以下の通り

  • Ruby 2.6.3
  • Rails 5.2.4.4
  • jQuery 1.12.4

(Ruby on Rails使用してますが、今回の記事ではjQueryのみ取り上げているのであまり関係ないかと思います)

なお、bootstrapを使えばデフォルトでオシャレなボタンにできますが今回は使いません。

問題点



Export to xlsxボタンがとんでもないところにありますね。
普通のテーブルだともう少しマシかもしれませんが、このテーブルは見出し固定のスクロール式で、テーブルを横にスライドするとボタンも一緒に流されてしまうので余計見にくい代物になってしまいます。。



一応オプションでボタンの位置を指定できますが、前述した通り上下の指定しかできないので改善には至りませんでした。

$(".table").tableExport({
    ...
    position: "bottom",
    ...
});

解決策

至ってシンプルです。

①tableExport()で生成されるボタンを隠して代理ボタンを配置する
②代理ボタンが押されたらtrigger()でもともとのボタンが押されたことにする

htmlファイル
...

<table id="export-table">
    ...
</table>

<button type="button" id="export-btn">Excel出力</button>

...

jqueryファイル
...

$('#export-table').tableExport({
    formats: ["xlsx"],
    bootstrap: false
});

$('#export-table caption').hide();

$('#export-btn').on('click', function(){
    $('.table caption button').trigger('click');  
});

...

うまくいきました!
もちろんCSVやテキストファイルの出力にも使用できます。

おわりに

TableExportはとても簡単に外部ファイルを出力できるプラグインですが、扱っている記事自体がとても少ないので、こういうちょっとした解決策っていうのはもっと増えていくと嬉しいですね。

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

RailsでTimecop/TimeHelpersを使って時刻を変える方法について

この記事は、Happy Elements Advent Calendar 2020の5日目です。
RailsでTimecop/TimeHelpersを使って時刻を変える方法についての記事です。

はじめに

ソーシャルゲームでは、イベントが yyyy年mm月dd日に始まるなど、特定の時刻になると発動することが定番です。

例: 架空のソーシャルゲームのイベントカレンダー
image.png

これらのようなイベントの動作を確認するために、サーバ側で時刻を変えることができると、動作テストがしやすくなります。
- 例1 12月2日 15:00開始のガチャAの動作確認をするために、サーバの時間を12/2 16時にセットする
- 例2 バグ報告があったので、1ヶ月前のイベントの時間にセットしてバグ調査をする など

サーバ側で時刻を変える方法について、以下の3つの方法に絞って調査しました。
- Timecop
- ActiveSupport::Testing::TimeHelpers
- libfaketime

この記事では、それぞれがどのようなものか、をまとめました。

Timecop

https://github.com/travisjeffery/timecop
こちらはRailsのgemです。
Time/Date/DateTimeクラスのオーバーライドとして実装されているものでした
https://github.com/travisjeffery/timecop/blob/master/lib/timecop/time_extensions.rb

調査を進めると、TimeHelpersという物がRailsに組み込まれていることがわかりました。
TimeHelpersは、Timecopより機能としては少なく、時刻を固定させるのみです。
Timecopは、固定の他に、過去や未来に設定した後に時刻が進むなども可能です。

TimecopとTimeHelperでは、Timecopの方がリッチな機能である、と言えると思います。

  • 標準のTimeHelpersで十分である
  • gemを入れたくない

などの場合は、TimeHelpersの利用が選択肢に上がってくると思います。
2020年現在もメンテナンスはされているようです。

ActiveSupport::Testing::TimeHelpers

https://edgeapi.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html

Rails4.1以降で標準のものです。
時刻を固定・解除するのみです。

libfaketime

https://github.com/wolfcw/libfaketime

こちらはRailsの話ではなく、OSにインストールして動くサービスです。
システムコールを改変するものです

  • メリット
    • プログラムを変えなくていい
  • デメリット
    • 導入が少し手間

システムコールとは?

libfaketimeでシステムコールの話が出てきたので、ここではシステムコールについて。

strace というコマンドを使って、コマンドを打つと大量にログが出てきます。これがシステムコールです(fstat, close, read, ...etc)。
libfaketimeは、このシステムコールを書き換えて時刻を変えるので、TimecopやTimeHelpersよりも深い部分で動いていると言えると思います。

strace date

execve("/bin/date", ["date"], 0x7ffc3bc97870 /* 28 vars */) = 0
brk(NULL)                               = 0x55b92bdc6000
openat(AT_FDCWD, "/usr/local/lib/faketime/libfaketime.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0$\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=66512, ...}) = 0
...
read(4, "@2020-6-16 20:30:00\n", 4096)  = 20
close(4)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "Tue Jun 16 20:30:00 JST 2020\n", 29Tue Jun 16 20:30:00 JST 2020
) = 29
close(1)                                = 0
close(2)                                = 0
munmap(0x7f46f7935000, 8)               = 0
munmap(0x7f46f7936000, 32)              = 0
exit_group(0)                           = ?
+++ exited with 0 +++

余談

調査中に、もしTimecopとlibfaketimeを同時に使った場合どうなるのだろう、と思いました。
実験してみたところ、Timecopで設定した時刻がRailsでは得られました。

libfaketimeで改変された時間を、さらにTimecopが上書きするという順序になります。
イメージ図は以下の通りです。

image.png

まとめ

自分が運営中のタイトルでは、開発者の手元では、TimeHelpersを使ってRailsの時刻を変えながらイベントの動作確認などに利用しています。libfaketimeも使えるようになっており、必要であればそちらも利用しています。
こちらの記事では、サーバ側で時刻を変える方法について、Timecop, TimeHelpers, libfaketimeを調査しました。
調査してわかりましたが、どれが優れているというものではありませんでした。
それぞれ、やりたいことの要件に合わせて選択すれば良いと思いました。
もしRailsで時刻を変えたいけど、どうしようと悩んで調べている方がいらっしゃいましたら、
参考にしていただけると幸いです。

(注) 記事の中の図は自分で用意しました

参考

https://andycroll.com/ruby/replace-timecop-with-rails-time-helpers-in-rspec/
https://techracho.bpsinc.jp/penguin10/2018_12_25/67780
https://qiita.com/ktrkmk/items/b1361dd43d22dcf5627e
https://qiita.com/Targityen/items/67682d6c80cdcbe1186c

終わりに

Happy Elements株式会社 カカリアスタジオでは、
いっしょに【熱狂的に愛されるコンテンツ】をつくっていただけるメンバーを大募集中です!

もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください。
https://recruit.happyelements.co.jp/

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

Railsでタグ機能を実装

はじめに

現在作成しているアプリでタグ機能を実装したのでその実装方法を残しておきます。
Railsにはタグ機能の実装を簡単にしてくれる acts-as-taggable-on というgemがありますが、関連付けの練習も踏まえて自前で実装します。
現在作っているアプリのPK,FKのみを示したER図は以下のようになります。
スクリーンショット 2020-11-25 9.00.20.png

実行環境

この記事は以下の環境で動作確認しています。
ruby 2.7.1
rails 6.0.3
DB MySQL

モデルの作成

Profileモデルは既に作ってあるという前提で進めていきます。
まず、tagモデルとtag_relastionshipモデルを作成します。

$ rails g model tag name:string
$ rails g model tag_relationship profile:references tag:references

複合キーインデックスを張ります。
こうすることにより、同じタグを二回保存できないようにします。

XXXXXXXXXXXXXX_create_tag_relationships.rb
class CreateTagRelationships < ActiveRecord::Migration[6.0]
  def change
    create_table :tag_relationships do |t|
      t.references :profile, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
    add_index :tag_relationships, [:profile_id, :tag_id], unique: true
  end
end

タグ名は必ず入力して欲しいのでnull:falseにします。

XXXXXXXXXXXXXX_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

モデルの関連付けとバリデーション

基本的な中間テーブルを用いる多対多の実装です。

中間デーブル経由でタグに紐付くprofileの情報を取得できるように has_many throughも定義します。これに関してはprofileモデルでも同じです。
タグ名はユニークで必ず保持していて欲しいので以下のようなバリデーションにします。

tag.rb
class Tag < ApplicationRecord
  has_many :tag_relationships, dependent: :destroy
  has_many :profiles, through: :tag_relationships

  validates :name, uniqueness: true, presence: true
end
tag_relationship.rb
class TagRelationship < ApplicationRecord
  belongs_to :profile
  belongs_to :tag

  validates :tag_id, presence: true
  validates :profile_id, presence: true
end
profile.rb
class Profile < ApplicationRecord
  belongs_to :user
  has_many :tag_relationships, dependent: :destroy
  has_many :tags, through: :tag_relationships
end

viewの作成

現在作成しているアプリではプロフィールの新規登録の際にタグも登録して欲しいのでprofiles/new.html.erbで実装します。
タグの部分だけ抜粋して載せます。

f,text_field :tagとすることでparams[:profile][:tag]でパラメーターを受け取れるようにします。

profiles/new.html.erb
<div class="input-field col s12">
  <i class="material-icons prefix">local_offer</i>
  <%= f.text_field :tag, placeholder: "タグを複数つけるには' , 'で区切ってください" %>
</div>

表示する際はeachで配列で保存されているタグを繰り返し表示します。

profiles/show.html.erb
<% @user_profile.tags.each do |tag| %>
  <div class="chip">
    <%= tag.name %>
    <i class="close material-icons">close</i>
  </div>
<% end %>

コントローラーの作成

ユーザーとプロフィールはhas_oneを用いて一対一の関係にしているので、buildする際は「インスタンス名.build_アソシエーション名」としています。

プロフィール情報と一緒に送られてきたタグを保存できるようにします。

profiles_controller.rb
  def new
    @user_profile = current_user.build_profile
  end

  def create
    @user_profile = current_user.build_profile(profile_params) # profile_paramsはストロングパラメーター 
    tag_list = params[:profile][:tag].split(',') # viewでカンマ区切りで入力してもらうことで、入力された値をsplit(',')で配列にしている。
    if @user_profile.save
      @user_profile.save_tags(tag_list) # save_tagsというクラスメソッドを使って保存している。
      flash[:notice] = "プロフィールの設定が完了しました"
      redirect_to root_url
    else
      render 'new'
    end
  end

save_tagsメソッドは下記に示します。

profile.rb
  def save_tags(profile_tag)
    profile_tag.each do |tag|
      new_tag = Tag.find_or_create_by(name: tag)
      self.tags << new_tag
    end
  end

「find_or_create_by」メソッドは引数で指定した値があればそれを取得し、なければ作成します。名前の通り、findかcreateするメソッドです。
self.tags << profile_tag ではプロフィールに関連したタグの配列に新たなタグを追加しています。
<< だけでなく、pushメソッドを使っても同じように要素を追加することができます。

タグの編集機能

プロフィールを編集する際にタグも変更できるようにします。

viewの一部を抜粋します。

profiles/edit.html.erb
<div class="input-field col s12">
  <i class="material-icons prefix">local_offer</i>
  <%= f.text_field :tag, value: @tag_list, placeholder: "タグを複数つけるには' , 'で区切ってください" %>
</div>

value: @tag_listとすることで既存の値を表示します。

editアクションではviewで既存の値を表示するために@tag_listを記述します。
pluck関数を使うことによってレシーバのカラムを簡単に取得します。
今回は@user_profile.tags.pluck(:name)としているのでプロフィールに関連したタグのnameカラムを配列で取得します。
join(',')で取得した配列を「,」で区切った文字列にします。

profiles_controller.rb
  def edit
    @user = Profile.find(params[:id]).user
    @user_profile = @user.profile
    @tag_list = @user_profile.tags.pluck(:name).join(',')
  end

  def update
    @user = Profile.find(params[:id]).user
    @user_profile = @user.profile
    tag_list = params[:profile][:tag].split(',')
    if @user_profile.update(profile_params)
      @user_profile.save_tags(tag_list)
      flash[:notice] = "プロフィールの変更が完了しました"
      redirect_to root_url
    else
      render 'edit'
    end
  end

今回は繰り返しの処理が単純なのでpluck関数を使わなくても「&:メソッド」を使えばタグの名前を同程度の記述量で取得することができます。
@user_profile.tags.map(&:name).join(',')
しかし、今回の場合、pluckは指定したカラムのみをSQLで取ってくるのでmapより早いと思われます。(間違っていたらご指摘いただければ幸いです)なのでこのままpluckを使用します。

save_tagsメソッドを更新でも使えるようにします。

profile.rb
  def save_tags(profile_tag)
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    old_tags = current_tags - profile_tag
    new_tags = profile_tag - current_tags

    # 古いタグを削除
    old_tags.each do |old_tag|
      self.tags.delete(Tag.find_by(name: old_tag))
    end

    # 新しいタグを追加
    new_tags.each do |new_tag|
      add_tag = Tag.find_or_create_by(name: new_tag)
      self.tags << add_tag
    end
  end

文字列の配列でも下記の例のように引き算できます。
a = ["first", "second", "third"]
b = ["first", "third", "forth"]
a - b => ["second"]
b - a => ["forth"]
これを用いて古いタグ、新しいタグを分けてそれぞれ処理します。

最後に

これでタグの作成、編集機能は完成です。
まだユーザーに取って良い形とは言えないのでjsを使うなどしてユーザーにとって使いやすいものに改善していこうと思います。

参考

pluckメソッドが便利な件について
pluckとmapの違いを調査する
Railsでタグ機能をgemを使わずに実装した際のメモ

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

【Rails】device機能【備忘録】

こんにちは。Web開発6週目、Rails初心者の あをか です。
初心者にとってdeviceというgemはブラックボックスで、何がどう繋がっているのか把握しづらかったです。

復習も兼ねて、deviceの導入過程を書いていきます。

■ ログイン機能(device)

①deviceを入れてモデルとテーブルを作ろう

 gem 'device'とGemFileに書き、
 ターミナルで①bundle install➡②rails g devise:installする。
 続いてログイン機能で欠かせないuser情報を扱うために、モデルとテーブルを作成したい。
 rails g model User string:name, string:email,...かと思いきや、deviceの時は違う。
 rails g devise UserだけでUserテーブルの中身まで作成してくれる。
 ただしdeviceで作成したテーブルのカラムは、デフォルトではemailとpasswordだけ。
 ユーザー名を管理したい場合、
 db/migrateの下にあるマイグレーションファイルにt.string :nameと追記する。
 (その他userテーブルで管理したいものはこの時に全て書いておくこと)
 マイグレーションファイルへの追記が終わったら、rails db:migrateする。

 (rails db:migrateをした後に間違いが見つかると少しめんどくさい。このコマンドで、この時のマイグレーションファイルがschemaファイルに反映され、マイグレーションファイルは読み取り専用になるから。ちなみにschemaファイルは現在のテーブル一覧が分かるので便利。
※詳しくは➡https://qiita.com/kakiuchis/items/2ed1604557ee29bbcbf7
※カラムを変えたくなったら➡https://qiita.com/azusanakano/items/a2847e4e582b9a627e3a)

②routesファイルとUserモデルを確認しよう(見るだけ)

 routes.rbにdevise_for :usersと書かれているはず。
 user.rbに devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
と書かれているはず。

③ユーザー登録画面とログイン画面のレイアウト

 image.png
 app/views/devise/registrations/new.html.erbがユーザー登録画面、
 app/views/devise/sessions/new.html.erbがログイン画面。
 適宜編集しましょう。name入力フォームを作る時はtext_field使うのに注意。emaiilとpasswordが特殊なんです。

④deviceのコントローラはどこ・・?

 結論から申しますと、deviceコントローラは触る事ができません(存在はするようです)。
 で、どうするかというと、application_controllerで操作します。
image.png
 1~4行目・13~16行目を書きましょう。
 privateではなくprotectedなのは、別のコントローラからも操作できるように。
 (5~11行目はログイン・ログアウト後のルートを変えたい場合の記述)

⑤バリデーション

 userテーブルのnameカラムにバリデーションを設定したいので、
 models/user.rbに記述する。
image.png

以上、device導入の備忘録でした。
初心者なので間違い等あるかもしれませんが、その際は教えて下さると幸いです!!

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

Railsの1対多の作成

本投稿の目的

・Rails学習の議事録です。


学習に使った教材

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


○1対多とは?

・紐づいたmodel間での関係の名称
・ex)ツイートmodelとコメントmodle
・この時,ツイート1つに対して複数コメントが可能
・この関係を"1対多"の関係と呼ぶ

【説明の簡略のために書き省略記号を使用】
・素となるmodel = model(1)
・紐づくmodel = model(多)
*(コーディング例では任意のmodel名が入る)

【1対多の関係におけるmodel関係の名称を2つ示す】
・has many = model(1)から見たmodel(多)の関係
・belongs to = model(多)からみたmodel(1)の関係

○model(他)作成後の操作

①model(多)の作成

qiita.rb
rails g model model() model(1): references

【解説】
○model(1): references
⇨ referencesでmodel(1)を紐けるように設定
⇨ これで,model(多)には, model名(1)_id というcolumnが作成される

【例:Q&Aアプリケーションのmodelを想定】

qiita.rb
rails g model answer questin: references

【解説】
⇨ answer model へ question model を紐付ける
⇨ questionのcolumnに answer_id が作成される

②model(多)のmigrationファイルへの記述

・model(多)を紐づけで作成しておけば自動記述される

qiita.rb
class Create+model() < ActiveRecord::Migration[5.0]
  def change
    create_table :model()s do |t|
      t.references :model(1), foreign_key: true
      t.string :column1, null: false
      t.text :column2, null: false

      t.timestamps
    end
  end
end

【解説】
○null: faluse
⇨ column値に 空 を受け付けない

○t.reference :model(1), foreign_key: true
⇨ model(1)のid にないと,model(1)_idとしてmodel(多)で保存できない

③model(多)のdbの作成

rails db:migrate

④model(多)のmodelファイルへの記述(自動記述)

・model(多)作成時に,以下が,model(多)のmodelファイルへ自動で記述される

qiita.rb
class model() < ApplicationRecord
  belongs_to :model(1)
end

【解説】
○belongs_to :model(1)
⇨ (*model(1)名称は単数係)

【例:answer modelに紐づくquestion modelの場合】

qiita.rb
class Answer < ApplicationRecord
  belongs_to :question
end

○belongs_to :question⇨ 親がmodel(1) で 子がmodel(多)
⇨ (model(多)から見るとmodel(1)は1つに定まる)
⇨ (子供は複数人いるが,親や一位に定まるというイメージ)

⑤model(1)のmodelファイルへの記述(手動記述)

・model(1)については,追加で以下の記述を追加する

qiita.rb
    has_many :model()s, dependent: :destroy

【解説】
○has_many :model(多)s
⇨ 一番上の行に記述する
⇨ (*モデル名(多)は複数形)

○dependent: :destroy
⇨ model(1)が削除されると紐づいたmodel(多)は自動で消される という設定

【例:Answerの素となるQuesitionの場合】

qiita.rb
class Question < ApplicationRecord
    has_many :answers, dependent: :destroy
end

⑥rails routesの更新

・config/routes.rb ファイルを開く
・内容を次のように更新 (do 移行が追加で更新する部分)
*(model名は複数形)

qiita.rb
resources :model(1)s do
 resources :model()s
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コントローラーとビューを同時に作成

コントローラーとビューを同時に作成

作成方法

% rails g controller 'コントローラー名/ビューのファイルの名前' index

(例) % rails g controller tests index

  • testsコントローラーにindexアクションが作られる
  • viewsのtestsフォルダにindex.html.erbファイルが作られる

以下の方法を取ると、オプションとして指定したアクションの定義と、
アクションに紐付いたビューファイルが作成できます。
(例)% rails g controller test index new

  • app/controllers/tests_controller.rb
  • app/views/tests/index.html.erb
  • app/views/tests/new.html.erb

が作成されます。

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

Heroku上のRailsアプリにScoutを導入

Herokuの管理画面からアドオンを追加

ダッシュボードから操作する方法と、コマンドラインから操作する方法があります。
アドオンを追加した時点で、環境変数が自動でセットされます。

ダッシュボードから操作する場合

  • Herokuのダッシュボードにアクセス
  • Resourcesタブを開く
  • Add-onsの下の検索窓に'Scout'と入力
  • Provisionをクリック

コマンドラインから操作する場合

Heroku CLIがインストールされている状態で以下のコマンドを叩く

$ heroku addons:create scout

Gemを追加

Gemfile
gem 'scout_apm'
$ bundle install

これだけ。アプリにリクエストが無いとメトリクスは表示されないので注意。導入直後に見ても多分反映されていなくて、本番のアプリを適当に触った上で、数時間後に確認すると反映されてます。

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

RailsでGoogleMapAPIをつかって地図投稿機能 地名検索 一覧表示 

作る機能

・投稿する際に緯度と経度を保存する
・すべての投稿内容を一つの地図上で一覧表示させる
・マーカーをクリックすると投稿の内容が出る
・地図上で詳細ページまでのリンクを表示させる

完成デモ

新規投稿画面
map_new.gif

一覧ページ
スクリーンショット 2020-12-03 040439.jpg

それでは作っていきましょう!

まずは先人の知恵を借ります。
以下の記事を参考に最後まで作り上げてください!

【Rails6 / Google Map API】初学者向け!Ruby on Railsで簡単にGoogle Map APIの導入する
https://qiita.com/nagaseToya/items/e49977efb686ed05eadb

※補足
自分が実装した際は課金設定のとこで少々手間取りましたが、
左のメニューバーから設定できるはずです。
image.png

以上が、実装できましたら
機能を追加していきます!

※YOURAPIKEYには自分自身のAPIキーを代入してください

投稿する際に緯度と経度を保存する

カラムの追加
$ rails generate migration AddDetailsToPosts lat:float lng:float
$ rails db:migrate
投稿フォームを追加

元記事から少々、書き換えているので注意して以下の文をコピペしてください。

posts/new.html.erb
<h1>New Post</h1>

<%= form_with(model: @post, local: true) do |f| %>
  <div class="actions">
    <%= f.label :body,"内容" %>
    <%= f.text_field :body %>
    <%= f.label :lat,"緯度" %>
    <%= f.text_field :lat,:value =>"下のグーグルマップのマーカーを動かしてね", id: :lat %>
    <%= f.label :lng,"経度" %>
    <%= f.text_field :lng,:value =>"下のグーグルマップのマーカーを動かしてね", id: :lng %>
    <%= f.submit %>
  </div>
<% end %>

<h2>Map</h2>

<input id="address" type="textbox" value="GeekSalon">
<input type="button" value="検索" onclick="codeAddress()">
<p>マーカーをドラック&ドロップで位置の調整ができます。<p>
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 600px;
}
</style>

<script>
//初期マップの設定
let map
let marker
function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center:  {lat: 35.6803997, lng:139.7690174},  //東京
    zoom: 15,

  });
}

//検索後のマップ作成
let geocoder
let aft
function codeAddress(){
  let inputAddress = document.getElementById('address').value;
  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    if (status == 'OK') {
        //マーカーが複数できないようにする
        if (aft == true){
            marker.setMap(null);
        }

        //新しくマーカーを作成する
        map.setCenter(results[0].geometry.location);
            marker = new google.maps.Marker({
            map: map,
            position: results[0].geometry.location,
            draggable: true // ドラッグ可能にする
        });

        //二度目以降か判断
        aft = true

        //検索した時に緯度経度を入力する
        document.getElementById('lat').value = results[0].geometry.location.lat();
        document.getElementById('lng').value = results[0].geometry.location.lng();

        // マーカーのドロップ(ドラッグ終了)時のイベント
        google.maps.event.addListener( marker, 'dragend', function(ev){
            // イベントの引数evの、プロパティ.latLngが緯度経度
            document.getElementById('lat').value = ev.latLng.lat();
            document.getElementById('lng').value = ev.latLng.lng();
        });
    } else {
      alert('該当する結果がありませんでした:' + status);
    }
  });   
}

</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEY&libraries=&v=weekly" defer></script>

ストロングパラメーターの設定も忘れずに!

controllers/posts_controller.rb
def post_params
      params.require(:post).permit(:body,:lat,:lng)
end

緯度と経度のtext_fieldにidを追加しのマーカーのドラック後に
valueをJavaScriptで書き換えるコードを追加しました。

場所や地名で検索し緯度と経度をtext_fieldに入力し、保存できるようになりました。
本当に保存できたか確認してみましょう。

$ rails c
$ Post.all

無事ターミナルにデータが表示されていればオッケーです!

すべての投稿内容を一つの地図上で一覧表示させる、マーカーをクリックすると投稿の内容が出る、地図上で詳細ページまでのリンクを表示させる

以上を一気に作っていきます

posts/index.html.erb
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 100%;
}
</style>

<!-- js部分 -->
<script>
    function initMap() {

      //初期表示位置
      let latlng = new google.maps.LatLng(38.60, 139.5);
      //デザイン
      let styles = [
           {
            stylers: [
             { "saturation": 0 },
             { "lightness": 0 }
            ]
           }
          ];

      let map = new google.maps.Map(document.getElementById('map'), {
          zoom: 5.5,
          styles: styles,
          center: latlng
      });
      let transitLayer = new google.maps.TransitLayer();
      transitLayer.setMap(map);

      //複数マーカー ここから
      <% @posts.each do |post|%>
        ( function() { 
          let markerLatLng = new google.maps.LatLng({lat: <%= post.lat %>, lng: <%= post.lng %>}); // 緯度経度のデータ作成
          let marker = new google.maps.Marker({ 
            position: markerLatLng, 
            map: map 
          });
       //マーカーをクリックしたとき、詳細情報を表示
          let infowindow = new google.maps.InfoWindow({
            position: markerLatLng,
            content: "<a href='<%= post_url(post.id) %>' target='_blank'><%= post.body %></a>"
          }); //ここで詳細ページへのリンクを表示させる
          marker.addListener('click', function() {
            infowindow.open(map, marker);
          });

       })();
      <% end %>
      //複数マーカー ここまで  
  }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEY&callback=initMap" async defer></script>

以上、お疲れ様でした!

「飲食店などの場所を投稿したい」ってよくありますよね。
ぜひこの記事が役に立ったら、幸いです!

おまけ①練習問題

詳細ページではどのように実装すれば、地図が表示されるでしょうか?

上記を参考にして少し考えてみてください!

答え

緯度と経度を取得して地図に反映させればオッケーですね!

posts/show.html.erb
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 100%;
}
</style>

<!-- js部分 -->
<script>
//初期マップの設定
let map
let marker
function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center:  {lat: <%= @post.lat %>, lng: <%= @post.lng %>},
    zoom: 15,
  });

   marker = new google.maps.Marker({
    position:  {lat: <%= @post.lat %>, lng: <%= @post.lng %>},
    map: map
  });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEYI&callback=initMap" async defer></script>

おまけ②turbolinksによって引き起こされるエラー

リンクで移動した場合、詳細ページにも関わらず、一覧の地図が表示されてしまう場合があります。
このままでもリロードすれば、正常に表示されるのですが。。。。
以下の記述を削除しサーバ―を再起動すればリンクで移動した場合にも、正常に表示されるはずです。

app/javascript/packs/application.js
require("turbolinks").start()
views/layouts/application.html.erb
'data-turbolinks-track': 'reload'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで地図投稿機能 地名検索 一覧表示 GoogleMapsAPI(JavaScript)

作る機能

・投稿する際に緯度と経度を保存する
・すべての投稿内容を一つの地図上で一覧表示させる
・マーカーをクリックすると投稿の内容が出る
・地図上で詳細ページまでのリンクを表示させる

完成デモ

新規投稿画面
map_new.gif

一覧ページ
スクリーンショット 2020-12-03 040439.jpg

それでは作っていきましょう!

まずは先人の知恵を借ります。
以下の記事を参考に最後まで作り上げてください!

【Rails6 / Google Map API】初学者向け!Ruby on Railsで簡単にGoogle Map APIの導入する
https://qiita.com/nagaseToya/items/e49977efb686ed05eadb

※補足
自分が実装した際は課金設定のとこで少々手間取りましたが、
左のメニューバーから設定できるはずです。
image.png

以上が、実装できましたら
機能を追加していきます!

※YOURAPIKEYには自分自身のAPIキーを代入してください

投稿する際に緯度と経度を保存する

カラムの追加
$ rails generate migration AddDetailsToPosts lat:float lng:float
$ rails db:migrate
投稿フォームを追加

元記事から少々、書き換えているので注意して以下の文をコピペしてください。

posts/new.html.erb

<%= form_with(model: @post, local: true) do |f| %>
  <div class="actions">
    <%= f.label :body,"内容" %>
    <%= f.text_field :body %>
    <%= f.label :lat,"緯度" %>
    <%= f.text_field :lat,:value =>"緯度", id: :lat %>
    <%= f.label :lng,"経度" %>
    <%= f.text_field :lng,:value =>"経度", id: :lng %>
    <%= f.submit %>
  </div>
<% end %>

<h2>Map</h2>

<input id="address" type="textbox" value="GeekSalon">
<input type="button" value="検索" onclick="codeAddress()">
<p>マーカーをドラック&ドロップで位置の調整ができます。<p>
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 600px;
}
</style>

<script>
//初期マップの設定
let map
let marker
function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center:  {lat: 35.6803997, lng:139.7690174},  //東京
    zoom: 15,

  });
}

//検索後のマップ作成
let geocoder
let aft
function codeAddress(){
  let inputAddress = document.getElementById('address').value;
  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    if (status == 'OK') {
        //マーカーが複数できないようにする
        if (aft == true){
            marker.setMap(null);
        }

        //新しくマーカーを作成する
        map.setCenter(results[0].geometry.location);
            marker = new google.maps.Marker({
            map: map,
            position: results[0].geometry.location,
            draggable: true // ドラッグ可能にする
        });

        //二度目以降か判断
        aft = true

        //検索した時に緯度経度を入力する
        document.getElementById('lat').value = results[0].geometry.location.lat();
        document.getElementById('lng').value = results[0].geometry.location.lng();

        // マーカーのドロップ(ドラッグ終了)時のイベント
        google.maps.event.addListener( marker, 'dragend', function(ev){
            // イベントの引数evの、プロパティ.latLngが緯度経度
            document.getElementById('lat').value = ev.latLng.lat();
            document.getElementById('lng').value = ev.latLng.lng();
        });
    } else {
      alert('該当する結果がありませんでした:' + status);
    }
  });   
}

</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEY&libraries=&v=weekly" defer></script>

ストロングパラメーターの設定も忘れずに!

controllers/posts_controller.rb
def post_params
      params.require(:post).permit(:body,:lat,:lng)
end

緯度と経度のtext_fieldにidを追加しのマーカーのドラック後に
valueをJavaScriptで書き換えるコードを追加しました。

場所や地名で検索し緯度と経度をtext_fieldに入力し、保存できるようになりました。
本当に保存できたか確認してみましょう。

$ rails c
$ Post.all

無事ターミナルにデータが表示されていればオッケーです!

すべての投稿内容を一つの地図上で一覧表示させる、マーカーをクリックすると投稿の内容が出る、地図上で詳細ページまでのリンクを表示させる

以上を一気に作っていきます

posts/index.html.erb
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 100%;
}
</style>

<!-- js部分 -->
<script>
    function initMap() {

      //初期表示位置
      let latlng = new google.maps.LatLng(38.60, 139.5);
      //デザイン
      let styles = [
           {
            stylers: [
             { "saturation": 0 },
             { "lightness": 0 }
            ]
           }
          ];

      let map = new google.maps.Map(document.getElementById('map'), {
          zoom: 5.5,
          styles: styles,
          center: latlng
      });
      let transitLayer = new google.maps.TransitLayer();
      transitLayer.setMap(map);

      //複数マーカー ここから
      <% @posts.each do |post|%>
        ( function() { 
          let markerLatLng = new google.maps.LatLng({lat: <%= post.lat %>, lng: <%= post.lng %>}); // 緯度経度のデータ作成
          let marker = new google.maps.Marker({ 
            position: markerLatLng, 
            map: map 
          });
       //マーカーをクリックしたとき、詳細情報を表示
          let infowindow = new google.maps.InfoWindow({
            position: markerLatLng,
            content: "<a href='<%= post_url(post.id) %>' target='_blank'><%= post.body %></a>"
          }); //ここで詳細ページへのリンクを表示させる
          marker.addListener('click', function() {
            infowindow.open(map, marker);
          });

       })();
      <% end %>
      //複数マーカー ここまで  
  }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEY&callback=initMap" async defer></script>

以上、お疲れ様でした!

「飲食店などの場所を投稿したい」ってよくありますよね。
ぜひこの記事が役に立ったら、幸いです!

おまけ①練習問題

詳細ページではどのように実装すれば、地図が表示されるでしょうか?

上記を参考にして少し考えてみてください!

答え

緯度と経度を取得して地図に反映させればオッケーですね!

posts/show.html.erb
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 100%;
}
</style>

<!-- js部分 -->
<script>
//初期マップの設定
let map
let marker
function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center:  {lat: <%= @post.lat %>, lng: <%= @post.lng %>},
    zoom: 15,
  });

   marker = new google.maps.Marker({
    position:  {lat: <%= @post.lat %>, lng: <%= @post.lng %>},
    map: map
  });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEYI&callback=initMap" async defer></script>

おまけ②turbolinksによって引き起こされるエラー

リンクで移動した場合、詳細ページにも関わらず、一覧の地図が表示されてしまう場合があります。
このままでもリロードすれば、正常に表示されるのですが。。。。
以下の記述を削除しサーバ―を再起動すればリンクで移動した場合にも、正常に表示されるはずです。

app/javascript/packs/application.js
require("turbolinks").start()
views/layouts/application.html.erb
'data-turbolinks-track': 'reload'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルを使ったお気に入り機能の実装!

実装したいこと

  • 掲示板にブックマーク機能を追加したい。

  • ユーザーがブックマークした掲示板を一覧できるページを実装したい。

これだけの機能なのに、めちゃくちゃ難しい。いろんなことを調べるいい機会になりました。
かなり多くのものを調べたので順序立てて説明をしていきます。

実装の大まかな流れ

  • 中間テーブルとなるBookmarkモデルの実装
  • UserモデルとBoardモデルとのアソシエーションを実装
  • Userモデルにお気に入り登録のギミックを定義
  • BookmarkControllerの実装
  • Routingの設定
  • Viewの実装

Bookmarkモデルの仕組み(多対多)

で、いざお気に入り機能をつけよう!となってもどうやって実装するの・・・?となります。なので、まずどうUserモデルとBoardモデルに紐づけていくのか考えて見ましょう。

UserとBookmarkとBoardの関係

ユーザーと掲示板とブックマークの関係を見ていきましょう。

ユーザーはたくさんの掲示板をブックマークすることができます。反対に、掲示板はたくさんのユーザーにフォローされることができます。

つまり、UserもBoardもBookmarkをたくさん持っているということになります。このような関係を多対多の関係と言われています。

中間モデル

多対多のモデルを実装するにはお互いのforeign_keyを知っている必要があります。そのために、お互いのidを格納するテーブル、中間テーブルを実装する必要があります。

Bookmarkモデルの作成

中間テーブルとなるBookmarkモデルを作成していきます。

ターミナル
ruby
rails g model Bookmark user:references board:references

migrateファイル

class CreateBookmarks < ActiveRecord::Migration[6.0]
  def change
    create_table :bookmarks do |t|
      t.references :user, null: false, foreign_key: true
      t.references :board, null: false, foreign_key: true

      t.timestamps
    end
    add_index :bookmarks, [:user_id, :board_id], unique: :true
  end
end

ユーザーが同じ掲示板をお気に入り登録しないようにunique: :trueをつける必要があります。

add_index :bookmarks, [:user_id, :board_id], unique: :trueでuser_idとboard_idの組み合わせがuniqueであることを設定します。
これでrails db:migrateを行います。

bookmark.rb

class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :user_id, uniqueness: { scope: :board_id}
end

migrationにもunique: :trueを付けたので、モデルにもバリデーションを記載します。

uniquenessとscopeについて

validates :user_id, uniqueness: { scope: :board_id}
end

上記は各掲示板idと同じユーザーidがお気に入り関係にならないように一意性制約を付けています。
rails cで確認して見ます。

irb(main):001:0> user = User.first
irb(main):002:0> board = Board.first

# userが掲示板をお気に入り登録する。
irb(main):003:0> user.bookmark(board)
   (0.1ms)  begin transaction
  Bookmark Exists? (0.9ms)  
  Bookmark Create (3.2ms)  
   (0.7ms)  commit transaction

=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">]>


# もう一度同じユーザーで掲示板のお気にいり登録を試みる。
irb(main):004:0> user.bookmark(board)

# すでにお気に入り登録されているので、バリデーションに引っ掛かりrollbackされる。
(0.1ms)  begin transaction
  Bookmark Exists? (0.2ms)  
   (0.1ms)  rollback transaction
Traceback (most recent call last):
        2: from (irb):4
        1: from app/models/user.rb:27:in `bookmark'
ActiveRecord::RecordInvalid (バリデーションに失敗しました: Userはすでに存在します)
# 違うユーザーを指定
irb(main):005:0> user_2 = User.second

# 違うユーザーで掲示板をお気に入り登録を試みると成功する。
irb(main):006:0> user_2.bookmark(board)
(0.1ms)  begin transaction
  Bookmark Exists? (0.2ms)  
  Bookmark Create (0.8ms)  
   (1.4ms)  commit transaction

=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">]>


bookmarkメソッドはUserモデルに定義しています。また後で紹介しますがconsoleで出てくるので載せておきます。

user.rb

  # お気に入りにしている掲示板を取得する
  has_many :bookmarks_boards, through: :bookmarks, source: :board


  # お気に入り追加
  # <<で引数で渡した掲示板の情報がbookmark_boardsに入っている
  def bookmark(board)
    bookmarks_boards << board
  end

参考記事

[https://qiita.com/j-sunaga/items/d7f0e944baad6e56206c:title]

[https://railsguides.jp/active_record_validations.html#uniqueness:title]

[https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1#%E3%81%8A%E6%B0%97%E3%81%AB%E5%85%A5%E3%82%8A%E6%A9%9F%E8%83%BD%E3%82%92er%E5%9B%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%A8%AD%E8%A8%88%E3%81%97%E3%82%88%E3%81%86:title]

UserモデルとBoardモデルのアソシエーションの設定

Bookmarkモデルの実装が終わったので、他のモデルにもアソシエーションなどの設定を行っていきます。

Boardモデル

board.rb

class Board < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy # 追記
  mount_uploader :board_image, BoardImageUploader

  validates :title, length: { maximum: 255 }, presence: true
  validates :body, length: { maximum: 65535 }, presence: true
end

掲示板はたくさんのBookmarkを持つことができるのでhas_manyを使います。

Userモデル

user.rb

  has_many :bookmarks, dependent: :destroy
  # お気に入りにしている掲示板を取得する
  has_many :bookmarks_boards, through: :bookmarks, source: :board

UserもたくさんのBookmarkを持つことができるのでhas_manyを使っていきます。

ちょっと待って、has_many :bookmarks_boardsって何?throughとかsourceも使っているけどよくわからない・・・。
というわけでこれからhas_many throughについて調べたことをまとめていきます。

has_many throuth

まずhas_many throughはUserモデルでどういう働きをしているのかというと、ユーザーがお気に入りしている掲示板を取得することができるようになります。

Twitterでいいね一覧が表示できる機能がありますよね。それと同じようにお気に入り登録した掲示板を一覧で表示できるページを作るために必要となってきます。

has_many throughを使わずにユーザーのお気に入りした掲示板を取得したいとなると、このようなコードになります。

# user.bookmarksでuserがお気に入り登録した掲示板のidが入っているレコードの集合を取得することができる。
irb(main):014:0> user.bookmarks

=> #<ActiveRecord::Associations::CollectionProxy [#<Bookmark id: 7, user_id: 1, board_id: 1, created_at: "2020-11-20 00:50:02", updated_at: "2020-11-20 00:50:02">, #<Bookmark id: 10, user_id: 1, board_id: 3, created_at: "2020-11-20 01:57:05", updated_at: "2020-11-20 01:57:05">, #<Bookmark id: nil, user_id: 1, board_id: 1, created_at: nil, updated_at: nil>]>

# userが最初にお気に入り登録した掲示板のレコードを取得
irb(main):005:0> user.bookmarks.first
  Bookmark Load (0.5ms)  
=> #<Bookmark id: 7, user_id: 1, board_id: 1, created_at: "2020-11-20 00:50:02", updated_at: "2020-11-20 00:50:02">

# 上記で取得したレコードにboardメソッドを実行するとお気に入り登録した掲示板の内容が取得できる!
irb(main):006:0> user.bookmarks.first.board
  Bookmark Load (0.1ms)  
  Board Load (0.2ms)  
=> #<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">

# つまり、user.bookmarksのひとつひとつのレコードにboardメソッドを実行すればユーザーがお気に入りにした掲示板の内容の集合を取得することができる!

# なのでmapメソッドを使ってユーザーがお気に入り登録した掲示板のレコードにboardメソッドを実行し、それを配列に組み込んでいく。
irb(main):007:0> user.bookmarks.map{|bookmark| bookmark.board}
  Bookmark Load (0.4ms)  
  Board Load (0.2ms) 
  Board Load (0.1ms)  
=> [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]

# 上記の式を(&:)を使って書き換えます。
irb(main):008:0> user.bookmarks.map(&:board)
=> [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]

つまり、user.bookmarks.map(&:board)を使えばユーザーのお気に入り登録している掲示板の情報の集合を取得できるというわけです!

このコードをcontrollerなどに書いて実装するのも一つの方法だと思いますが、あまり直接的ではないのと、このコードをビューに落とし込むのも大変です。

そこでhas_many throughの登場です。
これを使えばuser.bookmarks.map(&:board)をモデル内に簡単に実装できちゃいます。

has_many :bookmarks_boards, through: :bookmarks, source: :board

:bookmarks_boardsと定義することでメソッド化して使うことができます。

user.bookmarks.map(&:board)このコードを見ながら解説していくと

Userのインスタンスにbookmarksメソッド(through:で定義)を実行し、それで得られたBookmarksのインスタンスデータのひとつひとつの要素に対してboardメソッド(source:で定義)を実行する

ということです。

なので、多対多のモデルを作った時に必ずと言っていいほど活躍するというわけです!

参考記事

実はRailsチュートリアルの第14章の動画を見るとすごくわかりやすいです。

[https://railstutorial.jp/:title]

Railsガイドの文献

[https://railsguides.jp/association_basics.html#has-many-through%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91:title]

Bookmarks_Controllerの実装

よし、モデルのアソシエーションも終わったしcontroller作ろう!
ちょっと待ってください。controllerの可読性を上げるためにまずはモデルにお気に入り登録のギミックを定義していきましょう。すると驚くほどにcontrollerの実装が完結になりますよ!

controllerを作る前にモデルにBookmarkのギミックを定義する。

Userモデルにお気に入り登録のギミックとなるメソッドを定義していきましょう。

user.rb
```ruby
# お気に入り追加
# <<で引数で渡した掲示板の情報がbookmark_boardsに入っている
def bookmark(board)
bookmarks_boards << board
end

# お気に入りを外す
def unbookmark(board)
bookmarks_boards.delete(board)
end

# お気に入り登録しているか判定するメソッド
def bookmark?(board)
bookmarks_boards.include?(board)
end

早速先ほど定義した`bookmarks_boards`が使われていますね。
一つずつメソッドを見ていきます。

####bookmarkメソッド

```ruby
  # お気に入り追加
  # <<で引数で渡した掲示板の情報がbookmark_boardsに入っている
  def bookmark(board)
    bookmarks_boards << board
  end

掲示板の情報のレコードが引数boardに格納されbookmarks_boards<<で追加されています。

<<は指定されたオブジェクトの末尾に破壊的に追加できるメソッドです。
強制的に追加されて保存もされているのでsaveメソッドなどは必要ありません。

<<メソッドについて詳しくはこちら

[https://docs.ruby-lang.org/ja/latest/method/Array/i/=3c=3c.html:title]

unbookmarkメソッド

  # お気に入りを外す
  def unbookmark(board)
    bookmarks_boards.delete(board)
  end

bookmarks_boardsからboardの引数に入っている掲示板idが入ったレコードを探し出して削除(delete)するメソッド。

bookmark?メソッド

  # お気に入り登録しているか判定するメソッド
  def bookmark?(board)
    bookmarks_boards.include?(board)
  end

bookmarks_boardsにboardの引数に入っている掲示板idが含まれているレコードがあるかどうか判定するメソッド。

def bookmark?(board)
    Bookmark.where(user_id: id, board_id: board.id).exist?
end

このように書くこともできますが、include?の方が直感的でわかりやすいです。

bookmarks_controllerの実装

ここまできたら、bookmarks_controllerを作っていきましょう。
viewも必要ないのでcontrollerファイルだけ作って記載します。

bookmarks_controller.rb

class BookmarksController < ApplicationController


  def create
    board = Board.find(params[:board_id])
    current_user.bookmark(board)
    redirect_back fallback_location: root_path, success: 'ブックマークしました'
  end

  def destroy
    board = current_user.bookmarks.find_by(params[:id]).board
    current_user.unbookmark(board)
    redirect_back fallback_location: root_path, success: 'ブックマークを外しました'
  end
end

お気に入り登録のギミックをモデルに定義したことによって、かなり直感的なcontrollerになりました!

redirect_back fallback_location : root_path

redirect_backはユーザーが直前にリクエストを送ったページに戻すことができます。

fallback_locationは直前にリクエストを送ったページがない場合のデフォルトのリダイレクト先を指定しています。

Routingの設定

controllerも書けたので、次はRoutingの設定を行っていきます。

routes.rb

Rails.application.routes.draw do
  root 'static_pages#top'

  resources :users

  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'

  resources :boards, shallow: true do
    resources :comments, only: %i[create destroy]
    resource :bookmarks, only: [:create, :destroy]
    collection do
      get :bookmarks
    end
  end
end

BoardとBookmarkは親子の関係なのでbookmarks_controllerのRoutingはboards_controllerにネストするように記載しています。

collectionルーティング

railsの基本的なアクションはresourcesで定義した時に作られる7つのアクションですが、更に別のアクションを追加したい時があります。

その時に使えるのがcollectionルーティングです。boards_controllerに新しくbookmarksというアクションを追加することができます。
ちなみに、controllerのメンバーに対してアクションを追加する場合(idが伴う場合)はmemberルーティングを使います。

collection以外を抜いたルーティングがこちら。

resources :boards, shallow: true do
    collection do
      get :bookmarks
    end
  end

bookmarks_boards GET /boards/bookmarks(.:format) boards#bookmarks

上記でこのようなルーティングが出来上がります。
このルーティングとアクションはお気に入りされた掲示板一覧を表示するページとして使っていきます。

Boards_controllerにbookmarksアクションを追記

ルーティングが書けましたので、Boards_controllerにbookmarksアクションを追加していきます。

boards_controller.rb

def bookmarks
    @bookmark_boards = current_user.bookmarks_boards.includes(:user).order(created_at: :desc)
end

無駄にSQL文を発行させない様にincludes(:user)を記載して関連するuserの情報も取得しています。(n + 1問題の解消)

irb(main):001:0> user = User.first

irb(main):002:0> user.bookmarks_boards
  Board Load (2.2ms)  SELECT "boards".* FROM "boards" INNER JOIN "bookmarks" ON "boards"."id" = "bookmarks"."board_id" WHERE "bookmarks"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]>

irb(main):003:0> user.bookmarks_boards.includes(:user)
  Board Load (0.3ms)  SELECT "boards".* FROM "boards" INNER JOIN "bookmarks" ON "boards"."id" = "bookmarks"."board_id" WHERE "bookmarks"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
=> #<ActiveRecord::AssociationRelation [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]>

Viewの実装

ここまで仕組みを実装したら、あとはViewを作るだけです!
お気に入りボタンとお気に入りした掲示板一覧ページを作成していきます。

お気に入りボタンの作成

まずはパーシャルでブックマークするボタンとブックマーク解除ボタンを切り替えるページを作ります。

bookmarks/_bookmark_area.html.erb

<% if current_user.bookmark?(board) %>
  <%= render 'bookmarks/unbookmark', board: board %>
<% else %>
  <%= render 'bookmarks/bookmark', board: board %>
<% end %>

user.rbで定義されたbookmark?メソッドがここで使われます。
掲示板がブックマークされていたら掲示板解除ボタン、掲示板がブックマークされていなかったらブックマーク登録ボタンに切り替わる仕組みです。

次に、お気に入り登録ボタンを実装していきます。
bookmarks/_bookmark.html.erb

<%= link_to board_bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <%= icon 'far', 'star' %>
<% end%>

次に、お気に入り解除ボタンを実装していきます。
bookamrks/_unbookmarks.html.erb

<%= link_to board_bookmarks_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", method: :delete do %>
  <%= icon 'fas', 'star' %>
<% end %>

current_userに紐づいているbookmarkインスタンスの中から掲示板idが含まれているものを探し、取得しています。

これで、お気に入りボタンが完成しました。これを掲示板のパーシャルに組み込んでいきます。

boards/_board.html.erb

<% if current_user.own?(board) %>
   <div class='mr10 float-right'>
      <%= render 'crud_menus', board: board %>
<% else %>
     <%= render 'bookmarks/bookmark_area', board: board %>
   </div>
<% end %>

掲示板がログインしているユーザーのものだったらcrud_menusボタンが表示され、ユーザーのものではなかった場合はお気に入りボタンに切り替わる様になっています。

お気に入り掲示板一覧機能ページの作成

最後に、お気に入りした掲示板一覧ページを作成していきます。
掲示板一覧ページとレイアウトはあまり変わらないため、ほとんど掲示板一覧ページから引っ張ってきています。

boards/bookmarks.html.erb

<% content_for(:title, t('.title')) %>
<div class="container pt-3">
  <div class="row">
    <div class="col-lg-10 offset-lg-1">
      <!-- 検索フォーム -->
      <form>
        <div class="input-group mb-3"><input class="form-control" placeholder="検索ワード" type="search"/>
          <div class="input-group-append"><input type="submit" value="検索" class="btn btn-primary"/></div>
        </div>
      </form>
    </div>
  </div>

  <div class="row">
    <div class="col-12">
      <div class="row">
      <% if @bookmark_boards.present? %>
        <%= render partial: "board", collection: @bookmark_boards %>
      <% else %>
        <p>ブックマーク中の掲示板がありません</p>
      <% end%>
      </div>
    </div>
  </div>
</div>

これで完成です!お疲れ様でした!

おまけ

n + 1問題を解消するためにコードをリファクタリングしていきます。

boards_controller.rb

def index
# userのみキャッシュしている。
  @boards = Board.all.includes(:user).order(created_at: :desc)

 #bookmarkも取得できる様になる。
  @boards = Board.all.includes([:user, :bookmarks]).order(created_at: :desc)
  end

user.rb

# userを起点にしてSQLを走らせてしまっているためレコードを取得する時に毎回SQLが走ってしまう。
def bookmark?(board)
  bookmarks_boards.include?(board)
end

# boardを起点にしてSQLが走り、検索をかける。無駄なSQLが走らない。
def bookmark?(board)
  bookmarks_boards.pluck(:user_id).include?(id)
end

_unbookmark.html.erb

<%= link_to bookmark_path(board.bookmarks.find { |b| b.user_id == current_user.id }),
            id: "js-bookmark-button-for-board-#{board.id}",
            class:"float-right",
            method: :delete,
            remote: true do %>
  <%= icon 'fas', 'star' %>
<% end %>

なるべくfindを使う様にして無駄にSQLを走らせない様にする。

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

CSSの記述が間違っていてエラーが出た話

環境

macOS Catalina 10.15.7
Ruby on Rails 6.0.0
RSpec 4.0.1
pry rails 0.3.9

エラー内容

RSpecで visit root_path を実行したタイミングでエラーが発生した。

console
Failure/Error: <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

     ActionView::Template::Error:
       Invalid CSS after "...kground-color: ": expected expression (e.g. 1px, bold), was "##253141;"



     # (sass):8
     # ./app/views/layouts/application.html.erb:8:in `_app_views_layouts_application_html_erb__1634657128219108166_70256618570500'
     # ------------------
     # --- Caused by: ---
     # Sass::SyntaxError:
     #   Invalid CSS after "...kground-color: ": expected expression (e.g. 1px, bold), was "##253141;"
     #   (sass):8

デフォルトで app/views/layouts/application.html.erb に記述されている、外部スタイルシートを指定するリンクタグを生成するメソッドを読み込んだタイミングでエラーが発生している。

対処

エラー内容をみると、Invalid CSSとなっている。##253141;というtypoがあるようなので、VSCodeの文字列検索で探してみる。

messages.css
.side-bar {
  width: 300px;
  height: 100vh;
  background-color: ##253141;
}

あった。
#を一つ消し、visit root_pathが正しく動作することを確認できた。
今回、VSCodeの検索機能を初めて使用したが、めちゃくちゃ便利なので、今後も使っていきたい。

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

Ruby on railsとLalavelどっちが良い?

技術選定の仕方

・自分のやりたい事が実現しやすい
・情報量が多い

プログラミングはあくまでも手段でしかなく、プロダクトの価値を最大化させることこそが目的である。またプログラミング言語は、基礎文法は少し変わるぐらいで大した差はない。言語に縛られるよりも、自分が言語に合わせて勉強していくほうが、プロダクトの価値も開発効率も上がる。

Ruby on railsを選びました

①scaffoldが便利
MVCの作成はrouteの設定までいい感じに自動で生成してくれる。

②情報量が多い!
ruby自体が日本発の言語ということもあり、qiitaの記事の数をはじめ日本語の説明の記事がlalavelよりも多い。

③コミュニティー内に参考にできるソースコードがある
身近にコードについて質問でき参考にできるコードがあるのは、めっちゃ楽。

Rails: ある程度の規模まではレールに乗ってサクサク作れるが、アプリケーションが成熟してきて「規約」にはないことをやりたくなってくると、途端に求められる技術力や難易度が跳ね上がる。
Laravel: アプリケーションの雛形はあるものの、どのクラスに何を担わせるかなど、設計については Rails 以上に早い段階で意識する必要がある。その設計さえイケていれば、だいたいのことはしっかり書ける。
Rails 歴5年の僕が Laravel で開発するようになって思ったこと。
https://note.com/kurashicom_tech/n/n32ab25910783

結論

僕のような初学者には、railsをおすすめします。railsのレールに乗りながら、まずはMVCの基礎を自分にインプットしていく。その後、lalalvelに挑戦!で良いかなと思う。
どちらの言語も実行コマンドは似ているし、Railsはgem、Laravelはcomposerを使えば各種プラグインも簡単に導入できるので、あまり差はないと感じます。
最後まで読んでいただきありがとうございます。

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

Vagrant + VirtualBox で初めての開発環境こうちく|既存のRailsプロジェクト + Mysql + Apatch

はじめに

初めて Vagrant で開発環境を構築する方に向けて手順をまとめました。
初めて構築する方のために、専門用語の説明や、つまづきそうな箇所の説明も載せました。
それでは開発環境を構築していきましょう。

仮想マシンスペック

  • OS                            :  CentOS release 7.1

  • DB                            : mysql Ver 14.14 Distrib 5.5.41, for Linux (x86_64) using readline 5.1

  • Webサーバーソフト  : Apache/2.2.15 (Unix)

  • Ruby                         : ruby 2.1.5p273 [x86_64]

ローカルOSはWidnowsです。

■目次

■□■□■□■□■□■□■

… 1. 環境構築まえの準備

… 2. 仮想マシンの作成

… 3. 必要な関連ソフト及びRubyインストール | 開発環境の作成

… 4. Mysql, Apatchインストール | 開発環境の作成

… 5. Railsインストール | 開発環境の作成

… 6. Railsの起動

■□■□■□■□■□■□■

■ 1.環境構築まえの準備

Vagrant, Virtual boxとは ...

ソフト名 役割
Vagrant 仮想マシンの停止・起動などを簡単に管理できる 「コマンドラインツール」
Virtural box 仮想マシンを作る「仮想化ソフト」

つまり Vagrant を使えば、仮想マシンを 簡易にコマンド操作 できるようになります。

▼ Vagrant + Virtual box のインストール

まず次のサイトから、必要なソフトをダウンロードします。
・Vagtant     https://www.vagrantup.com/
・VirtualBox   https://www.virtualbox.org/

インストールはインストーラーに従うだけで完了します。

▼ インストールされたか確認

次のコマンドを入力してください。

$ vagrant -v
Vagrant 1.9.1

無事バージョンが返ってこればインストールは完了です。

▼ Vagrant 操作用のディレクトリを作成

次に CentOS インストール用ディレクトリを作成し、作成先のディレクトリへ移動しましょう。

$ mkdir workspace
$ cd workspace

■ 2.仮想マシンの作成

Virtual box を使用して、仮想マシンを起動するためには OS イメージファイル(※ boxファイル)が必要です。
※ 今回の OS イメージファイルは、Chef 社で用意されている「bento」を使用しました。

▼ OS インストール

仮想マシンへ CentOS 7.1 をインストールします。
初期設定を行うために、次のコマンドを実行します。

$ vagrant init bento/centos-7.1

実行すると「Vagrantfile」という初期設定ファイルが作成されます。

$ ls
Vagrantfile

▼ 初期設定ファイル(Vagrantfile)の書き換え

次の2箇所の設定を書き換えます。
1. ネットワーク
2. 共有フォルダ

▽ 1.ネットワーク

ローカルホスト(作業しているPC)から仮想マシン上の Rasil サーバーへ接続できるようにネットワーク設定を変更します。
Vagrantfile を好みのテキストエディタで開いてください。

▶︎ workspace / Vagrantfile

Vagrant.configure(2) do |config|
 # The most common configuration options are documented and commented below.
 # For a complete reference, please see the online documentation at
 # https://docs.vagrantup.com.
 # Every Vagrant development environment requires a box. You can search for
 # boxes at https://atlas.hashicorp.com/search.
 config.vm.box = "bento/centos-7.1"
 # Disable automatic box update checking. If you disable this, then
 # boxes will only be checked for updates when the user runs
 # `vagrant box outdated`. This is not recommended.
 # config.vm.box_check_update = false

 # Create a forwarded port mapping which allows access to a specific port
 # within the machine from a port on the host machine. In the example below,
 # accessing "localhost:8080" will access port 80 on the guest machine.



 # コメント(#)を外してポート番号を書き換える。
 config.vm.network "forwarded_port", guest: 3000, host: 3000 



 # Create a private network, which allows host-only access to the machine
 # using a specific IP.
 # config.vm.network "private_network", ip: "192.168.33.10"

 # Create a public network, which generally matched to bridged network.
 # Bridged networks make the machine appear as another physical device on
 # your network.
 # config.vm.network "public_network"

この設定でブラウザから仮想マシンに接続する接続先が http://localhost:3000 になります。

ここで一度 Vagrant を使って、仮想マシンを起動できるか確認してみましょう。

$ vagrant up

vagrantコマンド | 参考

コマンド 役割
vagrant up Vagrantfileに基づいて仮想サーバーを起動
vagrant ssh 仮想サーバーに接続され、コマンド操作できるようになる
vagrant halt 仮想サーバーを停止
vagrant reload Vagrantfileの設定を反映させる( vagrant halt + vagrant up )
vagrant destroy 仮想サーバーを消去

▽ 2.共有フォルダ

共有フォルダ機能とは ...

Vagrant には、ローカルと仮想マシン間のファイルを同期する機能があります。
この機能により、仮想マシン上にわざわざリモート接続(vagrant ssh)してファイルを編集する必要がなくなります。
つまり、ローカルファイルを編集するだけで、自動的に仮想マシン上のファイルに反映できます。

デフォルのフォルダ共有場所

場所 ディレクトリ
ローカル側 ./workspace
仮想マシン側                          /home/vagrant

別のフォルダ場所を共有したい場合は、Vagrantfile の次の箇所を変更します。

config.vm.synced\_folder “ローカルのフォルダ場所”, “仮想マシン上のフォルダ場所”

今回は以下の場所を共有しました。

▶︎ workspace / Vagrantfile

Vagrant.configure(2) do |config|
 # other config here

 config.vm.synced_folder "./workspace", "/home/vagrant/rials_projects"
end

この設定を反映させるために仮想マシン側で不足しているものがあるので、次の作業(■ 3.Ruby及び...)に行く前にインストールします。

右のエラーが出るため。「yum update→カーネルアップデート→ VitualBox GuestAddition 破損」

# ローカル
$ vagrant ssh

# 仮想マシン
$ sudo yum install kernel-devel
$ sudo yum install gcc make
$ exit

# ローカル
$ vagrant plugin install vagrant-vbguest
$ vagrant vbguest

ファイル共有できているか確認。

$ vgrant ssh
$ cd /rails_projects/
$ ls
Vagrantfile

ここまでで仮想マシンの基本的な設定は完了です !

以降は、必要なソフトウェア類をインストールしていきます。

■ 3.必要な関連ソフト及びRuby インストール | 開発環境の作成

▼ 必要な関連ソフトのインストール

yumを最新化して諸々のパッケージをまとめてインストール。

$ vagrant ssh
$ sudo yum update
$ sudo yum install -y gcc-c++ patch readline readline-devel zlib zlib-devel libyaml-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison nodejs git vi

▼ Git インストールされたか確認

$ git --version
ruby  git version 2.2.2

▼ rbenv のインストールと初期化

rbenv をインストールし、初期化を行います。

$ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ . ~/.bashrc

▼ ruby-build のインストール

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ cd ~/.rbenv/plugins/ruby-build
$ sudo ./install.sh

インストールされると、次のコマンドでインストール可能な ruby のバージョンを表示できます。

$ rbenv install -l

▼ Rubyのインストール

$ rbenv install 2.1.5
$ rbenv global 2.1.5
$ rbenv rehash

インストールされたか確認。

$ ruby -v
ruby 2.1.5

■ 4.Mysql, Apache インストール | 開発環境の作成

▼ Mysqlのインストールと初期設定

次のコマンドで Mysql の Yum レポジトリを追加。

$ yum localinstall http://dev.mysql.com/get/mysql57-community-release-el6-7.noarch.rpm

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

$ yum repolist all | grep mysql

mysql-connectors-community/x86\_64 MySQL Connectors Community 有効:

mysql-connectors-community-source MySQL Connectors Community - So 無効

mysql-tools-community/x86\_64 MySQL Tools Community 有効:

mysql-tools-community-source MySQL Tools Community - Source 無効

mysql55-community/x86\_64 MySQL 5.5 Community Server 無効

mysql55-community-source MySQL 5.5 Community Server - So 無効 \<---注目

mysql56-community/x86\_64 MySQL 5.6 Community Server 無効   

mysql56-community-source MySQL 5.6 Community Server - So 無効

mysql57-community/x86\_64 MySQL 5.7 Community Server 有効:    \<---注目

mysql57-community-source MySQL 5.7 Community Server - So 無効

上記の例では、5.7が有効、5.5が無効になっています。

バージョンの切り替えには Yum の設定変更用の yum-utils パッケージが必要なので、インストールします。

# yum-utilsがインストールされているか確認
$ yum list installed | grep yum-utils            

# 入ってなければyum-utilsをインストールする
$ yum -y install yum-utils                       

# 5.7を無効に設定
$ yum-config-manager --disable mysql57-community 

# 5.5を有効に設定
$ yum-config-manager --enable mysql55-community  

設定できているか再確認します。
5.5が有効になっていれば大丈夫です。

公式リポジトリにある、mysql-community-server パッケージをインストールします。
Mysql のバージョンが5.5になっているのを確認。

$ yum info mysql-community-server

...(略)...

Name : mysql-community-server

Arch : x86\_64

Version : 5.5.62

Release : 2.el6

Size : 38 M

Repo : mysql55-community

Summary : A very fast and reliable SQL database server

URL : http://www.mysql.com/

License : Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Under GPLv2 license as

: shown in the Description field.

Description : The MySQL(TM) software delivers a very fast, multi-threaded, multi-user,

...(略)...

インストールします。

$ yum -y install mysql-community-server

インストールしたらバージョンを確認。

$ mysql --version
mysql Ver 14.14 Distrib 5.5.62, for Linux (x86\_64) using readline 5.1

root やセキュリティの設定を一括で行います。

$ sudo mysql_secure_installation

root で入れるか試します。

$ mysql -u root -p

▼ Apache のインストール

yum でインストールできる Apache のバージョンを確認。

$ yum list | grep httpd

Apache のインストール。

$ sudo yum install -y httpd

インストールされたバージョンを確認。

$ httpd -v
Server version: Apache/2.2.15 (Unix)
Server built:   Jun 19 2018 15:45:13

起動と自動起動設定。

$ sudo /etc/init.d/httpd start
$ sudo chkconfig httpd on

■ 5.Rails インストール | 開発環境の作成

▼ bundlerのインストール

$ gem install bundler 
# バージョンを指定する場合は 「gem install bundler -v '~\>1'」のようにオプションを指定。

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

$ bundler -v
Bundler version 1.17.3

▼ Rails + その他のライブラリをインストール

bundler を使って、gemfile に記載された諸々のライブラリ(指定バージョンの Rails を含む)をインストール。

$ bundle install --path=~/vendor/bundle 

「--path=vendor/bundle」ではないことに注意。
フォルダ共有機能で設定しているディレクトリ(--vendor/bundle)にbundle installしようとするとエラーが出る。
参考記事

「rails コマンド」をつかえるように、グローバルに rails をインストール。

$ gem install rails 
# バージョンを指定する場合は 「gem install rails -v '5.2.4'」のようにオプションを指定。

■ 6. Rails の起動

▼ vagrant に設定を反映

vagrant に設定を反映するために、以下を実行します。

# 停止
$ vagrant halt

# 立ち上げ
$ vagrant up

# 仮想マシンへ入る
$ vagrant ssh

仮想マシン上のデータベースに migration ファイルの設定を反映させます。

$ rails db:migrate

rails server の起動。

$ rails server -b 0.0.0.0 

「-b 0.0.0.0」オプション(全てのIPアドレスからアクセス可)は、ブラウザからアクセスできるように指定。
rails server のみだとIPアドレスが 127.0.0.1 に指定され、localhost なので 外部から(仮想環境の外から)はアクセスできません。
参考記事

ブラウザで http://localhost:3000/ にアクセス。

まとめ

というわけで、初めてVagrantを使う方に向けて開発環境を構築する手順を紹介しました。

基本的には一つ一つ調べながら手順をまとめましたが、まだ自身も十分に理解できていない部分もあるかと思います。

ミスがあった場合は忌避なく指摘をいただけると幸いです !

参考書籍

独習Ruby on Rails

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

個人アプリ開発日記 #5

投稿したり、投稿詳細表示、削除、一覧表示を実装していきます

まずは,何はともあれ

rails g controller drinks
rails g model Drink

を実行

コーヒーの感想を投稿しあうアプリなので、モデル名や、コントローラー名をcoffeeにしたかったのですが、
coffeeは不可算名詞なので、できるだけ避けた方が賢明。
という事でdrinkと命名しました、、、!!

画像も投稿したいので

% brew install imagemagick

をターミナルで実行

gem 'mini_magick'
gem 'image_processing', '~> 1.2'

というgemをinstallして

bundle install

drinks_controller

class DrinksController < ApplicationController
  include SessionsHelper
  before_action :logged_in_user, only: [:index,:destroy]
  def index
    @drinks = Drink.includes(:user).order("created_at DESC")
  end

  def show
    @drink = Drink.find(params[:id])
    @user = @drink.user
    @drinks = @user.drinks
  end

  def new
    @drink = Drink.new
  end

  def create
    @drink = current_user.drinks.build(drink_params)
    if @drink.save
      redirect_to drinks_path
    else
      redirect_to 'new'
    end
  end

  def destroy
    Drink.find(params[:id]).destroy
    redirect_to root_path
  end

  private
  def drink_params
    params.require(:drink).permit(:name,:price,:explain,:image).merge(user_id: current_user.id)
  end
end


  def index
    @drinks = Drink.includes(:user).order("created_at DESC")
  end

一覧画面で表示する投稿の情報には、ユーザー情報も紐付いて表示されます。
この場合、投稿に紐付くユーザー情報の取得に、投稿の数と同じ回数のアクセスが必要になるので、N+1問題が発生します。

要は投稿に紐ずくユーザーの情報を一挙に取得するから早くなるよってことです

  def create
    @drink = current_user.drinks.build(drink_params)

は、user.rbとアソシエーションを組むことで.buildというメソッドが使えます
このメソッドを使うことで勝手にuserと上手い感じに紐付けた投稿が可能になります

drink.rb

class Drink < ApplicationRecord
  belongs_to :user
  has_one_attached :image
  with_options presence: true do
    validates :name
    validates :explain
  end
end

with_optionメソッドはまとめてバリデーションを設定することができるメソッドです。
存在性だけでなく、フォーマットの検証にも使えて便利なのでぜひ調べて使ってみてください

まずはコーヒーに関する投稿をしていきたいと思います!

投稿する内容は,コーヒーの名前、画像、値段、説明です

drinks/new.html.erb

<%= form_with model: @drink,local: true do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
      <%= f.text_field :name, placeholder: "商品名" %>
      <%= f.text_field :price, placeholder: "値段" %>
      <%= f.text_area :explain, placeholder: "商品の説明", rows: "10" %>
      <h3>コーヒーの写真を投稿してください</h3>
      <%= f.label :image %>
      <%= f.file_field :image %>
      <%= f.submit "SEND" %>
<% end %>

drinks:new.html.erb.jpg

ビューにするとこんな感じ

何か投稿してみましょう

drinks/index.html.erb

<% @drinks.each do |drink| %>
  <%= drink.name %>
  <%= image_tag drink.image.variant(resize: '500x250') if drink.image.attached? %>
  <%= drink.price %>
  <%= drink.explain %>
  <%= link_to drink.user.nickname ,drink_path(drink) %>
<% end %>
<%= link_to "投稿する", new_drink_path %>
<%= link_to "トップページ", root_path %>

投稿後はdrinks#indexを呼び出すようにしてます

drinks:index.jpg

レイアウト崩れまくりですがしっかり画像つきで表示されてて安心しました

次は投稿詳細をみてみましょう

drinks/show.html.erb

<%= @drink.user.nickname%>
  <% @drinks.each do |drink| %>
      <%= drink.price%>
      <span class="name"><%= drink.name %></span>
      <p><%= drink.explain %></p>
      <%= image_tag drink.image.variant(resize: '500x250') if drink.image.attached? %>
      <%= link_to "削除",drink_path(drink.id),method: :delete %>
  <% end %>

drinks:show.jpg

今のところは投稿の詳細ページを押したところで殺風景ですが、ここにこの投稿がついたコメントがダラーっと表示される感じにしたいですね、、、!!

削除はまぁいいでしょう、、、、

一通りの基本的な機能はできたので次回は見た目をめちゃくちゃよくしていきたいと思います、、、!!!

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

【Rails】コントローラーの処理をモデル内のクラスメソッドとして記載する。

テーマ

コントローラー内に処理を記載していき、コードが冗長になったり、同じ処理を様々なアクションで使用する場合、一つのメソッドとしてモデル内で定義することがあると思います。
簡単なやり方と、その際にちょこっとだけつまづいた点をまとめています。

バージョン

ruby 2.5.7
Rails 5.2.4.4

やり方

shops_controller.rb
def members

 @table_customers = 2
 @tables = 3

 @all_customers =  @table_customers * @tables
 Shop.update(members:@all_customers)
#店内の総客数を計算するアクション
end

上記のコードでは店内の客数を計算するアクションになります。
(簡略化のために今回は1つのテーブルの客数を2人、テーブル数を3卓と定義しています。)

このアクションをモデル内に定義する場合を順を追って解説します。

shops_controller.rb
def members

 @table_customers = 2
 @tables = 3

 Shop.count_members(@table_customers,@tables)

end

コントローラーの記述

shop.rb
def self.count_members(table_customers,tables) 

  @all_customers =  @table_customers * @tables
  Shop.update(members:@all_customers)

end

モデルの記述

このままではエラーが発生しますが、解消方法はのちに記載します

・コントローラー側の記載
Shop.count_members(@table_customers,@tables)
Shop = メソッドを定義したモデル名
count_members = メソッド名(モデル内に記載してあるもの)
(@table_customers,@tables) = 処理する際に渡す変数

それぞれの記述を簡単に説明するとこのようになります。

・モデル側の記述
def = メソッドを定義
self = クラスメソッドを使用する際に記述(インスタンスメソッドの場合は不要)
count_members = メソッド名
(table_customers,tables) = コントローラーで受け取った値を変数としてそれぞれ再定義(今回の場合は、table_customers = 2, tables = 3 になる)

インスタンスメソッドとクラスメソッドの違いについては別記事を参照して見てください。今回はクラスメソッドを使用しています。

注意ポイント①

前述した通り、単純に処理内容をコピペした上記のコードでは想定した処理が行われずエラーが発生します。
その理由はモデル内で変数を再定義していることためです。

上記のコードでは、コントローラーで定義した@table_customers,@tablesの値をモデルに渡し、table_customers,tablesとして再定義していますが、モデルメソッド内で@table_customers,@tablesが使われており、そのような変数は定義されていないと言うことでエラーが発生します。

コントローラーで定義した変数は、モデルの処理では使えないと言う感じで覚えれば問題ないかと思います。

そのため、エラーを解消するには、、、

shop.rb
def self.count_members(table_customers,tables) 

  all_customers =  table_customers * tables #2つの変数の@を削除
  Shop.update(members:all_customers)

end

モデル内の@table_customers,@talesの@を削除することで正常に動作します。
(all_customersはメソッド内で新たに定義される変数なので@を削除しなくても動作自体は問題ありませんが、消去した方がベターです)

注意ポイント②

また、モデル内で定義した変数をメソッド外の処理や、view内で使用したい場合は注意が必要です。
コントローラーで定義した変数がモデル内で使用できないのと同様に、モデル内で定義した変数もモデル外では使用することができません。

クラスメソッド内で定義した変数をview等で使用したい場合は戻り値としてコントローラーに返す必要があります

仮にall_customersを戻り値として返したい場合

shop.rb
def self.count_members(table_customers,tables) 

  all_customers =  table_customers * tables #2つの変数の@を削除
  Shop.update(members:all_customers)

  return all_customers #returnメソッドで戻り値を指定

end
shops_controller.rb
def members

 @table_customers = 2
 @tables = 3

 @all_customers = Shop.count_members(@table_customers,@tables)
 #戻り値を格納するための変数を定義

end

モデル内で戻り値の指定、コントローラー内で変数の定義を行うことで、モデル内で扱った値をモデル外でも使用できます。また、カンマで区切ることで、複数の戻り値を取得することも可能です。

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

【Rails】Mysql2::Error::ConnectionErrorの解消

Railsアプリにバリデーションを定義した後の、テストデータ登録の成功が目的の備忘録です。その為、バリデーションの定義、記述は終わっている状態です。

データ保存したいのに、Mysql2に接続できない

Docker環境下のRailsアプリで、app/modelsにバリデーションを定義し、動作チェックの為にMySQLにデータ登録を試みました。そうしたら.....

Mysql2::Error::ConnectionError at /Unknown MySQL server host 'db' (0)

MySQLに繋がってないよと怒られました。

全く同じエラーにハマっている方も居ましたが、解決策は載っていません。
記事に載っていたdocker-compose upを実践するも、dockerは正常に起動していました。

いずれにせよ、データが登録できていないので、次はデータベースのチェックを行いました。

①rails db:create
②rails db:migrate

しかし、ダメでした。
下記はrails db:create実行後にターミナルに出力されたエラー

Unknown MySQL server host 'db' (0)
Couldn't create 'myapp_development' database. Please check your configuration.
rails aborted!
Mysql2::Error::ConnectionError: Unknown MySQL server host 'db' (0)
/Users/athlaliel/test/qa-coin/bin/rails:9:in `<top (required)>'
/Users/athlaliel/test/qa-coin/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:create
(See full trace by running task with --trace)

この後も、色々試した結果、原因が分かりました。

Docker環境でアプリを作成しているのに、実際はローカル環境でひたすらコマンドを叩いていました。Docker上のコンテナでMySQLも管理されてる以上、見つかるわけがなかったです。

下記のコマンドを順に実行する事で無事にデータベースは作成され、更新も可能となりました。

①docker-compose exec web rails db:create 

②docker-compose exec web rails db:migrate

バリデーションのデータ登録

データベースがDocker上に出来たので、テストデータを登録しバリデーションが機能しているか試します。rails consoleを起動し、テストデータを登録していきます。

バリデーションに記述する文言はここでは書きませんが、変数を定義して<モデル名.create>と入力します。
このコマンドでデータ登録が成功すれば、SQLと同じ文言が表示され、データの保存まで行います。その為、別途<モデル名>.saveコマンドを入力する必要はありません。
validate.png

validate2.png
もしエラーが起きたら、下記コマンドでチェックして原因の追求を行います。

pry(main)> <モデル名>.errors

※user.rbというmodelsファイルがあれば、user.errorsとターミナルに入力します。

validate3.png

Mysql2::Error::ConnectionErrorのようなエラーに遭遇したら、自分が今どの環境で作業しているかのチェックを行ってみると良いでしょう。

参考記事
Unknown MySQL server host 'db' (0)エラーが消えない
Active Record バリデーション

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