SQLについてのメモ

今日はSQLの勉強を行い、忘れないための自分用のメモを書きます。

AS

これはRails を使用していてたまに使うものではあります。

SELECT game.name AS 'ゲーム名', grade.name AS '階級名'

これを行うことでカラム名をシングルクォーテーションの中の文字に変換する事ができます。

LEFT JOIN

これは左に合わせた外部結合を行う方法です。

SELECT *
FROM players
left join games
on players.game_id = games.id;

これを行う事で、プレーヤーに対して、ゲーム名のカラムを結合する事ができます。

今後また学んだら随時記録します。

APIキーの暗号化

今回はAPIキーを暗号化してAPIを保存する方法を記録したいと思います。

バージョン

実装内容

  • maps/index.html.erb
<script src="https://maps.googleapis.com/maps/api/js?key=(個人のAPIキー)&callback=initMap" async defer></script>

今回はgoogle mapのAPIキーを暗号化する方法です。 Basic認証と同じ方法を用いります。

まずは暗号化のためのターミナル操作です。

% vim ~/.zshrc
  • このコマンドで環境変数の設定します。
  • そうすると環境変数の画面が出てくるので、キーボードのiをクリックして、

    この場合は

 export GOOGLE_API='APIキー'

を入力します。 その後にescキーを押してキーボードの :wqをクリックして、

% source ~/.zshrc

を入力します。

次に実装するための動作です。

  • application_controller.rb
class ApplicationController < ActionController::Base
  private
  def url
    @url = ENV["GOOGLE_API"]
  end
end

@urlを定義することで、APIキーをインスタンス変数で扱える様になるます。

次にマップのurlを使用するため、 before_actionを定義します。

class MapsController < ApplicationController
  before_action :url, only: :index

  def index
  end

end

これでindex.html.erbでも@urlが変数として使える様になっています。 最後に

  • maps/index.html.erb
<script src="https://maps.googleapis.com/maps/api/js?key=<%= @url %>&callback=initMap" async defer></script>

で完成です。

コメント機能の実装

内容

  • コメント機能の中身
  • コメント機能の単体テストの内容

バージョン

今回はコメント機能を非同期で実装する流れについて書いていきます。

まずは、ルーティングです。 routes.rb

 resources :posts do
    resources :comments, only: [:create, :destroy]
end

基本的には動画機能に付属させる形で表示しているので、 posts(動画)にネストさせておきます。

次にコントローラーです。

class CommentsController < ApplicationController
  
  def create
    @post = Post.find(params[:post_id])
    @comment = Comment.new(comment_params)
    if @comment.save
      @comments = Comment.where(post_id: @post.id)
      flash.now[:notice] = 'コメントを投稿しました'
    else
      flash.now[:alret] = 'コメントの投稿失敗しました'
    end
  end

  def destroy
    if @comment.destroy
      @comments = Comment.where(post_id: @post)
      flash.now[:notice] = 'コメントを削除しました'
    else
      flash.now[:alret] = 'コメントの削除に失敗しました'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
  end
end

今回の場合だとコメント内容(text)とユーザーid, 動画のidが必要になるので、private以下のcomment_paramsに必要なカラムを記入しておきます。

そして今回は非同期の実装を目標にしているため、redirect_toは記述しません。 次にビューです。 今回は動画のページに貼る様にしています。

# 中略
<div class="comments-area">
      <%= render 'comments/comments', commentss: @comments,  remote: true %>
    </div>
    <div class="comment-forms">
      <% if user_signed_in? %>
        <%= form_with model: [@post, @comment], remote: true, method: :post do |f| %>
          <p class="comment-form">コメント投稿画面</p>
          <%= f.text_field :text, class: "comment-form" %>
          <%= f.submit "投稿する", class: "comment-form", id: "comment-botton" %>
        <% end %>
      <% end %>
    </div>
# 中略

今回は動画のページに付属させる様にしています。 次にコメントの中身です。 _comments.html.erb

<% @comments.each  do |comment| %>
  <div class="comments">
    <div class="comment">
        <p class="comment-text"><%= comment.text %></p>
        <p class="comment-name"><%= comment.user.nickname unless comment.user.blank? %></p>
        <% if user_signed_in? && current_user.id == comment.user_id %>
          <%= link_to "削除", post_comment_path(@post.id, comment.id), method: :delete, remote: true, id: "comment-delete" %>
        <% end %>
    </div>
  </div>
<% end %>

本人のみコメントを削除する機能を導入しています。

そして非同期機能を実装するための処理になります。 create.js.erb

$("#comment_text").val("");
$(".flash").html("<%= j(render 'shared/flash') %>");
$(".comments-area").html("<%= j(render 'comments/comments', comments: @comments) %>");
setTimeout("$('.alert').fadeOut('slow')", 2000);

まずは作成の非同期です。 機能の内容としては、送信ボタンが押されたらコメントフォームが空になる様にします。 次にflashメッセージで投稿完了を知らせ、一定時間で消滅します。 そして、コメントが追加されます。

次に削除する場合です。

destroy.js.erb

$(".flash").html("<%= j(render 'shared/flash') %>");
$(".comments-area").html("<%= j(render 'comments/comments', comments: @comments) %>");
setTimeout("$('.alert').fadeOut('slow')", 2000);

コメント下の削除ボタンが押された際に削除を完了を知らせるflashを発生させ、 動画のデータが更新されて、消去されたことがページに反映されます。

追伸

初めて行う機能ではありましたが、いくつか記事を参考にさせていただくことでなんとか終わらせることができました。 今後は非同期で実装する工程が増えてくるかもしれないので、javascript関連の勉強がしなければいけないと感じましたが、必要でなければ、自分からはまだやらなくてもいいという感覚もありました。

検索機能の実装

今回は検索機能についての紹介します。 今回のコードはこちらです。 post.controller.rb

 def search
    if params[:keyword].present?
      @posts = Post.search(params[:keyword]).page(params[:page]).per(6)
      if @posts.length == 0
        flash.now[:alert] = '検索した内容は見つかりませんでした'
        @posts = Post.includes(:game, :grade, :user).page(params[:page]).per(6)
      else
        flash.now[:notice] = '検索した内容は見つかりました'
      end
    else
      flash[:alert] = '検索した内容が空白です'
      redirect_to root_path
    end
  end

post.rb(モデル)

def self.search(search)
    if search != ""
      Post.where('title LIKE(?)', "%#{search}%")
    else
      Post.includes(:game, :grade, :user)
    end
  end

まず検索の流れについてです。

  if params[:keyword].present?
      @posts = Post.search(params[:keyword]).page(params[:page]).per(6)
#中略
  else
      flash[:alert] = '検索した内容が空白です'
      redirect_to root_path
  end

この部分で空欄になるかどうかを判定します。 これをすることで、空欄場合は一覧機能(root_path)に遷移して、flashで空白であることを通知して、検索機能を機能させない様にすることができます。一応万が一この機能を突破された場合は、一覧を表示するようにモデルに記述しています。 次に検索ワードが入っている場合です。

      if @posts.length == 0
        flash.now[:alert] = '検索した内容は見つかりませんでした'
        @posts = Post.includes(:game, :grade, :user).page(params[:page]).per(6)
      else
        flash.now[:notice] = '検索した内容は見つかりました'
      end

@postsにデータが入っているかどうかを識別し、データベースからキーワードにあった情報を抜き取れなかった場合には、@postsには何もない状態となってしまうため、 @postsに一覧を再代入して、何かしらの動画を表示する様にしています。

追伸

ちなみに.page(params[:page]).per(6)はkaminariというページネーション機能を用いるためのgemになります。 個人的には@postsに当たる部分をhtml.erbで@postのように違う変数を作るべきかどうか非常に迷いましたが、再代入すれば問題なく再利用できるということに気づき、新しい発見をすることができました。 通常の場合何かしら検索にヒットする可能性が高いため、見つけた場合を上に持ってくるべきだと考える人もいるかもしれませんが、現状自分しか投稿予定がないため、ヒットしない方を前提に見つからないを上に持ってきています。

フォロー機能のテスト

今回は動画をフォローする機能を用いたものを結合テストする場合をの記述を記録します。

完成形

require 'rails_helper'

RSpec.describe "Relationships", type: :system do
  before do
    @user1 = FactoryBot.create(:user)
    @user = FactoryBot.create(:user)
    @game = Game.create(:game_title => 'マリオカート')
    @grade = Grade.create(:grades => '初級')
    @post = Post.create(:title => 'test', :youtube_url => 'IsXVebXtzwY', :content => 'test', :game_id => @game.id, :grade_id => @grade.id, :user_id => @user.id)
  end

   it 'フォローを実行解除することができる' do
    basic_pass root_path
    sign_in(@user1)
    visit post_path(@post.id)
    expect(page).to have_text('フォロー')
    click_on('follow-on')
    expect(page).to have_text('フォロー解除')
    click_on('follow-off')
    expect(page).to have_text('フォロー')
  end

  it '開いているユーザーが動画投稿者と同一であるとフォロー機能が見えずフォローを実行解除できない' do
    basic_pass root_path
    sign_in(@user)
    visit post_path(@post.id)
    expect(page).to_not have_text('フォロー')
  end
end

*こちらはモデル単体テストが成功している体で作成しており、未経験でもあるため、それを考慮して読んでいってください。

before do
    @user1 = FactoryBot.create(:user)
    @user = FactoryBot.create(:user)
    @game = Game.create(:game_title => 'マリオカート')
    @grade = Grade.create(:grades => '初級')
    @post = Post.create(:title => 'test', :youtube_url => 'youtubeのアカウント11桁', :content => 'test', :game_id => @game.id, :grade_id => @grade.id, :user_id => @user.id)
  end

まずはこの部分です。 今回はフォローをしていくためには、2つ以上のアカウントが必要があるため、@userと@user1を記述しています。 フォロー機能は動画を見る機能ごとにつけているため、動画を先に作成しておく必要があります。

it 'フォローを実行解除することができる' do
    basic_pass root_path
    sign_in(@user1)
    visit post_path(@post.id)
    expect(page).to have_text('フォロー')
    click_on('follow-on')
    expect(page).to have_text('フォロー解除')
    click_on('follow-off')
    expect(page).to have_text('フォロー')
  end

次にこの部分です。今回のテストに関してはフォローを実行したか判断するためにフォローに成功した時にフォロー解除に変化する使用であるため、フォロー解除が表示されているか確認するコード(expect(page).to have_text('フォロー解除'))を記述しています。 click_onに使用している部分に関しては()の中にhtml.erb記述しているidを書いています。

it '開いているユーザーが動画投稿者と同一であるとフォロー機能が見えずフォローを実行解除できない' do
    basic_pass root_path
    sign_in(@user)
    visit post_path(@post.id)
    expect(page).to_not have_text('フォロー')
  end

次に動画投稿者と開いているユーザーが同じ場合はフォローボタンがなくフォローできないということを確認するテストです。 expect(page).to_not を使用し、これがあると以降の文字列が存在しない場合はtrueを返す機能を果たし、今回の場合はフォローボタンが存在しない場合がテストが成功する仕様になっています。 今回は以上です。

基本的にテストで何を書けばどのようにテストしてくれるかを確認していくためには、公式ドキュメントを確認して、色々試してみることをお勧めします。ドキュメントは書いてあることが膨大で英語で探すのに苦労しますがもし何かをクリックするための機能を調べたいと思ったときにはcommand + F を使って、clickなどと入れてみるとclick関係の構文が出てくるので、それを試して行けばきっと答えに辿り行けると思います。

Rspecでrails db:seedで使う予定のデータを直書きする

今日は結合テストを用いて、動画投稿機能について書いたので、記録していきます。

require 'rails_helper'

RSpec.describe "Posts", type: :system do
  it '動画を投稿することができる' do
    @user = FactoryBot.create(:user)
    @game = Game.create(:game_title => 'マリオカート')
    @grade = Grade.create(:grades => '初級')
    # 投稿ページへ移動する
    basic_pass root_path
    sign_in(@user)
    visit new_post_path

    # 投稿をする
    fill_in 'title', with: 'test'
    fill_in 'youtube-url', with: 'youtubeのURL'
    fill_in 'content', with: 'kaisetu'
    select @game.game_title, from: 'post[game]'
    select @grade.grades, from: 'post[grade]'
    expect{
      find('input[name="commit"]').click
    }.to change { Post.count }.by(1)
  end

  it '動画を削除することができる' do
    @user = FactoryBot.create(:user)
    @game = Game.create(:game_title => 'マリオカート')
    @grade = Grade.create(:grades => '初級')
    @post = Post.create(:title => 'test', :youtube_url => 'youtubeのURL', :content => 'test', :game_id => @game.id, :grade_id => @grade.id, :user_id => @user.id)

    basic_pass root_path
    sign_in(@user)
    visit post_path(@post.id)
    expect{
      click_link "削除"
    }.to change { Post.all.count }.by(-1)
  end

end

動画を投稿に必要な情報は、タイトル、youtubeのURL、解説文、ゲーム名、自己判断で決めていただいたゲームの実力(以降は階級)です。 ゲーム名、階級はあらかじめこちらで登録して扱うことにしています。

    @user = FactoryBot.create(:user)
    @game = Game.create(:game_title => 'マリオカート')
    @grade = Grade.create(:grades => '初級')

そのため、Rspecで書いていくと初めらデータがない状態になってるので、初めに作成する必要性があります。 今回は何個も登録する必要がないのでこのデータのみをあらかじめ作成します。

やっていて非常に難しかった部分はセレクトボックス部分です。 そもそも知識がなかったということもありますが、 何よりも参考にするべきQiitaの記事で個人的に理解できず、苦労しました。 結局のところ、Rspec公式ドキュメントによると、

select('Option', from: 'Select Box')

出典:Rspec公式ドキュメント https://github.com/teamcapybara/capybara#the-dsl

という様な形に書いていくそうです。

これだけだとかなりわかりずらいと感じるので、実際にやっているものがこちらです。

    select @game.game_title, from: 'post[game]'
    select @grade.grades, from: 'post[grade]'

こちらが今回使用したコードになりますが、

できる限りわかりやすく伝えようと試みると

    select 'マリオカート', from: 'post[game]' #ゲーム名のセレクトボックスのname属性
    select '初級', from: 'post[grade]'#階級のセレクトボックスのname属性

このようにすると少し伝わりやすくなったでしょうか? 実際の挙動だとマリオカートを初心者の方が投稿したということになります。 'post[game]'というのは、検証ツールを使用することで見ることができます。 あまり詳しくは書きませんがname=に続くのが今回だと'post[game]'です。 この記事で理解してほしいのは、seedで記入している情報はテストで反映されていないということ、 セレクトボックスには、selectを用いることです。 本当はRspecのテストにseedを反映させる手段があるかもしれませんが、その場合は検索で見つけてください。 今回は以上になります。