大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【バリデーション】numericality

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

本記事ではnumericalityについて説明します。

これは数値に対して使われるバリデーションです。例えば、整数しか入力できないとか、〇〇以上〇〇未満しか入力できない、といった制約をつけることができます。

特に難しいということはないので、早速やってみましょう。

また、今回は数値型のバリデーションがしっかりと反映されているかを確認するために、RSpecを使用しモデルスペックを作成します。

テキトーにカラムを追加

アプリケーション自体今回は関係ないのでテキトーなモデルに対し、数値型カラムを追加してくださいww

カラムの追加の方法は下記のとおりです。

 $ rails g migration Addカラム名Toテーブル名 カラム名:型

私は既存のUserモデルに対して、test_numericalityカラムをinteger型で追加します。

$ rails g migration AddTestNumericalityToUsers test_numericality:integer
      invoke  active_record
      create    db/migrate/20210217081751_add_test_numericality_to_users.rb

マイグレーションファイルを念のため確認。

class AddTestNumericalityToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :test_numericality, :integer
  end
end

問題なさそうですね。DBレベルに反映しましょう。

$ rails db:migrate
== 20210217081751 AddTestNumericalityToUsers: migrating =======================
-- add_column(:users, :test_numericality, :integer)
   -> 0.0024s
== 20210217081751 AddTestNumericalityToUsers: migrated (0.0026s) ==============

モデルスペックの作成

モデルスペックを作成します。Rspecはインストール済みであるという認識で進めていきます。

モデルスペックファイルの作成

$ rails generate rspec:model user
      create  spec/models/user_spec.rb
      invoke  factory_bot
      create    spec/factories/users.rb

FactoryBotにはtest_numericalityにとりあえず10を入れておきましょう。なお、nameカラムに関しては自分のUserモデルにあるカラムなので注意してくださいね!

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'test_name' }
    test_numericality { 10 }
  end
end

あと、FactoryBot作成時に省略記法を使用したいので、rails_helper.rbに下記内容を記載します。

# spec/rails_helper.rb
config.include FactoryBot::Syntax::Methods

モデルスペックファイルに下記内容を記載

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe "バリデーションのテスト" do
    it '全てのカラムが正常値の時、Userモデルはvalidである' do
      user = build(:user)
      expect(user).to be_valid
      expect(user.errors).to be_empty
    end
  end
end

これでテストを実行すると当然通ります。バリデーションがないためです。

$ bundle exec rspec spec/models/user_spec.rb
.

Finished in 0.02555 seconds (files took 1.75 seconds to load)
1 example, 0 failures

numericalityを試す

大変お待たせいたしました笑。

指定のモデルに対してnumericalityを使用してバリデーションをかけてみましょう。

numericalityは基本的に以下のように記述します。

validates :カラム名, numericality: { オプション: オプションに対応した値 }

では最初に簡単なものから紹介します。

only_integer

これは値を整数のみ入力できるようにするバリデーションです。

# app/models/user.rb
validates :test_numericality, numericality: { only_integer: true }

現状はFactoryBotにデフォルトで10が格納されているためテストは通るはずです。この値を小数点ありの10.5にしてみましょう。

RSpec.describe User, type: :model do
  describe "バリデーションのテスト" do
    it '全てのカラムが正常値の時、Userモデルはvalidである' do
      user = build(:user, test_numericality: 10.5)
      expect(user).to be_valid
      expect(user.errors).to be_empty
    end
  end
end

これでテストを実行すると、

$ bundle exec rspec spec/models/user_spec.rb
F

Failures:

  1) User バリデーションのテスト 全てのカラムが正常値の時、Userモデルはvalidである
     Failure/Error: expect(user).to be_valid
       expected #<User id: nil, name: "test_name", created_at: nil, updated_at: nil, test_numericality: 10> to be valid
             , but got errors: Test numericality must be an integer
     # ./spec/models/user_spec.rb:8:in `block (3 levels) in <top (required)>'

Finished in 0.03144 seconds (files took 1.03 seconds to load)
1 example, 1 failure

エラー内容の一部に、「Test numericality must be an integer」とあります。

user.errors[:test_numericality]
=> ["must be an integer"]

次のオプションに移る前に作成するFactoryBotを元の状態に戻しておいてください。

RSpec.describe User, type: :model do
  describe "バリデーションのテスト" do
    it '全てのカラムが正常値の時、Userモデルはvalidである' do
      user = build(:user)
      expect(user).to be_invalid
      # binding.irb
      expect(user.errors).to be_empty
    end
  end
end

greater_than、greater_than_or_equal_to

次は「〜より大きい」、「〜より以上」を表すバリデーションです。「〜より大きい」はgreater_thanです。さきほどのオプションであるonly_integerではboolean型であるtrueを入れましたが、今回は数値型を入れます。数値は10を入れてみましょう。

# app/models/user.rb
validates :test_numericality, numericality: { greater_than: 10 }

これでテストを実行すると失敗します。

$ bundle exec rspec spec/models/user_spec.rb
F

Failures:

  1) User バリデーションのテスト 全てのカラムが正常値の時、Userモデルはvalidである
     Failure/Error: expect(user).to be_valid
       expected #<User id: nil, name: "test_name", created_at: nil, updated_at: nil, test_numericality: 10> to be valid
             , but got errors: Test numericality must be greater than 10
     # ./spec/models/user_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.02056 seconds (files took 0.95972 seconds to load)
1 example, 1 failure

エラー内部には以下の文字列がありますね。

"Test numericality must be greater than 10"

「10より大きい数」と制約しているので、10は含まれないということですね。10を含ませるためには「10以上の数」と制約をかけます。greater_than_or_equal_toを使用すると「〜以上の数」という制約をかけることができます。

# app/models/user.rb
validates :test_numericality, numericality: { greater_than_or_equal_to: 10 }

これでテストが通ります。

$ bundle exec rspec spec/models/user_spec.rb
.

Finished in 0.02247 seconds (files took 1.64 seconds to load)
1 example, 0 failures

「〜より大きい」、「〜より以上」の反対として「〜より小さい」、「〜より以下」もあります。less_thanless_than_or_equal_toです。この2つについては皆さんの手で試してみてください。

equal_to, other_than

指定した値以外が格納されるとエラーが出るようにしたい場合はequal_toです。

validates :test_numericality, numericality: { equal_to: 100 }

これでは現在のテストは当然通りません。

$ bundle exec rspec spec/models/user_spec.rb
F

Failures:

  1) User バリデーションのテスト 全てのカラムが正常値の時、Userモデルはvalidである
     Failure/Error: expect(user).to be_valid
       expected #<User id: nil, name: "test_name", created_at: nil, updated_at: nil, test_numericality: 10> to be valid, 
             but got errors: Test numericality must be equal to 100
     # ./spec/models/user_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.05692 seconds (files took 1.78 seconds to load)
1 example, 1 failure

Test numericality must be equal to 100」とあるように値が必ず100でなければなりません。

反対に指定の数値だけ絶対に格納されてはいけないという制約もあります。other_thanを使います。

validates :test_numericality, numericality: { other_than: 100 }

テストが通りました。

$ bundle exec rspec spec/models/user_spec.rb
.

Finished in 0.02088 seconds (files took 1.72 seconds to load)
1 example, 0 failures

odd、even

oddは奇数、evenは偶数を制約します。only_integerと同様にboolean型で指定します。

oddtrueとしておけば、

validates :test_numericality, numericality: { odd: true }

テストは通りませんが、

$ bundle exec rspec spec/models/user_spec.rb
F

Failures:

  1) User バリデーションのテスト 全てのカラムが正常値の時、Userモデルはvalidである
     Failure/Error: expect(user).to be_valid
       expected #<User id: nil, name: "test_name", created_at: nil, updated_at: nil, test_numericality: 10> to be valid, but got errors: Test numericality must be odd
     # ./spec/models/user_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.04629 seconds (files took 1.79 seconds to load)
1 example, 1 failure

evenをtrueにすると、

validates :test_numericality, numericality: { even: true }

テストはパスしました。

$ bundle exec rspec spec/models/user_spec.rb
.

Finished in 0.02802 seconds (files took 1.8 seconds to load)
1 example, 0 failure

終わりに

本記事では雑にカラムを追加しているので具体的な用途がわからない方もいるのかもしれませんが、数値の制約をすることで、入れたい値、入れたくない値を指定できるので安心できます。

格納されるべき値を簡単に制限できるので、使うタイミングがあればすぐに導入できるので本当に便利で使い勝手が良いと思います。

以上、大ちゃんの駆け出し技術ブログでした!

N + 1問題

はじめに

Ruby on RailsなどDB(データベース)を使用するサーバーサイド言語で必ず上がる「N + 1問題」。

主に1対多のアソシエーション関係がある時に起こる問題です。 Railsでは一覧表示機能を含む1対多の関係が不可欠な機能が実装されるので、N + 1問題は理解しておくべきかと思います。

対象者

rails tutorialを学習済みの方 SQL初学者の方

使用環境

ruby 2.6.4 ・Rails 5.2.3 ・MySQL 5.4

N+1問題とは

一言で表すと、

「テーブル参照のSQLが大量に発行されてしまうこと」

実際に見ていきましょう。 今回は掲示板投稿機能を実装するためのrailsのファイルを使用します。

まず、N + 1問題を引き起こす投稿機能モデルのコントローラーとビューのファイルの中身です。

app/controllers/boards_controller.rb(コントローラー)

class BoardsController < ApplicationController

  def index
    @boards = Board.all
  end
end

app/views/boards/index.html.erb(ビュー)

  <div class="row">
    <div class="col-12">
      <div class="row">
        <% if @boards.present? %>
        <%= render @boards %>
        <% else %>
        <p><%= t('.no_board') %></p>
        <% end %>
      </div>
    </div>
  </div>

/render @boards/ これによってrailsはファイルの同じディレクトリにある_board.html.erbファイルをパーシャルとして読み込んでくれる仕様になっています。 (よしなにやってくれるrailsの特徴ですね)

同時にコントローラーで取得した@boardsを繰り返し処理で1つずつboardとしてパーシャルに渡します。 (長いですが、一言で言うと掲示板の投稿が1つずつ作成されているコードです)

app/views/boards/_board.html.erb(ビュー)

<div class="col-sm-12 col-lg-4 mb-3">
  <div id="board-id-<%= board.id %>">
    <div class="card">
      <%= image_tag 'board_placeholder.png', class: 'card-img-top', width: 300, height: 200 %>
      <div class="card-body">
        <h4 class="card-title">
          <%= link_to  board.title, "#" %>
        </h4>
        <div class='mr10 float-right'>
          <%= link_to '#', id: 'button-edit-#{board.id}' do %>
          <%= icon 'fa', 'pen' %>
          <% end %>
          <%= link_to '#', id: 'button-delete-#{board.id}', method: :delete, data: {confirm: ''} do %>
          <%= icon 'fas', 'trash' %>
          <% end %>
        </div>
        <ul class="list-inline">
          <li class="list-inline-item"><i class="far fa-user"></i>
            <%= board.user.decorate.full_name %>
          </li>
          <li class="list-inline-item"><i class="far fa-calendar"></i>
            <%= l board.created_at, format: :short %>
          </li>
        </ul>
        <p class="card-text">
          <%= board.body %>
        </p>
      </div>
    </div>
  </div>
</div>

この掲示板一覧を表示するタイミングで発行されるSQL文が以下になります。

 Rendering boards/index.html.erb within layouts/application
  Board Load (1.6ms)  SELECT "boards".* FROM "boards"
  ↳ app/views/boards/index.html.erb:16
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/boards/_board.html.erb:19
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]

めちゃくちゃ長い、、、 これrails tutorialではマイクロポストで投稿一覧を表示しますが、同じように長いSQL文が発行されます。

なぜこのようなことが起きるのか? 諸悪の根源はたった一文です

@boards = Board.all

ん?この部分はrails tutorialで習った通りじゃないですか?

何が問題かというと、@boardsに格納されている投稿(board)が呼び出される度に、そのboardがどのuserのものであるかを検索してい状態です。

つまり、投稿画面に遷移する時に、掲示板の全ての投稿を取得するために

Boardテーブル全体を参照するSQL文が1回発行される

Board Load (1.6ms)  SELECT "boards".* FROM "boards"

そして、のUsersテーブルを参照するSQLの文章が

掲示板の投稿の数(N回)だけ発行されている状態

  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]

この投稿数の数(N) + 最初の全体参照(1)が"N + 1問題"です。

解決方法

しかし、このN + 1問題は問題の文を以下のように書き換えるだけで簡単に解決できてしまいます。

@boards = Board.all.includes(:user)

includesメソッドによりBoardテーブル参照時に、(boardの外部参照のためのuser_idカラムを元に)Userテーブルも同時に参照するようにしています。 よってテーブル参照回数も2回となります!! (たとえ掲示板の投稿が増えたとしても)

  Board Load (3.0ms)  SELECT "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC
  ↳ app/views/boards/index.html.erb:16
  User Load (0.9ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [["id", 2], ["id", 1], ["id", 3]]
  ↳ app/views/boards/index.html.erb:16

SQL文の発行も2回で済みます。 1対多のアソシエーション関係を持つモデルを扱う場合は是非覚えておきましょう!

Swiperをカスタマイズ

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

昨日出した記事ではSwiperの公式ドキュメントに合わせて、既存のscaffloldの雛形アプリケーションに追加して以下のようなスライダー画面を実装しました。

sakitadaiki.hatenablog.com

本日はSwiperの仕組みと、Swiperを使用して様々なスライダー画面を実装していきたいと思います!

Swiperの仕組み

昨日、公式のドキュメントをほぼ思考停止で実装していきました。しかし、完成した画面を見ると今まで再現したことのないスライダー画面をしっかりと作ることができましたね。この画面はどうやって作ることができたかわかりましたか?

ポイントはスライダーに与えたクラスとapplication.scss、application.jsの関係にあります。まず、現在スライダーを表示している画面は以下のファイルです。

 <!-- app/views/users/show.html.erb -->
<strong>Portrait:</strong>
<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>
  <!-- ページネーション -->
  <div class="swiper-pagination"></div>

  <!-- ナヴィゲーションボタン -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバー -->
  <div class="swiper-scrollbar"></div>
</div>

ここでスライダー画面を実際に表示しているHTMLは下記部分です。

  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>

そして、スライダーにページネーションだったり、スクロールバーだったりを実装しているHTMLが下記部分です。

<!-- ページネーション -->
<div class="swiper-pagination"></div>

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

つまり何が言いたいのかというと、「スライダー画面を実装しているHTML部分とその下のスライダーの機能を実装しているHTML部分は明確に役割が分かれている」、ということとです。

昨日説明しませんでしが、スライダー機能を実装しているHTMLのクラスにのみにapplication.jsで機能を与えています。paginationnavigationscrollbarの部分です。Optional parametersはスライダー全体の設定です。direction: 'vertical'は垂直方向に画像をスライドさせる設定をしています。loop: trueは1番最後に表示している画像から次の画像に移ると最初の画像に戻る設定をしています。

// app/assets/javascripts/application.js
const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

よって、スライダー画面で与えられているクラスであるswiper-wrapperswiper-slideにはこの遂ライダーの機能には一切関与していません。言い換えれば、スライダー画面で与えられているこれらのクラスは、現状ただクラスが与えられているだけで何の役割も果たしていないということです。

これらのクラスがなぜあるのかというとただレイアウトを整えることを想定しています。つまり、scssにそれらのクラスを用いてレイアウトを整えるということですね!

例えば、scssに適当にクラスにレイアウトを定義します。

// app/assets/stylesheets/application.scss
.swiper-container {
  .swiper-wrapper {
    width: 100%;
    height: 600px;
    .swiper-slide {
      object-fit: cover;
    }
  }
}

object-fit: cover;をつけることで現在表示している画像の縦横の比率を整えてくれます。width: 100%;とすることで画面いっぱいに画像を表示します。実際に見てみましょう。

https://i.gyazo.com/aca61c09f425a4a62501f6fa0bcca155.gif

当然ですが、scssが反映されました。こんな感じでswiper-wrapperswiper-slideは画像の表示幅だったりのレイアウトを整えるために使用されることが想定されています。

スライダーのカスタマイズ

ではpaginationnavigationscrollbarの部分を設定しているjsファイルを変更してみましょう。今回はAmazon Prime videoのような画面になるように編集していきます。

https://i.gyazo.com/0b05b7cec8266df792398cb13a0bb922.gif

まず画像がPrime Videoの方だと水平方向に流れているのがわかります。先ほどOptional parametersdirection: 'vertical'の箇所でスライダーが垂直方向に流れるように設定していました。これをverticalからhorizontalに変更しましょう。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'horizontal',
  loop: true,

画面を読み込み直すと表示が変わり、水平方向に画面が動くようになりました!

https://i.gyazo.com/8693f7a25926f99da10c284a5a10ee98.gif

あとPrime Videoと異なる部分として、数秒ごとに自動でスライドが切り替わるように設定されています。この自動で切り替わるようにする方法も実は簡単に導入することができます。autoplayという設定を加え、ms単位で切り替わる間隔を指定するだけです。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'horizontal',
  loop: true,
  autoplay: {
    delay: 2500,
  },

画面を再度読み込み直すと、

https://i.gyazo.com/2a15f45e08d583bb07ddafe0ab38bbc1.gif

切り替えボタンを押さずとも勝手に切り替わるようになりました。もっと早く自動でスライドさせたいなら、2500の秒数をより短い間隔(1000(1秒))などに設定するだけです。

Optional parametersの設定だけでもかなり自由にスライドの設定を変えることができたと思います。実際、他のスライド方法を調べてみるとそのほとんどがOptional parametersの設定を変えることで再現できています。例えば、下のボックスが回転するようなお洒落なスライダーもOptional parametersの設定を変更することで実装できます。

https://i.gyazo.com/a0d2c28a16e7bcef4ebeea39d900222d.gif

JSファイルを下記のように変更します。navigationscrollbarの部分は削除しましょう。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  effect: 'cube',
  grabCursor: true,
  cubeEffect: {
    shadow: true,
    slideShadows: true,
    shadowOffset: 20,
    shadowScale: 0.94,
  },

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },
});

app/views/users/show.html.erbでも同様に下記部分を削除しましょう。

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

そしてscssで画像が正方形に表示されるように設定します。

.swiper-container {
  width: 300px;
  height: 300px;
  .swiper-wrapper {
    .swiper-slide {
      object-fit: cover;
    }
  }
}

これで画面を読み込み直すと、

https://i.gyazo.com/a504b7930d915b46cfd2458f49e553a3.gif

このように簡単に再現できてしまいました。RUNTEQでは9割りの学習がバックエンドでRailsしかほぼ学びません。しかし、ほんの少しのフロントエンドの知識があるだけでこんなふうにスライダーが実装できてしまいます!本当に便利ですよね。

加えていうと、paginationnavigationscrollbarの設定はもはやオプションと思ってください。そこにページネーションを付けるか、ボタンを付けるか、スクロールバーを付けるかの違いを作る、scssとは別で付ける装飾のようなものです。jsファイルにある設定でもほとんどいじることはありません。スクロールバーのデザインを変えるなどのオプションがあるだけです。

終わりに

3つの記事を通してswiperについて紹介しましたが、細かい仕組みの説明はしませんでしたが、正直仕組みを理解する必要はさほどない気がしています。結局調べてコピペするという作業で大体実装することができました。このほかにもおしゃんなスライダー画面を実装する方法はいくつもあるので皆さんも試してください!

以上、大ちゃんの駆け出し技術ブログでした!

Swiper

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

昨日の記事で複数の画像をアップロードできるように実装しました!

sakitadaiki.hatenablog.com

こちらは「パーフェクト Ruby on Rails」という本でActive Storageを実装した状態から改修するようにして実装しました。今回も現状の複数画像をアップロードできるようになった状態から改修するようにして実装を行いたいと思います!

今回は、複数アップロードした画像をスライダーで表示されるように実装します。スライダー表示とはなんぞやと思われるかもしれませんが、Amazon Prime Video の表示でおすすめの作品が順番にスライダーで表示されているかと思います。

https://i.gyazo.com/0b05b7cec8266df792398cb13a0bb922.gif

Swiper

この画像をスライド表示させるための方法として、RUNTEQではSwiperというjQueryプラグインを学習しました。

swiperjs.com

こちらの公式サイトでは以下のように説明されています。

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior. It is intended to be used in mobile websites, mobile web apps, and mobile native/hybrid apps. Swiper is not compatible with all platforms, it is a modern touch slider which is focused only on modern apps/platforms to bring the best experience and simplicity.

[日本語訳] Swiperは、ハードウェアで加速されたトランジションと驚くべきネイティブ動作を持つ、最もモダンな無料のモバイルタッチスライダーです。モバイルウェブサイト、モバイルウェブアプリ、モバイルネイティブ/ハイブリッドアプリでの使用を想定しています。 Swiperはすべてのプラットフォームと互換性があるわけではなく、最高のUXとシンプルさを提供するために、モダンなアプリ/プラットフォームのみに焦点を当てたモダンなタッチスライダーです。

「最もモダンな無料のモバイルタッチスライダー」、「最高のUXとシンプルさを提供する」とあるように、インストール方法も簡単であり、かなりシンプルにスライダー機能の実装を行うことができました。

今回はバッグエンドではなくフロントエンドの内容となるため、フロントエンドに慣れていない方は難しいと思われるかもしれません。しかし、公式にインストール方法、実装方法が詳しく載っていたので心配しなくても大丈夫です!

インストール手順

今回は公式のインストール方法に従い実装をしていきたいと思います。

There are few options on how to include/import Swiper into your project

[日本語訳] スワイパーをプロジェクトにインクルード/インポートする方法には、いくつかのオプションがあります。

インストール方法には方法がいくつかあるようですね。今回はCDNからSwiperをインストールする方法を採用します。下記参考サイトではCDNを以下のように説明しています。(今回引用多くてごめんなさい、、、)

CDNとは「Content Delivery Network(コンテンツデリバリーネットワーク)」の略で、ウェブコンテンツを効率的かつスピーディーに配信できるように工夫されたネットワークのことです。

https://www.kagoya.jp/howto/network/cdn/

なるほど。プログインを含んだウェブコンテンツを素早く導入できるようにしたシステムのようですね。より詳しく知りたい方は参考記事を熟読してみてください!

CDNからSwiperを使用する

If you don't want to include Swiper files in your project, you may use it from CDN. The following files are available

[日本語訳] プロジェクトにSwiperファイルを入れたくない場合は、CDNから使用することができます。以下のファイルが利用可能です。

<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.css" />
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" />
<script src="https://unpkg.com/swiper/swiper-bundle.js"></script>
<script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script>

上記のファイルをapp/views/layouts/application.html.erbに記述すればダウンロードできそうですね!しかし、erbファイルですので、erbの記法に則って記述しましょう!(試していないのですが、上記のHTMLのままでも大丈夫なはずです)

<%= stylesheet_link_tag 'https://unpkg.com/swiper/swiper-bundle.css' %>
<%= stylesheet_link_tag 'https://unpkg.com/swiper/swiper-bundle.min.css' %>

<%= javascript_include_tag 'https://unpkg.com/swiper/swiper-bundle.js' %>
<%= javascript_include_tag 'https://unpkg.com/swiper/swiper-bundle.min.js' %>

スワイパーのHTMLレイアウトを追加

Now, we need to add basic Swiper layout to our app

[日本語訳] ここで、アプリに基本的なSwiperのレイアウトを追加する必要があります。

<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <div class="swiper-slide">Slide 1</div>
    <div class="swiper-slide">Slide 2</div>
    <div class="swiper-slide">Slide 3</div>
    ...
  </div>
  <!-- If we need pagination -->
  <div class="swiper-pagination"></div>

  <!-- If we need navigation buttons -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- If we need scrollbar -->
  <div class="swiper-scrollbar"></div>
</div>

こちらもHTMLを例にしています。画像がどこで表示するかというと<!-- Slides -->の部分ですね。実際の画像は指定していませんが、Slide 1、Slide 2、Slide 3というように、複数のスライドがここに表示されていることを想定しているかと思います。

また、スライド以降のHTMLはスライダーのオプションのようですね。

<!-- ページネーション -->
<div class="swiper-pagination"></div>

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

それでは実際にファイルに反映していきましょう。画像を表示しているユーザー詳細ページのビューファイルを編集します。現在のファイルは以下のとおりです。

# app/views/users/show.html.erb
<strong>Portrait:</strong>
<% @user.portraits.each do |portrait| %>
  <%= image_tag portrait.variant(resize_to_limit: [100, 100]) %>
<% end %>

これを公式の指示通りに実装すると以下のようになります。

<strong>Portrait:</strong>
<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>
  <!-- ページネーション -->
  <div class="swiper-pagination"></div>

  <!-- ナヴィゲーションボタン -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバー -->
  <div class="swiper-scrollbar"></div>
</div>

だいぶややこしいファイルになりましたが、公式のコメント部分をそのまま反映しています。画像がアップロードされていたら、画像の枚数だけimage_tagを使ってimgを生成します。逆にアップロードされている画像がなければ、一枚の画像を表示するようにしておきます。ですので、app/assets/images/配下に好きな画像を1枚格納しておいてください!

<% if @user.portraits.present? %>
  <% @user.portraits.each do |portrait| %>
    <%= image_tag url_for(portrait), class: 'swiper-slide' %>
  <% end %>
<% else %>
    <%= image_tag '/images/sample.jpg', class: 'swiper-slide' %>
<% end %>

Swiper CSS スタイル/サイズ

In addition to Swiper's CSS styles, we may need to add some custom styles to set Swiper size:

[日本語訳] SwiperのCSSスタイルに加えて、Swiperのサイズを設定するためにいくつかのカスタムスタイルを追加する必要があるかもしれません。

.swiper-container {
  width: 600px;
  height: 300px;
}

画像を表示する部分のレイアウトを整理するためにCSSを記載します。CSSについては特に自由にかけると思うので、今回はこちらで用意したCSSを記載してください。(application.cssapplication.scssにファイル名を変更してください!)

// app/assets/stylesheets/application.scss
.swiper-container {
  width: 600px;
  height: 300px;
}

スワイパーの実装

Finally, we need to initialize Swiper in JS:

[日本語訳] 最後に、SwiperをJSで実装する必要があります。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

このjavascriptファイルはこのまま記載してしまいましょう。

// app/assets/javascripts/application.js
const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

プリコンパイルするように実装

application.scssapplication.jsを読み込む設定を追加していないので、現状のままでは両方のファイルを読み込めていません。ですので読み込めるように設定します。

  • app/views/layouts/application.html.erb
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
  • config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( application.css application.js )

現状確認

これで公式のインストール方法と簡易的な実装方法に合わせた実装は終わりです。試しに画像をアップロードしてみましょう。プライムビデオを真似てみると面白いかなと思いますので、各々好きな作品の画像を3枚ダウンロードしてみてください!自分はアニメが好きなのでアニメ3作品を用意します!笑

https://i.gyazo.com/1aa80fafa65d40d206795780aab148f5.gif

スライド機能が実際に導入されていますね!

思ったよりも簡単に実装できたのではないでしょうか!

終わりに

今回はSwiperについて紹介しましたが、インストールと簡単な実装方法のみを公式にしたがって実装しただけですね。こんな簡単な方法だけでなく、swiperにはスライド方法は細かく指定する方法があるようですね。

swiperjs.com

他のスライド指定方法も発信したいので、次回はプライムビデオのような見た目にする方法を発信したいと思います!

以上、大ちゃんの駆け出し技術ブログでした!

【Active Storage】単数画像→複数画像への移行方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

画像アップロード機能で少しだけ詰まったところがあるので、本記事ではそれを紹介します。

詰まった箇所は「Active Storageによる複数画像のアップロード方」です。画像アップロード方法として、自分は今までRUNTEQの基礎編課題でCarriWaveを学習していました。調べてみると、Active StorageはRails5.2よりgemではなく公式にファイルアップロード機能として追加されたようです。

今回使用するアプリはいわゆる「パルビ」と呼ばれる書籍のActive Stoageの項目を実装した状態から始めます!

パーフェクト Ruby on Rails 【増補改訂版】 (Perfect series)

現在のファイルの状況

アプリケーションはscaffoldを使用して作成されています。ファイルアップロードの機能がある箇所はユーザー作成ページです。

https://i.gyazo.com/8e35e80d762668e51e116d56e7454d4a.png

こちらに画像をアップしてユーザーを作成すると、ユーザー詳細ページにて画像が表示されます。

https://i.gyazo.com/3f40d2b691abdaa3b60dd08616a1d25b.png

詳しくコードを見ていきます。

まずUserモデルにはhas_one_attached :portraitとアソシエーションが指定されています。

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :portrait
end

この:portraitは実際にはUserモデルのカラムにはありません。これはActive Storageがポリモーフィックなアソシエーションを採用しているからです。ポリモーフィックなアソシエーションは説明が大変複雑なので今後別の記事で説明する予定です。現状は下記記事を自分は参考にしているので、気になる方は下記を参照してください。

Railsのポリモーフィック関連とはなんなのか - Qiita

ここでの理解は、「ユーザーとは別のモデルであるActiveStorage::Blobが:portraitを持っており、ユーザーは:portraitと関連づけられている」という理解で大丈夫かなと思います。

ユーザー作成画面の名前入力とファイルアップロード箇所のerbの現状です。

<!-- app/views/users/_form.html.erb -->
<div class="field">
  <%= form.label :name %>
  <%= form.text_field :name %>
</div>

<div class="field">
  <%= form.label :portrait %>
  <%= form.file_field :portrait %>
</div>

こちらで入力されたnameとアップロードしたportraitがパラメーターとして渡されます。user_paramsでストロングパラメーターを介して値を取得しています。ユーザー作成であるcreateアクションでそれらの値を受け取って保存できればユーザー詳細ページをレンダリングします。

# app/controllers/users_controller.rb

def create
  @user = User.new(user_params)

  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: "User was successfully created." }
      format.json { render :show, status: :created, location: @user }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @user.errors, status: :unprocessable_entity }
    end
  end
end
・
・
・
def user_params
  params.require(:user).permit(:name, :portrait)
end

ユーザー詳細ページです。image_tagで画像を表示していることがわかります。

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

<p>
  <strong>Portrait:</strong>
  <%= image_tag @user.portrait.variant(resize_to_limit: [100, 100]) %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

variant(resize_to_limit: [100, 100])のメソッドはデフォルトでgemにコメントアウトされた状態で記載されているimage-processingをインストールすることで使用できます。これにより画像のサイズを指定することができます。

# Gemfile
gem 'image_processing', '~> 1.2'

以上ががざっくりとした現状のファイルの状態です。Active Storageのファイルアップロード機能は比較的簡単に実装することができます。しかし、現在のファイル状態では画像を一つしかアップロードすることができません。そこで、現在のファイル状態を変更し、複数の画像をアップロードできるように実装したいと思います。

複数の画像をアップロードできるように変更

モデルの編集

まず、複数のファイルを格納することができるように、モデルを変更します。Active Storageではファイルをそのモデルが所有する関係性をかなり直感的に表しています。has_one_attached :portraitとあるように、Userモデルがportrait(1つの画像)をアタッチメントとして所有していることがわかります。

では、複数の画像をアップロードする場合は?

実は、has_many_attached :portraitsと変更をするだけです。

# app/models/user.rb
class User < ApplicationRecord
  # has_one_attached :portrait
  has_many_attached :portraits
end

これにより、「Userモデルがportraits(複数の画像)をアタッチメントとして所有している」ことを示しています。

アップロード画面を編集

次に、パラメーターを渡すビューを変更します。

これも直感的で、portraitをportraitsに変更します。

<div class="field">
  <%= form.label :portrait %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits %>
</div>

ただ、これだけでは複数のファイルをパラメーターとして渡すことはできるようにしたけど、複数のファイルをアップできるようにHTMLが対応されていません。対応するためにはオプションにmultiple: trueを追記します。

<div class="field">
  <!--  <%= form.label :portrait %> -->
  <%= form.label :portraits %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits, multiple: true %>
</div>

ストロングパラメーターを編集

次はストロングパラメーターです。

もうどこを変更すればいいかわかると思います。「portraitをportraitsに変更」ですね。

def user_params
    # params.require(:user).permit(:name, :portrait)
  params.require(:user).permit(:name, :portraits)
end

と私も最初は思ったのですが、実は違います!!!!

正しい実装は以下のとおりです。

def user_params
    # params.require(:user).permit(:name, :portrait)
    # params.require(:user).permit(:name, :portraits)
    params.require(:user).permit(:name, portraits: [])
end

これはアップロードされるファイルが複数であることに対応するためです。空の配列を指定しないと複数のファイルを格納する器がないということです。

詳細ページを編集

最後にユーザー詳細ページです。現状image_tagは1つしかないので複数の画像を表示できません。複数画像を表示するために、アップロードされたportraitsの数だけimage_tagが生成されるように繰り返し処理をします。

<p>
  <strong>Portrait:</strong>
  <% @user.portraits.each do |portrait| %>
    <%= image_tag portrait.variant(resize_to_limit: [100, 100]) %>
  <% end %>
</p>

これで複数の画像が表示するように実装することができました!!

https://i.gyazo.com/53ab15b583c1ac5ff7afff3ae70aac6f.gif

終わりに

単数から複数の画像をアップロードすること自体は難しくありません!単数形を複数形に変更するみたいな作業ですからね。

ただ、大きなアプリケーションともなると、様々なファイルで画像のカラムが使われていたら大変です。例えば、画像のバリデーションをするデコレイターがあったとすると、それが単数ようのファイルで定義されていた場合は、複数のファイルを持つ配列ではエラーが起きてしまいます。なのでアプリケーションを作成する際は、なるべく単数か複数かを明確な理由を持って決めておくと、後々変更するという手間を減らすことができるでしょう。

以上、大ちゃんの駆け出し技術ブログでした!

Kaminari

ページネーションとは

pagination ・・・ ページ付け、ページを示す数字 weblioより引用

ページ付けとあるように、"ページを持たせる"という認識でOKです。

例えば、多くの人が使う投稿アプリで一覧表示で全ての投稿を表示するとしましょう。 ページネーションなしだと、全ての投稿が1ページの中に表示されてしまいます。 投稿数が10個程だと問題ありませんが、複数人が使うようなアプリで全ての投稿を一度に表示すると、1ページに100, 1000と表示されてしまい、非常に見づらくなります。カオス状態です。。。

しかし、ページネーションを使えば、非常に多くの投稿があっても、1ページに表示する投稿数が決まっています。 よって、カオス状態とならず、数ページにわたって投稿を表示することができます。

kaminari

Railsのgemの1つで簡単にページネーション機能を実装することができます。 なお、今回は掲示板一覧表示機能にページネーション機能を実装します。

英語ですが、Github上で仕様を確認することができますので、こちらも参照してくださいね。 https://github.com/kaminari/kaminari

kaminari導入

インストール方法

まずがkaminariをinstallするためにGemfileに記載

gem 'kaminari'

そしてお決まりbundle install

$ bundle install

コントローラの編集

一覧表示機能なので、indexアクションを編集していきます。 まずは編集前の状態を確認します。

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc)
  end

Board掲示板モデルです。 Board.allでDBに保存されている全ての投稿を参照し取得します。 そしてincludes(:user)で、BoardモデルのユーザーID(user_id)をBoardモデル参照時に同時に参照します。 order(created_at: :desc)で作成された日時が新しい順番で投稿を取得します。

なぜそのようなことをするのかですが、詳しくは自分の別の記事で紹介しているので、よかったらそちらを見ていただけると幸いです。 N + 1問題 - Railsで解説!

上のファイルの編集後がこちら

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc).page(params[:page])
  end

page(params[:page])が出てきました。これがkaminariをインストールすることで使えるメソッドです。 実は、kaminariを導入したことで、パラメータのキーが一つ増えています。

params
=> <ActionController::Parameters {"page"=>"2",,,,

なるほど。どうやらpageキーでどのページかを判別しているようです。 このキーの増加でpage(params[:page])で何ページ目の投稿であるかを取得することが可能となっています。

ビューにpaginateを追加

ページごとの投稿は取得できるようになったので、ページを移動するための実装をしましょう。 見た目は以下のようになります。

image.png

それではビューに以下の文を追加します。

<div class='mb-3'>
  <%= paginate @boards %>
</div>

paginateによって次のページや前のページに移動することができるようになります。

おそらく見た目が画像と違うのでは? 以下のコマンドで表示が同じになると思いますので、ターミナルに打ち込んでみてください。

$ bin/rails g kaminari:views bootstrap4

これで主なページネーションの実装は終了です。 とっても簡単ですよね!

###ページに表示する数を指定

実は、各ページに表示する投稿数は今の時点では25個となっています。 25個はデフォルトで決まっています。 下記コマンドを実行してkaminariのデフォルトを設定をみることができます。

$ rails g kaminari:config

作成されたファイルを見てみると

Kaminari.configure do |config|
  # config.default_per_page = 25
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end

いろいろな設定が表示されていると思います。 今回はデフォルトのページ数を25から10に変えてみようと思います。

Kaminari.configure do |config|
  # 25から10に変更
  # config.default_per_page = 10
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end

これでページごとに表示される投稿数が10となりました。

しかし、先ほども言いましたように、こちらのファイルはkaminariのデフォルト値を設定するファイルです。 つまり、全体で共通の設定となります。 投稿一覧機能にのみページネーションするのであれば問題ありません。 しかし、投稿に対しコメント機能を実装し、そのコメントに対しページネーションをすると、そのコメントはページごとに10個のコメントを表示することになります。

これを避ける方法があります。 各モデルに対し個別でページごとの表示数を設定するのです。

もしページごとの投稿の表示数を10とする場合、直接モデルに対しその設定を加えます。

class Board < ApplicationRecord
  # specify default per_page value per each board
  paginates_per 20

paginate_perでデフォルト値を上書きすることができます。 コメント機能にページネーションを実装したい場合、コメントのモデルに対し同じようにpaginate_perを書けばいいのです。

Notionは最強!!

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今日は技術系ではなく、普段自分が愛用しているアプリ「Notion」を紹介したいと思います!

このアプリは自分がブログを書く時、プログラミング学習をする時はずっと開いています。それぐらい使える便利なアプリですので、是非プログラミング学習初学者の方々に使っていただきたいです!

今回の記事の構成は以下の3項目になります。

  • Notionとは
  • プログラミング勉強としてのNotion
  • ☆私のNotionの使い方☆

Notionとは

Notionのキャッチフレーズは以下のとおりです。

All-in-one workspace
One tool for your whole team. 
Write, plan, and get organized.

日本語に訳すと、

オールインワンのワークスペース
チーム全体のための1つのツール。
書く、計画する、整理する。

です。

つまり、チーム全体のドキュメントを1つのツールで管理するために使われるアプリであり、Googleカレンダースプレッドシートをチームで共有しているイメージで合っています。

https://i.gyazo.com/c0d358e1e84e7836ecf42c6f805da710.png

画像のように項目ごとに分別し、それをチーム全体で共有することができるので、チームで仕事をする全員がこのファイルを閲覧することができ、情報を書き込むことができます。これにより、チームのドキュメントが統一化され、そしてドキュメントの共有により共通認識が用意になります。しかも、ドキュメントの保存はファイルが編集されると自動で行ってくれるので、保存し忘れなどの心配は入りません!とても便利ですよね!

プログラミング勉強としてのNotion

「でもこれってチームのためのアプリであって個人の勉強には関係ないじゃん」

こう思った方もいるとおもいますが、このNotionはプログラマーのメモ帳として最適だと自分は思っています。理由は多々ありますが、個人的に1番使い勝手がいいと思っているNotionの機能はマークダウン記法が簡単にできることです。

マークダウン記法はQiitaブログやはてなブログでアウトプットする際によく使用する記法です。コードや大文字に装飾するために文字の前後にマーク(例: *, ```, など)を配置する方法です。例えば以下のように記載したとしましょう。

https://i.gyazo.com/c1f56e4bf73fcb71792d465ca3ccb6f0.png

それが実際のプレビューでは次のように表示されています。

https://i.gyazo.com/8fb1527f2182a0621d3c1d71c90cd56b.png

皆さんもQiita、はてなブログといった技術ブログを書いたことがあるなら共感していただけると思うのですが、、このマークをブログ全体に書くのは手間ではないでしょうか。いちいちコードのある部分をマークで記載し、大文字の部分は**を使用して囲まなければいけません。そして、自分のマークダウンが反映されているかを確認しなければならないので、一度プレビューのページに移る必要があります。つまり、編集ページでマーク → プレビューで確認 → 編集ページに戻るの3つの工程が確認する度に必要になります。 https://i.gyazo.com/10e9fe9382cfe56bde9de3ee328289ab.png

この手間をNotionでマークダウンで書くことで解決できます。Notionでは上述したように簡易的にマークダウン記法を行うことができます。方法は簡単で、文字を選択したら表示される装飾方法を選択するだけです。

https://i.gyazo.com/ad9c304c795b56c085c29307ded40e0d.png

例えば、「Text」のプルダウンは見出し、リスト形式などが選択できます。

https://i.gyazo.com/34f2610d0a789a5629e12061cd5f504b.png

A」のプルダウンはテキストカラーや背景色を選択できます。しかも色も豊富です!

https://i.gyazo.com/5f4ba83bcdc33ff1c060762d5bb12c33.png

個人的にテキスト装飾で最も助かっているのはコードの記述です。技術ブログだとプログラムのコードを表現するためにバッククオーテーションを使う方法が2つあると思います。

  • 文章の中にあるメソッド名など単語コード(文字をバッククオーテーションで囲む)

例)

sample_method

  • ファイル全体やメソッドの中身など文章コード(バッククオーテーション3つ(```)で囲む)
def sample_method
 puts 'sample_method'
end

コードを記事の中で沢山表示させようとすると、バッククォーテーションを連打しなければならずとても手間ですよね。特に単語コードは文章中にコードが出るたびにマークしなければならないため、マーク忘れなどが頻発してしまうかと思います。

ですが、Notionを使えば簡単にコードのマークを行うことができます。

  • 単語コードに関して

装飾範囲を選択して、「<>」のボタンを押すだけです。ショートカットで「⌘ + E」でコード装飾も行えます(ショートカットの方が楽です!)。

  • 文章コードをに関して

Text」のプルダウンから「Code」を選択します。文章の先頭でバッククオーテーションを3回入力することで文章コードを作成できます。

加えて、Notionの文章コードのさらに凄いところは、左上のプルダウンから言語ごとに文字の色の装飾を指定できることです。

https://i.gyazo.com/6dfc635634c52cbe548c7f6de35038dd.png

さらにさらに!

Notionで書いたマークダウンはコピペすると、マークダウン記法を反映してペースとされます!

つまり、Notionの便利なマークダウン機能でブログの内容を書けば、あとはコピペするだけでブログの投稿準備が完了するのです!いちいちマークダウンが反映されているかを確認しながら装飾していく面倒な方法から完全におさらばできます!

私自身も最近はNotionにブログの内容を書いてそれをブログにコピペする方法を採用していて、ブログ投稿のスピードがかなり上がりました!本当にNotionには感謝しかないです!

☆私のNotionの使い方☆

最後にNotionを愛用し始めた私のプログラミング学習時のNotionおすすめ使用方法を紹介します!

※ 私はRUNTEQに入校しているので、RUNTEQの課題を日々行っています。そのため、スクールに通っていない人、つまり独学の人とは少し状況が違いますが、独学の人は独学の人なりの方法で参考にしてください。

  • 課題と新しい知識でグループを分ける

Notionはファイルを管理する際、タグで分けるように設定できます。

https://i.gyazo.com/3e3bed857864c85469684baf4270928d.png

画像のように、左には課題全般のメモをし、右には課題で出てきた知らないメソッドや項目をタイトルにしファイルを作成しておきます。ファイル内にはテキトーに公式で調べた内容や自分で調べたことなどをメモしておきましょう。

https://i.gyazo.com/c6a2ba1a3dbc9d558a4ba07f78c5fa7a.png

  • ブログの管理ページでアウトプットしたい内容を管理

そしてファイルから自分がアウトプットしたい項目をピックアップし、Blog専用のページにファイルを移します。

https://i.gyazo.com/b2f002272a9647d0d042210fdf8e7f31.png

Not started」はブログ記事未着手のファイル,「In progress」はブログ着手中だけどまだ投稿していないファイル、「Completed」はブログ投稿済みのファイルです。お好みでタグの名前は簡単に変えられるので変えたいという方は変えてください!

終わりに

基本的には課題と新しい知識でグループを分け、アウトプットしたい内容をブログの管理ページで管理するという極めてシンプルな方法をとっています!しかし、Notionは他にも機能が盛り沢山です。個人的に好きなの機能は、ページによってアイコンを変えられるところとかですかね。かわいい(笑)

https://i.gyazo.com/8bebc1edd9aa878598530e9662c79094.png

是非Notionを使い倒してみてください!勉強が効率化し幅が広がります!

以上、大ちゃんの駆け出し技術ブログでした!

YouTube動画を表示

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回はYouTube動画とツイートを表示させる方法について書きたいと思います。これはRUNTEQの応用編で習うものなのです。 RUNTEQで紹介している技術なので現場でも使うのかなと思いまして今回記事にしてみることにしました。

今回はRUNTEQのYouTube動画は以下の動画です。

youtu.be

本記事での使用アプリ

今回はいわゆる現場Railsと言われるRUNTEQで推奨している書籍のアプリを使用します。

www.amazon.co.jp

いつも自分はrails newを使ってscaffoldを作り1から実装したり、みなさんが使用していないアプリを使っていますが、みなさんが手元で同じアプリを使用して実装する方が遥かに理解ができるかなと思いました。

現場RailsのChapter7まで終えていればアプリの状態は同じです。今回の実装ではTaskモデルに対して、新たにカラム「media_url」を実装して、それをタスク詳細画面のビューに表示するように実装します。

現状のタスク詳細画面は以下のようになっています。

Image from Gyazo

ここにYouTube動画の埋め込みをできるように実装します。

カラムを追加

media_urlカラムをタスクテーブルに追加します。

$ rails g migration AddYoutubeUrlToTasks media_url:string
Running via Spring preloader in process 14590
      invoke  active_record
      create    db/migrate/20210210071748_add_youtube_url_to_tasks.rb

マイグレーションファイルは以下のような感じになっています。

class AddYoutubeUrlToTasks < ActiveRecord::Migration[5.2]
  def change
    add_column :tasks, :media_url, :string
  end
end

このままDBにカラムを反映させましょう。

$ rails db:migrate

無事カラムが追加されました。

# db/schema.rb
create_table "tasks", force: :cascade do |t|
  t.string "name", null: false
  t.text "description"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.bigint "user_id"
  t.string "media_url"
  t.index ["user_id"], name: "index_tasks_on_user_id"
end

パラメーター取得の設定

次にapp/views/tasks/_form.html.slimに新しく作成したカラムを入力できるようにします。

# app/views/tasks/_form.html.slim
.form_group
  = f.label :image
  = f.file_field :image, class: 'form-control'
# 追加↓↓↓↓↓↓
.form_group
  = f.label :media_url
  = f.text_field :media_url, class: 'form-control'

Image from Gyazo

コントローラのパラメーターの取得にmedia_urlを追加します。

# app/controllers/tasks_controller.rb
def task_params
  params.require(:task).permit(:name, :description, :image, :media_url)
end

URLハッシュ値取得用のメソッドを定義

また、独自メソッドとしてsplit_id_from_youtube_urlを定義します。

# app/models/task.rb
def split_id_from_youtube_url
  media_url.split('/').last if media_url.present?
end

これはURLの後ろの部分を取得するためです。例えば、YouTubeの共有URLを見てみましょう。

https://youtu.be/nB3ceSVmHeY

YouTubeのURLはその動画を表すハッシュ値がyoutu.be/以降に存在します。split('/')によってURLを'/'ごとに分割した配列に変更します。

task.media_url.split('/')
=> ['https:', 'youtu.be', 'nB3ceSVmHeY']

そしてlastメソッドにより最後のハッシュ値のみを取得しています。

Task.media_url.split('/').last
=> 'nB3ceSVmHeY'

これによりその動画固有のハッシュ値が取得できるようになります。

詳細ページを編集

最後にタスク詳細ページにビューを追加します。src:オプションで先ほど定義したsplit_id_from_youtube_urlメソッドを使用しています。

tr
  th= Task.human_attribute_name(:image)
  td= image_tag @task.image if @task.image.attached?
# 追加↓↓↓↓↓↓
tr
  th= Task.human_attribute_name(:media_url)
  td= content_tag 'iframe', nil, src: "https://www.youtube.com/embed/#{@task.split_id_from_youtube_url}", \
      frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true

実装完了

これで必要なファイルへの記載は全て終わりました。

では実際にYouTube動画を表示させて意味ましょう。 先ほどの例で紹介した共有URL(https://youtu.be/nB3ceSVmHeY)を使用します。

編集画面で共有URLを入力しUpdate Taskを押します。(国際化されていませんね、申し訳ないです。)

そして更新したタスクの詳細画面に遷移すると、、、

Image from Gyazo

動画が表示されたかと思います。

終わりに

YouTubeのURLを入力して、その動画が表示されるサイトはたくさんあると思います。 例えば、はてなブログもそうですね。 したがって、はてなブログも今回実装したような方法を使用していると言えるかなと!

以上!大ちゃんの駆け出し技術ブログでした!

Pundit

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

最近は朝投稿が多いのでわりと朝活成功できています。

今日はRUNTEQの応用編で学習したgemの「Pundit」について発信しようと思います!

認可とは

Punditは"認可"の設定を効率よく行うためのgemです。

ん?認可とは何ぞや?

あまり聴き慣れない言葉ですよね。

認可というのは とある特定の条件に対して、リソースアクセスの権限を与えること

引用: https://dev.classmethod.jp/articles/authentication-and-authorization/

認可されることを具体例として管理者ページへのアクセスを使って説明すると

とある条件が「ユーザーが管理者ユーザーか」とであるとき、管理者ユーザーであれば管理者ページ(リソース)にアクセスする権限が与えられること」となります。

逆に一般ユーザーは管理者ページへのアクセスができないです。これが認可されないということになります。

gyazo.com

Punditの導入

Gemfileに追記してbundle install

# Gemfile
gem "pundit"
$ bundle
・
・
Fetching pundit 2.1.0
Installing pundit 2.1.0

アプリケーションコントローラに Punditincludeします。

class ApplicationController < ActionController::Base
  include Pundit
end

次に下記コマンドを実行します。

$ rails g pundit:install
Running via Spring preloader in process 47291
      create  app/policies/application_policy.rb

すると行数の多いファイルが作成されました。

# app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end
end

app/policies配下にファイルが置かれたように、Punditのファイルは全てこのディレクトリ配下に格納されます。applicationとあるように、こちらのファイルにはPunditのファイル全てに共通する内容を書きます。application_controller.rbなどと同じですね。

しかし、今回はよりPunditを理解するために固有の認可システムを作成します。例えば、このアプリケーションにBoard(掲示板)というモデルがあるとします。

このBoardを操作するにあたり、board_controller.rbupdateアクションに管理者ユーザーのみが行えるようにするとします。実装するためにapp/policies配下にboard_policy.rbを作成します。

# app/policies/board_policy.rb
class BoardPolicy
  attr_reader :user, :board

  def initialize(user, post)
    @user = user
    @post = post
  end

  def update?
    user.admin?
  end
end

次にboard_controller.rbupdateアクションにauthorizeメソッドをインスタンス変数を引数に使用します。

# app/controllers/boards_controller.rb
def update
  authorize @board
  if @board.update(board_params)
    redirect_to @board, success: t('defaults.message.updated', item: Board.model_name.human)
  else
    flash.now['danger'] = t('defaults.message.not_updated', item: Board.model_name.human)
    render :edit
  end
end

ここで何が行われるのかというと、updateアクションが実行時(掲示板を更新した時)、@boardからauthorizeメソッドはboard_policy.rbがあることを推測します。そして、現在実行されているアクション名に?が追加されたメソッドを探します。今回でいうと、board_policy.rbupdate?メソッドです。

def update?
  user.admin?
end

そしてuser.admin?tureを返さなければ、例外が発生します。これで管理者ユーザーのみがupdateアクションを実行することができ、一般ユーザーはupdateアクションを実行できなくなりました。これが認可を定義するPunditの力です!

終わりに

今回は簡単にざっくりとPunditを紹介しました!

1からアプリを実装しようとすると、ログイン機能を実装したりしなければならないので、今回は既存のアプリで試しました。みなさんもお手元の簡単なアプリで是非Punditを実装してみてください!

以上、大ちゃんの駆け出し技術ブログでした!

RUNTEQに入校して2ヶ月

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

先月にRUNTEQに入学して1ヶ月という記事を投稿しました。

sakitadaiki.hatenablog.com

この記事は私のはてなブログ初投稿の記事でしたが、1番読まれた記事です。

なぜかというと、

RUNTEQの社長さんがリツイートしてくれたから!ww

自分のような駆け出しエンジニアがやっているブログなんて、検索自体にかかることがないですし、そこまで見られることがないのは承知の上です。

それに、自分が覚えたことをさらに記憶に定着させる目的もありますし。この一週間毎日欠かさず記事を1本投稿していますし。

この1ヶ月でまた新たにRUNTEQの良い部分を見つけましたが、悪い部分(ネタ)も見つけられました! ぜひ、プログラミングスクールを探している人などに読んで欲しいと思います。

基礎編→応用編へ

「ある一定の期間を天気で例えて」という質問が研修や業務として人事の人やメンターから聞かれることがあると思います。先月のこの頃はRUNTEQの課題で言うところの基礎編の最後あたりでした。そして、その後に応用編(間にRSpecの課題を30時間ほど挟みますが)になることを知っていたので、さらに大変になるいう予想をしていました。しかし、上記質問を雨、晴、曇りとかから答えるとすると、徐々に太陽が見えてきたかなーとみたいな感じです。

「何で?むしろ簡単になったの?」

なんて思われれるかもしれませんが全然そんなことないですwwむずいよ本当にもう、、、

応用編の内容としては既にほとんど完成されられているアプリに対して、バグを修正したり、機能を追加したりします。

ただ、基礎編で学習していないメソッドとかライブラリとかが多く使われており、アプリ構成も基礎編で学習したものとは比べ物にならないくらい複雑です。応用編を開始して10日目ですが、いまだに全体の構成の半分程度も理解しているか怪しいです、、、

しかしながら、毎回出される課題はずっとわからないことだらけで、とりあえず調べてみることを繰り返した結果、わからないことに慣れてしまいました笑。わからない→調べる・質問する→解決するを繰り返し行ったので、わからないことに対して感じるストレスが減ったんだと思います。

それに、現場ではもっと複雑な既に完成させられたアプリに対して改修を行うことになります。ですので、本当に現場レベルのことをRUNTEQは学ばせてくれるのだなと改めて感じました。応用編自体も実際半分程度はもう終わっており、ペースも順調と個人的には感じています。

次の一ヶ月で応用編も終わって、いよいよPF作成に入っていくと思うので今から楽しみです!!

RUNTEQ生のレベルの高さには慣れない、、、

RUNTEQの課題には慣れてきましたが、他に慣れないものがあります。

それはRUNTEQ生です。

これは最近RUNTEQ生のコミュニティに馴染んできたのが原因かなと思うのですが、RUNTEQに入る人の意識が本当に高いです。

毎日勉強をするマインド、その人の頑張りやひたむきさ、それが見てわかります。

前の記事に書いたのですが、RUNTEQには生徒一人一人のslackチャンネルを開設します。そこで生徒は学んだことをアウトプットしたり、勉強の進捗、成果を報告したりするのですが、本当にみんな毎日チャンネルを更新しているので感心しています。

特に、最近「RUNTEQ生凄すぎ!!!」と感じたのは、editchというプログラミングスクールの合同PFコンテストです。

editch.org

このイベントでは内定をもらっていないプログラミングスクールの生徒が作成したPFを審査形式で発表するイベントなのですが、これが本当にレベル高いんです、、、。

まず何がすごいかて審査員の方々なんですよね。

安川 要平さん、西 武史さん、井上 慎也さん、兼城 駿一郎さんと、全員がIT企業の代表取締役なんですよね。審査員レベル高すぎて自分なら緊張して声が出るだろうかわかりません、、、

そして、このコンテストでRUNTEQからは3人の方が出場しました。そして、3人ともクオリティの高いPFをプレゼンし、素晴らしい評価をいただいていました。

自分が出場した3人のようにレベルの高いPFを作成することは想像できません。ぶっちゃけた話、周りがレベル高いと良い環境になると思いますが、高すぎるとどうしようもない劣等感に襲われますねww

ですが、editchは出場すると本当に就活に有利になると思いますし、自分のPFもこれぐらいハイレベルにして就活したいと考えています。

次のeditchは4月下旬です。あと2ヶ月程度しか時間がありませんが、出場できるように膝下入りと学習を進めていきたいと思います。

高まるコミュニティ力

RUNTEQは本当にコミュニティ内でのgive & giveが活発です! 例えば、エラーに詰まったりすると、卒業生が講師に代わって助けてくれたりアドバイスをくれたり。

RUNTEQはdiscordというチャットツールも導入しているため、話したいときに話にいくこともできるということで、最近コロナのせいで減った雑談もすることができています!

discord.com

受講生同士がお互いに励ましあったり笑いあったりできるのは本当にRUNTEQの暖かいコミュニティのおかげです。 ここに卒業しても入れるなんて本当にすごいことだと思います!無料のオンラインサロンになってます。

さらにRUNTEQ自体も自明度が上がっているのか最近は入学する生徒が昔に比べてとても多いようです。自分より半年に入学した人はわずか5人程度なのですが、現在RUNTEQ生は月に約30名は入ってくるのでslackチャンネルは人で溢れています。

自分はRUNTEQに入って2ヶ月なのでだいぶコミュニティにも馴染みはじめようやく受講生の顔と名前が一致してきました。 自分も覚えてもらえてるみたいで、自分のslackチャンネルにはコメントやスタンプもよくもらえるようになったのは嬉しいです。

しかし、これからまたどんどん人が増えていくと、大量のgive & giveがまた生まれてくると思います! 自分もgiveしてコミュニティを盛り上げていければと思います!

RUNTEQに入る = 別れる!?

これは冒頭で触れたRUNTEQの悪い部分(ネタ)にあたります。

どういうことかというと、「RUNTEQの学習で忙しくパートナーとの時間が取れなくなるせいで別れる人が続出している」という、ただの自分の仮説ですww

ですが、この仮説は正しいと思っています。実際、RUNTEQに入校してから2ヶ月経ちましたが、別れたという報告をした人は3人ほどいました。自分も最近彼女と会う頻度が減ったことに不満を言われたので、フラグが立ってしまいました、、、ww

RUNTEQのカリキュラムは本当に難しいので、それ相応の時間を取らないといけません。実際、RUNTEQのYouTubeチャンネルであるエンジニア転職チャンネルでは、RUNTEQのカリキュラムは1000時間の学習時間を想定していると説明しています。

www.youtube.com

確かにRUNTEQ入学前、説明会では週にできれば20時間以上と言っていました。でも、体感的に20時間じゃ全然足りないと思います。絶対最低30時間は必要です。

そして、自分は働きながらRUNTEQを受講している状態です。したがって、自分に残された自由時間は以下の通りになります。

1週間(168時間) -
 ( 通勤時間を含めた1週間の就業時間(50時間) + 
RUNTEQのカリキュラム時間(30時間) + 
睡眠時間(50時間) + 
食事や掃除等の一人暮らしの家事時間(15時間) = 
自由時間(23時間)

「23時間もあるなら週一で遊べるじゃん」

とか思われるかもしれませんが、RUNTEQのカリキュラム時間とは別に個人で学習する時間も取らなくてはいけません。例えば、ブログや市販テキスト学習です。これらも1日2~3時間以上は当然かかります。23時間の自由時間があるのではなく23時間の自由"学習"時間があるんですねww

といっても捻出すれば、全然彼女とも会うことができるのですが、それでも会う頻度は激減しました。

なので、RUNTEQに入る際はパートナーに会う頻度が激減することをしっかりとお伝えし、了承を得ておきましょう(本当に重要です)。もしくは、RUNTEQさんがパートナー保証を制度として設けてくれることを期待しましょうww

終わりに

今回の記事は個人的な体験だけを書いたので、RUNTEQのことについてちゃんと紹介しなくて申し訳ないです。 また1ヶ月後に報告として、「RUNTEQに入校して3ヶ月」も書くかと思いますが、次はしっかりとRUNTEQをお勧めできるような記事ないようにします!

以上、大ちゃんの駆け出し技術ブログでした!