ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

【Rspec】build,create / sequence, trait, transientなど

build, createなどの違い

DBへ保存するか否かなど違いがある。

メソッド 戻り値 DB保存 DB保存(アソシエーション) ID
build() モデルインスタンス x o nil
build_stubbed() モデルインスタンス x x 適当な値
create() モデルインスタンス o o DB保存された値
attributes_for() パラメータハッシュ x x なし

sequence(連番)

sequenceで連番を作成できる。

(spec/factories/foods.rb)
FactoryBot.define do
  factory :food do
    sequence(:name) { |n| "name-#{n}" }
    calorie { 0 }
    sequence(:calorie_theory) { |n| "calorie_theory-#{n}" }
    labels { '' }
  end
end
p build(:food)
p build(:food)

#<Food id: nil, name: "name-1", labels: "", calorie: 0, calorie_theory: "calorie_theory-1", created_at: nil, updated_at: nil>
#<Food id: nil, name: "name-2", labels: "", calorie: 0, calorie_theory: "calorie_theory-2", created_at: nil, updated_at: nil>

trait

複数の属性をグループ化できる。 例えば、userモデルのfactoryデータが存在するとして、あるときは一般ユーザーのデータを、あるときは管理ユーザーのデータを生成したい、という時に便利。

(spec/factories/foods.rb)
FactoryBot.define do
  factory :food do
    sequence(:name) { |n| "name-#{n}" }
    calorie { 0 }
    sequence(:calorie_theory) { |n| "calorie_theory-#{n}" }
    labels { '' }

    trait :rice do
      name { '白ごはん' }
      calorie { 0 }
      calorie_theory { '白い食べ物はカロリーが白紙に戻る。' }
      labels { [''] }
    end

    trait :ice_coffee do
      name { 'アイスコーヒー' }
      calorie { 0 }
      calorie_theory { 'コーヒーを抽出している時、実はゼロカロリーの成分だけが抽出されている。' }
      labels { [''] }
    end
  end
end

transient

モデルの属性以外のデータを一緒に生成できる。 transientは、各attributeの値にも利用できるし、callbackでも利用できる。

transientは生成した後に参照することはできない。 生成時にパラメータとして渡し、組み立ててファクトリの動的な生成に使える。

FactoryBot(FactoryGirl)チートシート - Qiita

FactoryGirlのtransientとtraitを活用する - Qiita

食べ物のテーブルのFactory。食べ物のジャンルのテーブルと多対多の関係。

(spec/factories/food_genres.rb)
FactoryBot.define do
  factory :food_genre do
    sequence(:genre_name) { |n| "genre_name-#{n}" }
    calorie { 0 }
    sequence(:calorie_theory) { |n| "calorie_theory-#{n}" }
  end
end

食べ物のジャンルのFactory。食べ物のテーブルと多対多の関係。

(spec/factories/foods.rb)
FactoryBot.define do
  factory :food do
    sequence(:name) { |n| "name-#{n}" }
    calorie { 0 }
    sequence(:calorie_theory) { |n| "calorie_theory-#{n}" }
    labels { '' }
    
    # 多対多
    trait :with_food_genre do
      transient do
        sequence(:genre_name) { |n| "genre_name-#{n}" }
        sequence(:calorie_theory) { |n| "calorie_theory-#{n}" }
        # calorie { 0 }
      end

   # 中間テーブルを作成する定義を内部で統合
      after(:build) do |food, evaluator|
        food.food_genres << build(:food_genre, genre_name: evaluator.genre_name, calorie_theory: evaluator.calorie_theory)
      end
    end

  end
end

中間テーブル。なんも書いてない。

(spec/factories/food_food_genres.rb)
FactoryBot.define do
  factory :food_food_genre do
  end
end

これでテストファイルの中でデータを生成したところ、関連テーブルのデータも生成されていた。

(テストファイル内)

let(:food) { create(:food, :with_food_genre) }


[6] pry(#<RSpec::ExampleGroups::MealRecordDecorator::DrawingCalorieTheory::Food_2::FoodGenre>)> food
=> #<Food:0x00007fddf8e920b8
 id: 4,
 name: "name-1",
 labels: "",
 calorie: 0,
 calorie_theory: nil,
 created_at: Fri, 30 Oct 2020 10:24:06 JST +09:00,
 updated_at: Fri, 30 Oct 2020 10:24:06 JST +09:00>

# 多対多のテーブル
[7] pry(#<RSpec::ExampleGroups::MealRecordDecorator::DrawingCalorieTheory::Food_2::FoodGenre>)> food.food_genres
=> [#<FoodGenre:0x00007fddf4fb7a28
  id: 1,
  genre_name: "genre_name-1",
  calorie: 0,
  calorie_theory: "calorie_theory-1",
  created_at: Fri, 30 Oct 2020 10:24:06 JST +09:00,
  updated_at: Fri, 30 Oct 2020 10:24:06 JST +09:00>]

# 中間テーブル
[8] pry(#<RSpec::ExampleGroups::MealRecordDecorator::DrawingCalorieTheory::Food_2::FoodGenre>)> food.food_food_genres
=> [#<FoodFoodGenre:0x00007fddf8ed24b0 id: 1, food_id: 4, food_genre_id: 1, created_at: Fri, 30 Oct 2020 10:24:06 JST +09:00, updated_at: Fri, 30 Oct 2020 10:24:06 JST +09:00>]

# 配列の一つ目を取り出す
[9] pry(#<RSpec::ExampleGroups::MealRecordDecorator::DrawingCalorieTheory::Food_2::FoodGenre>)> food.food_genres[0]
=> #<FoodGenre:0x00007fddf4fb7a28
 id: 1,
 genre_name: "genre_name-1",
 calorie: 0,
 calorie_theory: "calorie_theory-1",
 created_at: Fri, 30 Oct 2020 10:24:06 JST +09:00,
 updated_at: Fri, 30 Oct 2020 10:24:06 JST +09:00>

【個人開発】ポートフォリオ作成にオススメなtipsやツールをまとめる

自分がポートフォリオ作成に活用したtipsやツールを以下にまとめてみました。

qiita.com

↪︎「こんな課題があるのか〜」と知ることも大事だと思います。

creators.eightbit.jp

↪︎ツクログで色々な個人開発を見て、アイデアや発想力を養うのもアリだと思います。

qiita.com

↪︎クソアプリをいっぱい見て、脳みそを柔らかくしましょう。

webdesign-trends.net

↪︎少しでもBootstrap臭を避けたければ是非。

hatchful.shopify.com

↪︎無料でアプリのロゴを作成できます。便利な時代ですね。

icons8.com

↪︎トップページのおしゃれなイラストとかを作成できます。 しかも、色とかスタイルとか柔軟に変更できます。

www.kiyaku.app

↪︎面倒な利用規約などを自動で作成してくれます。

uxmilk.jp

MVPとは、製品を提供する上で必要最小限の機能のみをもつ、もっともシンプルな製品です。

↪︎個人開発においても、MVPを作成して、ユーザーの反応を見て改善を繰り返すのも大事だと思います。

qiita.com

↪︎どうせならアプリのREADMEを綺麗に整えたいですよね。

tech-blog.s-yoshiki.com

↪︎READMEとかでよく見るバッジを作成できる。

【Rails】404、500エラーをハンドリング(slack通知も)

エラーハンドリングのメモが残ってたから、体系的な内容に纏めてみた。
どうせなのでコッチに移しておく。

エラーハンドリングをする

  • 404エラー時は、404エラー用のテンプレートを表示させる。
  • 500エラー時は例外(エラー)クラス、エラーメッセージ 、バックトレースをログに出力させ、500エラー用のテンプレートを表示させる。

application_controller.rbにエラー時の設定を書く
→どんなページを表示するか、どんなログを残すかなど。

(application_controller.rb)
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :render_404
  rescue_from StandardError, with: :render500

  def render_404
    render file: Rails.root.join('public/404.html').to_s, status: :not_found, layout: false, content_type: 'text/html'
  end

  def render_500(error)
    logger.error("エラークラス: #{error.class}")
    logger.error("エラーメッセージ : #{error.message}")
    logger.error('バックトレース -------------')
    logger.error(error.backtrace.("\n"))
    logger.error('-------------')
    render file: Rails.root.join('public/500.html').to_s, status: :internal_server_error, layout: false, content_type: 'text/html'
  end
end

config.consider_all_requests_localをfalseにするだけで、development環境でエラーページの動作を確認できる

trueに設定すると、どのような種類のエラーが発生しても、詳細なデバッグ情報がHTTPレスポンスに出力される。
developmentとtest環境ではtrue、production環境ではfalseに設定されている。

(config/environments/development.rb)

# Show full error reports.
# falseにするだけでdevelopment環境でエラーページの動作を確認できる

config.consider_all_requests_local = false

意図的にエラーを発生させる

get '*path', to: 'application#render_500'
get '*path', to: 'application#render_404'

エラーログの参考資料

エラーログ

log/development.log
Started GET "/boards/599" for ::1 at 2020-04-29 11:58:10 +0900
Processing by BoardsController#show as HTML
 Parameters: {"id"=>"599"}
 [1m[36mUser Load (2.5ms)[0m  [1m[34mSELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?[0m  [["id", 43], ["LIMIT", 1]]
 ↳ vendor/bundle/ruby/2.6.0/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98
 [1m[36mBoard Load (0.2ms)[0m  [1m[34mSELECT  "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ?[0m  [["id", 599], ["LIMIT", 1]]
 ↳ app/controllers/boards_controller.rb:26
 Rendering public/404.html
 Rendered public/404.html (4.7ms)
Completed 404 Not Found in 61ms (Views: 16.7ms | ActiveRecord: 2.7ms)
log/development.log
Started GET "/login" for ::1 at 2020-04-29 11:59:39 +0900
Processing by UserSessionsController#new as HTML
エラークラス: NameError
エラーメッセージ : undefined local variable or method `hello' for #<UserSessionsController:0x00007fd3cda92010>
バックトレース -------------
Completed 500 Internal Server Error in 219ms (ActiveRecord: 0.0ms)
NoMethodError (undefined method `call' for #<Array:0x00007fd3c77ebf88>):
app/controllers/application_controller.rb:20:in `error500'

エラーページを作成する

Railsは標準で、public/404.htmlとpublic/422.html、public/500.htmlが作成されているが、以下のサイトなどを使って自前のエラーページを作るのもロマンがあるというか、面白味があると思う。

たった1分で素敵なエラーページが作れるサイト「Better Error Pages」 | ライフハッカー[日本版]

エラー時にslack通知する

gemのインストール。

gem 'slack-notifier'
gem'exception_notification'

チャンネルの右上の「iマーク」を押す。 f:id:ryota21silva:20200916193556p:plain

詳細が表示されるから、その他→アプリを追加する。 f:id:ryota21silva:20200916193635p:plain

こんなページへ飛ぶ。 f:id:ryota21silva:20200916193714p:plain

チャンネルのWebhookURLを取得 f:id:ryota21silva:20200916194212p:plain

例外エラーをslackチャンネルに通知するよう設定

$ be rails g exception_notification:install                                                                                                             
      create  config/initializers/exception_notification.rb
(config/initializers/exception_notification.rb)
ExceptionNotification.configure do |config|
 config.add_notifier :slack, {
     webhook_url: Rails.application.credentials.dig(:slack, :webhook_url),
     channel: Settings.slack.exception_notification_channel
 }
end
config.add_notifier :slack,
                       webhook_url: Rails.application.credentials.slack[:webhook_url],
                       channel: チャンネル名

settingsファイルで設定もあり

(config/settings/development.yml)
slack:
 exception_notification_channel: '#基礎編通知'

※500番エラー(サーバー側の問題)が発生した時だけ、通知が来るようになっている
→通知が来ることで、改修などを行える
※404などの間違ったURLを入力した等のエラー(クライアント側の問題)で毎回通知が来る必要はない。

credentialsファイルに秘匿情報(チャンネルURL)を保存しておく。

$ EDITOR="vi" bin/rails credentials:edit
# credentialの中身 
slack:
  webhook_url: 取得したURL

config/credentials.yml.encに、暗号化した情報が入る(リモート上で管理したくない秘匿情報)。
→そしてmaster_keyに暗号を解くための鍵が入っており、情報の暗号化、複合を行ってくれる。

また、master_keyはバージョン管理対象外となっている。

.gitignore
# Ignore master key for decrypting credentials and more.
/config/master.key

こんな感じでslackに通知される。 f:id:ryota21silva:20200916194702p:plain

参考資料

Rackとは何か - Qiita

Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた - Qiita

Rails のエラー処理について知ってる範囲でまとめ - Qiita

rescue_fromの走査は下から上だった。 - Qiita

先輩と覚える HTTP ステータスコード · GitHub

【初学者向け】Gitのコマンドの使い方をよく忘れるから、纏めてみた

はじめに

Git: もう怖くないGit!チーム開発で必要なGitを完全マスター 山浦 清透を受講した時のメモがあったので、今の知識をもとに整理してみました。 これから新しいコマンドの使い方を覚えた時には、この記事に纏めていこうと思います。


1. 基本コマンド

  • git init
    • ローカルリポジトリ(=.gitディレクトリのこと。gitに必要なものがほどんど入っている)の新規作成。
      ドット(.)は隠れフォルダを表している。
  • git clone URL
    • ローカルにリモートリポジトリのファイルをコピーする。
  • git add -A
    • 現在のディレクトリ配下の変更ファイルが、ステージング (Staging) という待機用リポジトリに置かれる。addしたファイルは本当にコミットの対象にすべきなのかgit statusgit diffとかで確認しておきたい。
  • git commit

    • 変更を記録する(=コミット)。ステージの内容をスナップショットとしてリポジトリに記録する(変更、新規作成、削除、複数ファイルの変更・作成・削除)。
  • git commit -m 'メッセージ'

  • git commit -v
    • エディターを立ち上げて、「どのファイルにどんな変更をしたのか」エディター上で確認できる。結構オススメ。
  • git status

    • 以下のような変更されたファイルを確認する。
      1. ワークツリーとステージの間で変更されたファイル(前回、ステージに追加してから変更したワークツリーのファイル)
      2. ステージとリポジトリの間で変更されたファイル(前回、コミットしてからステージに追加されたファイル)を確認できる。
  • git remote add origin URL名

  • git push -u origin master(ブランチ名)

    • ローカルリポジトリの内容をリモートリポジトリに送る(master=デフォルトのブランチ名)。
    • 最初に-uを付ければ、アップストリームブランチを指定してくれるので、次回からgit pushに省略可能。
    • featureブランチを切った時は、最初にgit pushとだけ実行し、アップストリームブランの指定を行うサジェスチョンを表示させるのが良い。

2. git diff系

  • git diff
    • git addする前の変更差分を確認する。ワークツリーとステージの間の変更差分を確認する。
  • git diff ファイル名
    • 特定のファイルの変更差分を見る。
  • git diff --staged
    • ステージしたけど、まだコミットしてない変更差分を見る。git addした後の変更内容を確認する。

3. git log系

  • git log
    • 今までの変更履歴(コミット)を確認する(最新のコミットから順に表示)。
  • git log --oneine
    • 変更履歴を1行で表示。
  • git log -p ファイル名
    • 指定したファイルの変更差分を表示する。
  • git log -n コミット数
    • 表示するコミット数を制限(最新順で表示)。よく使う。

4. git rm系

  • git rm ファイル名

    • ファイルの削除を記録する(削除された変更状態がステージに記録される)。
      これは、ファイルごと削除するということ(=コミットした記録だけではなく、ワークツリー内のファイルも削除)。ワークツリーからもローカルリポジトリからも削除したい時に使う。
  • git rm -r ディレクトリ名

  • git rm --cached ファイル名
    • コミットした記録は削除したいけど、ワークツリー内のファイルは残したい(ローカルリポジトリからだけ削除して、その変更状態がステージに記録される。
    • これらのコマンド実行後、lsでワークツリーのディレクトリ内を確認、git statusで変更されたファイルを確認をすれば、記録が分かる。

5. git mv系

  • git mv 旧ファイル 新ファイル
    • ファイルの移動(=ファイル名の変更)を記録する。つまり、ワークツリーのファイル名が変更されて、かつ、ステージにも変更が反映される。

6. ワークツリーのファイル内の変更を取り消したい

  • git checkout -- ディレクトリ名
  • git checkout --.
    • 全変更を取り消す。

7. addしたけど、その変更を取り消したい(=commit対象外にしたい)

更新内容自体は取り消さず、addしてインデックスに登録するのを取り消す。

  • git reset ファイル名
  • git resetだけだと、ステージングにあるファイル全てを取り消す。

8. git commitした後に、直前のコミットを取り消したい(git reset HEAD)

1. git logでコミット履歴を確認する。

$ gl -n 3                                                                                                                                                           【 feature/19_meal_record_crud_exception 】
commit 4419fad760dd4e703559660195577047d88f60c5 (HEAD -> feature/19_meal_record_crud_exception)
Author: ryota1116 <ryota@gmail.com>
Date:   Thu Aug 27 16:30:34 2020 +0900

    [Add]MealRecordの各ページを修正

commit 2d14341fb814920c1072bd0a1e09678ad43abcef (origin/feature/19_meal_record_crud_exception)
Author: ryota1116 <ryota@gmail.com>
Date:   Thu Aug 27 00:39:01 2020 +0900

    [Fix]MealRecord検索フォームのjsファイルを修正

commit 14e7eaaf4b36535e32b342b33b7e54120f11f9bb
Author: ryota1116 <ryota@gmail.com>
Date:   Thu Aug 27 00:17:06 2020 +0900

    [Add]MealRecordフォームのi18nを追記

2. 取り消したいcommitをrevertする。

  • git reset HEAD^で、HEADの1個前のコミットに戻る。git reset コミットIDでも可。
  • HEAD^^^で、HEADの3個前のコミットに戻る。
HEADの意味
  • HEAD -> aaa(ローカルブランチ名)が現在のブランチの場所を指定(記憶)しており、その指定されているブランチが最新のコミットの位置を記憶している。
  • origin/ブランチ名はリモートにpush済。

qiita.com

9. git pushした後に、直前のコミットを取り消したい(git revert)

  • git revert コミットID

revertするコミットを指定する感じ。
git logすると、commitとrevertのログが残ってるのが分かる。

  • ただし、revertしたコミットのファイルがローカルからも消えちゃう(前のコミットに戻ったから)ので、「revertしたコミットの変更」をrevertしてあげれば、変更内容をローカルに残せる。以下のように。

git revert <revertしたコミットID> -nで、revertしたコミットの変更を取り消す(直前のコミットIDの変更を削除したコミットを取り消す感じかね。

10. 直前のコミットを修正したい(=今のステージの状態をもとに、コミットの内容を上書きしたい)

  • git commit --amend
    • 例えば、ファイルの修正を忘れてた時に、ワークツリー内のファイルに変更を加え、git addgit commit --amendを行う感じ。

git commit --amendでaddし忘れたファイルを救済する - Qiita

注意点

  • リモートリポジトリにpushした後のコミットは絶対にamendで修正してはいけない!!!

    pushした後にそのコミットを修正したければ、git commitでもう一度新しいコミットを作って修正すること。

11. git remote系

リモートリポジトリの情報を確認する

  • git remote
    • 設定しているリモートリポジトリを一覧表示する
  • git remote -v
    • 設定しているリモートリポジトリのURLを一覧表示する。

リモートリポジトリを複数登録する

  • メリット

    • 複数のチームで作業するとき
    • 自分のリモートを持ちたいとき
  • コマンド

    • git remote add リモート名 リモートURL
      • リモート名(ショートカット)で、URLのリモートリポジトリを新規登録する。

12. git fetch系

  • リモートリポジトリから情報を取得する(フェッチ)

    • git fetch リモート名
    • git fetch origin
  • 特徴など

    • ローカルリポジトリに情報を保存するけど、ワークツリーには保存されない。
    • 具体的にはremotes/リモート/ブランチというリモートブランチに保存される。
      • git mergeでローカルリポジトリ(ローカルブランチ)に保存された情報を、ワークツリーに落とす。

13. git pull系

  • git pull リモート名 ブランチ名

  • リモートから情報を取得し、マージまでを一度にやりたい」ときに使う。

    • git fetchとgit mergeを同時に行っている

14. fetchとpull

  • fetchを使うのが基本オススメみたい。

    • pullは特殊な挙動を起こす。今自分がいるブランチに、pullしてきたブランチがマージされる。異なるブランチ同士が統合される恐れがある。ファイルがグチャグチャになる可能性があるということ。
  • 自分が「マスターブランチにいて、何の変更もしていないときに限って」、pullで情報を取得するなどを行うべき。

    • 安全なケースの時にだけpullを使うのが良さそう。

15. リモートの詳細情報を表示する

  • git remote show リモート名
    • fetchとpushのURL、リモートブランチ、git pullの挙動、git pushの挙動などが表示される。

16. リモート名を変更する、リモートを削除する

  • git remote rename 旧リモート名 新リモート名
  • git remote rm リモート名

17. ブランチとは

  • コミットを指すポインタのことで、並行して複数機能を開発するためにあるもの。
    • ブランチは分岐して開発するためにあるもので、他の人の行った開発が、自分の開発には影響されないメリットを持つ(複数の機能の共同開発が可能となる)。

18. HEADとは

  • 今自分が作業しているブランチを指し示したポインタのこと。
  • このブランチとHEADの2つ(リポジトリ内に保存されている)により、最新のコミットがどれか分かる(順にHEAD→ブランチ→コミットと指している)

19. branch関連のコマンド

ブランチの新規作成

  • git branch ブランチ名
    • ブランチを新規追加する。(ブランチの切り替えまでは行わない)

ブランチの表示

  • git branch
    • 今あるブランチの一覧を表示。
  • git branch -a
    • 全てのブランチを表示。リモートリポジトリも表示できる。

ブランチの切り替え

  • git checkout 既存ブランチ名
    • ブランチを切り替える。(HEADが指し示すブランチを切り替えるということ)
  • git checkout -b 新ブランチ名
    • ブランチを新規作成し、切り替えも行う。

ブランチ名を変更する

  • git branch -m 新ブランチ名
    • 自分が現在作業しているブランチの名前を変更する。

リモートにpushした後にブランチ名を変更する

git branch -m 新ブランチ名
git push origin :旧ブランチ名  # リモート上にある旧名のブランチを削除
git push origin 新ブランチ名

ブランチを削除する

  • git branch -d ブランチ名
    • masterにマージされていない変更が残っている場合、そのブランチは削除しないようになっている。安全。
  • git branch -D ブランチ名
    • 強制削除する。慎重に使うこと。

20. merge系

マージ・・・他の人の変更内容を自分のブランチに取り込む作業のこと。

  • git merge ブランチ名
    • 指定したブランチ名の変更内容を、自分が今いるブランチに取り込む。
  • git merge リモート名 ブランチ名
  • git merge origin master(リモートブランチ名)
    • origin(=GitHubのこと)のmasterブランチの内容を、自分の作業ブランチに取り込む。

21. コンフリクトとは

  • 複数人の人が、同じファイルの同じ行に対して、異なる編集を行った時に起こること。
    • マージしたら、コンフリクトしてるよと言われ、Autoマージが失敗する。コンフリクトしても、落ち着いてファイルの内容をきれいに書き換えればOK。

22. コンフリクトの事故が起きにくい運用ルール

  1. 複数人で同じファイルを変更しない。
  2. pullやmergeする前に変更中の状態を無くしておく(commitstash(変更中のファイルを一時保管するコマンド)で変更中のファイルを無くしておく)。
  3. pullするときは、pullするブランチに移動してからpullする(同じ名前のブランチに移動してからpullしろってこと)。

23. ブランチを利用した開発の流れ

  • masterブランチをリリース用ブランチとして使い、開発はトピックブランチを作成して進めるのが基本。
    • masterブランチを常に最新のリリース状態に保っているから、最新のリリース内容がどんなものかmasterを見れば分かるようになっている!
    • 何かバグが起きても、一つ前のマスターブランチのバージョンに戻せばいいだけ。
  • masterで開発すると、最新のリリース状態がわからなくなるし、前のバージョンに切り戻すのが難しくなる。
    • 開発するときは、最新のマスターからトピックごとにブランチを切って、そのトピックブランチで開発を行う。そして、開発が完了したらmasterにマージする。

24. プルリクエストとは

  • 自分の変更したコードをリポジトリに取り込んでもらえるよう依頼する機能。バグを発生させない、コードの質を保つために、コードレビューを行ってもらう。

25. rebaseで変更履歴を修正する

  • git rebase ブランチ名
    • 「ブランチの基点となるコミット」を別のコミットに移動する。コミットの履歴を一直線に綺麗に整えられる。

26. リベースとマージの違い

履歴が一直線なのか、枝分かれなのか

- rebase...作業の履歴を消したい場合

  • 履歴をきれいに保つことが可能
  • コンフリクトの解決が若干面倒(各コミットごとにコンフリクトが発生する)

- merge...作業の履歴を残したい場合

  • コンフリクトの解決が比較的簡単(コンフリクトが一度しか発生しない)
  • マージコミットがたくさんあると履歴が複雑化する

27. リベースとマージの使い分け

  • プッシュしていないローカルの変更には、リベースを使う
  • プッシュした後はマージを使う。コンフリクトしそうな時もマージを使う。

注意点

  • githubにプッシュしたコミットをリベースするのは絶対NG!!

    • git push -fは絶対NG

28. pullにはマージ型とリベース型がある

  • マージ型
    • マージコミットが残るから、マージしたという記録を残したいときに使う。
  • リベース型
    • マージコミットが残らないから、Githubの内容を取得したいだけのときは以下のコマンドを使う。
      • git pull --rebase リモート名 ブランチ名

29. 複数のコミットをやり直す(git rebase)

  • git rebase -i コミット名
  • git rebase -i HEAD~3
    • 直前3つのコミットをやり直す

30. Git管理対象外に置きたくないファイルをpushしてしまった時

秘匿情報の入ったファイル、アプリケーションとは関係のないスクショなどを.gitignoreの中に指定するのを忘れ、pushしちゃった時に、そのファイルをGit管理対象外にする。

git rm --cached ファイル名

このコマンドで対象にされたファイルは、.gitignoreが適用されるようになる。

git rm --cached ファイル名とすると、ローカルからファイルが一旦消えるため、どこかにバックアップを取っておく。
push→pullしたあと、ファイルがdeletedになるから、ローカルにコピーして書き直せばおk。

31. git pushrejectされた時の対処法

ローカルでコミットした後に、Githubへpushしようとすると以下のようなエラーが表示されることがある。

~/workspace/app/cat_meow_miaow
$ git push --set-upstream origin master 【 master 】
To https://github.com/ryota1116/cat_meow_miaow.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/ryota1116/cat_meow_miaow.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

これは自分がpushするまでの間に、他の人が同じブランチ(以下の例ではmasterブランチ)にpushしているなどリモートの内容が変更されている場合に表示されるエラー。解決方法としては、リモートの最新の内容をローカルに反映させてあげる必要がある。
git pull リモートブランチ名 作業ブランチ名としてあげればいいので、今回の例でいけば、git pull origin masterを実行する。

もしくは、git pull --rebase リモートブランチ名 作業ブランチ名としてあげると、remoteの変更をpullした上で、その後にローカル上の変更をマージしてくれるので、コッチのが手っ取り早くはある。

$ git pull --rebase origin master    【 master 】
From https://github.com/ryota1116/cat_meow_miaow
 * branch            master     -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
warning: unable to rmdir 'sound': Directory not empty
Applying: [Add]キー入力で音声出力
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...

これでpushできるようになったはず。

その他Gitでよく使うコマンド

  • git stash
  • git stash -u
  • git stash apply
  • git stash pop
  • git reset -soft HEAD^
  • git reset -soft HEAD^^
  • git reset -soft HEAD2^
  • git reset -hard HEAD2^(危険)
  • git merge --abort

【Ruby】Date型で色んなメソッドを試してみる

今日、昨日、○日前、○日後とか

# 今日
[1] pry(main)> Date.today
=> Sat, 22 Aug 2020
[6] pry(main)> Date.current
=> Sat, 22 Aug 2020

# 昨日
[7] pry(main)> Date.current.yesterday
=> Fri, 21 Aug 2020

# 昨日(1日前)
[8] pry(main)> Date.current.ago(1.day)
=> Fri, 21 Aug 2020 00:00:00 JST +09:00

# 3日前
[9] pry(main)> Date.current.ago(3.day)
=> Wed, 19 Aug 2020 00:00:00 JST +09:00

# 10日前
[8] pry(main)> Date.today.prev_day(10)
=> Mon, 17 Aug 2020

# 10日語
[7] pry(main)> Date.today.next_day(10)
=> Sun, 06 Sep 2020

# 2ヶ月後
[9] pry(main)> Date.today.next_month(2)
=> Tue, 27 Oct 2020

日・週・月などを丸々取得

# 今日24時間丸々
[16] pry(main)> Date.current.all_day
=> Sat, 22 Aug 2020 00:00:00 JST +09:00..Sat, 22 Aug 2020 23:59:59 JST +09:00

# 今週丸々
[17] pry(main)> Date.current.all_week
=> Mon, 17 Aug 2020..Sun, 23 Aug 2020

[18] pry(main)> Date.current.all_month
=> Sat, 01 Aug 2020..Mon, 31 Aug 2020
[19] pry(main)> Date.current.all_year
=> Wed, 01 Jan 2020..Thu, 31 Dec 2020

日・週・月などの始まり・終わり

# 今日の0時
[21] pry(main)> Date.current.beginning_of_day
=> Sat, 22 Aug 2020 00:00:00 JST +09:00

# 今週最初の日(月曜日)
[13] pry(main)> Date.current.beginning_of_week
=> Mon, 17 Aug 2020

# 今週最後の日(日曜日)
[14] pry(main)> Date.current.end_of_week
=> Sun, 23 Aug 2020

# 今月最後の日 
[15] pry(main)> Date.current.end_of_month
=> Mon, 31 Aug 2020

# 今年最後の日
[20] pry(main)> Date.current.end_of_year
=> Thu, 31 Dec 2020

指定したオブジェクトが、どんなメソッド等を使用できるのか確認する

ある型を持つオブジェクト(今回で言えばDate型)が、どのようなメソッドを使用できるのか確認する癖は付けておいた方がいいと思う。

[16] pry(main)> today = Date.today
=> Thu, 27 Aug 2020

[18] pry(main)> today.class
=> Date

[17] pry(main)> ls today
ActiveSupport::ToJsonWithActiveSupportEncoder#methods: to_json
ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency#methods: require_dependency
Comparable#methods: <  <=  ==  >  >=  between?  clamp
DateAndTime::Zones#methods: in_time_zone
DateAndTime::Calculations#methods:
  after?       all_year                 at_end_of_month    beginning_of_month    days_since          end_of_year   last_weekday  next_occurring  on_weekend?     prev_weekday  weeks_since
  all_day      at_beginning_of_month    at_end_of_quarter  beginning_of_quarter  days_to_week_start  future?       last_year     next_quarter    past?           sunday        years_ago
  all_month    at_beginning_of_quarter  at_end_of_week     beginning_of_week     end_of_month        last_month    monday        next_week       prev_occurring  today?        years_since
  all_quarter  at_beginning_of_week     at_end_of_year     beginning_of_year     end_of_quarter      last_quarter  months_ago    next_weekday    prev_quarter    tomorrow      yesterday
  all_week     at_beginning_of_year     before?            days_ago              end_of_week         last_week     months_since  on_weekday?     prev_week       weeks_ago
Date#methods:
  +                ajd                  at_noon                   cwyear           gregorian   jd            midday                  new_start              prev_month        step            to_s
  -                amjd                 beginning_of_day          day              gregorian?  jisx0301      middle_of_day           next                   prev_year         strftime        to_time
  <<               as_json              blank?                    day_fraction     hash        julian        midnight                next_day               readable_inspect  succ            tuesday?
  <=>              asctime              change                    default_inspect  httpdate    julian?       minus_with_duration     next_month             rfc2822           sunday?         upto
  ===              at_beginning_of_day  compare_with_coercion     downto           in          ld            minus_without_duration  next_year              rfc3339           thursday?       wday
  >>               at_end_of_day        compare_without_coercion  end_of_day       infinite?   leap?         mjd                     noon                   rfc822            to_date         wednesday?
  acts_like_date?  at_midday            ctime                     england          inspect     marshal_dump  mon                     plus_with_duration     saturday?         to_datetime     xmlschema
  advance          at_middle_of_day     cwday                     eql?             iso8601     marshal_load  monday?                 plus_without_duration  since             to_default_s    yday
  ago              at_midnight          cweek                     friday?          italy       mday          month                   prev_day               start             to_formatted_s  year

参照記事

Rails6にSemanticUIを導入

Rails6でのSemantic UIの導入

BootstrapのフレームワークであるSemantic UIをRails6で使ってみたので、以下に導入方法を記しておきます。

Semantic UI公式

以下の記事もよければ参考にしてください。

【Rails】Semantic UIを使って動的にフラッシュメッセージを表示する方法 - Qiita

前提

Webpackerでの導入方法が分からなかったので、従来のassetsフォルダでの導入にしました。。。
Webpackerでの導入方法が分かり次第、更新したいと思います。

追記

公式のisuueを追ったところ、semantic-ui-sassはwebpackerで導入する方法が見当たらなかった。

How to add semantic-ui-sass on Rails 6 · Issue #144 · doabit/semantic-ui-sass · GitHub

導入方法

  • Gemをインストールする。
(Gemfile)
gem 'semantic-ui-sass'
(app/assets/stylesheets/application.scss)
@import "semantic-ui";
(app/assets/javascript/application.js)

// Loads all Semantic javascripts
// = require semantic-ui

500エラーが表示されるので、エラーメッセージを確認。

ActionView::Template::Error - Asset `application.js` was not declared to be precompiled in production.
Declare links to your assets in `app/assets/config/manifest.js`.

  //= link application.js
and restart your server:
  app/views/layouts/application.html.slim:11:in `view template'

app/assets/config/manifest.js//= link application.jsを追記。

(app/assets/config/manifest.js)
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link application.js

これだけでSemantic UIの記述を反映できる。

Rails6にjQueryを導入

Rails6でjQueryを導入する方法

yarn add jquery

package.jsonyarn.lockを確認すると、jQueryが追加されていることが分かる。

(package.json)
{
  "name": "zero_calorie",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.2.2",
    "jquery": "^3.5.1",
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
} 
(yarn.lock)
jquery@^3.5.1:
  version "3.5.1"
  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
  integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== 
(config/webpack/environment.js)

const { environment } = require('@rails/webpacker')

module.exports = environment

以下のように変更

(config/webpack/environment.js)

const { environment } = require('@rails/webpacker')

const webpack = require('webpack')

environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment

JS、jQueryのファイルを読み込む

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

// 追加
require("jquery")
require("jquery/submit_food_image")

htmlが記述されたslimファイル

h1 id="red"
  | 食事記録の一覧

h2 食事記録

jsファイルを作成。これでJS、JQueryで記述した内容が反映される。

(app/javascript/jquery/submit_food_image.js)
$(function () {
  $(document).ready(function(){
    $("#red").css("color", "red");
  });
});

slimで直書きVer.

h1 id="red"
  | 食事記録の一覧

h2 食事記録

javascript:
  $(function () {
    $(document).ready(function(){
      $("#red").css("color", "red");
    });
  });

参照サイト

Rails 6にjQueryとBootstrapを入れる - Qiita

【Rails】マイグレーションファイルをmigrateする前に戻す方法(その他マイグレーション関連のコマンド)

be rails db:migrate:statusでmigrateの状況を確認。 upになっているファイルがmigrateされている。

$ be rails db:migrate:status

database: zero_calorie_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200802075924  Create meal records
   up     20200802080924  Create active storage tablesactive storage
   up     20200804123950  Create foods

be rails db:rollback STEP=3で3つのファイル(最新順)をmigrate前に戻す。

$ be rails db:rollback STEP=3 
== 20200804123950 CreateFoods: reverting ======================================
-- drop_table(:foods)
   -> 0.0920s
== 20200804123950 CreateFoods: reverted (0.0986s) =============================

== 20200802080924 CreateActiveStorageTables: reverting ========================
-- drop_table(:active_storage_attachments, {})
   -> 0.1429s
-- drop_table(:active_storage_blobs, {})
   -> 0.0940s
== 20200802080924 CreateActiveStorageTables: reverted (0.2376s) ===============

== 20200802075924 CreateMealRecords: reverting ================================
-- drop_table(:meal_records)
   -> 0.0528s
== 20200802075924 CreateMealRecords: reverted (0.0530s) =======================

Model files unchanged.

downになっているファイルは、migrateされる前の状態に戻ったということ。 down状態のファイルは直接編集おk。 (ただし、「新機能を実装するためにFoodテーブルと他のテーブルにアソシエーションを組みたい」といった場合、rollbackするのではなく新しくマイグレーションファイルを作成してあげればよい。マイグレーションファイルは歴史の積み重ね的なやつだから時系列で残してあげた方がよいと思われる)

$ be rails db:migrate:status
database: zero_calorie_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200802075924  Create meal records
  down    20200802080924  Create active storage tablesactive storage
  down    20200804123950  Create foods
$ be rails db:migrate
== 20200802075924 CreateMealRecords: migrating ================================
-- create_table(:meal_records)
/Users/funesakisuke/workspace/app/zero_calorie/vendor/bundle/ruby/2.7.0/gems/migration_comments-0.4.1/lib/migration_comments/active_record/connection_adapters/mysql2_adapter.rb:39: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/funesakisuke/workspace/app/zero_calorie/vendor/bundle/ruby/2.7.0/gems/activerecord-6.0.3.2/lib/active_record/connection_adapters/mysql/schema_statements.rb:80: warning: The called method `create_table' is defined here
/Users/funesakisuke/workspace/app/zero_calorie/vendor/bundle/ruby/2.7.0/gems/migration_comments-0.4.1/lib/migration_comments/active_record/connection_adapters/table_definition.rb:10: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/funesakisuke/workspace/app/zero_calorie/vendor/bundle/ruby/2.7.0/gems/activerecord-6.0.3.2/lib/active_record/connection_adapters/abstract/schema_definitions.rb:364: warning: The called method `column' is defined here
   -> 0.0480s
== 20200802075924 CreateMealRecords: migrated (0.0481s) =======================

== 20200802080924 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs, {})
   -> 0.0524s
-- create_table(:active_storage_attachments, {})
   -> 0.0477s
== 20200802080924 CreateActiveStorageTables: migrated (0.1003s) ===============

== 20200804123950 CreateFoods: migrating ======================================
-- create_table(:foods)
   -> 0.0516s
== 20200804123950 CreateFoods: migrated (0.0517s) =============================

Annotated (3): app/models/meal_record.rb, spec/models/meal_record_spec.rb, spec/factories/meal_records.rb
$ be rails db:migrate:status 

database: zero_calorie_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200802075924  Create meal records
   up     20200802080924  Create active storage tablesactive storage
   up     20200804123950  Create foods

その他コマンド

  • 全てのテーブルをドロップして、全てのmigrationを実行してテーブルの再作成を行う。
be rails db:migrate:reset

情報収集のやり方と好きなメディアサービスやブログを纏めておく

はじめに

Feedlyを使って効率的に情報収集しよう

私は10個くらいのメディアを使って情報収集しているのですが、全メディアのサイトを訪問しているようでは、時間がいくらあっても足りません。
そこで色んなサイトの記事を1つのプラットフォームで一元管理してくれるFeedlyというサービスを使っています。
Feedlyのページを見るだけで、各サイトの欲しい内容は大体漏れなくキャッチアップできますし、情報収集のハードルも下がります。控えめに言ってめっちゃオススメ。

f:id:ryota21silva:20200804193158p:plain
Feedlyのトップページ

この方の動画見たら使い方分かると思います www.youtube.com

好きなメディアやブログ

以下にザックリ纏めてみました。

はてなブログ

安定のはてぶ b.hatena.ne.jp

TechCrunch

スタートアップ企業の紹介やインターネットの新しいプロダクトのレビュー、そして業界の重要なニュースを扱うテクノロジーメディア jp.techcrunch.com

業界カオスマップもよく見ている jp.techcrunch.com

Monthly Pitch

創業期の企業家向けのピッチイベント monthly-pitch.com

スタートアップタイムズ

スタートアップのインタビュー記事 これめっちゃ面白いのでオススメ startuptimes.jp

R25

これからの時代を生きる若者の仕事や人生のバイブル的存在らしい。 読んでてワクワクする。 r25.jp

News Picks

学生時代から読んでる一番お世話になってるメディアかも。 難しいテーマをイラスト付きで解説してくれるまとめコラムみたいなのが面白い。 newspicks.com

CodeZine

プログラミングに役立つソースコードと解説記事が満載な開発者のための実装系Webマガジン codezine.jp

TechFeed

"地上最強"のエンジニア向け情報サービスらしい
TechFeed Pro - "地上最強"のエンジニア向け情報サービス

Menthas

プログラマ向けのニュースサイト menthas.com

エンジニア採用・育成・組織づくりラボ by Findy

Findy(ファインディ)Inc.が運営するCTO等のエンジニアマネジメント層や人事・採用担当者向けのメディア あんまり見ないけど一応。 blog.findy.us

https://techfeed.io/

クックパッド開発者ブログ

techlife.cookpad.com

食べチョク開発者ブログ

tech.tabechoku.com

【Rails】RailsでAPIを叩く基本編

APIの特徴などを軽く抜粋(dysonさんメモ)

  • APIインターフェイス=エンドポイントと思っておk
    →要はURL(http://localhost3000/users/1など)

  • APIを叩く=HTTPリクエストを送る

  • 決められたインターフェースに適合するようにリクエストを送ると、それに応じたレスポンスが返ってくる

  • 外部のAPIを使う
    →あるパラメータをあるエンドポイントに送ると、あるレスポンスが返ってくる。

  • データベースに情報を持っている
    →それを皆に使ってもらえるようにAPIを作る

  • 決められたインターフェースに適合するように=こういう条件を満たすように

  • JSON形式のデータを返すことが主流 render json: hoge

jsonのserializerとは?

jsonを生成する仕組みのこと。
→この生成したJSONのデータをレスポンスとして返す。

ActiveRecordは ActiveModel::Serializationをinclude している。なので、以下のようにrender jsonと、そのまま記述できる。

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        render json: articles
      end
    end
  end
end
  • PostmanでAPIを叩くとJSONレスポンスが返ってきていることが分かる。
    f:id:ryota21silva:20200726122953p:plain
    PostmanでAPIを叩いた

今回実装してみる要件

fast_jsonapiというserializerを使って、 記事一覧のjsonレスポンスを返す。

  • Articleモデル。Userモデルと一対多の関係。
(app/models/article.rb)
class Article < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy

  has_one_attached :eye_catch
  enum status: { draft: 0, in_review: 10, published: 20, archived: 30 }
end
  • Articleモデルのシリアライザーを作る
$ be rails g serializer Article title contents status                                    
Running via Spring preloader in process 21798
      create  app/serializers/article_serializer.rb
(app/serializers/article_serializer.rb)
class ArticleSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :contents, :status
  belongs_to :user
end
(app/controllers/api/v1/articles_controller.rb)
# frozen_string_literal: true

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        json_string = ArticleSerializer.new(articles).serialized_json
        # 上と同じ
        # json_string = ArticleSerializer.new(articles).serializable_hash.to_json
        render json: json_string
      end
    end
  end
end

PostmanでAPIを叩いてみると、ちゃんとデータが取れた。

{
    "data": [
        {
            "id": "1",
            "type": "article",
            "attributes": {
                "title": "title-1",
                "contents": "contents-1",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "1",
                        "type": "user"
                    }
                }
            }
        },
        {
            "id": "2",
            "type": "article",
            "attributes": {
                "title": "title-2",
                "contents": "contents-2",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "2",
                        "type": "user"
                    }
                }
            }
        },
...
......
........

Userログインを叩くとこんな感じ f:id:ryota21silva:20200726123531p:plain


関連モデルのデータを返す

  • ArticleSerializer.new(@article).serialized_jsonの場合
def show
        # options = { include: [:user, :'user.name', :'user.email'] }
        json_string = ArticleSerializer.new(@article).serialized_json
        render json: json_string
      end

以下のようなJSONレスポンスが返ってくる。記事に関連付いたユーザーのidも返ってくる。

{
    "data": {
        "id": "1",
        "type": "article",
        "attributes": {
            "title": "title-1",
            "contents": "contents-1",
            "status": "draft"
        },
        "relationships": {
            "user": {
                "data": {
                    "id": "1",
                    "type": "user"
                }
            }
        }
    }
}
  • options = { include: %i[user user.name user.email] }ArticleSerializer.new(@article, options).serialized_jsonの場合
def show
  # options = { include: [:user, :'user.name', :'user.email'] }
  options = { include: %i[user user.name user.email] }
  json_string = ArticleSerializer.new(@article, options).serialized_json
  render json: json_string
end

記事に関連付いたユーザーのレコードも返ってくる。

    "data": {
        "id": "1",
        "type": "article",
        "attributes": {
            "title": "title-1",
            "contents": "contents-1",
            "status": "draft"
        },
        "relationships": {
            "user": {
                "data": {
                    "id": "1",
                    "type": "user"
                }
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "user",
            "attributes": {
                "name": "user-1",
                "email": "user-1@example.com"
            },
            "relationships": {
                "articles": {
                    "data": [
                        {
                            "id": "1",
                            "type": "article"
                        }
                    ]
                }
            }
        }
    ]
}

qiita.com

リクエストスペックの書き方も

require 'rails_helper'
RSpec.describe "Api::V1::Articles", type: :request do
  let!(:user) { create(:user) }
  
  describe "GET /api/v1/article/:id" do
    let!(:article) { create(:article, user: user) }
    it "掲示板詳細を返す" do
      get api_v1_article_path(article.id), headers: {
        'CONTENT_TYPE': 'application/json',
        'ACCEPT': 'application/json'
      }
      json = JSON.parse(response.body)
      expect(response).to be_successful
      expect(response).to have_http_status(200)
      expect(json["data"]["id"].to_i).to eq(article.id)
      # to_iでid='1'という文字列を数値に変換する
      # digを使う
    # expect(json["data"]["relationships"]["user"]["data"]["id"].to_i).to eq(article.user_id)
      expect((json.dig("data", "relationships", "user", "data", "id")).to_i).to eq(article.user_id)
    end
  end