<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kaziu</title>
    <description>The latest articles on DEV Community by Kaziu (@kaziusan).</description>
    <link>https://dev.to/kaziusan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F211048%2F802876cd-1431-467c-8ca8-8263ca29514a.jpeg</url>
      <title>DEV Community: Kaziu</title>
      <link>https://dev.to/kaziusan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaziusan"/>
    <language>en</language>
    <item>
      <title>エンティティ・値オブジェクト・サービス・UseCase をざっくり整理</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:13:25 +0000</pubDate>
      <link>https://dev.to/kaziusan/enteiteizhi-obuziekutosabisuusecase-wozatukurizheng-li-131</link>
      <guid>https://dev.to/kaziusan/enteiteizhi-obuziekutosabisuusecase-wozatukurizheng-li-131</guid>
      <description>&lt;p&gt;まぁRailsというか、DDD(ドメイン駆動設計)やクリーンアーキテクチャの範囲ですけどな&lt;/p&gt;




&lt;h2&gt;
  
  
  一覧
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用語&lt;/th&gt;
&lt;th&gt;ひとことで&lt;/th&gt;
&lt;th&gt;例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;エンティティ&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ID で見分けるもの&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ユーザー、案件、注文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;値オブジェクト&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;中身で見分けるもの&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;金額、電話番号、住所、期間&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;サービスオブジェクト&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;手続きを入れる箱&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;税額を計算する、S3 にアップロードする&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UseCase&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ユーザーがやりたいこと1つ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;案件を登録する、注文を確定する&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  エンティティ ─ ID で見分けるもの
&lt;/h2&gt;

&lt;p&gt;人は、名字が変わっても引っ越しても&lt;strong&gt;同じ人&lt;/strong&gt;です。逆に、同姓同名・同い年の人が2人いても&lt;strong&gt;別人&lt;/strong&gt;です。&lt;/p&gt;

&lt;p&gt;このように、中身ではなく &lt;strong&gt;ID（その人であること）で見分けるもの&lt;/strong&gt;がエンティティです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'鈴木'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 名前が変わっても id: 1 のまま。同じユーザー&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails では、&lt;code&gt;id&lt;/code&gt; を持つ ActiveRecord モデルの大半がエンティティです。&lt;/p&gt;




&lt;h2&gt;
  
  
  値オブジェクト ─ 中身で見分けるもの
&lt;/h2&gt;

&lt;p&gt;財布の1000円札とあなたの1000円札は、交換しても誰も困りません。お札の番号は気にせず、&lt;strong&gt;中身（金額）が同じなら同じもの&lt;/strong&gt;として扱います。これが値オブジェクトです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;PhoneNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'不正な電話番号'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A0\d{9,10}\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mobile?&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A0[7-9]0\d{8}\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'09012345678'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'09012345678'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;特徴は次の3つです。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ID を持たない&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;中身を変えない&lt;/strong&gt;（変えたいときは新しく作る）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ルールを持てる&lt;/strong&gt;（「0 から始まる10〜11桁」「携帯番号か判定する」など）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;電話番号をただの String で持っていると、「携帯番号か判定する」処理が User にも Company にもコピペされていきます。値オブジェクトにすれば、電話番号のルールが1か所にまとまります。&lt;/p&gt;




&lt;h2&gt;
  
  
  サービスオブジェクト ─ 手続きを入れる箱
&lt;/h2&gt;

&lt;p&gt;「税額を計算する」「S3 にファイルをアップロードする」のように、&lt;strong&gt;特定のモデルに入れるとモデルが太ってしまう手続き&lt;/strong&gt;を入れておくクラスです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaxCalculator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;よくある約束ごとは次のとおりです。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1クラス1つの仕事&lt;/li&gt;
&lt;li&gt;公開メソッドは &lt;code&gt;call&lt;/code&gt; などの1つだけ&lt;/li&gt;
&lt;li&gt;名前は「何をするか」がわかるものにする（&lt;code&gt;UserService&lt;/code&gt; のようなあいまいな名前は避ける）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  サービス ＝ ビジネスロジック？
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;イコールではありません&lt;/strong&gt;。業務のルールはエンティティや値オブジェクトにも入りますし、サービスには「S3 にアップロードする」のような技術的な処理も入ります。サービスは「&lt;strong&gt;ビジネスロジックの置き場所の1つ&lt;/strong&gt;」くらいに考えておくのが正確です。&lt;/p&gt;




&lt;h2&gt;
  
  
  UseCase ─ ユーザーがやりたいこと1つ
&lt;/h2&gt;

&lt;p&gt;「案件を登録する」「注文を確定する」のように、&lt;strong&gt;ユーザーがアプリでやりたいこと1つ分&lt;/strong&gt;をクラスにしたものです。アプリの機能一覧をそのままクラスにしたイメージです。&lt;/p&gt;

&lt;p&gt;UseCase は&lt;strong&gt;指揮役&lt;/strong&gt;です。自分では細かい作業をせず、エンティティやサービスを呼び出して手順を組み立てます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConfirmOrderUseCase&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s1"&gt;'権限がありません'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;     &lt;span class="c1"&gt;# 権限チェック&lt;/span&gt;

    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TaxCalculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;                &lt;span class="c1"&gt;# サービスを使う&lt;/span&gt;
      &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;tax: &lt;/span&gt;&lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :confirmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# エンティティを更新する&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;現場によっては、UseCase を &lt;strong&gt;Operation&lt;/strong&gt;、&lt;strong&gt;Interactor&lt;/strong&gt;、&lt;strong&gt;Command&lt;/strong&gt; などの別の名前で呼ぶこともあります。名前は違っても「ユーザーの操作1つ分の手続き」という役割は同じです。&lt;/p&gt;




&lt;h2&gt;
  
  
  4つの関係
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UseCase「注文を確定する」（指揮役）
   │
   ├─ エンティティを読み書きする   … 注文（Order）
   │      └─ 値オブジェクトを持つ  … 金額、電話番号
   │
   └─ サービスを呼ぶ              … 税額の計算、メール送信、S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UseCase&lt;/strong&gt; が段取りを決める&lt;/li&gt;
&lt;li&gt;その中で、&lt;strong&gt;エンティティ&lt;/strong&gt;を操作したり、&lt;strong&gt;サービス&lt;/strong&gt;に作業を任せたりする&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;値オブジェクト&lt;/strong&gt;は、エンティティやサービスの中で「値」として使われる&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;呼び出す向きは&lt;strong&gt;上から下への一方通行&lt;/strong&gt;です。サービスが UseCase を呼ぶことはありません。&lt;/p&gt;

</description>
      <category>rails</category>
      <category>architecture</category>
    </item>
    <item>
      <title>もうActiveJob - Job,Que,Adaptorもレストランに例えますかーー</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Fri, 30 Oct 2026 12:08:21 +0000</pubDate>
      <link>https://dev.to/kaziusan/mouactivejob-jobqueadaptormoresutorannili-emasuka-44nh</link>
      <guid>https://dev.to/kaziusan/mouactivejob-jobqueadaptormoresutorannili-emasuka-44nh</guid>
      <description>&lt;p&gt;わかってるんですが、レストランに例えさせてください。&lt;/p&gt;




&lt;h2&gt;
  
  
  登場人物
&lt;/h2&gt;

&lt;p&gt;まずは対応表です。この記事はずっとこの例えで進めます。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rails の用語&lt;/th&gt;
&lt;th&gt;レストランでいうと&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webリクエスト&lt;/td&gt;
&lt;td&gt;来店したお客さんの注文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;コントローラ&lt;/td&gt;
&lt;td&gt;レジ係&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ジョブ&lt;/td&gt;
&lt;td&gt;注文票&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;perform_later&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注文票をボードに貼ること&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;キュー&lt;/td&gt;
&lt;td&gt;注文票を貼っておくボード&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ワーカー&lt;/td&gt;
&lt;td&gt;料理人&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;アダプター&lt;/td&gt;
&lt;td&gt;ボードの種類&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  なぜ「あとでやる」必要があるのか
&lt;/h2&gt;

&lt;p&gt;ユーザー登録したら歓迎メールを送る、という機能を考えます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;WelcomeMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver_now&lt;/span&gt; &lt;span class="c1"&gt;# ← ここで数秒待たされる&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;これはレジ係が&lt;strong&gt;注文を受けたあと自分で厨房に入って料理を作り、出来上がるまでレジに戻らない&lt;/strong&gt;状態です。後ろに並んだお客さんは、その間ずっと待たされます。&lt;/p&gt;

&lt;p&gt;メール送信、CSV 出力、外部 API 連携のように&lt;strong&gt;時間がかかる処理&lt;/strong&gt;は、「あとでやる」ことにしてすぐお客さんに返事をしたい。これがジョブの出番です。&lt;/p&gt;




&lt;h2&gt;
  
  
  ジョブ = 注文票
&lt;/h2&gt;

&lt;p&gt;ジョブは「この仕事をやっておいて」という&lt;strong&gt;注文票&lt;/strong&gt;です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WelcomeMailJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;WelcomeMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver_now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;レジ係（コントローラ）は注文票を書いてボードに貼るだけです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;WelcomeMailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# ← 注文票を貼るだけ。一瞬で終わる&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;perform_later&lt;/code&gt; は「後で（later）実行して（perform）」という意味です。レジ係はすぐ次のお客さんの対応に戻れます。&lt;/p&gt;

&lt;h3&gt;
  
  
  注文票には「料理」ではなく「テーブル番号」を書く
&lt;/h3&gt;

&lt;p&gt;引数には &lt;code&gt;user&lt;/code&gt; そのものではなく &lt;code&gt;user.id&lt;/code&gt; を渡しています。&lt;/p&gt;

&lt;p&gt;注文票に料理そのものを貼り付ける人はいません。「3番テーブル、ハンバーグ」と書いておき、料理人が作るときに最新の情報を確認します。ジョブも同じで、&lt;strong&gt;ID だけ渡して、実行するときに DB から取り直す&lt;/strong&gt;のが基本です。キューに積んでから実行されるまでの間に、データが変わっているかもしれないからです。&lt;/p&gt;




&lt;h2&gt;
  
  
  キュー = 注文票ボード
&lt;/h2&gt;

&lt;p&gt;キューは注文票を&lt;strong&gt;到着順に並べておくボード&lt;/strong&gt;です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; レジ係                 注文票ボード（キュー）            料理人（ワーカー）
┌──────┐  貼る    ┌──────────────────────────┐  取る   ┌──────┐
│      │ ───────▶ │ [票3] [票2] [票1]         │ ──────▶ │ 調理 │
└──────┘          └──────────────────────────┘         └──────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;レジ係は&lt;strong&gt;貼るだけ&lt;/strong&gt;、料理人は&lt;strong&gt;取って作るだけ&lt;/strong&gt;。お互いの仕事を待ちません&lt;/li&gt;
&lt;li&gt;料理人は常に厨房で待機していて、票が貼られたらすぐ取りに来ます&lt;/li&gt;
&lt;li&gt;注文が殺到したら、票がボードに&lt;strong&gt;並ぶだけ&lt;/strong&gt;です。厨房はパンクしません&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  「注文のたびに料理人を雇えばいいのでは？」
&lt;/h3&gt;

&lt;p&gt;ボードに並ばせず、注文ごとに新しい料理人を雇って即調理させればいい、と思うかもしれません。Rails でいえば「ジョブごとに新しいプロセスを起動する」方式です。&lt;/p&gt;

&lt;p&gt;これは次の理由でうまくいきません。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;雇うのが高い&lt;/strong&gt;: Rails のプロセスは起動に数秒、メモリに数百MB かかります。毎回の研修コストが重すぎます&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;増えすぎる&lt;/strong&gt;: 1秒に1000件の注文が来たら、1000人が同時に厨房に入ってきます。CPU より先に、メモリや DB 接続（厨房の広さやコンロの数）が限界を迎えます&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;注文が消える&lt;/strong&gt;: 料理人が途中で倒れたら、その注文は誰にも知られずに消えます&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;やり直しができない&lt;/strong&gt;: 失敗しても、誰も作り直してくれません&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ボードと常駐の料理人なら、&lt;strong&gt;人数を一定に保ったまま、注文をなくさず、確実にこなせます&lt;/strong&gt;。キューは「待たせるための仕組み」ではなく、そのための仕組みです。&lt;/p&gt;




&lt;h2&gt;
  
  
  アダプター = ボードの種類
&lt;/h2&gt;

&lt;p&gt;注文票ボードにはいろいろな種類があります。ホワイトボード、紙の伝票、本部と繋がった受注システム……。&lt;/p&gt;

&lt;p&gt;ActiveJob は、&lt;strong&gt;どのボードを使っても注文票の書き方は同じ&lt;/strong&gt;になるように作られています。ボードごとの違いを吸収する「変換プラグ」がアダプターです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 注文票の書き方は、どのボードでも同じ&lt;/span&gt;
&lt;span class="no"&gt;WelcomeMailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# どのボードを使うかは設定 1 行で切り替える&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:async&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;代表的なボードを見ていきます。&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;:async&lt;/code&gt; ─ 店員が頭の中で覚えておく
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────── お店（Rails プロセス 1 つ）─────────────┐
│  レジ係 ──▶ 店員の頭の中のメモ ──▶ 店内の料理人       │
└──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails のデフォルトです。キューも料理人も、&lt;strong&gt;Rails プロセスの中に全部入っています&lt;/strong&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;追加で何もインストールしなくても動くので、&lt;strong&gt;学習や開発に便利&lt;/strong&gt;です&lt;/li&gt;
&lt;li&gt;ただし注文は店員の&lt;strong&gt;頭の中&lt;/strong&gt;にあるだけです。店を閉める（プロセスを再起動する、デプロイする）と、まだ作っていない注文は&lt;strong&gt;全部忘れられます&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;:inline&lt;/code&gt; ─ レジ係がその場で作る
&lt;/h3&gt;

&lt;p&gt;ボードを使わず、レジ係が&lt;strong&gt;その場で自分で料理します&lt;/strong&gt;。&lt;code&gt;perform_later&lt;/code&gt; と書いても非同期になりません。&lt;/p&gt;

&lt;p&gt;開発中に「ジョブがちゃんと動くか」をすぐ確かめたいときに便利です。&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;:test&lt;/code&gt; ─ 研修用のボード
&lt;/h3&gt;

&lt;p&gt;実際には料理を作らず、&lt;strong&gt;「注文票が正しく貼られたか」だけを記録&lt;/strong&gt;します。RSpec でテストするときに使います。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_enqueued_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WelcomeMailJob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;:solid_queue&lt;/code&gt; / &lt;code&gt;:sidekiq&lt;/code&gt; / &lt;code&gt;:sqs&lt;/code&gt; など ─ 店の外にあるボード
&lt;/h3&gt;

&lt;p&gt;本番用です。注文票は&lt;strong&gt;店の外にある受注システム&lt;/strong&gt;（DB、Redis、Amazon SQS など）に記録されます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌── お店（Rails プロセス）──┐      ┌── 店の外の受注システム ──┐
│  レジ係                    │ ───▶ │  注文票ボード             │
└────────────────────────────┘      └───────────────────────────┘
   ↑ 何度閉店・開店しても               ↑ 注文はこっちに残る
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;店を閉めても（Rails を再起動・デプロイしても）、&lt;strong&gt;注文は残ります&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;料理人を&lt;strong&gt;別の厨房（別のサーバー）&lt;/strong&gt;に置いて、必要なだけ増やせます&lt;/li&gt;
&lt;li&gt;失敗した注文を「5分後にもう一度」と&lt;strong&gt;貼り直せます&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ただし、ボード自体が壊れたときにどこまで残るかは、ボード次第です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; もメモリに置いています。永続化（スナップショットや追記ログ）を設定していないと、Redis の再起動で消えます&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB&lt;/strong&gt; や &lt;strong&gt;SQS&lt;/strong&gt; はディスクや複数拠点に保存されるので、より消えにくいです&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;:async&lt;/code&gt; との本当の違いは「メモリかディスクか」ではなく、&lt;strong&gt;ボードがお店（Rails）と一緒に閉まるかどうか&lt;/strong&gt;です。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;アダプター&lt;/th&gt;
&lt;th&gt;ボードの置き場所&lt;/th&gt;
&lt;th&gt;Rails 再起動で消える？&lt;/th&gt;
&lt;th&gt;ボード自体の再起動で消える？&lt;/th&gt;
&lt;th&gt;主な用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:async&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;店員の頭の中（Rails のメモリ）&lt;/td&gt;
&lt;td&gt;消える&lt;/td&gt;
&lt;td&gt;─&lt;/td&gt;
&lt;td&gt;学習・開発&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:inline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;なし（その場で作る）&lt;/td&gt;
&lt;td&gt;─&lt;/td&gt;
&lt;td&gt;─&lt;/td&gt;
&lt;td&gt;開発・デバッグ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;研修用の記録&lt;/td&gt;
&lt;td&gt;─&lt;/td&gt;
&lt;td&gt;─&lt;/td&gt;
&lt;td&gt;テスト&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:sidekiq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Redis（別サーバーのメモリ）&lt;/td&gt;
&lt;td&gt;消えない&lt;/td&gt;
&lt;td&gt;永続化の設定次第&lt;/td&gt;
&lt;td&gt;本番&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:solid_queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DB（ディスク）&lt;/td&gt;
&lt;td&gt;消えない&lt;/td&gt;
&lt;td&gt;ほぼ消えない&lt;/td&gt;
&lt;td&gt;本番&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;:sqs&lt;/code&gt; 系&lt;/td&gt;
&lt;td&gt;Amazon SQS&lt;/td&gt;
&lt;td&gt;消えない&lt;/td&gt;
&lt;td&gt;複数拠点に保存され消えにくい&lt;/td&gt;
&lt;td&gt;本番&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  本番で気をつけたいこと
&lt;/h2&gt;

&lt;p&gt;店の外にあるボードを使うと、&lt;code&gt;:async&lt;/code&gt; や &lt;code&gt;:inline&lt;/code&gt; では見えなかった問題が出てきます。&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 同じ注文票が 2 枚届くことがある
&lt;/h3&gt;

&lt;p&gt;SQS のような分散キューでは、まれに&lt;strong&gt;同じジョブが 2 回実行される&lt;/strong&gt;ことがあります。ハンバーグが 2 皿届いたら困りますよね。&lt;/p&gt;

&lt;p&gt;ジョブは「何回実行しても結果が同じになる」ように作ります（&lt;strong&gt;冪等性&lt;/strong&gt;）。例えば「送信済みフラグが立っていたら何もしない」といった工夫です。&lt;/p&gt;

&lt;h3&gt;
  
  
  2. 会計が確定する前に注文票を貼らない
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;WelcomeMailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# ← 危険&lt;/span&gt;
  &lt;span class="c1"&gt;# ...この後で例外が起きてロールバックすると？&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;トランザクションの中で &lt;code&gt;perform_later&lt;/code&gt; すると、次のことが起こりえます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;料理人が先に注文票を見て厨房に行ったのに、&lt;strong&gt;まだ注文が確定していない&lt;/strong&gt;（コミット前なので &lt;code&gt;User.find&lt;/code&gt; で見つからない）&lt;/li&gt;
&lt;li&gt;注文が&lt;strong&gt;キャンセルされた&lt;/strong&gt;（ロールバック）のに、料理人は作り始めてしまう&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注文票は&lt;strong&gt;会計が確定してから&lt;/strong&gt;（トランザクションのコミット後に）貼りましょう。&lt;code&gt;after_commit&lt;/code&gt; を使うのが定番です。&lt;/p&gt;

&lt;p&gt;なお Rails 7.2 以降には、トランザクション中に積んだジョブをコミット後まで遅らせる仕組み（&lt;code&gt;enqueue_after_transaction_commit&lt;/code&gt;）もあります。&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>レストランで理解する Rack・プロセス・スレッド・Unicorn・Puma・Action Cable</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Thu, 29 Oct 2026 12:21:32 +0000</pubDate>
      <link>https://dev.to/kaziusan/resutorandeli-jie-suru-rackpurosesusuretudounicornpumaaction-cable-5d30</link>
      <guid>https://dev.to/kaziusan/resutorandeli-jie-suru-rackpurosesusuretudounicornpumaaction-cable-5d30</guid>
      <description>&lt;p&gt;普段 Controller や Model を書いているぶんには、Rails の下で何が動いているかを意識することはあまりありません。&lt;br&gt;
ところが、&lt;strong&gt;Rails アプリにリアルタイムチャットや通知のような「つなぎっぱなし」の機能を入れようとすると&lt;/strong&gt;、急に「Unicorn だと相性が悪い」「Action Cable なら大丈夫」といった話が出てきます。&lt;/p&gt;

&lt;p&gt;この記事では、そうした話を理解するための土台となる Rails の足元の仕組みを、&lt;strong&gt;レストラン&lt;/strong&gt; にたとえて整理します。&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Rack：注文票の共通フォーマット
&lt;/h2&gt;

&lt;p&gt;Rack は、&lt;strong&gt;Ruby の Web サーバーと Web アプリをつなぐ共通の約束事&lt;/strong&gt;です。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;call(env)&lt;/code&gt; を持ち、&lt;code&gt;[ステータス, ヘッダ, ボディ]&lt;/code&gt; を返すオブジェクトなら Rack アプリ&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloApp&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'content-type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text/plain'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Hello'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;レストランでいうと &lt;strong&gt;注文票のフォーマット&lt;/strong&gt; です。フォーマットが決まっているので、ホール（Unicorn、Puma）と厨房（Rails、Sinatra）を自由に組み合わせられます。&lt;/p&gt;

&lt;p&gt;注文票が厨房に届くまでには、&lt;strong&gt;ミドルウェア&lt;/strong&gt;（入口の警備員や受付）を通ります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;お客さん → 警備員（Rack::Attack） → 受付（Rack::Cors） → 厨房（Rails）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. 層：ホール → 受付 → 厨房
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Web サーバー（Unicorn / Puma）   ホール：お客さんを迎え、店員を割り当てる
Rack ＋ ミドルウェア             注文票と、受付・警備
アプリケーション層（Rails）      厨房：Controller や Model が動く
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ここでいう「アプリケーション層」は、OSI 参照モデルのものとは別物で、&lt;strong&gt;Rails のコードが動いている段階&lt;/strong&gt;のことです。&lt;/p&gt;




&lt;h2&gt;
  
  
  3. プロセスとスレッド：店舗と店員
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;プロセス ＝ 店舗（専用の厨房・冷蔵庫を持つ）
スレッド ＝ 店員（同じ店の冷蔵庫を共用する）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;プロセス（店舗）&lt;/th&gt;
&lt;th&gt;スレッド（店員）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;メモリ&lt;/td&gt;
&lt;td&gt;別々&lt;/td&gt;
&lt;td&gt;共有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;作るコスト&lt;/td&gt;
&lt;td&gt;重い&lt;/td&gt;
&lt;td&gt;軽い&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;怖いところ&lt;/td&gt;
&lt;td&gt;メモリを食う&lt;/td&gt;
&lt;td&gt;共有データの取り合い&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;
&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; 100（共有しているので書き換わる）&lt;/span&gt;

&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;fork&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; 0（別の店舗なので影響しない）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;店員同士が冷蔵庫を共有していると、「2人が同時に卵の数を数えて、在庫数がずれる」といった事故が起きます。これが &lt;strong&gt;スレッドセーフ問題&lt;/strong&gt; です。Ruby では、クラス変数やグローバル変数がこの「共有の冷蔵庫」にあたります。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. ブロッキング：テーブルの横で立ち尽くす店員
&lt;/h2&gt;

&lt;p&gt;スレッドが何かを待っている間、他のことが何もできずに止まっている状態を &lt;strong&gt;ブロッキング&lt;/strong&gt; と呼びます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;店員：「ご注文は？」
お客さん：（メニューを30分眺めている……）
店員：（ずっとテーブルの横で待っている）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;普通のリクエストの待ち時間は一瞬なので問題になりません。困るのは、&lt;strong&gt;ロングポーリングや WebSocket のような「つなぎっぱなし」の通信&lt;/strong&gt;です。&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Unicorn：店員1人の店舗をたくさん出す
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unicorn マスター（オーナー）
 ├ ワーカー1（店員1人）
 ├ ワーカー2（店員1人）
 └ ワーカー3（店員1人）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;1ワーカーが同時に処理できるのは1件だけ&lt;/li&gt;
&lt;li&gt;店舗が独立しているので安全。ただしメモリを食う&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;つなぎっぱなしの通信が来ると、こうなります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ワーカー1〜3：全員がお客さんの横で待機
→ 次のお客さんの相手が誰もできない 💀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Puma：1店舗に店員をたくさん置く
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Puma プロセス
 ├ スレッド1（店員）
 ├ スレッド2（店員）
 └ スレッド3（店員）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Unicorn より少ないメモリで、多くのお客さんをさばける&lt;/li&gt;
&lt;li&gt;コードはスレッドセーフである必要がある&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ruby には &lt;strong&gt;GVL&lt;/strong&gt; があり、Ruby のコードを実行できるのは同時に1スレッドだけです（包丁が1本しかないイメージ）。それでも DB などを待っている間は包丁を手放すので、Web アプリなら十分に速く動きます。&lt;/p&gt;

&lt;p&gt;ただし、つなぎっぱなしの通信ではスレッドが待機で埋まり、結局 Unicorn と同じく詰まります。&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Action Cable：呼び出しベルで見回るホール係
&lt;/h2&gt;

&lt;p&gt;(Rails上で動きます)&lt;/p&gt;

&lt;p&gt;発想を変えて、&lt;strong&gt;テーブルの横で待つのをやめます。&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ホール係（1人）：
  各テーブルに呼び出しベルを置く
  → ベルが鳴ったテーブルにだけ、さっと対応する
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;これが &lt;strong&gt;イベントループ&lt;/strong&gt; です。待機中の接続に誰も張り付かないので、接続が大量にあってもさばけます。&lt;/p&gt;

&lt;p&gt;Action Cable は、Rack の &lt;code&gt;rack.hijack&lt;/code&gt; で接続そのものを引き取り、このイベントループに渡します。そのため Web サーバーのワーカーはすぐに手が空き、&lt;strong&gt;Unicorn の上でも動きます&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;弱点&lt;/strong&gt;：ホール係が1件の重い仕事に没頭すると、全テーブルが待たされます。&lt;br&gt;
→ Action Cable の上では重いクエリや &lt;code&gt;sleep&lt;/code&gt; を避け、重い処理はバックグラウンドジョブに任せます。&lt;/p&gt;




&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;待機中の接続に張り付くもの&lt;/th&gt;
&lt;th&gt;接続が増えると&lt;/th&gt;
&lt;th&gt;重い処理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unicorn&lt;/td&gt;
&lt;td&gt;プロセス&lt;/td&gt;
&lt;td&gt;メモリが足りなくなる&lt;/td&gt;
&lt;td&gt;強い&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puma&lt;/td&gt;
&lt;td&gt;スレッド&lt;/td&gt;
&lt;td&gt;スレッドが足りなくなる&lt;/td&gt;
&lt;td&gt;まあまあ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action Cable&lt;/td&gt;
&lt;td&gt;なし&lt;/td&gt;
&lt;td&gt;ほぼ平気&lt;/td&gt;
&lt;td&gt;弱い&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>infrastructure</category>
      <category>rails</category>
    </item>
    <item>
      <title>ngrok で「手元のサーバー」を一時的に公開して Webhook の送信をテスト</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:00:51 +0000</pubDate>
      <link>https://dev.to/kaziusan/ngrok-deshou-yuan-nosaba-wo-shi-de-nigong-kai-site-webhook-nosong-xin-wotesuto-hia</link>
      <guid>https://dev.to/kaziusan/ngrok-deshou-yuan-nosaba-wo-shi-de-nigong-kai-site-webhook-nosong-xin-wotesuto-hia</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;SaaS の開発で、&lt;strong&gt;Webhook の送信機能&lt;/strong&gt;を作っています。&lt;/p&gt;

&lt;p&gt;お客さんは SaaS のデータを自社の基幹システムへ連携していますが、今は SaaS の API を&lt;strong&gt;ポーリング&lt;/strong&gt;（定期的に問い合わせる方式）して変更を取りに来ているため、リアルタイムには反映されません。&lt;br&gt;
そこで、&lt;strong&gt;SaaS 側でデータが変わった瞬間に Webhook でお客さんのシステムへ送り、基幹システムのデータを自動で更新できるようにする&lt;/strong&gt;のが目的です。&lt;/p&gt;

&lt;p&gt;困ったのは、&lt;strong&gt;「送られる側」をローカルでどう用意するか&lt;/strong&gt;でした。&lt;/p&gt;

&lt;p&gt;送り先 URL は利用者が自由に入力できるため、社内ネットワークのアドレスを指定されると、サーバーに内部へのリクエストを送らせる &lt;strong&gt;SSRF（Server-Side Request Forgery）&lt;/strong&gt; 攻撃が成立してしまいます。&lt;br&gt;
その対策として、&lt;strong&gt;インターネット上に公開された HTTPS の URL 以外には送信しない&lt;/strong&gt;制限を入れました。そのため、手元の &lt;code&gt;localhost&lt;/code&gt; に受け口を立てても送れません。&lt;/p&gt;
&lt;h2&gt;
  
  
  ngrok とは
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;手元の PC で動いているサーバーに、一時的な公開 URL を付けてくれるツール&lt;/strong&gt;です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;外部のサービス ──→ https://xxxx.ngrok-free.app ──(ngrok が中継)──→ 自分の PC の localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;公開された HTTPS の URL になるので、SSRF 対策のチェックを緩めずにローカルで受信を確認できます。&lt;br&gt;
起動中は &lt;code&gt;http://localhost:3040&lt;/code&gt; で、届いたリクエストの中身も見られます。&lt;/p&gt;
&lt;h2&gt;
  
  
  使い方
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. 仮の受け口を作る
&lt;/h3&gt;

&lt;p&gt;POST を受け取って中身を表示し、200 を返すだけのサーバーです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# webhook_receiver.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPServer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 500 や 401 にすれば失敗時の挙動も確認できる
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nc"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 webhook_receiver.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. ngrok で公開する
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ngrok
ngrok config add-authtoken &amp;lt;あなたのトークン&amp;gt;
ngrok http 4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;表示された &lt;code&gt;https://xxxx.ngrok-free.app&lt;/code&gt; が公開 URL です。&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 送信側に URL を設定して動かす
&lt;/h3&gt;

&lt;p&gt;アプリの Webhook 設定にこの URL を入れてデータを更新すると、受け口のターミナルに届いた JSON が表示されます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01T12:00:00+09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"サンプル注文"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  注意点
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;公開 URL は誰でもアクセスできるので、確認が終わったらすぐ止める&lt;/li&gt;
&lt;li&gt;実データは流さず、ダミーデータで確認する&lt;/li&gt;
&lt;li&gt;細かいパターン（エラー・タイムアウトなど）は、自動テストで HTTP 通信をスタブして確認し、ngrok は最後の通し確認に使う&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ポーリングではリアルタイムにならないため、変更を即座に送る Webhook を作っている&lt;/li&gt;
&lt;li&gt;SSRF 対策で公開 URL にしか送れないが、ngrok を使えばチェックを緩めずにローカルで確認できる&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>「先頭カラムが同じインデックス、それ1つ無駄ちゃう？」— 冗長インデックスの見つけ方と消し方</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Tue, 15 Sep 2026 07:02:01 +0000</pubDate>
      <link>https://dev.to/kaziusan/xian-tou-karamugatong-ziindetukusu-sore1tuwu-tuo-tiyau-rong-chang-indetukusunojian-tukefang-toxiao-sifang-13g7</link>
      <guid>https://dev.to/kaziusan/xian-tou-karamugatong-ziindetukusu-sore1tuwu-tuo-tiyau-rong-chang-indetukusunojian-tukefang-toxiao-sifang-13g7</guid>
      <description>&lt;p&gt;テーブルのインデックスを眺めていて、こんな2本が並んでいるのを見つけたことはないだろうか。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;        &lt;span class="nv"&gt;`idx_logs_on_import_id`&lt;/span&gt;            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`import_id`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="nv"&gt;`idx_logs_on_import_id_and_file`&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`import_id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;`file_path`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;結論から言うと、&lt;strong&gt;上の単独インデックスはたいてい消せる&lt;/strong&gt;。この記事では「なぜ消せるのか」「本当に消していいのか（消せない例外）」「どう機械的に見つけるか」「安全な消し方」までを一通りまとめる。&lt;/p&gt;




&lt;h2&gt;
  
  
  なぜ「単独 &lt;code&gt;(import_id)&lt;/code&gt;」が冗長なのか
&lt;/h2&gt;

&lt;p&gt;ポイントは &lt;strong&gt;複合インデックスの “左端プレフィックス（leftmost prefix）” は、単独インデックスとしても使える&lt;/strong&gt; という B-Tree インデックスの性質だ。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;(import_id, file_path)&lt;/code&gt; というインデックスは、まず &lt;code&gt;import_id&lt;/code&gt; で並び、その中で &lt;code&gt;file_path&lt;/code&gt; で並ぶ。電話帳が「姓 → 名」の順に並んでいるのを想像するといい。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;「姓だけで探す」→ できる（左から順に並んでいるから）&lt;/li&gt;
&lt;li&gt;「名だけで探す」→ できない&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同じように &lt;code&gt;(import_id, file_path)&lt;/code&gt; は、&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHERE import_id = ?&lt;/code&gt; → &lt;strong&gt;効く&lt;/strong&gt;（左端が &lt;code&gt;import_id&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE file_path = ?&lt;/code&gt; → 効かない（2番目のカラム単独では使えない）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;つまり &lt;code&gt;WHERE import_id = ?&lt;/code&gt; の検索は、複合インデックスがそのまま賄える。&lt;strong&gt;単独の &lt;code&gt;(import_id)&lt;/code&gt; は複合と役割が丸かぶり&lt;/strong&gt;というわけだ。&lt;/p&gt;




&lt;h2&gt;
  
  
  冗長インデックスは「あって困る」
&lt;/h2&gt;

&lt;p&gt;「使わないだけで害はないのでは？」と思うかもしれないが、インデックスはタダではない。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;書き込みが遅くなる&lt;/strong&gt;：INSERT / UPDATE / DELETE のたびに、全インデックスを更新する必要がある。冗長な1本ぶん、無駄な更新コストが毎回乗る。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ディスク・メモリを食う&lt;/strong&gt;：インデックスは実データとは別に実体を持つ。バッファプールも圧迫する。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;オプティマイザの選択肢を無駄に増やす&lt;/strong&gt;：実行計画の検討対象が増える。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;読み取りのメリットはゼロ（複合で足りる）なのに、書き込みコストだけ払っている状態。これが「冗長」を消すべき理由だ。&lt;/p&gt;




&lt;h2&gt;
  
  
  どうやって機械的に見つけるか
&lt;/h2&gt;

&lt;p&gt;目視で全テーブルを追うのは非現実的。ツールに任せよう。&lt;/p&gt;

&lt;h3&gt;
  
  
  1. MySQL 標準ビュー &lt;code&gt;sys.schema_redundant_indexes&lt;/code&gt;（おすすめ・導入不要）
&lt;/h3&gt;

&lt;p&gt;MySQL 5.7 以降なら追加インストール不要で使える。左端プレフィックスの重複を判定し、&lt;strong&gt;削除用の SQL まで生成&lt;/strong&gt;してくれる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;redundant_index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- 冗長な方（消す候補）&lt;/span&gt;
  &lt;span class="n"&gt;redundant_index_columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dominant_index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- それを包含している方（残す）&lt;/span&gt;
  &lt;span class="n"&gt;dominant_index_columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sql_drop_index&lt;/span&gt;            &lt;span class="c1"&gt;-- そのまま実行できる DROP 文&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema_redundant_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'logs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;出力イメージ：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;redundant_index_name &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;idx_logs_on_import_id&lt;/span&gt;
&lt;span class="na"&gt;dominant_index_name  &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;idx_logs_on_import_id_and_file&lt;/span&gt;
&lt;span class="na"&gt;sql_drop_index       &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALTER TABLE `app`.`logs` DROP INDEX `idx_logs_on_import_id`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;pt-duplicate-key-checker&lt;/code&gt;（Percona Toolkit / CLI）
&lt;/h3&gt;

&lt;p&gt;DB 全体をまとめてスキャンして、重複・冗長インデックスと DROP 文を吐く定番ツール。CI や定期ジョブに組み込みやすい。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pt-duplicate-key-checker &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;readonly&lt;/span&gt; &lt;span class="nt"&gt;--ask-pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;sys.schema_unused_indexes&lt;/code&gt;（未使用インデックスの検出）
&lt;/h3&gt;

&lt;p&gt;「冗長」とは別軸で、「前回のサーバ起動以降、一度もクエリで使われていないインデックス」を洗い出せる。ただし後述の FK 注意あり。&lt;/p&gt;

&lt;h3&gt;
  
  
  4. ORM 付属のツール（例：Rails の &lt;code&gt;active_record_doctor&lt;/code&gt;）
&lt;/h3&gt;

&lt;p&gt;アプリ側から &lt;code&gt;detect_extraneous_indexes&lt;/code&gt;（冗長）や &lt;code&gt;detect_unindexed_foreign_keys&lt;/code&gt;（FK に索引が無い）を検出できる。マイグレーションと同じ言語で回せるので CI 相性が良い。&lt;/p&gt;




&lt;h2&gt;
  
  
  【重要】消してはいけない冗長候補：外部キー（FK）の索引
&lt;/h2&gt;

&lt;p&gt;ツールが「未使用」「候補」と出しても、&lt;strong&gt;消すとエラーになるインデックスがある&lt;/strong&gt;。それが &lt;strong&gt;外部キーの索引&lt;/strong&gt;だ。&lt;/p&gt;

&lt;p&gt;MySQL は「親テーブルの行が削除・更新されたとき、それを参照している子行がいないか」を毎回チェックする。これは実質 &lt;code&gt;WHERE fk_column = ?&lt;/code&gt; の検索で、&lt;strong&gt;索引が無いと毎回フルスキャンになって激遅&lt;/strong&gt;。だから MySQL は「FK を張るなら子カラムに索引を必ず持て」と要求し、無ければ自動で作る。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- これは「未使用」に見えても、FK のために必須。消すと ERROR 1553。&lt;/span&gt;
&lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="nv"&gt;`fk_logs_on_user_id`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_id`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;`fk_logs_on_user_id`&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_id`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nv"&gt;`users`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`id`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;見分け方&lt;/strong&gt;：その FK カラムを「左端」に持つ “別の” インデックスがあるか？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;あるなら（例：複合インデックスの左端が FK カラム）→ FK の要件はそちらが満たすので、単独インデックスは消せる。&lt;/li&gt;
&lt;li&gt;無いなら → その索引が FK 唯一の頼み → &lt;strong&gt;消せない&lt;/strong&gt;（消すと &lt;code&gt;ERROR 1553: Cannot drop index ... needed in a foreign key constraint&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;冒頭の例で単独 &lt;code&gt;(import_id)&lt;/code&gt; を消せるのは、複合 &lt;code&gt;(import_id, file_path)&lt;/code&gt; の左端が &lt;code&gt;import_id&lt;/code&gt; で、&lt;strong&gt;FK の索引要件も肩代わりできる&lt;/strong&gt;からだ。&lt;/p&gt;




&lt;h2&gt;
  
  
  安全な消し方
&lt;/h2&gt;

&lt;h3&gt;
  
  
  マイグレーション
&lt;/h3&gt;

&lt;p&gt;インデックスを1本 DROP するだけ。可逆（ロールバックで復活）にしておく。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RemoveRedundantIndexFromLogs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;remove_index&lt;/span&gt; &lt;span class="ss"&gt;:logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: :import_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'idx_logs_on_import_id'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;素の SQL なら：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`logs`&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`idx_logs_on_import_id`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  適用前に「本当に消せるか」を実地で裏取り
&lt;/h3&gt;

&lt;p&gt;とくに &lt;strong&gt;FK が絡むカラム&lt;/strong&gt;は、机上で「複合が包含するはず」と思っても、実際に DROP して &lt;code&gt;ERROR 1553&lt;/code&gt; が出ないかを一度確認しておくと安心だ。開発 DB で up → down して、元に戻せることも確認しておく。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 実際に DROP が通るか（FK エラーが出ないか）を確認&lt;/span&gt;
bin/rails db:migrate:up   &lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;version&amp;gt;
&lt;span class="c"&gt;# 確認できたら元に戻す（開発 DB を汚さない）&lt;/span&gt;
bin/rails db:migrate:down &lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;up&lt;/code&gt; が &lt;code&gt;ERROR 1553&lt;/code&gt; を出さずに通れば、「複合インデックスが FK の索引要件を満たしている」ことの動かぬ証拠になる。&lt;/p&gt;

</description>
      <category>sql</category>
      <category>rails</category>
    </item>
    <item>
      <title>Railsのstructure.sqlって何？ 何のため?</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:55:12 +0000</pubDate>
      <link>https://dev.to/kaziusan/railsno-structuresql-tutehe-he-notame-313n</link>
      <guid>https://dev.to/kaziusan/railsno-structuresql-tutehe-he-notame-313n</guid>
      <description>&lt;p&gt;Railsのプロジェクトを触っていると、&lt;code&gt;db/structure.sql&lt;/code&gt; という巨大なファイルに出会います。&lt;br&gt;
「これ何？勝手に書き換わるけど触っていいの？」と戸惑いがちなこのファイルを、初心者向けにまとめます。&lt;/p&gt;


&lt;h2&gt;
  
  
  結論：DBの「完成形の設計図」
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;structure.sql&lt;/code&gt; は、ひとことで言うと&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;今のデータベースの構造を、ゼロから丸ごと再現するためのSQL命令をまとめた「完成形の設計図（スナップショット）」&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;です。しかも&lt;strong&gt;Railsが自動で生成・更新する&lt;/strong&gt;ファイルで、人が手で書くものではありません。&lt;/p&gt;

&lt;p&gt;中身はこんなイメージです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`deals`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;              &lt;span class="c1"&gt;-- 「案件」テーブルを作る命令&lt;/span&gt;
  &lt;span class="nv"&gt;`id`&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="nb"&gt;unsigned&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`name`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`contacts`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;-- 「コンタクト」テーブルを作る命令&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="err"&gt;（全テーブル分つづく）&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;-- 末尾に「どの設計変更まで適用済みか」の台帳&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`schema_migrations`&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'20260806000000'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'20260805000000'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;前半に全テーブルの &lt;code&gt;CREATE TABLE&lt;/code&gt;、末尾に「どこまで変更を適用済みか」の台帳（&lt;code&gt;schema_migrations&lt;/code&gt;）が入っています。&lt;/p&gt;




&lt;h2&gt;
  
  
  前提知識：マイグレーションとの関係
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;structure.sql&lt;/code&gt; を理解するには、&lt;strong&gt;マイグレーション&lt;/strong&gt;とセットで考えるのが近道です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;マイグレーション&lt;/strong&gt; = DB構造の&lt;strong&gt;変更履歴&lt;/strong&gt;（＝設計変更の指示書の束）

&lt;ul&gt;
&lt;li&gt;例：「dealsテーブルにstatus列を追加する」といったファイルが1変更につき1つ&lt;/li&gt;
&lt;li&gt;ファイル名は &lt;code&gt;20260803000000&lt;/code&gt; のような &lt;strong&gt;日付＋番号&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structure.sql&lt;/strong&gt; = その履歴を&lt;strong&gt;全部適用し終えた最終結果のスナップショット&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;図にするとこうです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;① 開発者がマイグレーションファイルを書く（変更の指示書）
         例:「dealsにstatus列を追加」

② db:migrate を実行 → 実際にDBが変更される

③ ★このときRailsが自動でstructure.sqlを"今のDBの姿"に更新する★
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;つまり &lt;code&gt;structure.sql&lt;/code&gt; は、&lt;code&gt;db:migrate&lt;/code&gt; の&lt;strong&gt;副産物として自動更新される&lt;/strong&gt;もの。原則、手で編集しません。&lt;/p&gt;




&lt;h2&gt;
  
  
  何のためにあるの？
&lt;/h2&gt;

&lt;p&gt;一番の存在理由は、&lt;strong&gt;新しくDBを一から作るときに速いから&lt;/strong&gt;です。&lt;/p&gt;

&lt;p&gt;新メンバーが環境構築するとき、あるいはテスト用DBを毎回作り直すとき、DBを「まっさら → 最新の形」にする必要があります。その方法は2つあります。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;やること&lt;/th&gt;
&lt;th&gt;速度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;マイグレーションを全部実行&lt;/td&gt;
&lt;td&gt;何百個の指示書を1個ずつ順番に実行&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;遅い&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;structure.sql&lt;/code&gt; を読み込む&lt;/td&gt;
&lt;td&gt;完成形を一発で再現&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;速い&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  なぜマイグレーション全実行は遅いのか
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;理由①：歴史をもう一度なぞるので無駄が多い&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;マイグレーションは変更履歴なので、全実行すると過去の回り道まで再現します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;① dealsテーブルを作る
② dealsにstatus列を追加
③ ②のstatus列を削除する   ← 気が変わった
④ dealsにstate列を追加
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最終的に欲しいのは「statusは無く、stateがある」状態だけ。でも全実行は②で作って③で消す&lt;strong&gt;無意味な往復まで律儀に再現&lt;/strong&gt;します。&lt;code&gt;structure.sql&lt;/code&gt; は最終結果だけなので回り道しません。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;理由②：1個ずつ直列でしか実行できない（オーバーヘッド）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;マイグレーションは順序依存なので並列化できず、1個ごとに「Rails起動→コード読込→SQL変換→実行→台帳記録」の準備・後片付けが発生します。これが数百回積み重なる。&lt;br&gt;
&lt;code&gt;structure.sql&lt;/code&gt; は1本の巨大なSQLを流し込むだけなので、この繰り返しコストがほぼゼロです。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;理由③：中間状態の作り替えが積もる&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;同じ「state列を追加」でも、マイグレーションはテーブル作成→列追加→列削除→別の列追加…と中間状態を何度も作り替えます。インデックスの貼り直しなどが絡むとさらに遅くなります。&lt;/p&gt;

&lt;h3&gt;
  
  
  たとえ話
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;マイグレーション全実行 = 料理番組を&lt;strong&gt;1話目から全部見て&lt;/strong&gt;、途中の失敗作まで作り直して追体験する&lt;/li&gt;
&lt;li&gt;structure.sql = &lt;strong&gt;完成した料理の写真とレシピ&lt;/strong&gt;を見て、それだけ作る&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;欲しいのは完成品だけなので、後者が圧倒的に速い、というわけです。&lt;/p&gt;




&lt;h2&gt;
  
  
  だから2つを使い分ける
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;structure.sql&lt;/code&gt; とマイグレーションは、どちらか一方が偉いのではなく、&lt;strong&gt;場面で使い分けます&lt;/strong&gt;。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;場面&lt;/th&gt;
&lt;th&gt;使うもの&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;新環境構築・テストDB作成&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;structure.sql&lt;/strong&gt;（完成形）&lt;/td&gt;
&lt;td&gt;履歴をなぞる必要はなく、速く最新状態を再現したい&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;稼働中の本番DBを変更&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;マイグレーション&lt;/strong&gt;（差分だけ）&lt;/td&gt;
&lt;td&gt;既存データを保ったまま「今回の変更分」だけ安全に当てたい&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;新メンバーの環境構築は「まっさら → 最新形にしたいだけ」なので、&lt;code&gt;structure.sql&lt;/code&gt; が最適です。&lt;/p&gt;




&lt;h2&gt;
  
  
  補足：schema.rb と structure.sql
&lt;/h2&gt;

&lt;p&gt;Railsの設計図には2形式があり、プロジェクトごとにどちらか一方を選びます。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ファイル&lt;/th&gt;
&lt;th&gt;中身&lt;/th&gt;
&lt;th&gt;特徴&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db/schema.rb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rubyで書かれた設計図&lt;/td&gt;
&lt;td&gt;シンプル。Railsのデフォルト&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db/structure.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生のSQLで書かれた設計図&lt;/td&gt;
&lt;td&gt;トリガーやDB独自機能まで忠実に表現できる&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MySQLのトリガーや細かい制約など、Rubyの &lt;code&gt;schema.rb&lt;/code&gt; では表現しきれない機能を使っているプロジェクトでは &lt;code&gt;structure.sql&lt;/code&gt; を選びます。&lt;/p&gt;




&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;質問&lt;/th&gt;
&lt;th&gt;答え&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;structure.sqlって何？&lt;/td&gt;
&lt;td&gt;今のDB全体の構造を再現するSQLをまとめた「完成形の設計図」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Railsが作るの？&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;はい&lt;/strong&gt;。&lt;code&gt;db:migrate&lt;/code&gt; 実行時に自動生成・更新される&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;手で書く？&lt;/td&gt;
&lt;td&gt;原則ノー。自動更新される&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;何の役に立つ？&lt;/td&gt;
&lt;td&gt;新環境・テストDBを&lt;strong&gt;一瞬で最新構造にする&lt;/strong&gt;ため&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>rails</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQL複数カラムの計算結果をインデックスにまとめる設計手法と、書き込みオーバーヘッドの限界</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:42:59 +0000</pubDate>
      <link>https://dev.to/kaziusan/sqlfu-shu-karamunoji-suan-jie-guo-woindetukusunimatomerushe-ji-shou-fa-to-shu-kiip-miobahetudonoxian-jie-52de</link>
      <guid>https://dev.to/kaziusan/sqlfu-shu-karamunoji-suan-jie-guo-woindetukusunimatomerushe-ji-shou-fa-to-shu-kiip-miobahetudonoxian-jie-52de</guid>
      <description>&lt;p&gt;データベースで複数のカラムを掛け合わせた検索（例：&lt;code&gt;(単価 × 数量 - 割引) × 税率&lt;/code&gt;）を実行すると、通常は全件スキャン（Table Scan）が発生して大幅な遅延を引き起こします。&lt;/p&gt;

&lt;p&gt;この記事では、複数カラムの計算結果を単一または複合インデックスへ集約する方法と、気になる書き込み時のオーバーヘッド（遅延リスク）のレベル感・警戒時の対策について解説します。&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 複数カラムの計算結果をインデックスにまとめる2つの手法
&lt;/h2&gt;

&lt;p&gt;例えば、以下の 5 カラムを持つ &lt;code&gt;orders&lt;/code&gt; テーブルを考えます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt;（単価）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quantity&lt;/code&gt;（数量）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;discount&lt;/code&gt;（値引き）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tax_rate&lt;/code&gt;（税率）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;（注文ステータス）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;「合計請求金額が 10,000 円以上の完了済み（completed）注文」を高速検索したい場合、アプローチは 2 通り存在します。&lt;/p&gt;

&lt;h3&gt;
  
  
  アプローチA：式インデックス（Expression Index）を直接貼る
&lt;/h3&gt;

&lt;p&gt;計算式そのものをインデックスに定義し、&lt;code&gt;status&lt;/code&gt; カラムと組み合わせた複合インデックスを作成します。&lt;/p&gt;

&lt;h4&gt;
  
  
  SQL (PostgreSQL)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_total_amount_and_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; 
  &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Rails migration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddCalculatedIndexToOrders&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
              &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"((price * quantity - discount) * (1 + tax_rate))"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
              &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"idx_orders_total_amount_and_status"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  アプローチB：生成列（Generated Column） ＋ 複合インデックス【おすすめ】
&lt;/h3&gt;

&lt;p&gt;式が複雑な場合、テーブルに「計算結果カラム」を定義してそこにインデックスを貼る方が、SQLの可読性とメンテナンス性が格段に向上します。&lt;/p&gt;

&lt;h4&gt;
  
  
  Rails (Migration)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddTotalAmountToOrders&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;change_table&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="c1"&gt;# 1. 4つのカラムを使った計算列を定義（DB側で自動計算＆保存される）&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;virtual&lt;/span&gt; &lt;span class="ss"&gt;:total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="ss"&gt;type: :decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="ss"&gt;precision: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="ss"&gt;scale: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s2"&gt;"(price * quantity - discount) * (1 + tax_rate)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="ss"&gt;stored: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# 2. 生成列と status の複合インデックスを作成&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"idx_orders_total_amount_and_status"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. 書き込み（INSERT / UPDATE）オーバーヘッドのレベル感
&lt;/h2&gt;

&lt;p&gt;計算インデックスや生成列を使用すると、対象カラムのいずれか 1 つでも INSERT または UPDATE されるたびに「再計算 ＋ B-Treeインデックスの再配置」が発生します。&lt;/p&gt;

&lt;p&gt;パフォーマンスへの影響度は「データ件数」よりも「1秒あたりの書き込み・更新回数（TPS）」に依存します。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;警戒レベル&lt;/th&gt;
&lt;th&gt;1秒あたりの書き込み/更新数 (TPS)&lt;/th&gt;
&lt;th&gt;アプリへの影響・状況判定・推奨アクション&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;レベル 1：安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;〜 50 回/秒&lt;/td&gt;
&lt;td&gt;一般的なWebシステムやECサイト。ミリ秒以下の処理で完結する。&lt;br&gt;&lt;strong&gt;対策不要（積極的に導入して良い）&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;レベル 2：注意&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100 〜 500 回/秒&lt;/td&gt;
&lt;td&gt;スパイク時のアクセスやSNSアプリ。書き込みのレイテンシがわずかに上昇。&lt;br&gt;&lt;strong&gt;モニタリング継続（Slow Queryログを監視）&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;レベル 3：警戒&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,000 回/秒 以上&lt;/td&gt;
&lt;td&gt;IoTデータ受信、金融取引、大量バッチ処理。CPU使用率急増・ロック詰まりが発生。&lt;br&gt;&lt;strong&gt;要対策（アーキテクチャの見直しが必要）&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  どのくらいの「データ量」で影響が大きくなる？
&lt;/h4&gt;

&lt;p&gt;データ量が増えると、「インデックス自体のサイズがメモリ（RAM）に入り切らなくなったタイミング」でパフォーマンスが急激に落ちます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;メモリに収まるサイズ（〜数百万件）：&lt;br&gt;
インデックスの更新処理がすべてメモリ（RAM）上で完結するため、計算オーバーヘッドがあってもミリ秒以下（数ミリ秒）で終わります。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;メモリに入り切らないサイズ（数千万件〜億単位）：&lt;br&gt;
インデックスを更新するたびにディスク（SSD/HDD）への読み書きが発生します。こうなると、1回の UPDATE の遅延が数ミリ秒から数ミリ秒〜数秒へと跳ね上がります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  特に遅くなりやすい「ワーストケース」のパターン
&lt;/h4&gt;

&lt;p&gt;同じ書き込みでも、以下のパターンは特にオーバーヘッドが大きくなります。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;更新が超頻繁なカラム（例：updated_at や status）が含まれている&lt;br&gt;
price などの金額は一度決まると滅多に変わりませんが、status や updated_at が計算式に含まれていると、ステータスが変わるたびに計算＆インデックス再配置が発生します。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;一括更新（UPDATE ... WHERE や BULK INSERT）を行う&lt;br&gt;
1件ずつの更新なら数ミリ秒の遅延で済みますが、UPDATE orders SET price = price * 1.1 のように10万件を一括更新する場合、10万回の再計算とインデックス更新が同時に走り、DBが数秒〜数分フリーズします。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>rails</category>
    </item>
    <item>
      <title>Rails の `dependent: :destroy` が激重になる話を簡単な例で</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:28:26 +0000</pubDate>
      <link>https://dev.to/kaziusan/rails-no-dependent-destroy-gaben-fan-deji-zhong-ninaruhua-wojian-dan-nali-de-301a</link>
      <guid>https://dev.to/kaziusan/rails-no-dependent-destroy-gaben-fan-deji-zhong-ninaruhua-wojian-dan-nali-de-301a</guid>
      <description>&lt;h2&gt;
  
  
  何が起きるか
&lt;/h2&gt;

&lt;p&gt;親レコードを消すと、子レコードも一緒に消えて便利な &lt;code&gt;dependent: :destroy&lt;/code&gt;（いわゆる cascade 削除）。&lt;br&gt;
でも子が&lt;strong&gt;コールバック&lt;/strong&gt;を持っていると、削除のたびにそれが発火します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:books&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;touch: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# 消すと親の更新日時を更新（いわゆる touch）&lt;/span&gt;
  &lt;span class="n"&gt;after_destroy&lt;/span&gt; &lt;span class="ss"&gt;:reindex_search&lt;/span&gt;     &lt;span class="c1"&gt;# 1件消すたびに Elasticsearch を再インデックス（同期・重い）&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;author.destroy&lt;/code&gt; すると…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Author を1件destroy
 └─ books が1000件あれば 1件ずつ destroy（cascade）
      └─ そのたびに親を touch し、after_destroy で
         Elasticsearch 更新が「直列」で1000回走る 😱
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;件数が少ないうちは気づかず、本番のデータ量で突然タイムアウトします。&lt;/p&gt;

&lt;h2&gt;
  
  
  対策：重い子だけ「コールバック無し」で先に消す
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;delete_all&lt;/code&gt; はコールバック（cascade も touch も）を発火させず、SQL 1発で消します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author_id: &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;delete_all&lt;/span&gt;  &lt;span class="c1"&gt;# ① 重い子を静かに一掃&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;                               &lt;span class="c1"&gt;# ② もう子はいないので連鎖しない&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Elasticsearch の更新は捨てずに、あとで非同期・一括で1回だけ&lt;/span&gt;
&lt;span class="no"&gt;ReindexJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# ③ トランザクションの外で&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ポイント3つ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;やること&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;重い子を &lt;code&gt;delete_all&lt;/code&gt; で先に消す&lt;/td&gt;
&lt;td&gt;コールバック（cascade / touch）を発火させない（&lt;code&gt;destroy_all&lt;/code&gt; は発火する）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本体はそのあと消す&lt;/td&gt;
&lt;td&gt;連鎖する子が残っていないので重い処理が走らない&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重い処理（ES再インデックス）は非同期ジョブで一括&lt;/td&gt;
&lt;td&gt;「同期で1件ずつ」→「非同期でまとめて1回」に置き換え&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  用語メモ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cascade（カスケード）&lt;/strong&gt; … 親を消すと子も連鎖して消える挙動。&lt;code&gt;dependent: :destroy&lt;/code&gt; がこれ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;touch&lt;/strong&gt; … 中身は変えず「更新された印」として関連レコードの更新日時だけ更新すること&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;destroy&lt;/code&gt; = 1件ずつ読み込み、&lt;strong&gt;コールバックを呼ぶ&lt;/strong&gt;（安全だが遅い）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete&lt;/code&gt; = SQLで直接、&lt;strong&gt;コールバックを呼ばない&lt;/strong&gt;（速いが自己責任）&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQLの自己結合（self-join）を一番簡単な例で</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Mon, 14 Sep 2026 04:51:17 +0000</pubDate>
      <link>https://dev.to/kaziusan/sqlnozi-ji-jie-he-self-joinwo-fan-jian-dan-nali-de-o98</link>
      <guid>https://dev.to/kaziusan/sqlnozi-ji-jie-he-self-joinwo-fan-jian-dan-nali-de-o98</guid>
      <description>&lt;p&gt;「同じテーブルを自分自身に結合する」のが&lt;strong&gt;自己結合&lt;/strong&gt;です。&lt;br&gt;
初めて見ると「え、同じテーブルをJOIN…？」と戸惑いますが、&lt;br&gt;
⭐️ &lt;strong&gt;親子・上下の関係が1つのテーブルに入っているとき&lt;/strong&gt;の定番テクニックです。&lt;/p&gt;
&lt;h3&gt;
  
  
  例：社員テーブル（社員も上司も“社員”）
&lt;/h3&gt;

&lt;p&gt;上司も社員なので、同じ &lt;code&gt;employees&lt;/code&gt; テーブルに入っています。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;manager_id（上司のid）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;山田（部長）&lt;/td&gt;
&lt;td&gt;(なし)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;佐藤&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;鈴木&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;「各社員と、その&lt;strong&gt;上司の名前&lt;/strong&gt;を並べて出したい」とき、&lt;br&gt;
&lt;code&gt;employees&lt;/code&gt; を&lt;strong&gt;もう一度、別名で結合&lt;/strong&gt;します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;emp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;社員&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;boss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;上司&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;emp&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt;   &lt;span class="c1"&gt;-- ← 同じテーブルを別名で結合&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;emp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;結果:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;社員&lt;/th&gt;
&lt;th&gt;上司&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;山田（部長）&lt;/td&gt;
&lt;td&gt;(なし)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;佐藤&lt;/td&gt;
&lt;td&gt;山田（部長）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;鈴木&lt;/td&gt;
&lt;td&gt;山田（部長）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ポイントは「別名（AS）」
&lt;/h3&gt;

&lt;p&gt;同じテーブルなので、&lt;code&gt;AS emp&lt;/code&gt; / &lt;code&gt;AS boss&lt;/code&gt; と&lt;strong&gt;別名を付けて別テーブル扱い&lt;/strong&gt;にするのが肝です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;emp.name&lt;/code&gt; … 社員本人の名前&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;boss.name&lt;/code&gt; … 上司の名前&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;別名がないと「どっちの name？」が区別できません。&lt;/p&gt;

&lt;h3&gt;
  
  
  LEFT JOIN にする理由
&lt;/h3&gt;

&lt;p&gt;上司がいない人（部長など）も結果に残したいので &lt;code&gt;LEFT JOIN&lt;/code&gt; を使います。&lt;br&gt;
&lt;code&gt;INNER JOIN&lt;/code&gt; だと上司がいない行が消えてしまいます。&lt;/p&gt;

&lt;h3&gt;
  
  
  まとめ
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;親子・上下の関係が同じテーブルにあるとき、テーブルを別名で自分自身に結合すると「本人＋相手」を1行に並べられる。&lt;/strong&gt; これが自己結合。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;社員×上司、カテゴリ×親カテゴリ、取引先×その親の取引先など、&lt;br&gt;
「同じ種類のもの同士の関係」を1テーブルで持つ設計で活躍します。&lt;/p&gt;

</description>
      <category>sql</category>
    </item>
    <item>
      <title>「Blob」って何？ ブラウザでファイルをダウンロードする仕組み</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Fri, 11 Sep 2026 03:13:11 +0000</pubDate>
      <link>https://dev.to/kaziusan/blob-tutehe-burauzadehuairuwodaunrodosurushi-zu-mi-5hm4</link>
      <guid>https://dev.to/kaziusan/blob-tutehe-burauzadehuairuwodaunrodosurushi-zu-mi-5hm4</guid>
      <description>&lt;p&gt;CSVダウンロード機能を実装すると、こんな見慣れないコードが出てきます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objectURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;データ.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「Blob？」「URLを作る？」「それをダウンロード？どこから？」を、この記事でスッキリさせます。&lt;/p&gt;

&lt;h2&gt;
  
  
  Blob とは
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blob = 中身が何でもいい「データのかたまり」&lt;/strong&gt;。ブラウザにとっての"ファイルのようなもの"です。文字・画像・CSV・PDFなど何でも入り、「これはCSVです」といった種類ラベル（MIMEタイプ）を持てます。&lt;/p&gt;

&lt;p&gt;ブラウザに「これはファイルだよ、保存してね」と伝えるには、データを&lt;strong&gt;"ファイルの形"＝Blob&lt;/strong&gt;にする必要があります。生の文字データのままでは、ただの文字列としか扱われません。&lt;/p&gt;

&lt;h2&gt;
  
  
  手紙に例えると
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;手紙の例え&lt;/th&gt;
&lt;th&gt;実際のもの&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;書類（CSVの中身）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;response.data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;封筒に入れる&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new Blob(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;宛名シールを貼る&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createObjectURL&lt;/code&gt; のURL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;投函する＝保存&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt; をクリック&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  コードの意味
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ① バイナリのまま受け取る（これを忘れるとCSVが壊れることがある）&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;responseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ② データを「CSVファイル」の箱に入れる&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ③ その箱を指す一時的なURLを発行する&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objectURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ④ そのURLをファイルとして保存させる&lt;/span&gt;
&lt;span class="nf"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;データ.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ポイントは &lt;strong&gt;③のURL&lt;/strong&gt;。これは &lt;code&gt;https://...&lt;/code&gt; のようなサーバー上の場所&lt;strong&gt;ではなく&lt;/strong&gt;、&lt;br&gt;
&lt;strong&gt;いま手元（ブラウザのメモリ）にあるデータを指す案内シール&lt;/strong&gt; です。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;downloadFile&lt;/code&gt; の中身も単純で、「&lt;code&gt;download&lt;/code&gt;属性つきの&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;を作ってJSでクリックするだけ」。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;リンク先が &lt;code&gt;blob:&lt;/code&gt; URL なので、&lt;strong&gt;サーバーに行かず、手元のデータをそのままファイル保存&lt;/strong&gt; します。&lt;br&gt;
つまり「URLをダウンロード」と言っても、&lt;strong&gt;落とし直しではなく、すでに持っているデータを保存しているだけ&lt;/strong&gt;なのです。&lt;/p&gt;
&lt;h2&gt;
  
  
  補足：使い終わったURLは片付ける
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;createObjectURL&lt;/code&gt; のURLはメモリを占有し続けるので、使い終わったら解放するのが丁寧です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blob&lt;/strong&gt; = ブラウザにとっての"ファイルのようなデータのかたまり"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;createObjectURL&lt;/code&gt;&lt;/strong&gt; のURLは、サーバーではなく&lt;strong&gt;手元データへの案内シール&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt; クリック&lt;/strong&gt;で、そのデータがファイルとして保存される&lt;/li&gt;
&lt;li&gt;この流れは画像・PDF・ZIPなど&lt;strong&gt;どんなファイルでも同じ&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Ruby のブロックと yield を一番簡単な例で</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Thu, 10 Sep 2026 05:24:33 +0000</pubDate>
      <link>https://dev.to/kaziusan/ruby-noburotukuto-yield-wo-fan-jian-dan-nali-de-3c3k</link>
      <guid>https://dev.to/kaziusan/ruby-noburotukuto-yield-wo-fan-jian-dan-nali-de-3c3k</guid>
      <description>&lt;p&gt;Ruby を書いていると必ず出てくる「ブロック」と「yield」。&lt;br&gt;
難しく見えますが、要は &lt;strong&gt;「処理を渡す」「渡された処理を実行する」&lt;/strong&gt; だけです。&lt;/p&gt;
&lt;h2&gt;
  
  
  ブロックとは
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;メソッドに渡す「処理のかたまり（コードの切れ端）」&lt;/strong&gt; のこと。&lt;br&gt;
&lt;code&gt;{ }&lt;/code&gt; か &lt;code&gt;do...end&lt;/code&gt; で囲みます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;#              └──────────┘ これがブロック&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.each&lt;/code&gt; に「各要素で &lt;code&gt;puts n&lt;/code&gt; してね」という&lt;strong&gt;指示書&lt;/strong&gt;を渡しています。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
2
3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{ }&lt;/code&gt; と &lt;code&gt;do...end&lt;/code&gt; は同じもの。長いときは &lt;code&gt;do...end&lt;/code&gt; を使うだけです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  yield とは
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;メソッドの中の「yield と書いた場所」で、渡されたブロックを実行する命令&lt;/strong&gt;。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;あいさつ&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"こんにちは"&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;              &lt;span class="c1"&gt;# ← ここで渡されたブロックを実行&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"さようなら"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;あいさつ&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"元気ですか？"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;こんにちは
元気ですか？    ← yield の場所にブロックが差し込まれる
さようなら
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;yield は&lt;strong&gt;好きな場所に置け、何回でも呼べ、値も渡せます&lt;/strong&gt;。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;数える&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;数える&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;番目"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# 1番目&lt;/span&gt;
&lt;span class="c1"&gt;# 2番目&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2つの関係
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ブロック&lt;/strong&gt; = 渡す「やることメモ」（呼ぶ側が書く）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;yield&lt;/strong&gt; = そのメモを実行する命令（メソッド側が書く）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;差し込む&lt;strong&gt;場所&lt;/strong&gt;を決めるのがメソッド、差し込む&lt;strong&gt;中身&lt;/strong&gt;を決めるのが呼ぶ側。&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  実践編：yield で処理を「挟み込む」
&lt;/h2&gt;

&lt;p&gt;yield の本当に便利な使い方が、&lt;strong&gt;共通の前処理と後始末で、渡された処理を挟む&lt;/strong&gt;パターンです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_debug&lt;/span&gt;
  &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:debug&lt;/span&gt;   &lt;span class="c1"&gt;# 前処理：ログを debug に&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;                         &lt;span class="c1"&gt;# 呼び出し側の処理を実行&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;&lt;span class="err"&gt;　&lt;/span&gt;&lt;span class="c1"&gt;# エラーが起きても起きなくても、最後に必ず実行される(=finally)&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;      &lt;span class="c1"&gt;# 後始末：必ず元に戻す&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;使い方：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;with_debug&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# この中だけログが debug になる&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# ここを抜けると自動で元のログレベルに戻る&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  なぜ便利か
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;共通部分（ログ切替と復元）を1か所にまとめられる&lt;/strong&gt;。使う側は「何をしたいか」だけ書けばよい。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ensure&lt;/code&gt; で後始末が保証される&lt;/strong&gt;。ブロックの中でエラーが起きても、ログレベルは必ず元に戻る。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;もし yield を使わなければ、使う場所ごとに「切替 → 処理 → 復元」を毎回コピペする羽目になります。yield なら&lt;strong&gt;枠（前後処理）はメソッド側、中身は呼ぶ側&lt;/strong&gt;、ときれいに分けられます。&lt;/p&gt;

&lt;h3&gt;
  
  
  もう一つの例：処理時間を測る
&lt;/h3&gt;

&lt;p&gt;同じパターンで「実行時間の計測」も書けます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;秒"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"重い処理"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;heavy_task&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; 重い処理: 1.23秒&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;「共通の前後処理でブロックを挟む」&lt;/strong&gt; ——これは Ruby で頻出の設計パターンです。&lt;br&gt;
&lt;code&gt;File.open { |f| ... }&lt;/code&gt;（開いて必ず閉じる）なども、この仲間です。&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>coding</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>OCRで名刺を一括登録する機能をつくった話 — “1ページ複数名刺”という落とし穴</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Tue, 14 Jul 2026 11:38:57 +0000</pubDate>
      <link>https://dev.to/kaziusan/ocrdeming-ci-wo-gua-deng-lu-suruji-neng-wotukututahua-1pezifu-shu-ming-ci-toiuluo-tosixue-2gmg</link>
      <guid>https://dev.to/kaziusan/ocrdeming-ci-wo-gua-deng-lu-suruji-neng-wotukututahua-1pezifu-shu-ming-ci-toiuluo-tosixue-2gmg</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;展示会などでたまっていく紙の名刺を、CRMに手入力するのはなかなか大変です。そこで &lt;strong&gt;「名刺を詰め込んだPDF（や画像）をアップロードすると、取引先・担当者データを自動で一括登録する」&lt;/strong&gt; 機能を実務にて作りました。&lt;/p&gt;

&lt;p&gt;最初は「OCRに投げれば終わりだろう」と思っていたのですが、実際にやってみると &lt;em&gt;OCRは意外と万能ではない&lt;/em&gt; こと、そして &lt;em&gt;どこで処理を分けるか&lt;/em&gt; という設計判断が肝になることに気づかされました。この記事では、その勘所と、いちばん悩んだ &lt;strong&gt;「1枚のページに複数の名刺が並んでいたらどうするか」&lt;/strong&gt; という問題を共有します。&lt;/p&gt;

&lt;h2&gt;
  
  
  全体の流れ
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;ファイル（PDF / JPEG / PNG）をアップロード&lt;/li&gt;
&lt;li&gt;サーバー側でバリデーション（形式・サイズ・ページ数）&lt;/li&gt;
&lt;li&gt;「取り込み中」の履歴レコードをつくり、&lt;strong&gt;すぐにレスポンスを返す&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;重い処理（OCR → 構造化 → 登録/更新）は&lt;strong&gt;バックグラウンドジョブ&lt;/strong&gt;で実行&lt;/li&gt;
&lt;li&gt;フロントは履歴IDをポーリングして進捗を表示&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ポイントは &lt;strong&gt;「受付」と「処理本体」をきっぱり分ける&lt;/strong&gt; ことです。OCRや名寄せは数十秒〜数分かかることもあるので、リクエストの中で同期的に全部やろうとするとタイムアウトします。受付APIは「ファイルを保存して履歴をつくり、ジョブを起動するところまで」で潔く終わらせます。&lt;/p&gt;

&lt;h2&gt;
  
  
  OCRは“2段構え”
&lt;/h2&gt;

&lt;p&gt;名刺の読み取りは、実は2つの異なる処理に分かれます。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;文字を読む（OCR）&lt;/strong&gt; … 画像から文字列を抽出する。ここは Google Cloud Vision のような汎用OCRの担当。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;項目に振り分ける（構造化）&lt;/strong&gt; … 抽出したテキストのうち「どれが会社名で、どれが氏名で、どれが電話番号か」を判定する。ここは自前の項目抽出モデルの担当。
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;名刺画像 → ①OCR(文字を全部読む) → テキスト → ②構造化(項目に仕分け) → { 会社名, 氏名, email... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「OCR＝構造化まで全部やってくれる」と思いがちですが、&lt;strong&gt;①は文字と位置を出すだけ、②はそれを項目に整理するだけ&lt;/strong&gt;。役割が別なのです。&lt;/p&gt;

&lt;h2&gt;
  
  
  設計で効いたポイント
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;バリデーションは“中身”で判定する。&lt;/strong&gt;&lt;br&gt;
「PDFかどうか」を拡張子やContent-Typeで見ると、画像を &lt;code&gt;.pdf&lt;/code&gt; にリネームするだけで突破されてしまいます。&lt;strong&gt;ファイル先頭のマジックバイト&lt;/strong&gt;で実体を判定するのが安全です。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;大きいファイルはメモリに載せない。&lt;/strong&gt;&lt;br&gt;
名刺100枚分ともなるとファイルは大きくなります。ストレージ（S3等）へ送る際は&lt;strong&gt;全体をメモリに読み込まず、そのままストリームで流す&lt;/strong&gt;。サイズチェックもメタデータだけ見て、中身は読みません。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDFのページ数チェックは軽い読み取りで。&lt;/strong&gt;&lt;br&gt;
「◯ページまで」を受付時に弾きたいだけなら、画像化ライブラリ（ImageMagick等）は重すぎます。&lt;strong&gt;ページ数を数えるだけならレンダリング不要&lt;/strong&gt;で、軽量なPDFパーサでメタデータを読めば十分です。&lt;/p&gt;

&lt;h2&gt;
  
  
  いちばん悩んだ問題：「1ページに複数名刺」
&lt;/h2&gt;

&lt;p&gt;要件を詰める中で出てきたのが、&lt;strong&gt;1枚のスキャン画像（PDFの1ページ）に名刺が複数枚並んでいるケース&lt;/strong&gt;です。コピー機やフラットベッドスキャナで数枚まとめて取り込めば、当然こうなります。&lt;/p&gt;

&lt;h3&gt;
  
  
  OCRは“名刺単位”には分けてくれない
&lt;/h3&gt;

&lt;p&gt;ここが盲点でした。汎用OCR（Vision等）は「ページ内の全文字＋各文字の位置座標」は返してくれますが、&lt;strong&gt;「どの文字がどの名刺のものか」を論理的に分離する機能はありません&lt;/strong&gt;（Google Cloud Vision の公式ドキュメントにも、1ページ上の複数文書を識別・分離する組み込み機能はなく、ブロックの位置を解析する独自ロジックが必要、と明記されています）。&lt;/p&gt;

&lt;p&gt;さらに、OCRの「ブロック（文字の塊）」は &lt;em&gt;レイアウト上の近接&lt;/em&gt; でまとめられるため、&lt;strong&gt;隣り合う名刺が1つに融合したり、1枚の名刺が複数に割れたり&lt;/strong&gt;します。「1ブロック＝1名刺」にはなりません。&lt;/p&gt;

&lt;p&gt;構造化モデルも「名刺1枚分のテキスト」を仕分ける前提なので、複数名刺のテキストが混ざったまま渡すと、&lt;strong&gt;「A社の会社名＋B氏の氏名」が合体した誤ったデータ&lt;/strong&gt;が生まれてしまいます。&lt;/p&gt;

&lt;h3&gt;
  
  
  つまり「名刺ごとに切り分ける工程」を自前で挟む必要がある
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OCR(全文字＋座標) → 【自前】名刺ごとに切り分け → 名刺A分 → 構造化
                                            → 名刺B分 → 構造化 → …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この切り分けの難易度は、名刺の&lt;strong&gt;並び方&lt;/strong&gt;で大きく変わります。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;並び方&lt;/th&gt;
&lt;th&gt;アプローチ&lt;/th&gt;
&lt;th&gt;難易度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;固定テンプレート（常に2×3等）&lt;/td&gt;
&lt;td&gt;ページを決め打ちで等分&lt;/td&gt;
&lt;td&gt;低（ただし全ユーザーが同じ並べ方を強制される）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;整然と並ぶが枚数可変&lt;/td&gt;
&lt;td&gt;座標の“すき間”から行・列を動的に検出&lt;/td&gt;
&lt;td&gt;中（ルールベース。傾き・密着・空きマスに弱い）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;完全にバラバラ&lt;/td&gt;
&lt;td&gt;名刺領域を検出するAIモデルを用意&lt;/td&gt;
&lt;td&gt;高（モデルの学習・運用が必要）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;真ん中の「座標から格子を推測する」方法は、こんなイメージのルールベース処理です。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;すべての文字の座標を集める&lt;/li&gt;
&lt;li&gt;X座標の&lt;strong&gt;大きなすき間&lt;/strong&gt;を「列の境界」とみなす&lt;/li&gt;
&lt;li&gt;Y座標の&lt;strong&gt;大きなすき間&lt;/strong&gt;を「行の境界」とみなす&lt;/li&gt;
&lt;li&gt;各マスを1名刺として、文字をグルーピングする&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AIではなく幾何計算なので比較的軽い一方、&lt;strong&gt;傾き・名刺同士の密着・不揃いなサイズ・空きマス&lt;/strong&gt;があると崩れます。堅牢にするほど実データでのチューニングが必要になり、突き詰めると「AIで名刺そのものを検出する」方向に近づいていきます。&lt;/p&gt;

&lt;h3&gt;
  
  
  学び：仕様側で“制約”できると一気にラクになる
&lt;/h3&gt;

&lt;p&gt;技術で無理にバラバラ配置へ対応するよりも、&lt;strong&gt;「名刺は等間隔に並べてスキャンしてください」といった入力の制約を設ける&lt;/strong&gt;ほうが、開発コストも精度リスクも大きく下がります。「どこまでの入力を許すか」はエンジニアだけで決めず、プロダクト側と早めにすり合わせるべき論点でした。&lt;/p&gt;

&lt;h2&gt;
  
  
  重複データの扱い（名寄せ）
&lt;/h2&gt;

&lt;p&gt;一括登録では「すでに登録済みの取引先/担当者」との重複が避けられません。ここは&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;取引先&lt;/strong&gt;は「会社名」または「会社名＋住所」で一致判定（支店を区別したいニーズもあるため住所も選べる）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;担当者&lt;/strong&gt;はメールアドレスで一致判定&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;を事前に選ばせ、ヒットしたものは更新、しなかったものは新規登録として扱います。「重複時に既存データを名刺の値で上書きするか」もユーザーが選べるようにしました（勝手に上書きされたくないニーズがあるため）。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OCRは「文字を読む」＋「項目に仕分ける」の2段構え。&lt;strong&gt;OCRだけで構造化まで完結はしない&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;受付と処理本体を分離し、重い処理は非同期ジョブへ。大容量ファイルはストリーム、バリデーションは中身で。&lt;/li&gt;
&lt;li&gt;最大の落とし穴は &lt;strong&gt;「1ページ複数名刺」&lt;/strong&gt;。OCRは名刺単位に分けてくれないので切り分けは自前実装が必要で、&lt;strong&gt;並び方次第で難易度が激変&lt;/strong&gt;する。&lt;/li&gt;
&lt;li&gt;だからこそ &lt;strong&gt;「入力をどこまで制約できるか」をプロダクトと握る&lt;/strong&gt;ことが、技術選定と同じくらい重要。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“OCRに投げるだけ”に見える機能ほど、実は要件と制約の設計が肝になる——という良い事例でした。&lt;/p&gt;

&lt;h2&gt;
  
  
  (後日談)
&lt;/h2&gt;

&lt;p&gt;さんざん「1ページに複数名刺だったらどう切り分けるか…」と唸っていたのですが、プロダクト側に確認したら——&lt;/p&gt;

&lt;p&gt;「PDFは1ページに名刺1枚の想定です」&lt;/p&gt;

&lt;p&gt;……でした。盛大な取り越し苦労。笑&lt;/p&gt;

&lt;p&gt;しかし勉強にはなりました。技術で殴る前に、まず要件を疑う。良い教訓になりましたわい。&lt;/p&gt;

</description>
      <category>ocr</category>
      <category>rails</category>
    </item>
  </channel>
</rss>
