Tech Hotoke Blog

IT観音とは私のことです。

で、結局preload, eager_load, includesのどれを使うのがよろしいの? #Ruby on Rails #ActiveRecord

これは何?

preload, eager_load, includesを見かけるたびに、あれ、これってどういう挙動をするんだっけ?と調べているのでメモ。

前提

Tips

結論

  • eager_load
    • 1対1あるいはN対1のアソシエーションをJOINする場合に使う
  • preload
    • 多対多のアソシエーションの場合に使う
  • joins
    • メモリの使用量を必要最低限に抑えたい場合に使う
    • JOINした先のデータを参照せず、絞り込み結果だけが必要な場合に使う
  • includes
    • 使わない。私は断固として使わない。使ってるコードを見つけたら絶対に駆逐する。

Eager loading と Lazy loading

  • Eager loading

    • 予めメモリ上にActive Recordで情報を保持する方法。
    • ActiveRecordのメソッドで言えば、preload, eager_load, includesなど。
    • pros : 素早いレンダリングが可能になる。
    • cons : アソシエーションしているテーブルにある情報が膨大な場合、大量のメモリを消費することになる。
  • Lazy loading

    • JOINしたテーブルの情報が必要になった時に SQLを発行する方法。
    • ActiveRecordのメソッドで言えば、joinsなど。
    • pros : メモリを確保する量はEagerLoadingにくらべて少なくなる。
    • cons : JOINするテーブルを参照するたびSQLを発行するためWebサイト表示パフォーマンスを悪くする場合があります(N+1問題)

それぞれのメソッドの比較

メソッド クエリ アソシエーション先の参照 デメリット
eager_load LEFT JOIN できる JOIN先のデータ多いほど速度が低下する
preload SELECT できない データ量が多いと、IN句が肥大化してメモリを圧迫する
joins INNER JOIN できる N+1問題が発生するかも
incleds SELECT または LEFT JOIN できる 理解していないと予想外の挙動をするかも
  • どんな時に使うべきか??
    • eager_load
      • 1対1あるいはN対1のアソシエーションをJOINする場合。1回のSQLでまとめて取得した方が効率的な場合が多いと思うから。
    • preload
      • 多対多のアソシエーションの場合。データ量が多くなる場合が多いと思うので、使用するとパフォーマンスの向上につながるケースが多そうだから。
    • joins
      • メモリの使用量を必要最低限に抑えたい場合
      • JOINした先のデータを参照せず、絞り込み結果だけが必要な場合
    • includes
      • 使わない。eager_loadとpreloadを明示的に使い分けたほうが、思わぬボトルネックを埋め込まなくなると思うから。

【おまけ】includesの挙動について

  • includesしたテーブルでwhereによる絞り込みを行っている
  • includesしたassociationに対してjoinsかreferencesも呼んでいる
  • 任意のassociationに対してeager_loadも呼んでいる のうちいずれかに該当すると、eager_loadが呼ばれる

github.com

  • 例 下記の場合は、eager_loadの挙動をします。
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users).where(reviews: { article_id: 1})
  end
end

下記の場合は、preloadの挙動をします。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users)
  end
end

RFC 2397に定義されたデータURLスキームを読んでみた。

これは何?

AntD Reactの実装中にImageコンポーネントを使った。 その際に、srcのフォーマットについて調べてみた。

Tips

  • base64に変換されたPDFファイルの場合、Imageコンポーネントのsrcプロパティには、以下のような値を渡す必要がある。 data:application/pdf ; base64, <base64にエンコードされた文字列>

このフォーマットについてはRFC 2397 - The "data" URL scheme に定義されている。

[https://tex2e.github.io/rfc-translater/html/rfc2397.html:title]

[https://datatracker.ietf.org/doc/html/rfc1866:title]

#ReactQuery フェッチタイミングの制御Tips #invalidateQueries #resetQueries

これは何?

ReactQueryのフェッチタイミングの制御とかを触った。毎回調べている感じがするので、メモ。

前提

  • React: 18系
  • TypeScript: 4系
  • TanStack Query: 4系

Tips

  const { data } = usePlaylistAutoProgram(playlist?.playlistUid)

 useEffect(() => {
    if (data) {
      setValue('hoge', data.hoge)
    }
  }, [data])

  const onSubmitForm: SubmitHandler<FormInputs> = async (
    values
  ) => {
    await updatePlaylistAutoProgram({
      data: values
    })
    // 更新したデータでfieldの値を更新したい。 
  }
 const queryClient = useQueryClient()
  const { data } = usePlaylistAutoProgram(playlist?.playlistUid)

 useEffect(() => {
    if (data) {
      setValue('hoge', data.hoge)
    }
  }, [data])

  const onSubmitForm: SubmitHandler<FormInputs> = async (
    values
  ) => {
    await updatePlaylistAutoProgram({
      data: values
    })
    // 
    queryClient.resetQueries([
      '<任意のキー>',
      [任意のバリュー]
    ])
  }
  • ReactQueryはdefaultで5分間キャッシュされる。
  • キャシュしないか明示的にリフェッチするかは要件次第だと思いますが、今回は特定の用途でしかリフェッチする必要がないため、キャッシュ設定はそのままに明示的にリフェッチするようにしました。

useQuery | TanStack Query Docs

補足

  • キャッシュ時間を0にするには?
    • chacheTimeを0にする + retryフラグをfalseにする。(記憶ベースで恐縮なんですが、chacheTimeを0にするだけではキャッシュが無効にならず、GitHubリポジトリに同様の投稿があり、retryフラグをfalseにすれば治りました)
 cacheTime: 0,
 retry: false
  • resetQueriesinvalidateQueries の 使い分け
    • 今回実装するにあたって、どちらが適切か判断ができず、違いがよくわかっていなかったので改めてドキュメントを読んでみました。

    • resetQueriesは、指定したクエリをキャッシュから完全に削除し、初期状態に戻す。アクティブなクエリは自動的にリフェッチされる。

    • invalidateQueriesは、指定したクエリをキャッシュから無効化する。完全に削除はしません。アクティブなクエリが自動的に再フェッチされるかどうかは設定次第。

    • 上記を踏まえると、それぞれの使用例はこんな感じなのかなぁ。。。

      • resetQueriesの使用例

        • フォームの入力内容をリセットした際に、関連するクエリも初期状態に戻したい場合
        • アプリの設定を変更した際に、関連するクエリをすべて再フェッチしたい場合
      • invalidateQueriesの使用例

        • リストの一部分を更新した際に、そのリストに関連するクエリだけを無効化してリフェッチしたい場合
        • データを更新した際に、そのデータを使用しているクエリを無効化して、次回アクセス時にリフェッチさせたい場合

QueryClient | TanStack Query Docs

The resetQueries method can be used to reset queries in the cache to their initial state based on their query keys or any other functionally accessible property/state of the query. This will notify subscribers — unlike clear, which removes all subscribers — and reset the query to its pre-loaded state — unlike invalidateQueries. If a query has initialData, the query's data will be reset to that. If a query is active, it will be refetched.

QueryClient | TanStack Query Docs

The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background. If you do not want active queries to refetch, and simply be marked as invalid, you can use the refetchType: 'none' option. If you want inactive queries to refetch as well, use the refetchType: 'all' option

チリも積もれば山となる。パフォーマンスを測定してわかったリファクタリングの効果 #Ruby #Rails

これは何?

コードのリファクタリングの際に、パフォーマンスの計測まで行ってみたら結構な差が出てきたので、メモ。

前提

Tips

  • mapを使っていた箇所をpluckを使ってリファクタリングしてみた
    • before

      user_ids =Pls.where(pl_id: pl.id).map { |record| record['user_id'] }

    • after

      user_ids =Pls.where(pl_id: pl.id).pluck(:user_id)

結果

  • 出力されるSQLはSELECT句で全レコードを取得するか、特定のレコードを取得するかの差がある。
  • この差 + map処理が省略される差で処理速度がpluckの方が向上する場合が多い。

  • pluckを使った場合

SELECT `pl`.`user_id` FROM `pls` WHERE `pls`.`pl_id` = 3
  • mapを使った場合
SELECT `pl`.* FROM `pls` WHERE `pls`.`pl_id` = 3
  • パフォーマンスの比較をすると、 78倍 もの差があった。
  • もともと処理の時間が短いものの、こういうのがちりつもになって、パフォーマンスの劣化を招いていくのだろう。。。
  • 対象のテーブルのカラム数は10程度なので、これが増えていくとさらに差が出てくると思われる。
Before Refactoring: 0.93471800000043 seconds
After Refactoring: 0.01190639999913401 seconds

AWS SESの通知ステータスを環境別に振り分ける方法 #AWS #SES #Lambda #Ruby #技術メモ

これは何?

STG/PRD環境で、AWSアカウントを共有している時、AWS SESの通知ステータスを環境別に振り分ける場合の対処法の一つをメモ

前提

Tips

  • 要件

    • STG/PRD環境で、AWSアカウントを共有している
    • SESで配信されたメールの通知ステータスをSlackに通知する
    • Slackの通知先はSTG, PRDで別チャンネルにする
    • SESのID(メールアドレス)はアカウントと1:1で紐づく
    • SESの通知に設定できるSNSも1:1で紐づく
    • Slack通知のエンドポイントはシステムごとにドメインが異なるため、以下の画像のように環境別にSNSを作成する必要がある

  • 課題

    • メールが発信された環境に応じて、SNSの通知先を振り分けられない
  • どうしたか?

    1. メールヘッダに環境を識別するカスタムヘッダX-Envを設定
    2. 新規SNSを作成し、SESの通知先に設定する
    3. 新規Lambda関数を作成し、先程作成したSNSをトリガーに設定する
    4. Lambda関数の中で環境を識別するヘッダを抽出して、publishするSNSを環境別に振り分けた

  • Lambda関数サンプルコード
# frozen_string_literal: true

require 'json'
require 'aws-sdk-sns'

def handle_target_sns(event:, context:)
// 中略
 env = custom_headers_value['X-Env']

  sns_topic_arn = sns_topic_arn(env)

  sns = Aws::SNS::Client.new(region: 'ap-northeast-1')
  
  sns.publish(topic_arn: sns_topic_arn, message: event.to_json)

  { statusCode: 200, body: JSON.generate('Message published to SNS') }

end

def sns_topic_arn(env)
  case env
  when 'staging'
    ENV.fetch('STG_SNS_TOPIC_ARN', nil)
  when 'production'
    ENV.fetch('PRD_SNS_TOPIC_ARN', nil)
  else
    raise "Unknown environment: #{env}"
  end
end

#Lambdaレイヤー導入メモ #Ruby #AWS

これは何?

Lambdaのレイヤーを使用したので、操作手順などの備忘録

前提

Tips

  • Lambdaレイヤーはどんなときに使う?
  • デプロイパッケージのサイズを小さくするため。関数の依存関係を関数から切り離せるので。

  • コア関数ロジックを依存関係から分離するため。

  • 複数の関数間で依存関係を共有するため。今回の目的はこれに該当する。

  • Lambda コンソールのコードエディターを使用するため。

依存関係を分離するのは、今のところ楽にはなってもしんどくなることはほとんどなかったので積極的に切り分けていきたい。

  • 依存関係管理のためのレイヤーの使用を示す空のサンプルアプリケーションを用意してくれている。優しい。

github.com

  • 関数にレイヤーを追加すると、Lambda はレイヤーのコンテンツをその実行環境の /optディレクトリに読み込みます。例えば、アップロードするフォルダのディレクトリ構造がruby/3.2/* だったら、環境変数GEM_PATH /opt/ruby/3.2.0 を設定する必要があります。

  • レイヤーには、1 つまたは複数のレイヤーバージョンを含めることができる。

  • 既存のレイヤーバージョンの権限はいつでも変更することができる。

  • ただし、コードを更新したり、その他の構成を変更したりするには、新しいバージョンのレイヤーを作成する必要があります。

  • レイヤーのアップロードにはS3からもアップロードできる。

レイヤーを作成し設定する

  • ローカル環境でGemfileを作成し、bundle install を実行する(bundle install --path vendor/bundle でvendorディレクトリの配下に作成した)
  • bundle install の成果物をzipする(zip -r ../ruby_layer.zip .)
  • zip ファイルをアップロードする(手動)
  • Lambda関数にレイヤーの設定を行う(レイヤーはバージョン管理されるので、バージョンが更新されている場合はバージョンの指定を忘れずに。ここの更新忘れでハマりそう。。。)
  • 環境変数GEM_PATHを設定する(zipファイルのディレクトリ名もちゃんと確認すること)

#RSpec Mock: 引数に応じて返り値を変える方法 #Ruby

これは何?

RSpecを書くときの小技メモ。

前提

Tips

  • RSpecで特定のメソッドをMockする
  • Mockするメソッドはテストの中で流用されている
  • 引数などの条件によって返り値を変えたい
  • どうする?

  • A:

    • receiveメソッドにはブロックが渡せるので、ブロックの中で条件分岐させる
  before do
                allow(hoge).to receive(:hogehoge) do |h|
                  if h.dig(:criteria, :ids)
                    # idsが含まれる場合、空の配列を返す        
                    [{ id: 'PUsR1qvef3', start_date: Date.today }]
                  elsif h.dig(:criteria, :codes) 
                    # codesが含まれる場合、空の配列を返す
                    []
                  end
                end
              end

備考

  • 引数に差はないけど返り値を制御したい場合などはどうしよう