rosh-1228のメモ

自身の勉強したことをメモしてます。

Railwayの紹介

フィヨルドブートキャンプ Part 2 Advent Calendar 2022 の8日目の記事です。(Part1はこちら)

昨日は、ham-capさんの「Vimが好きだ」でした。

目次

はじめに

Minoru MaedaさんがHeroku の代替 OSS を試した話を書かれておりますが、私もHerokuの代替として使用できそうなRailwayというホスティングサービスについて簡単に紹介をします。

プラン

プランは以下の3つあり、私が使ったStarter PlanDeveloper Planについて説明します。

  • Starter Plan
  • Developer Plan
  • Team Plan Offering

Starter Plan

メモリは512MB、最大1GBのディスク容量、メモリは共有型になってます。

価格は無料ですが、上限が決まっており、毎月5ドル分のリソースを使いきるか、500時間(約20日)使用することができます。

逆にいうと、5ドル分のリソースを使用するか500時間稼働させるとデプロイしているサービスが停止されます。

上記のため、毎月約10日くらいはサービスが停止することになります。

Developer Plan

メモリは8GB、最大100GBのディスク容量、メモリは8Coreになってます。

価格はStarter Planと一緒で5$まで無料で、5$を超えたリソースを使用すると課金されます。

しかし、5$まで無料であるので、それを超えるリソースを使用しない限り無料であり、サービスが月の途中で停止される心配はありません。

また、5$を超えない限りでリソースを使用しながら、スペックはStarter Planより大幅にアップデートされます。

そのため、私が自作したWebサービスFjordBootCamp Contiburotrsでは、Developer Planを使用しています。

デプロイ

Githubリポジトリと連携させることで、mainブランチにマージされたタイミングで自動でデプロイをしてくれるようになります。

また、ブランチも選択できるので、mainではないブラントをデプロイしたい時は、設定から変更できます。

CLI

Railwayは、コマンドも使うことができ、ローカルのディレクトリをRailwayにデプロイすることができます。

詳細に知りたい方はこちら

使うであろう代表的なコマンド以下に紹介します。

インストール

NPMをインストールします。

npm i -g @railway/cli

ログイン

railwayにコマンドでログインします。 以下を実行するとブラウザが立ち上がります。

railway login

ブラウザを立ち上げない場合は、以下のように--browserlessをつけます。

railway login --browserless

コマンド実行

railway runを使うことで、Railwayの環境でコマンドを実行することができます。

私の場合、railsバッチ処理を実行しており、以下の様にgithub actionsにてrailway runを使用しています。

name: run batch
run: railway run bundle exec rails runner lib/batch/contributor_commit_collector.rb
  env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

デプロイ

デプロイしたいプロジェクトディレクトリに移動し、以下のコマンドを実行します。

cd [プロジェクトディレクトリ]
railway up

これだけで、ローカル環境をRailwayにデプロイできます。とても簡単ですね!

スケジューラ

Railwayではスケジューラはサービスとしてありません。

そのため、私はGithub Actionsを使い、Railway環境でバッチ処理を実装しております。

私のworkflowファイルはこちらです。

Github Actionsは時間通り動かないことがありますが、私の自作サービスの場合は、そこまできっちり時間通りにランキングを更新することは要件として重要ではないのでGithub Actionsで十分スケジューラとして機能しています。

おわりに

以上、Railwayというホスティングサービスについての紹介記事でした。

Railwayについては日本語記事が少なくあまり見かけることが少なかったのでここで紹介させていただきました。 詳しくRailwayについて知りたい方はこちらのDocumentを参考にしてください。

個人的にはシンプルで使いやすいという印象です。

想定外の事が起こり、日がずれちゃいましたが、なんとか記事を書かせてもらいました💦

明日はchocoさんが記事を書いてくださいます。

フィヨルドブートキャンプのシステム開発プラクティスにて、今までのコミット数ランキング化したアプリ「FjordBootCamp Contributors」をリリースしました

はじめに

フィヨルドブートキャンプの最終課題である自作サービスとして、bootcampアプリにPRを送ってマージされているかをランキング形式で確認できるFjordBootCamp Contributorsをリリースしました。 今までPRを送ってマージされたことのある人達のコミット数を期間別にランキング形式で表示されています。

github.com

目次

自己紹介

rosh-1228と申します。前職はSESの会社にて、ITインフラの運用/保守とSAP BASISをメインに設計/構築/運用/保守など仕事としておりました。

プログラミングをメインに仕事をしたいと思い、FjordBootCamp(フィヨルドブートキャンプ)というプログラミングスクールにて勉強をしております。

サービスについて

FjordBootCamp Contributorsは、元となったサービスがあり、Rails Contributersというものがあります。

こちらは、Railsに対して、PRを送った人をランキング形式で表示しており、期間別に今月 今年 今週 今日 コミットした日から今までの期間で表示されてます。

FjordBootCampでもRails Contributorsのように誰がどれくらいbootcampアプリにPRを送ってマージされているか確認されたいとあり、私がサービスを作らせていただきました。

FjordBootCamp Contributorsも、今月 今年 今週 今日 コミットした日から今までの期間の5つの期間別に、FjordBootCampAppにてマージされたPRに含まれるコミットの数をランキング形式で表示しております。

FjordBootCampAppの開発を行なっている人(システム開発ラクティスに参加されている生徒やメンターの方々)向けのサービスになります。

過去にコミットを行ったことがある人も、All Timeで自分がどのくらいコミットしたのか確認できます。

ちなみにAll Timeで自分は21位でしたw

また、各コントリビューターの今までのコミット一覧を確認することができます。

自分が今までどのようなコミットメッセージをしたのかを確認し、今後もっとわかりやすいメッセージに修正しようと見直すこともできます。

このランキングで、他の人よりも技術力がある・なしを図るものではないということだけ留意いただきたいです。

どこで使用するのか?

それでは、どういった場面を想定しているかというと、以下の使用場面を想定しています。

  • システム開発ラクティスで自分の今月や今週のコミットランクを確認して、やる気をあげる時
  • 就職活動で企業へ自分のコミット(どのようなコードを書いて貢献したのか)を見せる時
  • FjordBootCampで行われるイベントで、参加者全員でランキングを見て、誰がどんなコミットをしたのかなど話して盛り上がる

contribute数の集計時間

1日4回(0時/6時/12時/18時)Githubからコミット数を取得し、ランキングを表示します。

Github Actionsを使用しているため、タイムラグが生じる可能性もあります。参考

技術スタック

開発中に苦労したこと

触ったことがあるものはRuby on Railsのみで、他の技術スタックは0からのスタートでしたので、かなり時間がかかりました。

プログラミングより環境を整えることに悩み時間をかけたと思います。

Githubから欲しい情報を入手すること

1つ目に、GithubからFjordBootCampAppのコントリビューターとコミットを全て取得する必要があり、これをGraphQLを使って実現しようとしました。

しかし、1つのリポジトリに所属しているコントリビューター全員のコミットを取得するといったことにはあまり関心を持っている方はおられず、調べても参考になりそうなものはなく、書き方だけ学びました。

そのため、Githubの公式リファレンスを参考にexplorerで何回も試しました。 ここに時間を最も使ったと思います。

また、Githubapiは1度に100件しかコミット情報を取得することができず、FjordBootCampAppは10000件を超えるコミット数でした。

全コミット数/100回も繰り返しでGithubへ問い合わせすることになり、これで本当に良いのかと悩み他の方法を調べることにも時間を費やすほどでした。

https://docs.github.com/ja/graphql/overview/resource-limitations

今回はバックグラウンドで処理を行うので、サービスの操作が重くなるようなことはありませんが、ユーザーが何らかの登録や表示といった操作を行うサービスを作る際は、操作性について必ず考慮する必要があるなと思いました。

React.jsを初めて使った

今回、FjordBootCampのカリキュラムで勉強したVue.jsではなく、React.jsを使うことにしました。 これは、Vue.jsとReact.jsを比較して使うことを決めたとかはなく、React.jsを使ったことがないので、使ってみようという興味から決めたことです。 ただ、React.jsも使ったことがなかったので、どう書けば良いのかなど初めはわからず苦労しました。 特に、webpackerを使ってビルドする際に足りないモジュールがいくつかあったりして、システムテストでReact.jsを使ったコンポーネントが画面に描画されずに悩みました。

ホスティングサービス選択

Herokuが有料になるということもあり、代わりのホスティングサービスを探すことも苦労しました。

blog.heroku.com

今回はお金をかけずにサービスを作りたいと思っていましたので、Herokuが使えないとなると完全に代替できるサービスはありませんでした。

調べるとFly.ioRailwayのどちらかが有力な候補となりそうということがわかりました。

初めはFly.ioの方が無料で提供してくれるサーバーのスペックが良さそうだと思い、試しにデプロイを行なってみましたがここで挫折して使うことをやめました。

fly.io

なぜなら、Rails.application.credentialsを使用していたのですが、Fly.io側でmaster.keyを登録しているにも関わらずデプロイ時にRails.application.credentialsがnilになってしまいデプロイがいつまでもできませんでした。

解決のための調査にも時間をかけたのですが、解決せず焦りばかりが募りました。

そこで、Railway.appを使ってみると、非常に簡単にデプロイすることができました。

railway.app

Railway.appはブラウザ上でGithubと連携するだけでデプロイをしてくれる上に、登録したmaster.keyの情報もしっかり読み込み余計な時間をかけずに済みました。

Railway.appの無料プランは、毎月5$のリソースを与えてくれるのでそれを消費しきるか、約20日連続稼働させるかすると、システムがダウンするようでした。

ただ、developerプランという1つ上のプランでは、毎月5$のリソースをくれた上でそれ以上のリソースを消費した場合に初めて課金されるというものでした。

また、連続稼働時間の制限はなくなるものでしたので、システムがダウンするよりは良いかと考え、developerプランに加入しました。

無料プランよりも良いリソースを提供されているので、それもプラスでよかった点です。

最後に

webに公開して誰かに使ってもらう事を意識してサービスを作る経験ができてとても良かったと思います。

前職でお客様へシステムを提供していましたが、すでに出来上がっているパッケージを導入したり、使いにくい・値段が高いと言われ本当にお客様が欲しいものを提供できているのかずっと疑問でした。

そのため、自作サービス進捗会で、デモを動かした時にkomagataさん、machidaさんに自分達にとって価値があるものができたと仰っていただいた時、誰かの役に立つものを提供することができた!と嬉しくなりました。

受託開発の会社に就職したいと考えている自分としては、komagataさんmachidaさんの要望に合わせて、開発を行いデモで実際の使用感の感想をいただいたり、修正を行なったりできたことも良い経験でした。

やはり、誰かの需要に応えられるエンジニアになりたいと思えましたし、これからも応えられるようにスキルを上げていきたいと改めて思いました。

このサービスがFjordBootCampAppの開発に携わる人の一助となれば幸いです。

Choices.jsでセレクトボックスを複数作る・動的に作る

概要

フィヨルドブートキャンプでChoices.jsを使ったセレクトボックスを作ることになったが、Choices.jsの記事が少なく、複数作ったり動的にセレクトボックスを作ったりしている記事がなかったので、残しておきたい。

参考

@GaramMasala29さんのqiitaの記事を参考にさせていただきました。 非常にわかりやすかったです! ありがとうございました!

あとは公式を参考にした。

Choices.jsについて

以下のような文字検索ができたり、他にもオプションをつけることで様々なことができるライブラリ。

複数のセレクトボックスにChoices.jsを適用する

複数のセレクトボックスにChoices.jsを適用する場合、私はquerySelectorAllを使ってidを全て取得し、for文を使用することで、各セレクトボックスにChoices.jsを適用できた。

f.collection_select :book_id, all_books, :first, :last, {}, { id: 'js-book-select' }
document.addEventListener('DOMContentLoaded', () => {
  const bookSelectCount = document.querySelectorAll('#js-book-select').length
  const elements = document.querySelectorAll('#js-book-select')
  for (let i = 0; i < bookSelectCount; i++) {
    // eslint-disable-next-line no-new
    new Choices(elements[i], {
      allowHTML: true,
      searchResultLimit: 20,
      searchPlaceholderValue: '検索ワード',
      noResultsText: '一致する情報は見つかりません',
      itemSelectText: '選択',
      shouldSort: false
    })
  }
})

動的にセレクトボックスを作る

動的にセレクトボックス場合もquerySelectorAllを使うことで解決できた。 動的に作る場合は、querySelectorAllで取得した最後の要素に対してChoices.jsを適用することでセレクトボックスを作ることができた。

document.addEventListener('DOMContentLoaded', () => {
    const bookSelectCount = document.querySelectorAll('#js-book-select').length
      const elements = document.querySelectorAll('#js-book-select')
      for (let i = 0; i < bookSelectCount; i++) {
        // eslint-disable-next-line no-new
        new Choices(elements[i], {
          allowHTML: true,
          searchResultLimit: 20,
          searchPlaceholderValue: '検索ワード',
          noResultsText: '一致する情報は見つかりません',
          itemSelectText: '選択',
          shouldSort: false
        })
      }
    $('.reference-books-form__add').on('cocoon:after-insert', () => {
    const elements = document.querySelectorAll('#js-book-select')
    const element = elements[elements.length - 1]
    if (element) {
      return new Choices(element, {
        allowHTML: true,
        searchResultLimit: 20,
        searchPlaceholderValue: '検索ワード',
        noResultsText: '一致する情報は見つかりません',
        itemSelectText: '選択',
        shouldSort: false
      })
    }
  })
})

出来上がったセレクトボックスはこちら。

コントローラーを分けるとシンプルに記述できて良い感じだったので習慣にしたい

今回ユーザーの就職活動中かどうかのフラグをオンオフするというissueに取り組んでいて、その際にいただいたアドバイスについてここに残しておきたい。

条件としてはユーザー情報を更新するページと記事に対してコメントするページがあったとして、記事に対してコメントするページで就職活動中かどうかのフラグをオンオフするという実装が必要だった。

私は、以下のような感じで2種類のupdate処理を1つのコントローラー内に記述してレビュー依頼を行なった。

  • user_controller(だいぶぼかしてある)
class UsersController < ApplicationController
  def update
    update_job_seek if only_job_seek?
    if @user.update(user_params)
      redirect_to users_url, notice: 'ユーザー情報を更新しました。'
    else
       ... # 何らかの処理
    end
  end

  def update_job_seek
    if @user.update(user_job_seek_params)
      ... # 何らかの処理
      return
    else
      ... # 何らかの処理
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :age, :job_seek)
  end

  def user_job_seek_params
      ... # 何らかの処理
  end

  def only_job_seek?
    ... # job_seekというパラメータのみかをチェック
  end
end

このコードをレビューいただき、アドバイスとしてDHHはどのようにRailsのコントローラを書くのかという記事をもとに別のURLを作って更新処理を行なったほうが良いとうことだった。

この記事はだいぶ前であるが、教えていただいておりすっかり忘れてしまっていた。。。(今後はしっかり覚えておく!)

こちらの記事を読み、その通りに実践したところコントローラーの記述がシンプルになり、とても読みやすいコードになった。

  • user_controller
class UsersController < ApplicationController
  def update
    if @user.update(user_params)
      redirect_to users_url, notice: 'ユーザー情報を更新しました。'
    else
       ... # 何らかの処理
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :age, :job_seek)
  end
end
  • job_seek_controller
class JobSeeksController < ApplicationController
  before_action :set_user, only: %i[update]

  def update
    if @user.update(user_params)
       ... # 何らかの処理
    else
       ... # 何らかの処理
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:job_seeking)
  end
end

上記の通り、user_controlleronly_job_seek?というメソッドを使って、job_seekというパラメータだけかどうかを判断していたが、コントローラーを2つに分けることでuser_controllerjob_seek_controllerのそれぞれがすごくシンプルになった。

1つのコントローラーにたくさんの処理を任せれば任せるほどコードは読みにくくなるし、URLの設計の観点からもPUT /userjob_seekだけをupdate するかどうかわからなくなる。

そのため、コントローラー分ける(責任を分ける)ことで、コードは読みやすく、URLの設計もPUT /job_seekでわかりやすくなる。

記事の引用の引用になるけれど、以下の言葉は確かに後の自分に怒りたくなるのだろうと思った。

自分の作ったコントローラの状態を悔やむのは決まって、作ったコントローラの数が少なすぎた時です。多くの処理を任せようとしすぎてしまうんです。

Full Stack Radioのインタビュー

ここで書いたからには、忘れないようにしたいし、コントローラーに変更を加える際にもう一度読み直したい。

before_actionのifオプションとskip_before_actionで親コントローラーのbefore_actionを無効にする

before_actionの使い方

before_actionを使うと、各アクション実行前に、before_actionで指定したメソッドが実行される。

before_action :メソッド

この使い方は知っていたけど、今回はifオプションを使うと便利だったので、ifオプションについて残しておきたい。

ifオプション

今回以下のようにbefore_actionで認証系にメソッドが使われているが、特定のコントローラだけ無効にしたい時にifオプションが使えた。

class SampleController < ApplicationController
    before_action :require_login_for_api
end

以下のように条件チェック用のメソッドをSampleControllerで用意する。

class ApplicationController < ActionController::Base
    before_action :require_login_for_api, if: :check?

    def check?
        true
    end
end

そして継承先のコントローラーでもfalseになるメソッドを用意する。

class SampleController < ApplicationController
    def check?
        false
    end
end

こうすることで、SampleControllerだけ親コントローラーのrequire_login_for_apiを無効にすることができた。

skip_before_action

先ほどのifオプションでも親コントローラのbefore_actionを無効にできるが、この場合CIを通した時に tracerouteのエラーが発生した。

そこで、skip_before_actionのコールバックを使うともっとスッキリ書くことができ、CIを通すことができた。 こちらも自分のために残しておきたい。

class SampleController < ApplicationController
    skip_before_action :require_login_for_api
end

しかし、セキュリティに穴を開けるなどであれば使う時はしっかり考えたい。

railsテストのデバッグ方法をメモ

railsテストのデバッグ方法

railsのsystemテストでハマってしまい、その時に使ったデバッグ方法を残しておく。

binding.pry

binding.pryをコード中に書くと、コード実行時にbinding.pryの場所で止めてくれるgem

ブレークポイントとして機能する。

以下のように、確認したい処理の手前に書く。

def update
    binding.pry
    if @user.update(user_params)
        成功時の処理
    else
        失敗時の処理
    end
end

すると、以下のように処理が止まる。

    18: def update
    19:   binding.pry
 => 20:   if @user.update(user_params)
    21:     成功時の処理
    22:   else
    23:     失敗時の処理
    24:   end
    25: end

[1] pry(#<Controller>)>

ここで例えば以下のように@user.update(user_params)を実行した時の処理がわかる。

実際にupdateが成功しているのか失敗しているのかわからない時などに使える。

[1] pry(#<Controller>)> @user.update(user_params)
=> false

他にもこの時点のuser_paramsの内容なども確認できる。

@user.errors.full_messages

以下のように.errors.full_messagesを使うことで先程のupdateでどんなエラーが発生していたかがわかる。

今回はユーザー情報にos:があったのだが、テストデータで用意していたユーザにos:がなかったため、エラーが発生していた。

[1] pry(#<Controller>)> @user.errors.full_messages
=> [使用しているOSを選択してください。]

これはテストだけでなく、普通に実装している時も使用できる。

binding.pryも普通に実装している時も使えるが、今回のようにテストでエラーが発生した場合もデバッグに使える。

page.save_screenshot('~~~/screenshot.png')

capybaraをインストールすることで使用できる。

テストコード中に書くことで、スクリーンショットを取得できる。 以下の場合、更新ボタンをクリック後のページをスクリーンショットで取得できる。

test 'test' do
    click_on '更新'
    page.save_screenshot('~~~/screenshot.png')
end

save_and_open_page

こちらもcapybaraをインストールして使う。

こちらもテストコードの中に記述して使用する。

save_and_open_pageを書いた時点の画面の状態をブラウザで確認することができる。

おわりに

p や putsを使ってデバッグも可能だが、更新系の処理を実際に確かめたかったり、画面の状態を確かめたい場合は、これらのメソッドを使って行きたい。

日時をテストするときは、travel_toメソッドを使うと良い

モデルテストで日時をテストするときに、以下のように今日作成されたものかどうかを確認するテストを作成していた。

report = Report.create(title: 'test_title', content: 'test_content')

assert_match Time.zone.today.to_s, report.created_at.to_date.to_s

ただ、これだとReport.createが実施された時間が23:59:59.999で、assert_matchでテストした時間が24:00:00.000とかになると、日を跨いでしまい、エラーになる可能性がある。

そこで、travel_toを使うと、日時を固定できるので便利だった。

travel_to(Date.new(2022, 1, 1)) do
      report = Report.create(title: 'test_title', content: 'test_content') # <=2022/01/01で固定される
      assert_equal Date.new(2022, 1, 1).to_s, report.created_at.to_date.to_s
    end

travel_to do ~ end の中でreportを作成して、assertionで結果を確認すれば、いつテストを行っても、日が固定されているので、日を跨いだらテストにならないといったことは心配なくなる。

【参考】

ActiveSupport::Testing::TimeHelpers

Timecopを使わなくても時間を止められた話