研修で初めてRuby on Railsを触って学んだこと

サムネイル画像

はじめに

こんにちは。2022年4月に新卒で入社しました教育事業本部サービス開発部バックエンドセクションのyunです。

2022年7月末でエンジニア新入社員研修が終わり、8月からは配属部署で配属研修を受けました。

今回の記事では、配属研修で初めてRuby on Rails(以下Rails)を触って学んだことについてお話しします。

配属研修の課題について

エンジニア新入社員研修の個人課題:「JavaScriptでの開発」

約3ヶ月間のエンジニア新入社員研修ではN予備校を使った基礎研修1・個人課題・チーム課題の3段階のプログラムが実施されました。2

その中で個人課題では、基礎研修で学んだ内容を基にブログサービスのWebアプリケーションを開発しました。

自分の場合、バックエンド側をNode.js の Express、フロントエンド側を React.js としてAPIサーバとクライアントを分離する方式で作りました。

配属研修課題1:「RailsでAPIサーバのみ構築」

配属研修の課題は、エンジニア新入社員研修で取り組んだ個人課題をRails化することでした。

N予備校を支えるマイクロサービスでは主にRuby及びRailsが使われていますが、自分はRubyを触った経験がありませんでした。

そのため、Railsでアプリケーションを作ってみることで実際に業務で使われている言語とフレームワーク、各種ライブラリーの知識を身につけることが配属研修の目標として設定されました。

配属研修課題の課題1は、個人課題で作ったExpressのAPIサーバと同じ仕様のAPIサーバをRailsで作り、個人課題のReact.jsのフロントエンドでそのまま動作できるようにすることでした。

API実装の前にOpenAPIを使ったAPI定義ファイルを作成し、grapeActiveModelSerializerscommittee-rails のgemを活用してAPIを実装することで、業務で実際に使っている技術を習得するのが課題1の目的でした。

配属研修課題2:「Railsでフロントエンドも含めた開発」

課題2は個人課題で作ったフロントエンドに依存せず、個人課題と同じ機能を持つアプリケーションをフロントエンドも含めて新しく作ることでした。

ActionControllerとViewを使ってHTMLページを返す方式の、従来のRails開発手法でアプリケーションを開発することで、Railsに関する理解を深めるのが課題2の目的でした。

作ったアプリケーションの概要

ユーザが記事を投稿したり記事に対してコメントを付けたりするシンプルなブログサービスです。

実装した機能は以下のようになります。

  • 認証関連: OAuthを利用してGitHubアカウントでログインでき、認証済みではないと一部機能の利用に制限がかかる。
  • 記事一覧: 全ユーザの記事が一覧形式で表示される。タグでの絞り込みもできる。
  • 記事CRUD: 記事の作成・閲覧・修正・削除ができる。
  • コメントCRUD: コメントの作成・修正・削除ができる。コメントは各記事の閲覧ページの下端に一覧形式で表示される。
  • タグ機能: 記事作成や修正の際に、記事のテーマに関するタグを指定できる。
  • ユーザページ: ユーザ名・アバターなどの基本情報と、ユーザが投稿した記事一覧が表示される。

課題1と課題2の開発期間は8月から10月初旬までの約2ヶ月間でした。

初めてのRails開発でしたので分からない所が多発してしまいましたが、メンターからコードレビューとフィードバックをもらいながら少しずつ前へ進めていきました。

ここからは、JavaScriptで開発した時との違いに関する自分の感想と、研修課題を取り組みながら学んだことについてお話しします。

JavaScript・Expressで開発した時との違いに関する感想

JavaScript・Expressで開発した時と比べたRubyの文法やRailsの特徴について、他にもたくさんあると思いますが、自分の中で印象深かったものをまとめてみました。

letやconstが要らない変数定義

JavaScriptでは変数宣言後の値の再代入可否により letconst で区分して変数を定義します。 例えば、const で宣言した変数に別の値を再代入する場合エラーになります。

// letを使う場合
let x = 1;
x = 2; // 宣言後に別の値を再代入できる

// constを使う場合
const y = 3;
y = 4; // TypeError: Assignment to constant variable

RubyではJavaScriptの letconst のような存在はなく、変数に何かしらの値を代入することだけで変数を定義します。

z = 5

ある変数の値が今後変更されるか否かが把握できる点で、個人的にはJavaScriptの変数宣言方式が厳密で良いと思いました。

falsyな値の違い

JavaScriptでは false, null 以外に、undefinedNaN, 0, ""(空文字列) などがfalsyな値として扱われます。

[false, null, undefined, NaN, 0, "", 1].forEach((v) => {
  console.log(!!v);
});
// false false false false false false true

一方、Rubyでは falsenil のみfalsyな値になります。

[false, nil, 0, 1, ""].each do |v|
  p !!v
end
# false false true true true

falsyな値に関する扱いについては、RubyがJavaScriptに比べて考慮する点が少なくてシンプルでした。

ブロックをそのまま変数に代入できない

JavaScriptでは関数オブジェクトを変数に代入する関数式がよく使われます。

const printValue = (v) => {
  console.log(v);
};
[1, 2, 3].map(printValue); // 1 2 3

Rubyでは do ... end のようなブロックはオブジェクトではありませんので、そのまま変数に代入しようとすると構文エラーになります。

ブロックをオブジェクト化するためにはProcオブジェクトにする必要があり、ブロックをそのまま渡すことができるJavaScriptに比べて不便だと感じました。

# 構文エラー
# print_value = do |v|
#   p v
# end
# [1, 2, 3].map(&print_value)

# Proc.newでブロックをProcオブジェクト化
print_value_with_proc = Proc.new do |v|
  p v
end
[1, 2, 3].map(&print_value_with_proc) # 1 2 3

# アロー演算子を用いたラムダでブロックをProcオブジェクト化
print_value_with_lambda = ->(v) { p v }
[1, 2, 3].map(&print_value_with_lambda) # 1 2 3

暗黙のreturn

Rubyではメソッド内の return 文を省略する書き方が主流になっています。 最後に評価された式がメソッドの戻り値になりますので、場合によってはreturnを明記する必要があります。

def num_equals_to_one?(num)
  if num == 1
    # 最後に評価された式が戻り値になるので、ここにreturnを明記しないと正しく表示できない
    return "#{num}は1です"
  end
  "#{num}は1ではありません"
end

p num_equals_to_one?(1) # "1は1です"
p num_equals_to_one?(2) # "2は1ではありません"

最初は暗黙のreturnに少し抵抗を感じましたが、課題でRubyに慣れてきた現在はreturn文がないコードも違和感なく読めるようになりました。

条件文の後置

Rubyでは条件文を文の後ろに書くことも可能です。

x = 3
p "xが1ではない場合表示されます。" unless x == 1 # "xが1ではない場合表示されます。"

条件文を後ろに記述するワンライナーのコードを読むと、英文を読む時と同じ気分になり新鮮でした。

ただ、条件文の前の式が長い場合は条件文を確認しづらいと感じました。 可読性を考慮すると、コードが横に長くならないシンプルな処理に限って条件文の後置を利用した方が良いと思いました。

フレームワークの機能が豊富

Expressは必要な最小限の機能を提供するフレームワークです。 そのため、ORMやテストフレームワーク等の機能を利用したい場合はサードパーティライブラリーを導入するのが一般的だと思います。 サードパーティライブラリー導入の場合、インストールや設定コードの作成などの手間がかかります。

RailsにはExpressと比べてWebアプリケーション開発において必要な機能が事前に備わっている印象でした。

例えば、RailsではActiveRecordというORMが内蔵されています。 データベースのテーブルに該当するモデルをRubyクラスで定義し、ActiveRecordを通してマイグレーションすることでデータベースに反映できます。 SQL文を直接書く必要なく、マッピングされているモデルオブジェクトをメソッド操作することでデータを操作できます。

他にも、テストフレームワークのMinitest、メールの送受信のためのAction Mailer・Action Mailbox、ファイルをローカルやクラウドストレージへアップロードできるAction Storageなどの機能も提供されています。

Webアプリケーションでよく使われる機能が用意されているので、開発環境構築にかかる時間を比較的節約できると感じました。

ディレクトリ構造の一貫性

Expressにはディレクトリ構造に関する厳しいルールはありません。

個人課題の場合、modelsにORMモデルの定義ファイル、routesにエンドポイントに関するファイル, controllersにエンドポイントごとの具体的な処理ファイル, servicesにデータベース操作関連のメソッドを定義したファイルを配置しました。

blog-js
├── models
│   ├── post.js
│   └── ...
├── routes
│   ├── postRoutes.js
│   └── ...
├── controllers
│   ├── postController.js
│   └── ...
├── services
│   ├── postService.js
│   └── ...
└── ...

上記のディレクトリ構造はExpressが強制したものではなく、あくまで自分の任意で作成したものでした。 好きな方式で構造を設計していくのが可能な点で柔軟ですが、共同作業する場合は設計したディレクトリ構造の意図や規則をメンバーに周知する必要があります。

一方、Railsではプロジェクト生成とともにMVC構造をベースとしたディレクトリ構造が用意されます。

blog-rails
├── app
│   ├── controllers
│   │   ├── posts_controller.rb
│   │   └── ...
│   ├── models
│   │   ├── post.rb
│   │   └── ...
│   ├── views
│   │   ├── posts
│   │   │   ├── show.html.erb
│   │   │   └── ...
│   │   └── ...
│   ├── api(カスタムフォルダー)
│   │   └── v1
│   │       ├── posts.rb
│   │       └── ...
│   └── ...
├── config
│   └── ...
└── ...

モデル定義や操作に関するファイルはmodelsに、エンドポイントごとの具体的な処理に関するファイルはcontrollersに、html描画のためのテンプレートファイルはviewsに配置する必要があります。 もちろん、上ツリーのapiフォルダーのように別途カスタマイズもできます。

各ディレクトリやファイル、コードで扱うリソースの命名にもルールがあります。

例えば、モデル名は単数形、コントローラやビューの名前は複数形にしたり、ルーティングのリソース名はコントローラと同じような名前を指定したりする必要があります。 フレームワークからのルールに従って指定しないとRailsは認識してくれません。

フレームワークから用意されている構造やルールに従って開発できる点で、開発者が決定すべきことが減り、共同作業での認識のズレを防止できるのがRailsの特徴だと感じました。

リソースベースルーティング

ExpressでRESTfulなルートを設計するためには、下のように設定します。

const postController = require("../controllers/postController");

const router = express.Router();

router.get("/posts", postController.getPostsByPage);
router.get("/posts/:id", postController.getPost);
router.post("/posts", postController.createPost);
router.patch("/posts/:id", postController.modifyPost);
router.delete("/posts/:id", postController.deletePost);

Railsでは、Expressのように手動での設定もできますが、独自のルーティング作成方法を利用した自動生成も可能です。

# config/routes.rbにて定義
Rails.application.routes.draw do
  resources :posts
end

resources にコントローラを指定するだけで、コントローラのindex、show、new、edit、create、update、destroyアクションと対応付けられたルートが生成されます。

HTTPメソッド パス コントローラ#アクション ルーティングヘルパー
GET /posts posts#index posts_path
POST /posts posts#create posts_path
GET /posts/new posts#new new_post_path
GET /posts/:id/edit posts#edit edit_post_path
GET /posts/:id posts#show post_path
PATCH/PUT /posts/:id posts#update post_path
DELETE /posts/:id posts#destroy post_path

ルートごとに、パスとして返してくれるルーティングヘルパーが生成されます。 特に研修課題2ではルーティングヘルパーをよく使いましたが、コントローラやビューでの画面遷移処理時にパスを直接記入しなくて済んだのでとても楽でした。

アプリケーションのURL設計に応じてカスタマイズもできます。

Rails.application.routes.draw do
  root "posts#index"
  resources :posts, except: :index do
    resources :comments, controller: "post_comments", only: [:create, :edit, :update, :destroy]
  end
end

例えば、あるアクションをアプリケーションのルートパスに指定したり、複数のリソースをネストしたり、特定のアクションを除外したり、指定したアクションのみ対応付けたりもできます。

Railsのリソースベースルーティングで生成したルートの全体マップは rails routes コマンドで確認できます。 annotateを利用すると config/routes.rb にアプリケーション全体のルートマップをコメント表示してくれます。

ルーティング作業の手間が少なく、コードを見てどんなアクションが実行されるかが直観的に確認できる点がExpressと違うと感じました。

課題を取り組みながら学んだこと

OpenAPIを使ったAPI定義ファイルの作成

エンジニア新入社員研修の個人課題では、APIの各エンドポイントがどんな役割をするかに関する簡略なドキュメントしか作成しませんでした。

そのため、リクエストパラメータとしてどんな値が渡されるかやステータスコードごとにどんなレスポンスが返ってくるかなど、具体的な仕様を確認するためには直接コードを見るしかなかったです。

また、個人課題ではレスポンスが期待通りに返ってくるのかに関する検証が全然行われていなかった問題もありました。

これらの問題を解決するために、本格的にAPIを実装する前にOpenAPIを使ったAPI定義ファイルを作成しました。

以下はOpenAPI定義ファイルを作成するにあたって自分が参考した記事です。

ReadableなOpenAPI定義ファイルを書く

OpenAPIを使ったRailsスキーマ駆動開発

OpenAPIを使ったAPI定義ファイルを作成した結果、API仕様を確認できるドキュメントが作成できたのはもちろん、API定義ファイルをテストコードで上手く活用できるようになりました。

また、個人課題で作ったAPIの一部のレスポンスでキャメルケースとスネークケースのプロパティが混用されていたミスも見つけることができました。

N+1問題対策

記事一覧などの機能では、記事テーブルのレコードだけではなく、ユーザレコードなどの記事に関連付けられているレコードも取得する必要がありました。

最初は何の対策もしないまま実装し、記事ごと関連テーブルのデータを取得するSQLクエリが記事の件数分発行されてしまうN+1問題が発生しました。

posts = Post.all
{ data: ActiveModelSerializers::SerializableResource.new(posts) }

# クエリ発行ログ
# SELECT "posts".* FROM "posts"
# SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
# SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
# SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1
# ...

例えば、記事一覧で全体N件の記事を取得する場合、全体記事を取得するSQL文が1回と各記事の投稿者データを取得するSQL文がN回発行されます。 全体記事件数のNが多くなるほど、発行する関連レコードのSQL文も増えるため、このままだとアプリケーションのパフォーマンス低下に繋がる恐れがありました。

N+1問題の検知のためにはbulletを使いました。 bulletは発行されるクエリを監視し、関連付けられたレコードの一括読み込みが必要なところや逆に不要なところを検知するとアラートしてくれるgemです。

bulletで特定したN+1問題を対策するためには、関連レコードを一括読み込みする必要がありました。 一括読み込みの手法としてRailsのActiveRecordには includespreloadeager_load というメソッドが存在します。

その中で includes を利用した対策を試しましたが、includes の挙動はRailsが判断して eager_loadpreload を選択するだけでした。 フレームワークに判断を任せるとクエリを制御しにくいというフィードバックをもらい、eager_loadpreload を直接指定することにしました。

posts = Post.eager_load(:user)
{ data: ActiveModelSerializers::SerializableResource.new(posts) }

# クエリ発行ログ
# SELECT
#     "posts"."id" AS t0_r0,
#     ...
#     "users"."id" AS t1_r0,
#     ...
# FROM "posts"
#     LEFT OUTER JOIN "users"
#     ON  "users"."id" = "posts"."user_id"

対策の結果、関連テーブルのレコードもまとめて取得し、無駄なSQL文の発行を減らすことができました。

今回の課題は多くのユーザが使うのを想定していないですが、業務で扱うプロダクトはたくさんのユーザが利用しています。 パフォーマンス面もちゃんと考慮した実装の重要性を感じました。

テストコードに関する考えの変化

研修課題を行う前までは、テストコードの作成にあたってコードがただしく動くことを検証するだけに集中していました。 しかし、メンターからフィードバックを受ける中で、「テストコードは将来の開発を楽にするためにもある」という目的もあることに気付きました。

後から見たときに仕様を把握する補助手段として機能したり、実装したコードを修正する際に予期しない挙動が発生するのを検知できたりするように、テストコードを書く必要があることが新たに分かりました。

最初の段階で作成したテストコードは読みづらかったです。

it 以降のテストケース名にテスト内容が端的に明記されていなく、describecontext をむやみに使っていました。

# 悪い例
context "異常系" do
  describe "name属性のバリデーション" do
    it "ユーザ名の文字列の長さが0の場合" do
      ...
    end
  end
end

# 改善例
describe "name属性のバリデーション" do
  it "ユーザ名の文字列の長さが0の場合ユーザレコードを追加できない" do
    ...
  end
end

検証したい結果が不明確で把握しずらく、要らない context でネストしてしまってコードの可読性も良くなかったです。

不要に let! での変数宣言をしてしまった箇所もありました。

# 悪い例
let!(:user) { create(:user) }
let!(:headers) do
  Auth.generate_header_with_token_by_user(user)
end
# 今後userはどこにも利用されない
# ...
end

# 改善例
let!(:headers) do
  user = create(:user)
  Auth.generate_header_with_token_by_user(user)
end
# ...
end

複数のテスト項目にまたがって使うために let! で変数を宣言しますが、上記のuserはheaders以外にどこにも参照されません。 不要な let! 変数が多くなってしまうと、無駄なコードが増えたりコードを読む人を混乱させたりする恐れがあります。

他に、より適切にmatcherを使う必要がありました。

# 悪い例
describe "get_posts" do
  context "全体記事が6件の場合" do
    let!(:posts) = create_list(:posts, 6)

    it "レスポンスで返ってきた記事件数と生成した記事件数が一致する" do
      get "/api/v1/posts"
      hashed_body = JSON.parse(response.body)
      counts = hashed_body["counts"]

      expect(counts == posts.length).to be(true)
    end
    ...
  end
end

# 改善例
describe "get_posts" do
  context "全体記事が6件の場合" do
    let!(:posts) = create_list(:posts, 6)

    it "レスポンスで返ってきた記事件数と生成した記事件数が一致する" do
      get "/api/v1/posts"
      hashed_body = JSON.parse(response.body)
      counts = hashed_body["counts"]

      expect(counts).to eq(posts.length)
    end
    ...
  end
end

テストが通らなかった場合、expect(counts == posts.length).to be(true) だとコンソール上では false しか確認できず、比較対象とどう違っているかが分かりません。

単に失敗したことだけを教えてくれるより、何がどう失敗したかを教えてくれるように書くことが重要でした。

できるだけシンプルで分かりやすく、実装コードの変更にも対応しやすいテストコードを作成することで上記の例のような問題を解決しようとしました。

その結果、テストコードの可読性が改善されテスト自体も充実したことが確認でき、今後の業務でもこの経験を忘れず生かしていきたいと思いました。

N予備校を開発する際にバックエンドチームで守っているテストコードの書き方に関しては、以下の記事をご覧ください。

N予備校開発でのRSpecの書き方指針

おわりに

約2ヶ月間の配属研修を通して、業務で使われる技術をさわりながら多くのことを学びました。

単純にRubyやRailsに関する知識だけではなく、OpenAPIの活用とN+1問題の対策、テストコードの書き方に関する認識変化など、バックエンド開発一般に関する知識も広く習得できました。

分からないところも沢山ありましたが、その度にメンターからのサポートで安心して課題に取り組めました。

まだまだ足りないところだらけですが、これからも引き続き多くのことを吸収して、N予備校サービスに貢献できるエンジニアとして成長したいです。

We are hiring!

株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。

カジュアル面談も行っています。 お気軽にご連絡ください!

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com


  1. N予備校を使った基礎研修の詳細に関しては、「N予備校をエンジニア新卒研修にも活用する」の記事をご覧ください。

  2. 2021年度の内容になりますが、ドワンゴのエンジニア新卒研修の具体的な流れ等は「2021年度 エンジニア新入社員研修のご紹介」に紹介されています。