すずかのプログラミング勉強記

元教員からエンジニアを目指す、プログラミング勉強記録です。

Action Mailerのスキップ時に発生するZeitwerk::NameErrorの解決方法

はじめに

Rails でアプリケーションを作成中、あるgemのコマンドを実行するとZeitwerk::NameErrorが発生しました。

expected file /Users/suzuka/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/devise-4.9.3/app/mailers/devise/mailer.rb to define constant Devise::Mailer, but didn't (Zeitwerk::NameError)

調べたことの理解を深めるため、解決した手順をまとめます。

実行環境


エラーの原因

アプリケーションを作るときに、メール機能を使う予定がないため、Action Mailer をスキップしたことです。

rails new sample_app --skip-action-mailer

調べたこと

Action Mailerが無効な場合、Devise::Mailerクラスは定義されない

エラー文にある devise/mailer.rbのファイルを見に行ったところ、以下のようなコードが書かれていました。

deviseはActionMailerが定義されている場合のみ、Devise::Mailerクラスを定義しています。

if defined?(ActionMailer)
  class Devise::Mailer < Devise.parent_mailer.constantize
    include Devise::Mailers::Helpers
  ......
  end
end

このアプリではAction Mailerをスキップしているので、Devise::Mailerクラスは定義されません。

Zeitwerkはファイル名からクラス名を推測する

エラー文にある、Zeitwerkとは何なのでしょうか?

Rails6から導入された、命名規則に則ったファイルを自動で読み込むgemで、「ツァイトヴェルク」と読むようです。このgemがあることによって、requireをたくさん書かなくても、必要なファイルを読み込めるようになります。 github.com Rails6以降、デフォルトで読み込みはzeitwerkモードで実行され、ファイル名と異なる命名規則のモジュール名・クラス名が定義されていた場合はエラーが発生します。

Zeitwerk使用時の、ファイル構造と定義されているクラスとモジュールの例は、以下のとおりです。

lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo

つまり、devise/mailer.rbという名前のファイルがあることで、Zeitwerkは「Devise::Mailerクラスが定義されているはず」と判断します。しかし、このアプリでは実際にはDevise::Mailerクラスが存在しないため、Zeitwerk::NameErrorが発生しました。

解決方法

以下のIssueを参考にしました。

Rails6 without ActionMailer won't boot with zeitwerk eager-loading · Issue #5140 · heartcombo/devise · GitHub

このIssueでは、2 つの解決方法をあげていますが、今回はより簡単な「ダミーのDevise::Mailerクラスを定義する」という方法で行うことにします。

config/initializers/devise.rbに以下を追記しました。

if Rails.autoloaders.zeitwerk_enabled? && !defined?(ActionMailer)
  class Devise::Mailer; end
end

このコードによって Zeitwerkがファイル名から予測するクラスを定義することができ、エラーを解消することができました。

参考文献

Zeitwerkについて:

Zeitwerk::NameErrorの解決について:

フィヨルドブートキャンプでプログラミングの勉強をする【1年経過】

未経験からフィヨルドブートキャンプ(以下FBC)で勉強して、1年(12ヶ月)が経ちました。

3月上旬にチーム開発の作業を終え、そこから自作サービスを作り始めて開発していたら、3月はあっという間に終わっていました。

今月の振り返りと、1年間の振り返りを書いていきます😊


3月の過ごし方

5月にRubyKaigiと引越しの予定があり、取れる時間が減りそうなため、3~4月は自作サービスを何よりも優先して頑張ることに決めました😊

とはいえ、振り返ると3月は結構外出もしており、特にUrawa.rb#3の室内花見が楽しかったです!

飾り付けされたコバトン

3月末には元生徒からお誘いをもらって、昨年まで主顧問していた部活の卒業イベントにお邪魔してきました。元生徒・元同僚の努力する姿に刺激をもらい、自分も頑張ろうという気持ちになりました✨

勉強の状況

3/4~4/3で修了したプラクティスは4つです。

1ヶ月間で修了したプラクティス一覧

リソース・データ設計

技術検証をする

カンバンを作る

ペーパープロトタイプを作る

修了したプラクティス

作業が残っているプラクティスは、「Webサービスを作る」「CI」「デプロイ」「デザインレビュー」「コードレビュー」「リリース」のあと6つです。 自作サービスのリリースまで終わったら、卒業となります✨


勉強時間

学習時間は12ヶ月間(366日)累計で2257時間でした。

学習時間

月平均すると約190時間です。最初の方は学習習慣をつけるため、月200時間以上を目標に取り組みましたが、後半は時間を特に意識しなかったため微減しました。

退職時に決めていた、「最低限、仕事をしていた時間と同じ時間は勉強する」ということは達成できたと思います!


自作サービスの進捗

「YamaNotes」(仮)という、「山手線を徒歩で一周する人のための記録アプリ」を作ることにしました。

イメージはこんな感じです😊

自作サービスのイメージ

今月取り組んだこととしては、以下の通りです。

ペーパープロトタイプ作成→DBとリソースの設計→技術検証→カンバンを作る→rails new→リンターの導入→地図表示機能追加→ログイン・到着機能追加→CIの構築→テスト作成

フレームワークRails(+Hotwire)を使用し、テストはRSpecで書くことにしました。 メイン画面は一応動くようになり、現在はカスタムバリデーションの設定とテスト(ログインと到着)に苦戦中です。

できるようになったこと

  • RSpecの基本的な構文を書けるようになった
    RSpecFBCの必須のカリキュラムに入っていないので、自作サービスを作る段階で初めて勉強しました。
    「Everyday Rails - RSpecによるRailsテスト入門」という本を毎日少しずつ読み、3分の2ぐらい読み終えました。非常にわかりやすいので、実際テストコードを書くときも、この本を見ながら書いています。
    チーム開発ではMinitestを使っていましたが、私は何となくRSpecのほうが好きかもしれないです😊

  • Hotwireの基本的な使い方を理解できた
    こちらもFBCのカリキュラムにはないので、「猫でもわかるHotwire入門 Turbo編」を読んで勉強しました。(読んでいる途中に教材を書いたのがFBC卒業生の方と知り、テンションが上がりました✨)
    画面の一部だけを書き換える処理を、JavaScriptを使わずに簡単に実装できて感動しました。自作サービスでは、地図の描画・到着記録の編集・モーダルの表示などでHotwireを使っていく予定です。

  • GitHub ActionsでCIの構築をできるようになった
    コードをGitHubにプッシュしたら、自動でテスト・RuboCop・ESLintが動くようにしました。
    ドキュメントを読んで、ワークフローファイルを作って動かすところまではすんなりいきました。ただ、いざ実行してみるとMailerに関するエラー→selenium_chromeに関わるエラー→Tailwindに関わるエラーが出てて困りました😇
    その都度エラー文を読んで解決していくことで、無事動作するようにできたので良かったです。

苦労したこと

  • 開発できる環境を整えること
    「いざ作るぞ!」となってから、快適にコードを書く環境ができるまでが長かったです。
    リンターの指摘が厳しすぎる・エラーメッセージやflashが表示されない・CIの実行でエラーが出るなど、本来取り組みたいこととは異なる箇所で直したい箇所が出てきて、その都度修正を繰り返しました。
    今まで何も気にせずコードをかけていた、bootcampアプリの環境のありがたさを感じました😅
  • ログイン機能の作成
    自作サービスでは、DeviseとOmniAuthを使ってGoogleログインを実装することにしました。OAuthの解説サイト、Devise・OmniAuthの公式ドキュメントを読みながら試行錯誤しました。
    特に詰まったのが、「ログインボタンを押すとCORSエラーが出る」という問題です。Rails7系だけで起きる問題のようで、ボタンのTurboを無効化すると解決できたのですが、その情報を見つけるまでが大変でした。
    Turboについては、どこでどのように実行されるのかがよくわかっていないので、もう少し修行したいと思います😇

イベント参加など

  • Urawa.rb #3を主催
    第3回目は「室内花見会」と称して、もくもく会と懇親会を行いました。 童心に帰って沢山遊び、参加者の皆さんと親睦を深めることができました🌸
    活動の様子はこちら:Urawa.rb #3 活動報告 | Notion

  • FBC内のLT会で登壇
    テーマが「最近面白かったこと・もの」だったので、地域.rbの立ち上げについてお話をしました。Urawa.rbに興味を持ってくれる方がいたので、嬉しかったです。
    FBC内のLTは今回で4回目で、だいぶ慣れました。次は地域.rbなど、外部のLTに挑戦したいです!

1年間の振り返り

勉強を継続できた嬉しさと、1年間フルコミットしても卒業できていない悔しさが入り混じっています😅
1年間でできたこと・できなかったことをまとめておきます。

  • 入学時のバックグラウンド:

    • 高校の国語教員を退職直後。
    • 退職金でMacを購入し、使い方がよくわからない。
    • プログラミングの勉強歴ほぼなし。ProgateでHTML・CSSをちょっとやった。
  • 1年間でできたこと:

    • Railsで簡単なアプリケーションを作るために必要な知識をつけること。
    • 毎日日報を書くこと。(366回)
    • 毎月振り返りブログを書くこと。(12回)
    • FBCの皆さんと交流や、輪読会などで一緒に勉強すること。
    • 現役エンジニアさんの知り合いを作ること。
  • できなかったこと:

    • FBCを1年で卒業すること。
    • 定期的に技術記事を書くこと。
    • FBC外のイベントでLTをすること。

お世話になっているメンターさん、受講生・卒業生の皆さんには、大変感謝しています。卒業までもう少し、よろしくお願いいたします🙇‍♀️

4月の目標

  • 一通りの機能を完成させる。
  • デザインレビューを提出する。

フィヨルドブートキャンプでプログラミングの勉強をする【11ヶ月目】

フィヨルドブートキャンプ(以下FBC)で勉強して、11ヶ月目が終わりました。

2月はチーム開発の山場で、先月より難しめのIssueやレビューにも取り組みました!振り返りを書いていきます😊


2月の過ごし方

相変わらず寒い日が多いので、引きこもり気味で過ごしていました。 2月上旬に雪が降ったので、雪だるまを作りました。

雪だるまの写真

材料について、質問されることがあったので書いておきます。

ペットボトルのコーラの蓋(マジックで黒く塗りつぶす)
切れたヘアゴ
にんじんのしっぽ
ほっぺ 100均の造花の一部
マフラー 細めのタオル

雪だるまを作りたい方の参考になると嬉しいです😊

FBCのチーム開発について

今月はチーム開発中心に進めたので、FBCのチーム開発の中身を簡単に書いておきます。

FBCでの学習は、「bootcamp」というWebアプリを使っています。提出物や日報の作成・質問・イベントのお知らせなど、学習の大半はこれを使って行います。 github.com

チーム開発のプラクティスまで到達すると、受講生自身でこのbootcampのリポジトリにPRを作り、バグの修正・新機能の追加などを行います。 つまりFBCの学習サイトは、先輩方が書いたコードの積み重ねでできているということです✨(すごい!)

開発は、以下の流れで進みます。

Issue割り振り→作業→PR作成(→必要ならデザイン依頼)→受講生レビュー→メンターさんレビュー→マージ→ステージング環境で動作確認→本番環境で動作確認

Issueには難易度に応じてポイントがつけられていて、20pt貯まったら修了となります。私の感覚ですが、簡単なものだと1pt、3pt以上だと苦戦するかなという印象です。

詳しくはこちらもご確認ください。 bootcamp.fjord.jp

勉強の状況

2/4~3/3で修了したプラクティスは1つだけです。

1ヶ月間で修了したプラクティス一覧

どんなサービスを作るかを考える

修了したプラクティス

ラクティスは進んでいませんが、チーム開発はだいぶ進捗がありました。今の状況としては、以下のとおりです。

チーム開発のポイント数

ざっくりいうと、作業が終わっているのが15pt、作業中なのが3pt、これから取り組むのが2ptです。作業自体は、あと2つのIssueに取り組めば終わりとなります。


勉強時間

学習時間は11ヶ月間(336日)累計で2065時間でした。

学習時間

学習時間が2,000時間に到達しました。 「まだ卒業できないの?」という声が聞こえてきそうです💦

FBC卒業までにかかる期間は、入学時点のプログラミング経験・深掘り学習の度合いなどにより、かなり幅があるように感じます。

深く理解するために時間をかけて卒業される方も沢山いるので、私も焦りすぎずに進めていきます😊


できるようになったこと

  • 仕様が決まりきっていないIssueを進められるようになった
    先月は「文字を修正する」などの簡単なIssueが中心でしたが、今月は「ポートフォリオ一覧ページを追加する」という、使い勝手が大きく変わるIssueにも取り組みました。
    割り振られた時点で、表示する項目やURL、既存のコードとどこまで共通化するか等が決まってなかったので、迷った箇所は相談しながら進めました。 テキストコミュニケーションの難しさを感じることが多々ありましたが、以前よりは慣れてきました!

  • Net::HTTPやnokogiriを使って、スクレイピングができるようになった
    bootcamp上でGitHubの草(Contribution)が表示されていたのですが、GItHubの仕様変更により表示されなくなっていました。バグを修正するため、Net::HTTPライブラリとgem「nokogiri」を使用したコードの修正を行いました。
    「草を取得して表示できたが、GitHub上の草とほんの少し異なる」というバグに苦しみましたが、無事原因がわかり、 PRの作成→レビューまで進めることができました。

  • 自分でIssueを作れるようになった
    普段自分が使っているシステムなので、自分でIssueを立てることにも取り組みました。
    立てたIssueの中には、私自身にアサインしてもらえたものもあり、自分の意見でbootcampが変わっていくのが非常に嬉しかったです😊

苦労したこと

  • 難しめのレビューを依頼された時、コードを理解できない
    フロントエンドの知識不足で、ReactやVue.jsが関わるレビューの依頼をいただいた際、なかなか理解できず時間がかかってしまいました💦
    レビュイーの方をお待たせしたくないと思う一方で、Approveには責任が伴うことも感じています。今は、多少時間がかかってもコードの流れを丁寧に追い、数回読んでも理解できない部分を質問するという方法で進めようかなと思っています。
  • バグの再現や検証が難しい
    現在、bootcamp本体ではなく、bootcampのリポジトリ上で動いているGitHub Actions・関連する別のWebアプリのバグ修正に取り組んでいます。
    本体のリポジトリを汚さないよう、検証用のリポジトリとWebアプリを用意して、バグの再現に取り組みました。色々実験してもバグを再現させることができず、右往左往しました。
    今後はバグ修正に取り組む際は、闇雲に手を動かすのではなく、「ログや状況を整理して仮説を立てる」のを意識したいと思います。

イベント参加など

  • Urawa.rb #2を主催
    2回目も参加者の皆さんがとても素敵な方々で、和やかな時間を過ごせました。チーム開発のIssueのアドバイスもいただき、大変助かりました🙏
    第3回はいつものもくもく会にプラスして、エア花見会もやります🌸まだ人が集まっていないので、これを読んでいる皆様、もしご興味があればお申し込みください!(切実) urawarb.connpass.com

  • Rails Girls Tokyo 16thにスタッフとして参加
    昨年ガールズとして参加したRails Girlsに、今年はスタッフとして参加させていただきました✨
    プログラミングの楽しさや、Rubyコミュニティの素晴らしさを感じることができ、貴重な経験となりました。これについてはどこかのタイミングでブログを書きたいと思っています。

  • 就職相談や企業説明会に参加
    今まで目を逸らしがちでしたが、そろそろ就活のことを考えなければならなくなりました😅
    個別の就職相談を初めてお願いし、1時間以上親身に相談に乗っていただきました🙇‍♀️
    加えて合同企業説明会に参加し、4社の説明を聞いたり、直接質問したりしました。就職に向け、実力をつけることを意識しつつFBC卒業を目指します!

今の気持ち

チーム開発が終盤に差し掛かり、「早く卒業したい」という気持ちが増してきました。 入学時点では卒業がイメージできなかったことを考えると、結構進んだな思います😊

時間は無限にあるわけではないのでスピードも意識しますが、「スキルをつけて幸せなエンジニアになる」という目標を常に忘れずにいようと思います!

3月の目標

  • チーム開発を終わらせる。
  • 自作サービスの計画を立てる。

「追いかけるメソッド」は定義できるが「猫クラス」はエラーになる【Ruby】

はじめに

輪読会で「現場で使える Ruby on Rails 5速習実践ガイド」を読んでいると、Rubyの文法説明の例で「猫クラス」・「追いかけるメソッド」が出てきました。

実際に irbで「猫クラス」を作ったところ、class/module name must be CONSTANT (SyntaxError)というエラーが出て、定義できませんでした。

irb(main):* class 猫
irb(main):> end
/Users/suzuka/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/irb-1.11.2/lib/irb/workspace.rb:117:in `eval': (irb):1: class/module name must be CONSTANT (SyntaxError)

同じ日本語でも、「追いかけるメソッド」は定義できたため、「猫クラス」を定義できないのを不思議に思いました。

irb(main):* def 追いかける
irb(main):> end
=> :追いかける

この記事では、「猫クラス」が定義できない理由について、調べたことをまとめます。

初心者が書いた記事のため、間違いなどあれば指摘していただけると嬉しいです。


実行環境


結論

クラス名はアルファベット大文字始まりでなくてはいけないようです。

「猫クラス」はエラーになりますが、「A猫クラス」は定義できました。

irb(main):* class A猫
irb(main):> end
=> nil

メソッド名には「アルファベット大文字始まり」の決まりがないため、日本語はもちろん、絵文字や記号でも定義できました。

irb(main):* def 猫
irb(main):> end
=> :猫
irb(main):* def 🐈
irb(main):> end
=> :🐈
irb(main):* def -
irb(main):> end
=> :-


クラス名が大文字始まりの理由

では、なぜクラス名は大文字始まりでなくてはいけないのでしょうか?

「猫クラス」を定義した時の、エラー文を見てみましょう。

class/module name must be CONSTANT (SyntaxError)
訳:クラス/モジュール名は定数である必要があります (構文エラー)

一見クラス定義には関係なさそうな、「定数」という言葉が出てきました。

定数は大文字で始める

Ruby の変数・定数は、最初の一文字によってローカル変数・インスタンス変数・クラス変数・グローバル変数・定数のどれかに区別されます。

その中で、アルファベット大文字 (A-Z) で始まるものが定数です。

クラス定義では定数への代入が行われる

では、定数とクラスの定義はどんな関係があるのでしょうか?
公式ドキュメントに説明がありました。

クラス定義式はクラスオブジェクトの生成を行うと同時に、名前がクラス名である定数にクラスオブジェクトを代入する動作をします。クラス名を参照することは文法上は定数を参照していることになります。 変数と定数 (Ruby 3.3 リファレンスマニュアル)

少し説明が難しいので、「Catクラス」が定義された場合を例に説明します。

class Cat
end

このコードが実行された時、以下の 2 つの処理がされています。

  • 新しくクラスオブジェクトを作る。
    クラスもオブジェクトの一つで、Classクラスのインスタンスです。
  • クラスオブジェクトを定数「Cat」に代入する。

下のようなコードを思い浮かべると、オブジェクトの作成と代入のイメージしやすいかもしれません。(この書き方でもクラスを定義できます)

Cat = Class.new

つまり、クラス定義ではクラス名と同名の定数への代入が行われており、定数はアルファベット大文字で始まる必要があります。

まとめ

  • 「猫クラス」を定義するとエラーになる理由は、Rubyクラス名はアルファベット大文字始まりである必要があるから。
  • クラス名がアルファベット大文字始まりでないといけない理由は、クラス定義の際に内部で定数への代入が行われているから。


おまけ:「猫クラス」を作るには?

どうしても「猫クラス」を作りたかったので、方法を調べました。

1. gemを検討する

ruby-japanizeという、Rubyを日本語で書けるようにする素敵なgemがありました。

github.com

私の環境では Rubyのバージョンを切り替えてもエラーが出てしまい、動きませんでした。 下のようなコードが書けるみたいなので使ってみたかったです 🥲

  組(数値) {
  定義(:足す) {|他の数|
    自分.+(他の数)
    }
  }

  ある数 = 5.足す 6


2. 一度変数に代入する

「猫」という変数にクラスオブジェクトを代入するようにしたところ、猫.newができるようになりました。

猫 = Class.new do
  def 鳴く
    puts 'にゃー'
  end
end

タマ = 猫.new
タマ.鳴く
# => にゃー

ただし、名前なしのクラスオブジェクトを「猫」という変数に代入しているだけなので、クラス名を調べると「nil」になっています。

"文字列".class.name
# => "String"

タマ.class.name
# => nil

真の猫クラスを定義できたわけではなく、見た目上猫.newができているだけです😅

できる限り日本語にした

偽物とはいえ、せっかく猫.newができるようになったので、できる限り日本語のコードを書いてみました。

  • 準備:エイリアスの設定と、猫クラスの中に属性・メソッドを追加
alias 表示する puts

猫 = Class.new do
  attr_reader :名前

  def initialize(名前)
    @名前 = 名前
  end

  class << self
    alias 作る new
  end

  def 鳴く
    表示する 'にゃー'
  end
end
  • コードを日本語で動かす
ある猫 = 猫.作る('タマ')
ある猫.鳴く
表示する ある猫.名前
# => にゃー
# => タマ

日本語化はとても難しくて、初心者には短いコードで限界でした。

どうしても日本語でコードを書きたい時は、日本語プログラミング言語の「なでしこ」を使うのがいいと思いました。 nadesi.com

参考文献

フィヨルドブートキャンプでプログラミングの勉強をする【10ヶ月目】

フィヨルドブートキャンプ(以下FBC)で勉強して、10ヶ月目が終わりました。

1月からチーム開発に参加していますが、おかげさまでどうにか生き残っています。 今月も振り返りを書いていきます✨


1月の過ごし方

年末年始はアクティブに過ごしたので、その後は家に引きこもっていることが多かったです。
勉強中は寒さに耐えられず、作業机とこたつにいる時間が半々でした😅

あまり引きこもると体に悪いと思ったので、1月下旬に四谷にお出かけしました。 赤坂離宮の写真 初めて赤坂離宮に行ったのですが、噴水と建物が非常に綺麗で、貴族になった気持ちでした✨

勉強の状況

1/4~2/3で修了したプラクティスは5つでした。

1ヶ月間で修了したプラクティス一覧

アジャイル開発 /スクラム を理解する

開発に参加するための準備をする

Contextを使ってグローバルなstateを管理する

Webセキュリティ

ReactでSPAを作る

修了したプラクティス

残すところはあと「チーム開発」「自作サービス」のみです。いよいよ終盤に差し掛かりました。(噂によると、この後も長いらしいですが😅)


勉強時間

学習時間は10ヶ月間(307日)累計で1907時間でした。 学習時間

今月だけの勉強時間で見ると、約180時間で、年末でサボり気味だった12月とほぼ同じです。

年末年始休みで体力が回復したため、一月上旬は勉強時間が多めでした。 ただ下旬は、後述しますが、チーム開発の待ち時間を有効活用できなかったせいで、勉強時間が減ってしまいました😅


できるようになったこと

  • Reactでグローバルなstateを扱えるようになった
    Reactのメモアプリにログイン機能を追加し、Contextとカスタムフックを使って、ログイン状態を複数のコンポーネントで参照するようにしました。
    カスタムフックの使い方はドキュメントを読んでもなかなか理解できず、試行錯誤しましたが、実装できた時には簡潔に書けて感動しました。
  • Railsのセキュリティ対策について、基礎的な知識をつけられた
    Rails セキュリティガイドに一通り目を通し、セキュリティ対策が甘いブログを修正する課題に取り組みました。
    課題に加えてSQLインジェクションのまとめ記事を書いたことで、より理解を深められたと思います。
    RailsのSQLインジェクションを実験したまとめ - すずかのプログラミング勉強記

  • アジャイル開発」「スクラム」について自分の言葉で説明できるようになった
    本を二冊読み、理解した内容をレポートにまとめました。
    コピペではなく自分の言葉でまとめるため非常に時間がかかりましたが、図解することで理解が深まり、実際の現場へのイメージもわきました。

    作成した図 ⚠️FBC生はネタバレ注意

  • チーム開発でGood First Issueが初マージされた
    1/24の開発ミーティングからチーム開発に入り、2つのIssueがマージされました。
    初めて取り組んだIssueは「不要なボーダーを一つ消す」というものでしたが、普段使っているBootcampアプリで実際にボーダーが消えた時には感動しました✨

苦労したこと

  • GitHubやCircleCIの操作
    今までもGitHubは使っていましたが、リモートリポジトリとローカルの差を意識したり、レビュアー側で操作したりするのが初めてでした。
    ミスをしてプルリクエストをCloseすることはありましたが、手順のドキュメントや先輩方の日報がかなり丁寧なので、自信のない操作をその都度確認して乗り切りました。
  • コミュニケーションの取り方
    チーム開発では、開発ミーティングをはじめレビューや質問などで、コミュニケーションをとる機会が沢山あります。前職では対面で話すのが殆どだったので、オンラインのコミュニケーションの正解が分からず戸惑いました。
    特に現在取り組んでいるIssueは、Good First Issueより仕様が定まっていなかったり、技術的にわからないことがあったりして、どのタイミングで相談・質問するのか悩みました。
    でも、初歩的な質問をしても真剣に答えていただけるので、自分の中で質問へのハードルがかなり下がりました。
  • 待ち時間の活用
    チーム開発に入って最初の週は、どこで待ち時間が発生するのか分からず、CIの通過や質問の回答・新しいIssueの割り振りを待っているだけの時間が発生してしまいました。
    手が止まっている時間を最小化するため、今月は計画的にIssueを割り振ってもらおうと思います。

イベント参加など

  • Urawa.rb #1を主催
    沢山の方に拡散してもらったおかげで、満席の状態での開催でした。第一回目なので夫婦ともに緊張していましたが、参加者の皆さんが素敵な方々だったおかげで、楽しい時間になりました✨Urawa.rb #1の様子 | Notion
    あと数回開催したら、立ち上げまでの経緯を含めアウトプットしたいと思っています。
  • Omotesando.rbに参加
    1月が初参加で、LT会と懇親会に参加しました。自分には難しいLTもありましたが、それも含めて非常に勉強になり、特にテストカバレッジやN+1問題などは自作サービス・チーム開発に役に立ちそうです。
    懇親会では現役のエンジニアさんと沢山お話ができ、お仕事の話を伺ったり、就活のアドバイスを頂くことができました。
    居心地が良かったので、2/1の回にも参加し、こちらも有意義な時間になりました。

今の気持ち

FBCの卒業生の皆さんは、「チーム開発が一番楽しかった」と言っている方が多かったので、自分がチーム開発を楽しめるのか心配でした。今はまだ必死ですが、必死の中に楽しさを感じる瞬間が出てくるようになりました✨

これから難しいIssueやレビューが割り振られるようになると、当然辛くなる時期もあると思いますが、乗り越えられそうな気がしています!

今後もよろしくお願いします🙇‍♀️

2月の目標

  • チーム開発に全力で取り組む。
  • 自作サービスのペーパープロトタイプを作る。

RailsのSQLインジェクションを実験したまとめ

はじめに

Railsの勉強中、SQLインジェクションについての理解が浅く、つい危険なコードを書いてしまうことがありました。

この記事ではサンプルアプリを作って実際にSQLインジェクションを起こし、クエリを確認した結果をまとめます。 初心者が勉強のために書いた記事のため、間違いがあれば指摘していただけると助かります。


結論

Active Recordの検索メソッドwhereを使う時、条件文字列の中に変数を直接置くのは、SQLインジェクションの危険があるため、やってはいけない。

プレースホルダ を使うなど、自動的にエスケープされる書き方にする。

Project.where("name = '#{params[:name]}'") # NG
Project.where("name = ?", params[:name])   # OK


実行環境


サンプルアプリの準備

scaffoldを使い、必要最低限の実験用アプリを作りました。

scaffoldした時点のサンプルアプリ

コード:GitHub - SuzukaHori/Sample-app-for-experimentation

データベースは、memosテーブルにuser・content・draftのカラムがあります。 draftがtrueの場合はStatusに「下書き」、falseの場合は「公開」と表示されています。

データが3件入っており、長男と三男のメモは公開状態(水色)、次男のメモは下書き(ピンク)です。後ほど下書きメモは表示されないようにします。

このアプリに検索フォームを作り、SQLインジェクションを発生させ、発行されるクエリを確認していきます。


検証

1. 初めの状態を確認する

scaffoldした時点での、コントローラーのindexアクションのコードを抜粋します。

def index
  @memos = Memo.all
end

発行されるクエリは下の通りです。

SELECT "memos".* FROM "memos"
/* 訳: テーブルmemosからすべての列を選択する。*/


2. 下書きのメモを見えないようにする

このままだと下書きのメモまで見えてしまうので、draftカラムがfalseのメモだけが表示されるようにします。

コントローラーのindexアクションを以下のように書き換えます。

@memos = Memo.where(draft: false)

発行されるクエリは下の通りです。

SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0
/* 訳: テーブルmemosからdraftカラムがfalseのものを選択する。*/

0はfalseを示すので、下書き状態のメモだけが選択されます。

次男の下書きのメモは表示されなくなりました


3. 検索フォームで絞り込めるようにする

今回は検索フォームでSQLインジェクションを発生させるので、index.html.erbにコードを追加し、フォームを作りました。

コード

  <%= form_with url: memos_path, method: :get do |form| %>
      <%= search_field_tag :term, params[:term]%>
      <%= submit_tag 'Search', name: nil %>
  <% end %>

このフォームでメモを検索できるようにするため、コントローラーに絞り込みの条件を追加します。(脆弱性があるコードです)

@memos = Memo.where(draft: false).where("content LIKE '%#{params[:term]}%'") 

追加したwhere("content LIKE '%#{params[:term]}%'")の意味を確認しましょう。

手書きの説明

LIKEは検索、%は0文字以上の任意の文字列を示します。 #{}は式展開で、検索された文字を示すparams[:term]の値を取り出します。
つまりwhere("content LIKE '%#{params[:term]}%'")は、「contentが検索文字列を含むメモの絞り込み」を表しています。


検索文字列に「良い」を入れて、発行されるクエリを見てみましょう。

SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0 AND (content LIKE '%良い%') 
/* 訳: テーブルmemosから、draftカラムがfalseでありかつcontentカラムが「良い」を含むものを選択する。*/

ANDは「かつ」という意味なので、「下書きではない、かつ内容に良いを含むもの」を検索します。

実際に作ったフォームで「良い」を検索してみます。

長男の「良い日のメモ」だけを検索できました


SQLインジェクションを発生させてみる

さて、上で作った検索フォームはSQLインジェクション脆弱性があります。

検索フォームに') OR 1 = 1 --を入れて、実際にやってみましょう。

SQLインジェクションが発生し、次男の下書きメモも表示されてしまいました😱


クエリを確認します。

SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0 AND (content LIKE '%') OR 1 = 1 --%')
/* 訳: テーブルmemosから、draftカラムがfalseでありかつcontentカラムが任意の文字列を含むもの、または`1 = 1`の条件を満たすものを選択する。*/

結論から書くと、OR 1 = 1SQLの一部として解釈されることによって「すべてのデータを選択せよ」という意味になっています。

WHERE以降を分解します。

手書きの解説

AND (content LIKE '%')の部分では、%は任意の0文字以上を示すため、ここでは何も絞り込みません。

問題はOR 1 = 1です。ORは「または」という意味、1 = 1は常にtrueになりますので、すべてのデータがこの条件を満たします。さらに、--は行末までに記述された文字列をコメントとするため、不要な記号%')があることによる構文エラーも出ません。


4. プレースホルダを使って書き直す

このままではいけないので、SQLインジェクション対策をしましょう。

条件文字列の中には変数でなく?を置き、第二引数で変数を指定します。

@memos = Memo.where(draft: false).where("content LIKE ?", "%#{params[:term]}%")

ここで発行されるクエリは以下のとおりです。

SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0 AND (content LIKE '%'') OR 1 = 1 --%')
/* 訳: テーブルmemosから、draftカラムがfalseでありかつcontentカラムが「') OR 1 = 1 --」を含むものを選択する。*/

SQLインジェクション対策前後で比べてみましょう。

対策前:
SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0 AND (content LIKE '%') OR 1 = 1 --%') 
 
対策後:
SELECT "memos".* FROM "memos" WHERE "memos"."draft" = 0 AND (content LIKE '%'') OR 1 = 1 --%') 

違いがわかるでしょうか?

一つ目の%の後のシングルクォーテーションが2つに変わりました。 これは、Rails内部でシングルクォーテーションをエスケープしていることに起因しています。

Ruby on Railsには、特殊なSQL文字をフィルタするしくみが組み込まれており、「'」「"」「NULL」「改行」をエスケープします。Rails セキュリティガイド - Railsガイド


対策後のクエリのWHERE以降を分解してみましょう。

手書きの説明

SQLでシングルクォーテーションを2つ記述すると、最初の'が次の'エスケープします。
つまり対策後のコードでは、AND (content LIKE '%'') OR 1 = 1 --%')の2つ目のシングルクォーテーションが3つ目をエスケープしています。そのため、'%') OR 1 = 1 --%'全体が文字列とされ、SQLインジェクションが起こりません。


対策後のフォームで') OR 1 = 1 --を検索してみます。

SQLインジェクションは発生していません

単純に「') OR 1 = 1 --」という文字列を含むメモを検索するため、何も表示されなくなりました。


まとめ


感想

今回はサンプルアプリで実際にSQLインジェクションを発生させてみて、想像以上に簡単にデータが取り出せてしまうことに驚きました。

メソッドを正しく使えばRailsが自動で対策してくれることがわかったので、特にユーザが入力した文字列を使うときは、公式のドキュメントを読み込む癖をつけたいです。

また、データを処理するコードを書いた際に発行されているクエリを確認する重要性も再認識できました。to_sqlメソッドの使い方も覚えたので、今後は活用していきます。


参考文献

自作のnpmパッケージを公開しました

FJORD BOOT CAMPでは「自作のnpmをパッケージサイトへ公開する」という課題があります。

私は学校コード検索 APIを使用して、コマンドラインツール「gakkou-search」を作ったので、成果物について書きたいと思います。

ソースコードGitHub - SuzukaHori/gakkou_search www.npmjs.com

できること

校種・都道府県・設置区分・キーワードを指定して、日本の学校を検索します。 Image from Gyazo

特定の学校の選択すると詳細を表示します。「地図を開く」を選択するとブラウザが立ち上がり、Googleマップが表示されます。 Image from Gyazo

作った理由

Web APIを使ってみたかった

今までWeb APIを触ったことが無かったため、今回は使うと決めていました。 色々と検索して学校コード検索APIを見つけ、単純に学校を検索するのが面白かったこと・リリースされたばかりであることなどからこのAPIを使ってみたいと思いました。

学校データの活用に興味があった

教員時代、他校の住所・電話番号・地図などの学校情報を検索する機会が結構ありました。 似たような学校名が多いため、検索しても一発で出てこないことがあります。

例:「川越高校」の検索結果
埼玉県立川越高等学校三重県立川越高等学校川越市立川越高等学校・埼玉県立川越総合高等学校・埼玉県立川越女子高等学校・・・・などまだまだある。

いつか学校のデータがわかりやすく管理されて、先生たちの仕事が楽になったらいいなという気持ちで作りました。

起動方法

  • 「学校コード検索 API」への登録・トークンの生成
    学校コード検索 APIのウェブサイトにアクセスし、アカウントを作成します。 ログイン後、「トークンの一覧」ページから「トークンの生成」を行ってください。

  • API トークンを環境変数に設定
    export GAKKOU_SEARCH_API_TOKEN='あなたのAPIトークン'を実行し、トークンを設定します。

  • インストール・起動
    npx gakkou-searchを実行します。

工夫したこと

地図を開けるようにした

学校情報の詳細画面で「地図を開く」を選択すると、Googleマップが開きます。 地図を開く画面のスクリーンショット

実装は以下の方法で行いました。

  • 住所データからGoogleマップのURLを作る
    Googleマップの検索のURLhttps://www.google.com/maps/search/の後ろに住所を指定すると、学校の地図が表示できました。 URLの生成にあたっては、url-joinencodeURIを使用しました。
  • ターミナルからWebブラウザを開く
    child_processを利用してシェルコマンドのopenを実行し、Webブラウザで地図のURLを開くようにしました。

情報を取捨選択した

元のAPIでは、取得できる情報や指定できる検索条件がもっと多いのですが、シンプルに検索するため使用頻度が低いと思われるものを省略しました。(検索条件の「郵便番号」や詳細情報の「本校 or 分校」など)

悩んだのは「校種」で、「義務教育学校」や「専修学校」は一般の人には馴染みが薄いかもしれないため、当初省略することを考えました。しかし「世の中には色々な学校がある」のを知ってもらうのもいいと思い直し、13種類から選択できるままにしました。

学校種リスト

  ["0", "指定しない"],
  ["A1", "幼稚園"],
  ["A2", "幼保連携型こども園"],
  ["B1", "小学校"],
  ["C1", "中学校"],
  ["C2", "義務教育学校"],
  ["D1", "高等学校"],
  ["D2", "中等教育学校"],
  ["E1", "特別支援学校"],
  ["F1", "大学"],
  ["F2", "短期大学"],
  ["G1", "高等専門学校"],
  ["H1", "専修学校"],
  ["H2", "各種学校"],

例外処理

原因不明で何度やってもエラーになることを絶対に避けたいと思いました。最低限困らない例外処理として、「APIトークンの未設定」「APIトークンの間違い」「通信の失敗」などは日本語でメッセージが出るようにしました。

苦労したこと

どのAPIを使って何を作るか決めること

学校検索APIの他にも沢山のWeb APIを触り、うち2つでは実験的なnpmを作ってみました。しかし、使ってみると想像通りに動いてくれるものは思ったより少なかったです。
APIを使う場合は、作りたいものに対する十分な機能を持っているかを下調べする重要性を感じました。

何の機能を作って何を作らないのか決めること

当初の予定では、詳細検索と簡易検索の2つを作ったり、電話番号を取得できたりようにする予定でした。加えて、APIトークンの設定は面倒なので、本当はログイン不要のAPIを探したかったです😅
ただ、かけられる時間と天秤にかけて、やりたいことが実現できる最低限の機能に絞りました。

Enquirerの仕様を把握すること

対話型のCLIを作れるライブラリです。今回は単一選択や自動補完・ユーザの入力を受け付けるものなど、複数の質問形式を使用しました。
基本的に使いやすかったのですが、質問文が大量に出るなどバグ出てしまい、英語のREADMEや関連するIssueを調べるのが大変でした。

感想

何を作るか決めるところから取り組むのは初めてでしたが、非常に楽しかったです!

やりたいことの全てが実現できたわけではありませんが、初めて自分の力でプログラムを作り、それを公開して見てもらった時の喜びはひとしおでした✨

自作サービスで何を作るか悩み中ですが、作りたいものが決まれば楽しく開発できる気がしています。引き続き頑張ります。