ryota21silvaの技術ブログ

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

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

【環境構築】MySQLのパスワードが分からず、最終的にディレクトリごと削除して解決した話(オススメしない)。

(※2020/05のメモを記録)

はじめに

環境構築の際、MySQLのパスワードが分からず詰まったので、以下に解決まで至った流れを記していきます。 これはあくまで私の環境上の話なので、どこまで参考になるか分かりませんし、皆様のお時間を無駄にしたくはないので、最初に解決策と主に使用したコマンドを記載しておきます。

ご興味ある方は最後までご一読頂ければと思います。

解決策

  • mysqlの5.7系と8系がインストールされてたから8系を削除。
  • mysqlが5.7系のパスを指すようにzshrcで設定。
  • 過去にインストールしていたMySQL5.7系のパスワードが分からないから、sudo rm -rf /usr/local/var/mysqlディレクトリごと削除。
  • 改めてbrew install mysql@5.7を実行し、完全に新しいrootユーザーが作成されたことで、パスワード無しでログインできた。

主に使用したコマンド

MySQL - brew uninstall mysql - which mysql - ps aux | grep mysql - source ~/.zshrc - sudo rm -rf /usr/local/var/mysql

試したこと①とりあえずログインしようとしてみる

MySQLのバージョン確認。8系が入ってる。

$ mysql --version
> mysql  Ver 8.0.19 for osx10.15 on x86_64 (Homebrew)

パスワードが分からない。何も覚えてねえ、、

$ mysql.server start
Starting MySQL
.. SUCCESS! 

$ mysql -u root -p
> Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

$ mysql -u root   
> ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

勿論bundle exec rails db:createは実行できない。

$ bundle exec rails db:create
Access denied for user 'root'@'localhost' (using password: NO)
Couldn't create 'runteq_rails_advanced_development' database. Please check your configuration.
rails aborted!
Mysql2::Error::ConnectionError: Access denied for user 'root'@'localhost' (using password: NO)
$ which mysql
> /usr/local/bin/mysql

試したこと②MySQL8系をアンインストールし、5.7系を使ってみる

パスワードがわからんから一旦brew uninstall mysqlしてみると、8系がアンインストールされた。 which mysqlを打つとnot foundと出る。

$ brew uninstall mysql
Uninstalling /usr/local/Cellar/mysql/8.0.19... (286 files, 289.2MB)

$ which mysql
mysql not found
  • ps auxで現在実行されているプロセスを確認できる。
  • grepコマンドで文字列検索を行えるので、ps aux | grep mysqlを実行し、実行中のMySQLが無いか確認してみる。すると、mysqlの5.7系のプロセスが実行されている事が分かった。
    whichコマンドでは5.7系が見つからなかったけど、5.7系のプロセス自体は実行されているため、mysqlと打てば5.7系のディレクトリにパスが通るよう設定する。
$ ps aux | grep mysql
fune       617   0.0  0.0  4682956   2000   ??  S    30 420    1:29.79 /usr/local/opt/mysql@5.7/bin/mysqld --basedir=/usr/local/opt/mysql@5.7 --datadir=/usr/local/var/mysql --plugin-dir=/usr/local/opt/mysql@5.7/lib/plugin --log-error=ryota21.local.err --pid-file=ryota21.local.pid
fune       497   0.0  0.0  4280012      8   ??  S    30 420    0:00.04 /bin/sh /usr/local/opt/mysql@5.7/bin/mysqld_safe --datadir=/usr/local/var/mysql
fune     30815   0.0  0.0  4268280    636 s000  R+    3:27PM   0:00.01 grep mysql

zshrcexport PATH="/usr/local/opt/mysql@5.7/bin:$PATH"と記載。

export PATH="/usr/local/opt/mysql@5.7/bin:$PATH

sourceコマンドでzshrcファイルに記載されているコマンドを実行し、パスを通す。

$ source ~/.zshrc`

which mysqlと打つと、zshrcで指定したパスを指すようになった。

$ which mysql
/usr/local/opt/mysql@5.7/bin/mysql

まだpasswordを空でEnter押してもログインできない。。。

$ mysql -u root -p
> Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

MySQL5.7系もアンインストールし、再インストールしてみる。 やっぱりパスワードが分からず、ログインできない、、😭

$  mysql.server stop
Shutting down MySQL
.... SUCCESS!

mysqlの5.7系をアンインストール
$ brew uninstall mysql@5.7

mysqlの5.7系を再インストール
$ brew install mysql@5.7

mysqlを起動
$ mysql.server start

mysqlにログイン
$ mysql -uroot
> ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

$ mysql -u root -p
> Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

試したこと③:ディレクトリごとMySQLを削除

パスワードがマジで分からないので、もうディレクトリごと消してやれ、、!と決意。

一旦プロセスに残っているMySQLmysql.server stopで停止させる。

$ ps aux | grep mysql     
fune     32217   0.0  0.0  4278520    676 s000  S+    3:41PM   0:00.01 grep mysql
fune     32210   0.0  0.4  4681888  31748 s000  S     3:38PM   0:00.41 /usr/local/Cellar/mysql@5.7/5.7.29/bin/mysqld --basedir=/usr/local/Cellar/mysql@5.7/5.7.29 --datadir=/usr/local/var/mysql --plugin-dir=/usr/local/Cellar/mysql@5.7/5.7.29/lib/plugin --log-error=ryota21.local.err --pid-file=/usr/local/var/mysql/ryota21.local.pid
fune     32111   0.0  0.0  4280120    688 s000  S     3:38PM   0:00.04 /bin/sh /usr/local/Cellar/mysql@5.7/5.7.29/bin/mysqld_safe --datadir=/usr/local/var/mysql --pid-file=/usr/local/var/mysql/ryota21.local.pid

プロセスを停止する
$ mysql.server stop

grepしてみると、MySQLgrepしたというプロセスしか残ってない。 MySQL自体のプロセスは止まっているということ。

$ ps aux | grep mysql
fune     32236   0.0  0.0  4278520    672 s000  S+    3:42PM   0:00.00 grep mysql

ディレクトリごとMySQLを削除する。

MySQLをアンインストールする
$ brew uninstall mysql@5.7

ディレクトリの中に削除してはいけないものが無いか確認しておく。
$ open /usr/local/var/mysql

MySQLのディレクトリ自体を削除。
$ sudo rm -rf /usr/local/var/mysql

5.7系を再インストールしてみたら、やっとログインできた!!!

5.7系をインストール
$ brew install mysql@5.7

$ mysql.server start              
Starting MySQL
. SUCCESS! 

$ mysql -u root                    
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.29 Homebrew

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

おわりに

やっぱり環境構築は大変だと改めて実感しました。。。 いい勉強にはなったので、もっと場数踏んで、環境構築を行う速度を上げたいものですね。

参照

mergeメソッドのメモ

以下は、あるテーブルのデータと、そのテーブルの関連テーブルの外部キーを代入している。

しかし、処理が2行に分かれていて冗長。

@meal_record = current_user.meal_records.build(meal_record_params)
@meal_record.food_id = params[:food_id]

mergeメソッドを使えば、かんたんに書ける。

@meal_record = current_user.meal_records.build(meal_record_params.merge(food_id: params[:food_id]))

ブロックでも書ける。

@meal_record = current_user.meal_records.build(meal_record_params) do |meal_record|
  meal_record.food_id = params[:food_id]
end

変数の中身を確認してみる。

current_user.meal_records.build(meal_record_params)
=> #<MealRecord:0x00007f7fd4bf87f8 id: nil, meal_time: Sat, 31 Oct 2020 00:00:00 JST +09:00, created_at: nil, updated_at: nil, user_id: 2, food_id: nil>

current_user.meal_records.build(meal_record_params.merge(food_id: params[:food_id]))
=> #<MealRecord:0x00007f7fd0fc4a98 id: nil, meal_time: Sat, 31 Oct 2020 00:00:00 JST +09:00, created_at: nil, updated_at: nil, user_id: 2, food_id: 84>

current_user.meal_records.build(meal_record_params.merge(food_id: params[:food_id])).food
  Food Load (0.6ms)  SELECT `foods`.* FROM `foods` WHERE `foods`.`id` = 84 LIMIT 1
  ↳ (pry):27:in `create'
=> #<Food:0x00007f7fd0de8800
 id: 84,
 name: "アイスクリーム",
 labels: ["Food", "Ice cream", "Frozen dessert", "Dish", "Dondurma", "Cuisine", "Vanilla ice cream", "Ingredient", "Sorbet", "Gelato", "Dessert", "Frozen yogurt", "Cream", "Vanilla", "Dairy"],
 calorie: 0,
 calorie_theory: "",
 created_at: Fri, 30 Oct 2020 21:33:27 JST +09:00,
 updated_at: Fri, 30 Oct 2020 21:33:27 JST +09:00>

docs.ruby-lang.org

範囲オブジェクトrange。 ..でリテラル表現

範囲オブジェクトとは

範囲オブジェクトのクラス。範囲オブジェクトは文字どおり何らかの意味での範囲を表します。数の範囲はもちろん、日付の範囲や、「"a" から "z" まで」といった文字列の範囲を表すこともできます。 docs.ruby-lang.org

範囲の開始値と範囲の終了値を「..」で繋いで書けば、範囲オブジェクトのリテラル表現になる。

range = 1..10

range.class
=> Range

range.count
=> 10

range.map { |n| n * 2 }
=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

www.sejuku.net

mapメソッドでループする(ついでにreject(&:blank?)とflattenを使ってる)

わざわざ空配列を用意するのダサいし、冗長。

@food_lists = []

food_labels.each do |food_label|
  @food_lists = Food.search_by_label(food_label)
end

mapメソッドを使う。
reject(&:blank?)で配列からnilと空文字を除く。
flattenで多次元配列を一次元配列にする。

food_lists = food_labels.map do |food_label|
  Food.with_label(food_label)
end

# 空配列を削除し、多次元配列を一次元配列にする
@food_lists = food_lists.reject(&:blank?).flatten
 food_labels = response.responses.map do |res|
  res.label_annotations.map do |label|
    label.description
  end
end

# mapと&:を使えばシンプルに
food_labels = response.responses.map do |res|
  res.label_annotations.map(&:description)
end

techracho.bpsinc.jp

qiita.com

techacademy.jp

www.rubydoc.info

マイグレーションファイルについて

マイグレーションファイルとは

データベースの設計図。
マイグレーションファイルを実行すれば、マイグレーションファイルを元にDBが作成される。
マイグレーションファイルを作成しただけではDBに反映されない。

マイグレーションファイルを作成

rails g migration マイグレーション名
rails generate model モデル名 カラム名:型 カラム名:型
$ be rails g model ApiKey user:references access_token:string expires_at:datetime    
Running via Spring preloader in process 47115
      invoke  active_record
      create    db/migrate/20200719073831_create_api_keys.rb
      create    app/models/api_key.rb
      invoke    rspec
      create      spec/models/api_key_spec.rb
      invoke      factory_bot
      create        spec/factories/api_keys.rb

マイグレーションファイルを実行

dbにマイグレーションファイルの内容が反映された。

$ be rails db:migrate  
== 20200719073831 CreateApiKeys: migrating ====================================
-- create_table(:api_keys)
   -> 0.0065s
-- add_index(:api_keys, :access_token, {:unique=>true})
   -> 0.0020s
== 20200719073831 CreateApiKeys: migrated (0.0091s) ===========================

DBに反映されているマイグレーションファイルを確認する

upと書いているファイルがデータベースに反映されているもの(マイグレーション済みということ)

$ rails db:migrate:status

database: db/learning_api_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200627072315  Sorcery core
   up     20200627073523  Create social profiles
   up     20200627074538  Create articles
   up     20200627075238  Create comments
   up     20200719073831  Create api keys

スキーマファイルで現在のDBの構造を確認する

マイグレーションファイルの内容をDBに反映されていない状態に戻す

rails db:rollbackで、最新のマイグレーションファイルをdown状態にできる。
down状態のマイグレーションファイル = データベースに反映されていない状態にあるということ。
※up状態にあるマイグレーションファイルを削除・編集することは避けよう。

rails db:rollback

be rails db:reset

全てのテーブルを dropし、"db/schema.rb"を元にテーブルの再作成を行う。レコードは空になる。

be rails db:migrate:reset

migrateとresetを両方行う。

$ be rails db:migrate:reset   
                                                                                                                                   
Dropped database 'db/runteq_curriculum_learning_api_development.sqlite3'
Dropped database 'db/runteq_curriculum_learning_api_test.sqlite3'
Created database 'db/runteq_curriculum_learning_api_development.sqlite3'
Created database 'db/runteq_curriculum_learning_api_test.sqlite3'
== 20200627072315 SorceryCore: migrating ======================================
-- create_table(:users)
   -> 0.0037s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0021s
== 20200627072315 SorceryCore: migrated (0.0060s) =============================

== 20200627073523 CreateSocialProfiles: migrating =============================
-- create_table(:social_profiles)
   -> 0.0093s
== 20200627073523 CreateSocialProfiles: migrated (0.0094s) ====================

== 20200627074538 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0058s
== 20200627074538 CreateArticles: migrated (0.0059s) ==========================


$ rcs 
                         
Running via Spring preloader in process 60310
Loading development environment in sandbox (Rails 6.0.2.1)
Any modifications you make will be rolled back on exit
[1] pry(main)> User.first
   (0.3ms)  begin transaction
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> nil

be rails db:seed

Seedファイルのデータを、テーブルのレコードに投入

$ be rails db:seed                                                                                                                                                                      
$ rcs                                                                                                                                                                         
Running via Spring preloader in process 60874
Loading development environment in sandbox (Rails 6.0.2.1)
Any modifications you make will be rolled back on exit

[1] pry(main)> User.first
   (1.7ms)  begin transaction
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x00007fb1841d4808
 id: 1,
 name: "user-1",
 email: "user-1@example.com",
 crypted_password: "$2a$10$FWgdOQ7TTpcNzgGVxhRLfus46sJg8WP3DPi0yvj4U72rH7RW0PMxa",
 salt: "8QYWYUM7Vb-yp44eVzBP",
 created_at: Sun, 19 Jul 2020 08:06:58 UTC +00:00,
 updated_at: Sun, 19 Jul 2020 08:06:58 UTC +00:00>

Rakeタスクのメモ的な(メールの自動配信機能)

Rakeタスク

タスクファイル作成

rails g task mail_summary

タスクファイルの編集

(lib/tasks/mail_summary.rake)
namespace :mail_summary do
  desc '公開済の記事の件数と、昨日公開された記事の件数とタイトルをメールで送信'
  # task :article doだとDBには接続しない
  task article: :environment do
    # mailerのメソッドを呼び出す
    # articles = Article.all
    ArticleMailer.report_summary.deliver_now
  end
end

メールをcronjobなどから今すぐ送信したい場合は、deliver_now でいつでも実行できる。

# すぐにメール送信したい場合は#deliver_nowを使用
UserMailer.welcome(@user).deliver_now
# Active Jobを使用して後でメール送信したい場合は#deliver_laterを使用
UserMailer.welcome(@user).deliver_later

一般に、非同期キュー(.deliver_laterでメールを送信するなど)はRakeタスクに書いても動きません。Rakeが終了すると、.deliver_laterがメールの処理を完了する前にインプロセスのスレッドプールを削除する可能性があるためです。この問題を回避するには、.deliver_nowを用いるか、development環境で永続的キューを実行してください。

これをdeliver_laterにすると、メールが届かなかった。

ArticleMailer.report_summary.deliver_later

講師 deliverは非推奨、deliver_nowは同期処理、deliver_laterは非同期処理 nowだと同期処理だからキューに入らないと思う もしSidekiqとかResqueのような永続化キューを使用してない場合、rakeタスクの中でdeliver_laterするとメール届かない可能性があるみたい。deliver_laterでメールの送信処理を完了する前にrakeタスクのプロセスが終わる可能性があるかららしい。


タスク一覧

rake -vT

rake mail_summary:article              # 公開済の記事の総記事数と、昨日公開された記事の件数とタイトルをメールで送信
rake status_update:article_status          # 記事が公開待ち状態で、公開日時が過去の場合、ステータスを「公開」にする

タスク実行

rake status_update:article_status

Mailer

$ rails g mailer ArticleMailer report_summary
Running via Spring preloader in process 99411
      create  app/mailers/application_mailer.rb
      create  app/mailers/article_mailer.rb
      invoke  erb
      create    app/views/layouts/mailer.html.erb
      create    app/views/layouts/mailer.text.erb
      create    app/views/article_mailer
      create    app/views/article_mailer/report_summary.html.erb
      create    app/views/article_mailer/report_summary.text.erb
(app/mailers/application_mailer.rb)
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end
  • 良い書き方
(app/mailers/article_mailer.rb)
class ArticleMailer < ApplicationMailer
  # 共通処理を書く。
  # article_mailer/report_summary.~のビューを呼び出す

  def report_summary(articles)
    @published_articles = articles.published        
    @published_articles_yesterday = Article.published_yesterday
    mail(to: 'admin@example.org', subject: '公開記事の集計結果')
  end
end
  • 良くない書き方
# BAD:
# @published_articles_yesterday = @published_articles.where('? <= published_at && published_at <= ?', Time.current.yesterday.beginning_of_day, Time.current.yesterday.end_of_day)
# SOSO:
# @published_articles_yesterday = @published_articles.where(published_at: Time.current.yesterday.all_day)
  • scopeを定義
(app/models/article.rb)
scope :published_yesterday, -> { where(published_at: 1.day.ago.all_day) }
# メーラーメソッドでcountしておくのもあり
@published_articles_count = articles.published
(app/views/layouts/mailer.html.erb)
<html>
  <body>
    <%= yield %>
  </body>
</html>
(app/views/article_mailer/report_summary.html.erb)
<p>公開済の記事数: <%= "#{@published_articles.count}" %>件</p>


<% if @published_articles_yesterday.present? %>
  <p>昨日公開された記事数: <%= "#{@published_articles_yesterday.count}" %>件</p>
  <% @published_articles_yesterday.each do |article| %>
    <p>タイトル: <%= article.title %></p>
  <% end %>
<% else %>
  <p>昨日公開された記事はありません</p>
<% end %>

Letter Opener

https://github.com/ryanb/letter_opener ・実際にメールを送信するのではなく、デフォルトのブラウザでメールをプレビューする。 これにより、開発環境で実際にメールを送らずに済む。

(config/environments/development.rb)
# letter_opener_webの設定
  config.action_mailer.delivery_method = :letter_opener_web
  config.action_mailer.default_url_options = 'localhost:3000'
(app/views/password_resets/edit.html.erb)
<%= form_with model: @user, url: password_reset_path(@token), local: true do |f| %>
引数の@token無しでもいけた。。。
<%=form_withmodel:@user, url:password_reset_path, local:truedo |f| %>

Whenever

config/schedule.rb
# 初期設定ファイル
# このファイルを使って、すべてのcronジョブを簡単に定義することができます。

# Rails.rootを使用するために必要(cronはRailsとは全く別物だから)
require File.expand_path(File.dirname(__FILE__) + '/environment')

# cronを実行した際のログを出力するファイルを指定
set :output, "#{Rails.root}/log/cron.log"

# TODO: cronログの出力内容を記述できてないっぽい

# cronを実行する環境
set :environment, :development

every :minute do
  rake 'status_update:article_status'
end

every 1.day, at: '9:00 am' do
  rake 'mail_summary:article'
end

config/schedule.rbをcron構文に変換した内容を表示してくれてるが、crontabファイルが更新されているわけではない。

$ be whenever                                                                      
* * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake status_update:article_status --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'

* * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake mail_summary:article --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'

## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated.
## [message] Run `whenever --help' for more options.

設定されているcronを確認

$ crontab -l     

# Begin Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-14 16:09:28 +0900
0 * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake status_update:article_status --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'

# End Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-14 16:09:28 +0900

cronにデータを反映させる

$ bundle exec whenever --update-crontab                                        
[write] crontab file updated

OKを押す。

再度cronを確認するとデータが反映されていることがわかる

$ crontab -l                                                                              

# Begin Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-31 22:22:28 +0900
* * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake status_update:article_status --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'

* * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake mail_summary:article --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'

# End Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-31 22:22:28 +0900

cronからデータを削除する

$ bundle exec whenever --clear-crontab

qiita.com

qiita.com

qiita.com

qiita.com

【Ruby】procとblock

procとblockについて

block

ブロックとは

do...end{ }のカタマリのこと。

do...endと{ }

ブロックの中の|i|iはブロック引数と呼ばれるもの。 ブロックの中で| |を書くことで、eachメソッドから渡された配列の要素が順番に入ってくる。

[1, 2, 3].each do |i|
  puts i*2
end
2
4
6
=> [1, 2, 3]
[1, 2, 3].each { |a| p a }
1
2
3
=> [1, 2, 3]

値を受け取らない場合は、| |を書かなければいい。| |を使わなければ、単に配列の要素の数だけ処理が実行される。

3.times do
  p 'おはよ!'
end
"おはよ!"
"おはよ!"
"おはよ!"
=> 3
[1, 2, 3].each { p 'おはよ' }
"おはよ"
"おはよ"
"おはよ"
=> [1, 2, 3]
do...endの応用
# @meal_record = MealRecord.new(food_id: params[:food_id])
# @meal_record.meal_record_pictures = ActiveStorage::Blob.find(session[:meal_picture_id]) if session[:meal_picture_id]


@meal_record = MealRecord.new(food_id: params[:food_id]) do |meal_record|
  meal_record.meal_record_pictures = ActiveStorage::Blob.find(session[:meal_picture_id]) if session[:meal_picture_id]
end

ブロック引数

ブロック引数は引数と同じように、関数の()に入れてあげればいい。

そしてブロック引数を受け取る関数を定義するには&blockという仮引数を指定してあげるといい。この&blockでブロック引数を受け取る宣言をしているといった感じである。
注意点は、

  1. ブロック引数は1つしか受け取れない。
  2. 複数の引数が存在する場合、ブロック引数は最後の引数として指定してあげる。

以下の例では、&blockという仮引数に、puts "Hello block"というブロックが渡されている。

def receive_block(&block)
 puts "受け取ったブロックを実行する"
 # block.callで、受け取ったブロック(puts "Hello block"のこと)を実行
 block.call
 puts "実行したよ"
end
 

receive_block do
 puts "Hello block"
end
 
#出力結果
受け取ったブロックを実行する
Hello block
実行したよ

rails-study.net

proc

procとは、ブロックという処理のカタマリをオブジェクト化するためのクラスのこと。

callメソッドで呼び出せる。

hello_proc = Proc.new do
  'Hello'
end

# 上記と同じ
hello_proc = Proc.new { 'Hello' }


hello_proc
=> #<Proc:0x00007fcbae10af50 (irb):66>
hello_proc.call
=> "Hello"

引数を受け取る処理もできる。

proc = Proc.new do |message|
 puts message
end
 
proc.call("Hello proc")
=>Hello proc

以下の処理は、上記の処理と同じ挙動となる。 つまり、以下の&blockblockは、上記のブロックとProcインスタンスの関係にあるということ。以下では、内部的に受け取ったブロックをProcインスタンス化しているのである。

def receive_block(&block)
  block.call
end

receive_block do
  puts "Hello block"
end
=>Hello block

&はブロックを表す記号であり&blockのblockはブロック引数につけた仮引数名にあたるので実は何でも良い。 つまり引数として受け取ったblockという名のブロックを、blockという名のProcインスタンスに変換しているからblock.callで実行することができる。

Rubyのブロック、ブロック引数、Proc、yieldをまとめてみた – Ruby on Rails 始めました

&:method

配列の要素1つ1つに、(&:メソッド名)で与えられたメソッドを呼び出してくれる。

['a' ,'b'].map(&:upcase)
=> ["A", "B"]

docs.ruby-lang.org

以下の2つは同じ意味になる。
→do...end に限らず { |object| object.published! } も同じく短く出来る!

find_each do |object|
  object.published!
end

find_each(&:published)

qiita.com

以下であれば、array.map(&:メソッド名)の(&:メソッド名)部分は、:メソッド名.to_proc.callを省略した形。

# array.map(&:メソッド名)の記法
['a','b'].map(&:upcase) #=> ["A", "B"]

# 省略しない記法
['a','b'].map do |string|
  :upcase.to_proc.call(string)
end
#=> ["A", "B"]

さらに例文で理解する(余計ややこしいかも)

(app/models/article.rb)
class Article < ApplicationRecord
  enum state: { draft: 0, published: 1, publish_wait: 2 }

  # 公開日時が過去の日付になっている記事を取得
  scope :past_publish, -> { where('published_at <= ?', Time.current) }
end
(lib/tasks/status_update.rake)
namespace :status_update do
  desc '記事が公開待ち状態で、公開日時が過去の場合、ステータスを「公開」にする'
  task article_status: :environment do
    # 公開待ち(publish_wait)の記事の中で、公開日時が過去(past_publish)になっているものがあれば、ステータスを「公開(published!で)」に変更する
    # `Article.publish_wait`で、enum値がpublish_waitに該当するデータを取得
    @articles = Article.publish_wait.past_publish.find_each(&:published!)
    
    # 以下と同じ
    # @articles.each do |article|
    #   if article.published_at.past?
    #     article.state = :published
    #     article.save!
    #   end
    # end
  end
end

【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