行動すれば次の現実

テック中心の個人ブログ

Herokuアプリの監視サービス「Pingdom」のススメ

Herokuで本格的にアプリを運用するためには、アプリがダウンせずに動き続けているのかを監視する必要があります。

Herokuアプリの死活監視アドオンとして有名なのがPingdomですが、実は死活監視以外に様々な機能を提供しており、かなりコスパが高いアドオンです。 恥ずかしながら私自身は1年間ほどPingdomを死活監視のみで使用していました。

この記事ではPingdomでできる以下の4つの機能を事例を交えながら紹介します。

死活監視(Uptime)

Pingdomではアプリが利用できる状態であるか死活監視することができます。

Heroku単体でも監視することは出来ますが、Heroku自体が落ちてしまったら監視することは困難になります。 そのため、監視サーバーは基本的には外形監視(外部のサーバーから監視する)にするのが望ましいです。

Uptimeでは指定したURLに対して細かな条件を指定してアプリの死活監視することが出来ます。

  • 死活監視の頻度(1分~60分の間)を設定できる
  • サーバーのタイプをWeb(http・https)、メールサーバー、ネットワークアクセス(ping等)の3種類から選べる
  • どの国からアクセスするか選択できる
  • 問題があった場合の通知方法と復帰した場合の通知方法(webhook、メール) など...

Uptimeのダッシュボード画面イメージ

アプリがダウンした場合にエンドユーザーからの連絡により判明するというのは絶対に避けなければなりません。なるべく早くダウンを検知することで復旧時間を早めることも可能ですし、ユーザーにとってクリティカルな業務の代替案を提案することも可能になります。

ユーザーアクセス解析(Real User Monitoring)

PingdomではReal User Monitoring(RUI)という機能により、エンドユーザーがアプリをどのような操作・体験をしているのかを可視化することができます。イメージ的にはGoogle Analyticsの高機能版という感じです。

  • アプリにユーザー何人訪れているかリアルタイムおよび履歴が確認できる
  • ユーザーが使用しているデバイスやブラウザの割合
  • ページ閲覧時のユーザーの快適度をApdexという指標で確認できる
  • ページごとのアクセス数のランキングを表示できる
  • ページのロード時間の履歴を確認できる など...

RUIのダッシュボード画面イメージ

アクティブユーザー数をリアルタイムに監視できるので、どのくらいのユーザーが同時アクセスしているのか履歴を追って確認できます。

サーバーに負荷がある時間帯とアクティブユーザー数の相関関係を確認するデータとして有益に利用できます。また、リリースする際になるべく影響の少ない時間帯は何時なのか把握することが出来ますので、ユーザー数の少ない時間帯を狙ったリリース作業を行うことが可能です。

アクセス数ランキングでは、自分たちが想定していなかったページが意外と使われていたりすることがあり、今後の改善や新機能の提案に結びつくこともあります。

E2Eテスト(Transactions)

Transactionsという機能を使うことで、エンドユーザーがアプリ上で行う操作をシナリオを組むことで自動的にシミュレーションして、シナリオが正しく動くかどうかをテストすることが出来ます。 いわゆるE2Eテストをpingdom上で簡単に組むことが出来ます。

Transactionsの編集画面イメージ

E2Eテストというと導入と保守のハードルが高いというイメージがありますが、Transactionsを使用することでアプリ側には特段何も設定せずに、シナリオテストを簡単に構築することが出来ます。

複雑なシナリオも組もうと思えば組めるとは思いますが、あくまでもUI上での操作になりますので、弊社ではクリティカルな導線のみ取り入れております。

スピードテスト(Page Speed)

Page Speedを利用することで、様々な観点からページのスピードテストを行い、ページのパフォーマンス改善をするための材料を提供します。

弊社ではSaaSを運用しているため、ログイン後のページがメインになります。 Page Speedではオープンなページに対してスピードテストを提供していますので、弊社のようなプライベートなSaaSにおいてはあまり効果がないと判断し、使用を見送りました。

ちなみにPage Speedの簡易版は無償でも利用することができます。

tools.pingdom.com

異業界からWebエンジニアに転身するために必要なこと

コロナ禍により、リモートワークと相性が良いと言われているエンジニアの転職市場が加速しているように感じています。

SNSの世界にはエンジニアになるためにプログラミング勉強中系のアカウントをよく見かけます。 私の周りにも異業界からエンジニアに転身したいという人が多くいましたが、実際にエンジニアになれた人は半分も満たないというのが実情です。

「なぜ半分以上の人はエンジニアになれなかったのか?」を私なりに分析してみました。本記事ではエンジニアになれた人の共通点を元に私なりの持論を展開したいと思います。

アウトプットせざるを得ない環境をいかに作るかが重要

エンジニアになるために机上の勉強をすることはとても重要ですが、いつまでも勉強だけをしているのは危険です。どこかで区切りをつける必要があります。 プログラミング勉強中という人の中にはいつまでも勉強の域を超えられていない人が多く見受けられるように感じます。

インプットとアウトプットの割合は3:7が良いと言われています。

インプットだけしていては何も生まれません。インプットは続けようと思えばいつまでも続けられるゴールのないマラソンのようなものです。

目的なくインプットだけ続けていては次第に勉強を続けるモチベーションが低下してしまいます。勉強を続けるためにもアウトプットをする必要があるのです。

そのため、まずはアウトプットをせざるを得ない環境に身を投じることが重要です。例えば以下のようアウトプットが挙げられます。

  • スタートアップ企業にインターンシップとしてジョインする
  • プログラミング学習塾でアウトプットする場を設ける
  • 勉強したことをブログに記載してアウトプットする
  • ベンダー資格や基本情報技術者試験等を取得する

最近は、平日は自分の仕事をして週末だけインターンするという複業的な働き方もありますので、それも有効です。

反対に、学習したことをSNSで日報形式で投稿することはあまり有効ではないと思います。 あくまでも学習したことを活かして何かを生み出すことに意味がありますので、報告形式のアウトプットはあまり意味がないと感じています。

アウトプットをすることで自分に足りていない部分が明確になりますので、インプットの質も向上し、相乗効果が期待できます。 また、アウトプットというのは学習を続ける原動力になりますので、途中で投げ出すリスクも減らすことができます。

独学はダメ。なによりもメンターの存在が大きい

プログラミング学習を進めていくと必ず自分だけでは解決できない部分が出てきます。

書籍やブログを参考にして、なんとか動くまで持ってきたが「なぜ動いているのか?」「これが本当に正しい実装方法なのか?」と感じた経験は誰しもあるかと思います。 誰にも相談せずに、これが正しいのだと自分の体に刻んでしまうと、変な癖のある可読性の悪いプログラムを実装し続けることになります。

独学のみでプログラミング学習をステップアップしていくことはオススメできません。

右に行くか左に行くか道に迷ったときに間違った方向を選んでしまうと到達する場所も全く別の場所になります。 スポーツの世界でもコーチのもと正しいフォームでトレーニングを行うことで実力を伸ばせられるのと同じように、エンジニアの世界にもコーチ(メンター)の存在は絶対に必要です。

このコードが正しいのかレビューしてもらうことで新しい技術を習得する機会が与えられますので、確実な成長が見込めます。 独学で学習している方は、一旦学習を止めてメンターを探しに注力したほうが良いです。

メンターを探す方法としておすすめなのが、知人から現役エンジニアを紹介してもらうことです。

「エンジニアに転職したい人がいるから相談に乗って欲しい」という旨を伝えればほとんどのエンジニアは喜んで話を聞いてくれるでしょう。 これは不思議なことで、人は自分の喜びよりも他人の喜びのほうが幸福や充足感を感じる傾向があるので、意外と快く引き受けてくれます。

紹介してもらった現役エンジニアからメンターにしたい人を選ぶと良いでしょう。 現に私自身も現役エンジニアとしての立場から相談を受けてメンターになったことが経験が多くあります。

どうしても周りにエンジニアの知人がいないという方はMENTAというマッチングサービスもありますので、これを使用してみるのも有効かと思います。

終わりに

どのような業界でもはじめの一歩を踏み出すことはとても勇気が必要です。

アウトプットが重要だと理屈では分かっていてもインプットばかりしてしまうのは、変化を嫌う人間にとって当たり前の思考だと思います。 しかし、エンジニアになりたいと本気で思うのならば勇気を持って踏み込むことを強くおすすめします。

  • 強制的にアウトプットする機会を設ける
  • メンターを作る

まずはこの2点を厳守するだけでもエンジニアになれる確率が格段に上がると私は確信しております。

エンジニアになりたかったのに途中で挫折してしまうのはとても残念なことです。 そのような人をなるべく生み出さないためにも、私自身も微力ながらこのような発信を続けていきたいと思います。

【Axlsx】Cellのtypeに:dateを指定しているのに日付型のセル書式にならない

前回同様、またAxlsx関連のマニアック話ですが、なぞ仕様に嵌ってしまったので記録として残しておきます。 同じように悩んでしまっている方の一助になれば幸いです。

Cellのtypeに:dateを指定しているのに日付型のセル書式にならない

以下のようにadd_cellで日付型オブジェクト(Date.today)、typeに:dateを指定しているのにも関わらず、セルの書式が日付型として入力されない事象が発生しました。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

header_style = ws.styles.add_style(border: { style: :thin, color: '000000' }, bg_color: 'E4E4E4')
value_style = ws.styles.add_style(border: { style: :thin, color: '000000' })

row = ws.add_row
row.add_cell('日付', style: header_style, type: :string)
row.add_cell(Date.today, style: value_style, type: :date)

▼日付型ではなく数値データ入力されている

なお、add_cellメソッドというのはCellをnewしているだけのシンプルなプログラムです。

# Adds a single cell to the row based on the data provided and updates the worksheet's autofit data.
# @return [Cell]
def add_cell(value = '', options = {})
  c = Cell.new(self, value, options)
  self << c
  worksheet.send(:update_column_info, self, [])
  c
end

axlsx/row.rb at 8e7b4b3b7259103452c191f73ce0bf64f66033fe · randym/axlsx · GitHub

ちなみにこの事象は、以下のようにstyleは未指定、type :dateのみ指定する場合だと正しく日付型で入力されます。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

row = ws.add_row
row.add_cell('日付', type: :string)
row.add_cell(Date.today, type: :date)

▼日付型として入力されている

どうやら、Cellをnewする時にstyleとtype :dateを併用するとセルの書式設定が日付型として認識されないという動きのようです。

styleに明示的にnum_fmtを指定すると日付型の書式が適用される

小一時間ほど悩んでいましたが、以下のドキュメントに答えが記載されていました。

Method: Axlsx::Styles#add_style — Documentation for randym/axlsx (master)

styleにnum_fmtというオプションで日付フォーマットを指定することで、セルの書式に日付型が適用されるようです。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

header_style = ws.styles.add_style(border: { style: :thin, color: '000000' }, bg_color: 'E4E4E4')
date_style = ws.styles.add_style(num_fmt: Axlsx::NUM_FMT_YYYYMMDD, border: { style: :thin, color: '000000' })

row = ws.add_row
row.add_cell('日付', style: header_style, type: :string)
row.add_cell(Date.today, style: date_style, type: :date)

▼日付型として入力されている

Cellをnewする際に、styleとtype :dateを併用する場合は、明示的にnum_fmtを指定する必要があるようです。

なぜこのような動きになるかというと、Cellクラスのコードを見るとわかるように、デフォルトの日付用style(STYLE_DATE)がオプションで指定したstyleで上書きされてしまうからです。

def cast_value(v)
  return v if v.is_a?(RichText) || v.nil?
  case type
  when :date
    self.style = STYLE_DATE if self.style == 0
    v

axlsx/cell.rb at 8e7b4b3b7259103452c191f73ce0bf64f66033fe · randym/axlsx · GitHub

Cellに対してオプションでstyleを指定してしまうと、typeごとに定義されたデフォルトのstyleが上書きされてしまい、セルの書式設定が無効となるようです。

Axlsxで出力したExcelをRSpecでテストする方法

RubyでExcelのインポート、エクスポートを実装する場合は、Axlsxを使用することが多いかと思います。

Axlsxでエクセルをインポートする処理をRSpecでテストするというコードはよく見かけるのですが、 エクスポートに関してはあまり見たことがありません。

先日、弊社プロジェクトでExcelエクスポートのRSpecを書く機会がありましたので記事にまとめてみました。誰かの参考になれば幸いです。

github.com

Axlsxで出力したExcelの内容をRSpecでテストする方法

ExcelExportServiceというExcel出力用クラス(①)をテストするにあたり、出力されたExcelファイル(②)をRSpecでアサーション(③)する場合を想定して説明します。

▼ ① ExcelExportServiceの実装内容(一部抜粋)

class ExcelExportService
  def call
    Axlsx::Package.new do |p|
      p.workbook.add_worksheet(name: 'Materials') do |sheet|
        # 出力処理が実装されている
      end
    end
  end
end

▼ ② 出力されたExcel

▼ ③ RSpecのテストコード

package = ExcelExportService.new.call
expect(package.workbook.worksheets[0].rows[0][0].value).to eq 'No'
expect(package.workbook.worksheets[0].rows[0][1].value).to eq '名字'
expect(package.workbook.worksheets[0].rows[0][2].value).to eq '名前'
expect(package.workbook.worksheets[0].rows[0][3].value).to eq '年齢'

expect(package.workbook.worksheets[0].rows[1][0].value).to eq 1
expect(package.workbook.worksheets[0].rows[1][1].value).to eq '田中'
expect(package.workbook.worksheets[0].rows[1][2].value).to eq '太郎'
expect(package.workbook.worksheets[0].rows[1][3].value).to eq '30'

expect(package.workbook.worksheets[0].rows[2][0].value).to eq 2
expect(package.workbook.worksheets[0].rows[2][1].value).to eq '山田'
expect(package.workbook.worksheets[0].rows[2][2].value).to eq '花子'
expect(package.workbook.worksheets[0].rows[2][3].value).to eq '34'

ExcelExportService.new.call(①)はAxlsx::Packageオブジェクトを返却しています。 テストコード(③)には、出力されたエクセル(②)と対応する形で、1次元配列や2次元配列形式で対象データを特定してアサーションを実行しています。

Axlsx::Packageオブジェクトにはworkbookというオブジェクトが格納されており、さらにその下にworksheetsオブジェクト、rowsオブジェクトが格納されています。

# 以下の構成でオブジェクトが格納されている
Axlsx::Package >> Axlsx::Workbook >> Axlsx::Worksheet >> Axlsx::Row

Axlsx::Worksheetは一次元配列の形式であり、Excelのシートと対応しています。

Axlsx::Rowは二次元配列の形式であり、1つ目の配列が行番号、2つ目の配列が列番号に対応しています。 また、セルの表示形式によってAxlsx::Rowに格納されているデータタイプも変更されます。

単純な構成ですので、簡単にテストコードが実装できるかと思います。

react-virtualizedのMultiGridでアプリ開発したときのTIPS

f:id:furu07yu:20220415135534p:plain

react-virtualizedとは

SPAで構築されたReactアプリでも、大量のリストを画面いっぱいに表示しようとすると初期描画に時間がかかってしまいます。

これは、画面に見えてない部分も含めて全てのデータをレンダリングしてしまっていることが原因です。

react-virtualizedを使用することで画面に見えている部分のみレンダリングされることができます。

画面をスクロールしたときに、表示されている領域のみのデータが描画される仕様になっていますので、効率的なレンダリングが実現できます。

github.com

react-virtualizedで使用できる様々なコンポーネント

react-virtualizedにはいくつかのコンポーネントが用意されています。簡単に代表的なコンポーネントを紹介します。

  • Grid: エクセルのようにデータを表示することができるコンポーネントです。行と列の固定はできません。
  • MultiGrid: エクセルのようにデータを表示することができるコンポーネントです。行と列の固定が可能です。
  • List: SNSのタイムラインのようにリスト形式でデータを表示することができるコンポーネントです。
  • Table: Listにヘッダーが付いた形式で表示することができるコンポーネントです。
  • Collection: いろいろな大きさのカードを横と縦に配置できるコンポーネントです。

今回は私がよく使用するMultiGridコンポーネントについての知見を共有したいと思います。 MultiGridを使用すると下記のようなエクセルライクな画面が実装できます。

f:id:furu07yu:20220414185459g:plain

基本的な実装方法は公式リファレンスに記載されております。

しかし、あくまでもシンプルな構成を想定している内容になっています。実際にアプリ開発していくと結構なクセがありましたので、その辺りの知見を中心にいくつかピックアップしています。

※react-virtualized v9.22.3に準拠しています

カラムの横幅を動的に変更させたい

カラムの横幅を動的に変更させたい場合、columnWidthというプロパティで制御できます。 columnWidthには引数でindexが渡ってきますので、それを使って横幅を変えることが可能です。

<MultiGrid
  columnWidth={({ index }) => {
    if(index === 0) {
      return 50;
    }
    // index によって返却値の出し分けが可能
    return 100;
  }}
/>

カラムの高さを動的に変更させたい

カラムの高さを動的に変更させたい場合、rowHeightというプロパティで制御できます。 rowHeightには引数でindexが渡ってきますので、それを使って高さを変えることが可能です。

<MultiGrid
  rowHeight={({ index }) => {
    if(index === 0) {
      return 20;
    }
    // index によって返却値の出し分けが可能
    return 40;
  }}
/>

React Hooksでreact-virtualizedを使いたい

リファレンスはクラスベースで記載されておりますが、もちろんHooksでも使用できます。

注意点としてはrecomputeGridSize等のPublic Methodが使用できるようにuseRefでGrid要素への参照ができるように設定することです。

function Grids(props) {

  const gridsEl = useRef(null);

  useLayoutEffect(() => {
    // gridsEl経由でrecomputeGridSizeを実行する
    if (gridsEl) {
      gridsEl.current.recomputeGridSize();
    }
  }, [
    anyProps
  ])

  return (
    <MultiGrid
      // gridsElをrefに設定する
      ref={gridsEl}
    />
  )

セルを結合させたい

MultiGridでセル結合ができれば素晴らしいのですが、この記事を執筆時点(v9.22.3)では残念ながらサポートされていません。

have some cell merge properties? · Issue #1637 · bvaughn/react-virtualized · GitHub

しかし、セル結合されているかのように実装することは可能です。

例えば固定ヘッダーの中で特定のセルを列結合させたい場合は一つのcolumnIndex中に、あたかも2つ列が存在しているかのようにデザインすることでそれが実現できます。

f:id:furu07yu:20220414230507j:plain

上記のように各日付のヘッダーとして発注日を列結合したようなグリッドを作りたいとします。 その場合は、一つのcolumnIndexの中にそれぞれの行を実装することで実現できます。

1行目は単純に1つのcolumnIndexの中に発注日のセルを配置します。

2行目はセル結合されているようにしたいので、1つのcolumnIndexの中にtableタグで7列のテーブルを実装するようにします。

このようにすることで、かなり強引ではありますが、あたかもセル結合されているかのように実装することが可能です。

固定列でもスクロールさせたい

固定列でもスクロールさせたい場合、enableFixedColumnScrollにtrueを設定することで固定列でのスクロールを有効にできます。

そのままではスクロールバーが表示されてしまいますので、一緒にhideBottomLeftGridScrollbarもtrueにしておくことをおすすめします。

固定行でもスクロールさせたい

固定行でもスクロールさせたい場合、enableFixedRowScrollにtrueを設定することで固定行でのスクロールを有効にできます。

そのままではスクロールバーが表示されてしまいますので、一緒にhideTopRightGridScrollbarもtrueにしておくことをおすすめします。

Railsアプリが爆速になるパフォーマンスチューニング集

Railsアプリを速くするためのパフォーマンスチューニング集をまとめました。 これらの手法を用いることで、弊社アプリにおいて、今まで30秒ほど掛かっていた処理が1.5秒まで短縮することができました。

あくまでもアプリケーションレイヤーでできるパフォーマンスチューニングのみに絞っていますので、テーブルインデックスやサーバーのスケールアップなどは対象外としています。

パフォーマンスに悩んでいる方の助けになれば幸いです。

N+1問題の対策

言わずとしれたN+1問題の対策は、手っ取り早くパフォーマンス改善が見込めます。

「N+1問題とは何か?」についてはご存じの方が多いと思いますので、ここでは説明を割愛させていただきます。

N+1問題を解消する手段としては、テーブルからデータ取得する際にincludesなどにより関連するテーブルのデータも含めて取得するのが有効的です。

@blogs = Blog.includes(:user)

また、N+1問題が検出できるbulletというgemもありますので、導入を検討してみると良いでしょう。

includesは計画的に

includesはとても便利な機能ですが、過度にincludesしてしまうとメモリを消費してしまうというデメリットがあります。使用する際は後述のキャッシュ化などを適用した上で計画的にincludesするのがポイントです。

テーブルデータのキャッシュ化

テーブルデータのキャッシュ化もパフォーマンス改善にはとても効果的です。

ループ処理の中で、あるテーブルに対してアクセスしなければならない場合、ループ毎にテーブルをfindするのは非効率です。 事前に必要なテーブルデータを取得しておき、そのキャッシュを使ってアクセスするほうが何倍も速くなります。

例えば以下のような処理です。

blogs = Blog.order(:id)
blogs.each do |blog|
  # blogsの件数分findが実行されてしまう
  category = Category.find(blog.category_id)
  # 省略...
end

このようなコードだとblogsの件数分categoriesテーブルへのfindが実行されてしまいます。 blogsの件数が多い場合は遅くなる要因となります。

下記のようにincludesを使ってcategory一緒に取得する方法も有効的かと思いますが、 仮にblogsが1万件、categoryが10件しかない場合、1万件分のblogオブジェクトにcategoryが乗ることになりますので、無駄なメモリを消費してしまいます。

blogs = Blog.includes(:category).order(:id)
# blogが1万件、categoryが10件しかない場合
# 同じようなcategoryがblogオブジェクトに1万件セットされてしまう

blogs.each do |blog|
  category = blog.category
  # 省略...
end

そのため、categoryのテーブルデータを事前に取得しておくことで メモリ負荷を抑えられる且つ、パフォーマンスの向上も期待できます。

blogs = Blog.order(:id)
categories = Category.all.index_by(&:id)

blogs.each do |blog|
  category = categories[blog.category_id]
  # 省略...
end

index_by(&:id)を使用することで、idをkeyとしたcategoryのHashが構築されますので、Arrayを使うよりも高速にアクセスすることができます。

pluckで必要最低限のカラムのみ取得する

取得したテーブルデータの中で、特定のカラムのみしか使用しない場合はpluckを使用することをおすすめします。

例えばblogのタイトルを出力するプログラムの場合、下記のようにblogオブジェクトを全て取得する必要はないと思います。

blogs = Blog.order(:name)
blogs.each do |blog|
  p blog.name
end

必要なデータはnameだけですので、pluck(:name)をメソッドチェーンしてnameの配列を取得するようにします。

blog_names = Blog.order(:name).pluck(:name)
blog_names.each do |blog_name|
  p blog_name
end

pluckを使用することでメモリの効率化が図れますし、SQLクエリの負荷を抑えられることになりますので、パフォーマンス向上にも繋がります。

# pluckを使用しない場合
#   アスタリスク(*)で全項目のデータを取得してしまう
SELECT "blogs".* FROM "blogs" ORDER BY "blogs"."name" ASC

# pluckを使用した場合
#   nameのみに絞られる
SELECT "blogs"."name" FROM "blogs" ORDER BY "blogs"."name" ASC

バルクインサートで一括登録・更新する

SQLの中でもinsert(updateも同様)は特に遅いです。insertの回数を減らすことでパフォーマンス改善に繋がるケースは多くあります。

activerecord-importというgemを使うことで一括insert(バルクインサート)が可能となります。

Rails6以上であれば標準でバルクインサートがサポートされましたので、そちらも効果としては同様になります。

csv_records.each do |csv_record|
  Product.create(name: csv_record[:name], price: csv_record[:price])
end

このようなコードだとcsv_recordsの件数分productのinsert文が実行されてしまいます。

バルクインサートを用いることでinsert文が一回で済むようになります。

insert_products = []
csv_records.each do |csv_record|
  insert_products << Product.new(name: csv_record[:name], price: csv_record[:price])
end

Product.import insert_products

activerecord-importを使用する場合、バリデーションが効かないためエラーハンドリングが出来ないというのがデメリットかと思います。

その場合はvalid?メソッドを使用して事前にバリデーションを実行することでエラーハンドリングを実装することができます。

insert_products = []
csv_records.each do |csv_record|
  product << Product.new(name: csv_record[:name], price: csv_record[:price])
  unless product.valid?
    # エラーハンドリングをここに実装
  end
  insert_products << product
end

Product.import insert_products

関連オブジェクトを予め詰めておく

Railsのログを確認してみると大量のSQLが発行されていることがしばしばあります。 ほとんどのケースはN+1問題が原因となっています。

例えば以下のようなコードがあった場合、blog.userの実行によりN+1問題が発生してしまいます。

blogs = Blog.order(:id)
blogs.each do |blog|
  # 省略...
  blog.user.company_id
  # 省略...
end

下記のようなSQLが大量に発行されます。

SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2

includesを使用することでN+1問題は回避されますが、 メモリ効率を考えると過度なincludesは避けたいです。

そのような場合はN+1を防止するためにレシーバーとなるモデルオブジェクトに関連しているオブジェクトを予めセットしておくことが有効です。

blogs = Blog.order(:id)
users = User.all.index_by(&:id)

blogs.each do |blog|
  # 省略...
  user = users[blog.user_id]
  # あらかじめblogオブジェクトにuserをセットしておく
  blog.user = user
  blog.user.company_id
  # 省略...
end

このようにすることで、N+1が抑えられる且つ、過度なincludesによるメモリ負荷を抑えることができます。

ただし、この手法を用いると可読性が極端に悪化しますし、セットする関連オブジェクトの最新状態が保証されないというリスクも伴います。どうしてもパフォーマンスを向上させたいケースを除いては使用しないほうが良いでしょう。

モデルのバリデーションを無効にする

モデルのバリデーションといえど侮れません。負荷のかかる処理は存在します。

例えばモデルのバリデーションであるuniquenessは新規登録や更新処理が実行されると必ず以下のようなSQLが発行されてしまいます。

SELECT 1 AS one FROM "products" WHERE "products"."code" = $1 AND "products"."department_id" = $2 LIMIT

もしバリデーションエラーが発生しない前提のロジックなのであれば意図的にバリデーションを無効にすることでパフォーマンスが向上します。 バリデーションを無効にすることで予期せぬデータが混入するリスクが伴いますので、あまり推奨はしません。

あくまでもパフォーマンス向上を重視する場合は検討することも視野に入れてみてはいかがでしょうか。

以下のようにsaveを実行することでバリデーションを一時的に無効にすることが可能です。

product.save(validate: false)

過度なパフォーマンスチューンングはNG

以上のようなパフォーマンスチューンングを施すことでRailsアプリでも爆速な処理を実装することができます。

パフォーマンスチューニングと可読性はトレードオフの関係にあります。

大々的なパフォーマンスチューニングを実施する場合、個々のロジック単位ではなく、機能全体で最適化を図ります。 そのため、どうしてもプログラムとして不自然なコードが生まれてしまい可読性が悪くなります。

可読性と天秤にかけた上で、場合によっては過度なパフォーマンスチューンングは避けるようにすることも必要です。

また、適宜コメントアウト等で処理の意図を補足するなどして可読性の低下を抑えるなどの心掛けが大事になります。

Rails7でReactを使う場合はimportmapはオススメできない

f:id:furu07yu:20220401124238p:plain Rails7からJavascriptバンドラーとしてNode.jsが非依存のimportmapが標準機能に採用されました。

早速、importmapを使ってReactを使用した開発を試みてみましたが、結論としてはimportmapは使用しないことになりました。

Reactの場合はimportmapをオススメできない

Reactを使用する場合、importmapはオススメできません。理由はJSXが使用できないためです。

JSXを使用するにはJSXで書かれたコードを素のJavascriptにトランスパイルする必要があります。しかしimportmapを使用しているとトランスパイルができません。

JSXが使用できない代わりに、htmというトランスパイルの必要がないHTMLマークアップライブラリがありますが、以下の理由によりJSXの代わりとするには厳しいと判断しました。

  • Typescriptに対応していない
  • eslintに対応していない

htmはトランスパイルがどうしても使用できない環境制約がある場合や、eslintなどを使用しなくても問題のない小規模な開発の場合に適していると感じました。

また、今回の開発ではmuiというReact専用のスタイルコンポーネントを使用したいと考えています。muiもJSXで実装されていますので、使用するためにはトランスパイルが必要になりますので、importmapの使用は見送りました。

esbuildを使用するのがオススメ?

importmapを使用しない場合、他にはwebpack、rollup、esbuildという選択肢があります。

今回はビルド時間がwebpackの100倍速いと言われているesbuildを使用することにしました。 esbuildはホットリロード(HRM)の機能が備わっていないという点が少しネックですが、ひとまずHRMは使用せずに開発を進めてみようかと思います。

開発する上で不都合などあるかもしれませんが、使用感などのレポートはまた別の記事にまとめようと思います。

それでは。