<?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>「先頭カラムが同じインデックス、それ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>
    <item>
      <title>Elastic search 基礎の基礎</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Mon, 13 Apr 2026 05:56:56 +0000</pubDate>
      <link>https://dev.to/kaziusan/elastic-search-1802</link>
      <guid>https://dev.to/kaziusan/elastic-search-1802</guid>
      <description>&lt;p&gt;(自分用メモ)&lt;/p&gt;

&lt;h1&gt;
  
  
  Mysqlとの違い
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;ES&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;データベース&lt;/td&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行(１件のデータ)&lt;/td&gt;
&lt;td&gt;document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;カラムの型&lt;/td&gt;
&lt;td&gt;mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;なので「インデックス再構築」はデータベース作り直しってイメージ&lt;/p&gt;

&lt;h1&gt;
  
  
  データの形
&lt;/h1&gt;

&lt;p&gt;ESは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;"deal"&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;"TOYOTAへの提案"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"has_materials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;h1&gt;
  
  
  検索クエリの書き方
&lt;/h1&gt;

&lt;h2&gt;
  
  
  基本形
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;name/_search&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;"query"&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="err"&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;p&gt;query はSQLの WHERE 句&lt;/p&gt;

&lt;h2&gt;
  
  
  完全一致で検索したいっす (term)
&lt;/h2&gt;

&lt;p&gt;SQLでいうと&lt;br&gt;
&lt;code&gt;WHERE has_materials = true&lt;/code&gt;&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;"query"&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;"term"&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;"deal.has_materials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;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;
  
  
  複数の条件をANDでつなぎたい (= bool + filter)
&lt;/h2&gt;

&lt;p&gt;SQLでいうと&lt;br&gt;
&lt;code&gt;WHERE has_materials = true AND invoice_id = 3&lt;/code&gt;&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;"query"&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;"bool"&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;"filter"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&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;"deal.has_materials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&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;"deal.invoice.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;3&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;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="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;p&gt;bool + filter の配列に条件を並べるとAND&lt;/p&gt;

&lt;h2&gt;
  
  
  テキストを部分一致で検索したい（= match）
&lt;/h2&gt;

&lt;p&gt;SQLでいうと&lt;br&gt;
&lt;code&gt;WHERE name LIKE '%トヨタ%'&lt;/code&gt;&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;"query"&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;"match"&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;"deal.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;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;p&gt;MySQLの「カラムの型定義（int, varchar...）」と同じ。&lt;br&gt;
フィールドの型をあらかじめ宣言しておくもの。&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;"deal"&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="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;"integer"&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;"name"&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;"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;"text"&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;"has_materials"&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;"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;"boolean"&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;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;h1&gt;
  
  
  mapping変更したら？
&lt;/h1&gt;

&lt;p&gt;インデックス作り直しですわ&lt;/p&gt;

</description>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>NestJSの基本アーキテクチャと実プロジェクトでの活用事例</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Wed, 18 Feb 2026 04:57:00 +0000</pubDate>
      <link>https://dev.to/kaziusan/nestjsnoji-ben-akitekutiyatoshi-puroziekutodenohuo-yong-shi-li-55ma</link>
      <guid>https://dev.to/kaziusan/nestjsnoji-ben-akitekutiyatoshi-puroziekutodenohuo-yong-shi-li-55ma</guid>
      <description>&lt;p&gt;※ 本記事に掲載しているコード例は実際のプロダクトをベースにしていますが、プロジェクト名・サービス名・変数名など、特定のプロダクトを識別できる情報は変更・汎用化して記載しています。&lt;/p&gt;

&lt;p&gt;(０から全部自分で書いてなく、AI生成したのを修正しプロジェクトの辞書の様に使用する目的) &lt;/p&gt;

&lt;p&gt;NestJSはTypeScriptで構築されたサーバーサイドフレームワークで、Angularに影響を受けたモジュール指向のアーキテクチャが特徴です。本記事ではNestJSの基本概念を解説しつつ、実際のSaaSプロダクト（GraphQL API / マルチテナント構成）でどのように活用しているかを紹介します。&lt;/p&gt;

&lt;p&gt;(*追記)↑って立派に書いたけど、そのプロジェクトあまり従事しなかったので、この記事のことあんま頭に入ってないです笑&lt;/p&gt;




&lt;h2&gt;
  
  
  目次
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Module（モジュール）&lt;/li&gt;
&lt;li&gt;Controller / Resolver&lt;/li&gt;
&lt;li&gt;Service / UseCase&lt;/li&gt;
&lt;li&gt;DI（依存性注入）&lt;/li&gt;
&lt;li&gt;Guard（認証・認可）&lt;/li&gt;
&lt;li&gt;Entity定義の多層構造&lt;/li&gt;
&lt;li&gt;Prisma連携（Repository + Mapper）&lt;/li&gt;
&lt;li&gt;認証コンテキストの流れ&lt;/li&gt;
&lt;li&gt;まとめ：NestJS標準 vs 実プロジェクト&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Module（モジュール）
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;NestJSのアプリケーションは &lt;strong&gt;Module&lt;/strong&gt; を単位として構成されます。&lt;code&gt;@Module()&lt;/code&gt; デコレータで &lt;code&gt;providers&lt;/code&gt;（提供するサービス）、&lt;code&gt;imports&lt;/code&gt;（依存するモジュール）、&lt;code&gt;exports&lt;/code&gt;（外部に公開するサービス）を定義し、DIコンテナを構成します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CatService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DatabaseModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CatService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CatModule&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;本プロジェクトでは、レイヤードアーキテクチャに沿って3層のモジュールに分離しています。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.module.ts
  ├─ ResolverModule群（interface/di/）  ← プレゼンテーション層
  ├─ UseCaseModule群（app/di/）         ← アプリケーション層
  └─ RepositoryModule群（infra/prisma/di/） ← インフラ層
&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 plaintext"&gt;&lt;code&gt;packages/api/src/
├── app/                      # Application層
│   ├── di/                   # UseCaseモジュール定義
│   └── useCases/             # UseCase実装
├── domain/                   # Domain層
│   ├── entities/             # ドメインエンティティ・型定義
│   ├── restrictFilters/      # 権限フィルター
│   └── common/               # 共通型・ユーティリティ
├── interface/                # Interface層
│   ├── di/                   # Resolverモジュール定義
│   ├── resolvers/            # GraphQL Resolver
│   ├── schema/               # リクエスト/レスポンス型
│   └── auth/                 # 認証Guard
├── infra/                    # Infrastructure層
│   ├── prisma/               # Repository・Mapper
│   └── ...                   # 外部サービス連携
└── entities/                 # GraphQL ObjectType定義
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ResolverModuleは、UseCaseModuleを &lt;code&gt;imports&lt;/code&gt; して依存を解決します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentResolver&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;CommentUseCaseModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;UserUseCaseModule&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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentResolverModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UseCaseModuleは、RepositoryModuleや他のServiceModuleを &lt;code&gt;imports&lt;/code&gt; します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;PrismaCommentRepositoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;PrismaRoomRepositoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;NotificationServiceModule&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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentUseCaseModule&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;: NestJSのModule機構を活用して、クリーンアーキテクチャ的な依存の方向（interface → app → domain ← infra）を実現しています。&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Controller / Resolver
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;REST APIの場合は &lt;code&gt;@Controller()&lt;/code&gt; に &lt;code&gt;@Get()&lt;/code&gt; / &lt;code&gt;@Post()&lt;/code&gt; などのデコレータを使います。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CatController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All cats&lt;/span&gt;&lt;span class="dl"&gt;'&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="nd"&gt;Post&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="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateCatDto&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="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;GraphQLの場合は &lt;code&gt;@Resolver()&lt;/code&gt; に &lt;code&gt;@Query()&lt;/code&gt; / &lt;code&gt;@Mutation()&lt;/code&gt; / &lt;code&gt;@ResolveField()&lt;/code&gt; を使います。&lt;/p&gt;

&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;本プロジェクトではGraphQLを採用し、&lt;strong&gt;Code-First&lt;/strong&gt;アプローチでスキーマを自動生成しています。&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="c1"&gt;// app.module.ts&lt;/span&gt;
&lt;span class="nx"&gt;GraphQLModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forRoot&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ApolloDriverConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApolloDriver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoSchemaFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/schema.gql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// TypeScriptから自動生成&lt;/span&gt;
  &lt;span class="na"&gt;fieldResolverEnhancers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guards&lt;/span&gt;&lt;span class="dl"&gt;'&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;Resolverの実装例：&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Resolver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GqlComment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentResolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;commentUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="c1"&gt;// Query: データ取得&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GqlComment&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;commentList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetCommentListInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&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;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commentUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCommentList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Mutation: データ変更&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Mutation&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GqlComment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateCommentInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&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;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commentUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ResolveField: ネストされたフィールドの解決&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ResolveField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GqlUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getAuthor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  主なデコレータ
&lt;/h4&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;@Resolver(() =&amp;gt; Type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;クラスをResolver宣言&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Query(() =&amp;gt; Type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GraphQL Query定義&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Mutation(() =&amp;gt; Type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GraphQL Mutation定義&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ResolveField('name', () =&amp;gt; Type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ネストフィールドの解決&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Args('input')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;入力引数のバインド&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Parent()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;親オブジェクトへのアクセス&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Context()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GraphQL Context（認証情報等）の取得&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  InputType（リクエスト型）
&lt;/h4&gt;

&lt;p&gt;GraphQLの入力型もデコレータで定義します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InputType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateCommentInput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GraphQLJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;imageIds&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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;これにより、以下のGraphQLスキーマが自動生成されます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreateCommentInput&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageIds&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="nb"&gt;Int&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;
  
  
  3. Service / UseCase
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@Injectable()&lt;/code&gt; デコレータを付けたクラスをServiceとして使い、ビジネスロジックを集約します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CatService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;catRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CatRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;catRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;本プロジェクトでは「Service」ではなく &lt;strong&gt;「UseCase」&lt;/strong&gt; と命名し、ユースケース単位でクラスを分割しています。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentUseCase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;commentRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaCommentRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;roomRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaRoomRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationService&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;async&lt;/span&gt; &lt;span class="nf"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. ドメイン検証&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CoreAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneById&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. ドメインエンティティ作成&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCommentSeed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. DB保存&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commentRepository&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="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. 通知・外部連携&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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;return&lt;/span&gt; &lt;span class="nx"&gt;created&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ポイント&lt;/strong&gt;: UseCase内でドメイン検証 → DB保存 → 副作用（通知・外部連携）の順で処理を組み立てています。Repositoryや外部サービスは全てコンストラクタインジェクションで注入されるため、テスタビリティが高いです。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. DI（依存性注入）
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;NestJSのDIは、&lt;strong&gt;TypeScriptの型情報&lt;/strong&gt;からプロバイダーを自動解決します。&lt;code&gt;@Injectable()&lt;/code&gt; クラスをModuleの &lt;code&gt;providers&lt;/code&gt; に登録し、コンストラクタの型で自動注入されます。&lt;/p&gt;

&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;層をまたぐ依存は &lt;code&gt;imports&lt;/code&gt; / &lt;code&gt;exports&lt;/code&gt; で制御しています。&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="nx"&gt;Resolver&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// 型で自動解決&lt;/span&gt;
&lt;span class="nx"&gt;UseCase&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaCommentRepository&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;Module定義側：&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="c1"&gt;// UseCaseModule: CommentUseCaseを外部に公開&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentUseCase&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;          &lt;span class="c1"&gt;// 他のモジュールから利用可能に&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PrismaCommentRepositoryModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentUseCaseModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// ResolverModule: UseCaseModuleをimportして利用&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentResolver&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentUseCaseModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// CommentUseCaseが利用可能に&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentResolverModule&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;forwardRef()&lt;/code&gt; で回避します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;forwardRef&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;RelatedModule&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Guard（認証・認可）
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CanActivate&lt;/code&gt; インターフェースを実装したGuardクラスを作成し、&lt;code&gt;@UseGuards()&lt;/code&gt; デコレータで適用します。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;validateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;本プロジェクトはマルチユーザー型SaaSのため、ユーザータイプ（管理者 / ゲスト）に応じて異なる認証Guardを振り分けるパターンを採用しています。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MultiUserAuthGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;adminAuthGuard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminAuthGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;guestAuthGuard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GuestAuthGuard&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;async&lt;/span&gt; &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GqlExecutionContext&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="nx"&gt;context&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;req&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;userType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-user-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Admin&lt;/span&gt;&lt;span class="dl"&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;// JWT検証（外部認証サービス）&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adminAuthGuard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Guest&lt;/span&gt;&lt;span class="dl"&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guestAuthGuard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;さらに &lt;code&gt;@ResolveField&lt;/code&gt; レベルでの細かい権限制御も実装しています。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ResolveField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GqlUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserTypesGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;RequiredUserTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ADMIN&lt;/span&gt;&lt;span class="dl"&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;async&lt;/span&gt; &lt;span class="nf"&gt;getAuthor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;UserTypesGuard&lt;/code&gt; は &lt;code&gt;Reflector&lt;/code&gt; を使ってメタデータを読み取り、権限チェックを行います。&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserTypesGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Reflector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;requiredTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requiredUserTypes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;requiredTypes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;gqlContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GqlExecutionContext&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="nx"&gt;context&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gqlContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requiredTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userType&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Entity定義の多層構造
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;一般的なNestJSアプリケーションでは、1つのEntityクラスがDB定義とAPI定義を兼ねることが多いです。&lt;/p&gt;

&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;本プロジェクトでは&lt;strong&gt;3層のEntity定義&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;&lt;strong&gt;GraphQL Entity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;entities/Comment.entity.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ObjectType()&lt;/code&gt; でAPI出力型を定義&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain Entity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domain/entities/Comment/index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Brand型による型安全なビジネスモデル&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prisma Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DBスキーマ定義&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  GraphQL Entity（API層）
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;GraphQLJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommentImage&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;commentImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentImage&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Domain Entity（ドメイン層）
&lt;/h4&gt;

&lt;p&gt;Brand型を使って、同じ &lt;code&gt;number&lt;/code&gt; 型でもIDの種類を区別します。&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="c1"&gt;// Brand型の定義&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// IDを型レベルで区別&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CommentId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CommentId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UserId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 型エラー: CommentId と UserId は互換性がない&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CommentId&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;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commentId&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;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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RoomId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createCommentSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentSeed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CommentSeed&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;payload&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;h4&gt;
  
  
  Prisma Model（DB層）
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Comment {
  id        Int       @id @default(autoincrement())
  content   Json?
  roomId    String
  Room      Room      @relation(fields: [roomId], references: [id], onDelete: Cascade)
  userId    Int
  createdAt DateTime  @default(now())
  updatedAt DateTime  @default(now()) @updatedAt
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ポイント&lt;/strong&gt;: 3層に分けることで、API仕様の変更がドメインロジックに影響しない、DBスキーマの変更がAPI層に漏れないという利点があります。&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Prisma連携（Repository + Mapper）
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NestJSの基本
&lt;/h3&gt;

&lt;p&gt;NestJSではTypeORMやPrismaを直接サービスから利用するのが一般的です。&lt;/p&gt;

&lt;h3&gt;
  
  
  実プロジェクトでの活用
&lt;/h3&gt;

&lt;p&gt;本プロジェクトではRepository + Mapperパターンを採用し、Prismaの型をドメイン型に変換しています。&lt;/p&gt;

&lt;h4&gt;
  
  
  Repository
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrismaCommentRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommentSeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Comment&lt;/span&gt;&lt;span class="o"&gt;&amp;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;prismaComment&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&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="na"&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;contentJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Prisma型 → Domain型への変換&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PrismaCommentMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDomainComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;results&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&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;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PrismaCommentMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toDomainComment&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Mapper
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrismaCommentMapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;toDomainComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CommentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentJson&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;RoomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prismaComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  トランザクション
&lt;/h4&gt;

&lt;p&gt;複数テーブルにまたがる操作は &lt;code&gt;prisma.$transaction()&lt;/code&gt; で原子性を保証します。&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;result&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomRepository&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="nx"&gt;roomData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberRepository&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="nx"&gt;memberData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;room&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;strong&gt;ポイント&lt;/strong&gt;: Repositoryメソッドはオプショナルな &lt;code&gt;db&lt;/code&gt; 引数を受け取り、トランザクション内でもスタンドアロンでも動作するよう設計されています。&lt;/p&gt;




&lt;h2&gt;
  
  
  8. 認証コンテキストの流れ
&lt;/h2&gt;

&lt;p&gt;マルチテナントSaaSにおける認証情報の伝搬フローです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. GraphQLリクエスト
   ├─ Header: Authorization: "Bearer &amp;lt;JWT&amp;gt;"
   └─ Header: x-user-type: "Admin" or "Guest"

2. Guard（認証処理）
   ├─ Admin → JWT検証 → OperationContext生成
   └─ Guest → セッション検証 → OperationContext生成
   └─ request.user = OperationContext にセット

3. Resolver
   └─ @Context() context: OperationContext で取得

4. UseCase / Repository
   └─ contextを利用した権限制御・テナント隔離
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OperationContext&lt;/code&gt; はZodスキーマで型安全に定義されています。&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;operationContextSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tenantIdSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ADMIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userIdSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;roleSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GUEST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userIdSchema&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="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;operationContextSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;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;@Context()&lt;/code&gt; でResolverに注入します。&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createParamDecorator&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GqlExecutionContext&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;権限フィルターでDB操作時のテナント隔離を行います。&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commentRestrictFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OperationContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CommentWhereInput&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comments&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;AND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;commentRestrictFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. まとめ：NestJS標準 vs 実プロジェクト
&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;NestJS標準&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;API方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST (&lt;code&gt;@Controller&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;GraphQL Code-First (&lt;code&gt;@Resolver&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ビジネスロジック&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;UseCase（ユースケース単位で分割）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;データアクセス&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ORM直接利用&lt;/td&gt;
&lt;td&gt;Repository + Mapper パターン&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;認証&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;単一Guard&lt;/td&gt;
&lt;td&gt;MultiUser振り分けGuard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;型安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript標準&lt;/td&gt;
&lt;td&gt;Brand型 + Zod バリデーション&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;テナント隔離&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;なし&lt;/td&gt;
&lt;td&gt;OperationContext + RestrictFilter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;単一定義&lt;/td&gt;
&lt;td&gt;3層（GraphQL / Domain / Prisma）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Module構成&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;機能単位&lt;/td&gt;
&lt;td&gt;レイヤー単位（Resolver / UseCase / Repository）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  おわりに
&lt;/h2&gt;

&lt;p&gt;NestJSはそのままでも十分に強力なフレームワークですが、プロジェクトの規模や要件に応じてアーキテクチャをカスタマイズできる柔軟性も持っています。&lt;/p&gt;

&lt;p&gt;本プロジェクトでは、NestJSのDI機構を活かしつつ、以下のような工夫を加えています。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;レイヤードアーキテクチャ&lt;/strong&gt;: Module単位でレイヤーを分離し、依存の方向を制御&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code-First GraphQL&lt;/strong&gt;: TypeScriptのデコレータからスキーマを自動生成&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand型&lt;/strong&gt;: IDの取り違えをコンパイル時に検出&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RestrictFilter&lt;/strong&gt;: マルチテナントのデータ隔離をパターン化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NestJSの基本概念（Module / DI / Guard / Decorator）を理解しておけば、プロジェクトの要件に合わせた拡張も容易です。ぜひ参考にしてみてください。&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>たった1枚の引き伸ばされた画像から、AWS Lambdaのコード移行とCI/CD構築に至った話</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Sat, 08 Nov 2025 00:42:07 +0000</pubDate>
      <link>https://dev.to/kaziusan/tatuta1mei-noyin-kishen-basaretahua-xiang-kara-aws-lambdanokodoyi-xing-tocicdgou-zhu-nizhi-tutahua-252p</link>
      <guid>https://dev.to/kaziusan/tatuta1mei-noyin-kishen-basaretahua-xiang-kara-aws-lambdanokodoyi-xing-tocicdgou-zhu-nizhi-tutahua-252p</guid>
      <description>&lt;h2&gt;
  
  
  始まり
&lt;/h2&gt;

&lt;p&gt;ある日、開発中の画面でサムネイル画像が不自然に引き伸ばされて表示されていることに気づきました。&lt;/p&gt;

&lt;p&gt;「ああ、このパターンな。CSSのobject-fitがcoverになってるのをcontainにすれば直るやつじゃ」&lt;/p&gt;

&lt;p&gt;そう高を括ってコードを修正。しかし、表示は変わらず引き伸ばされたまま。ブラウザの開発者ツールで画像のURLを直接開いてみると、Amazon S3から返されている画像そのものが既に引き伸ばされていました&lt;/p&gt;

&lt;p&gt;「まじすか先輩」&lt;/p&gt;

&lt;p&gt;しかし、わしはプロジェクトに参加したばかりでしかもインフラはよぉ知らん。該当の画像生成処理がどこに書かれているのか見当もつきませんので同僚に聞いてみましたわ&lt;/p&gt;

&lt;p&gt;「すんまへん、このサムネイル画像、どうもAWS側でリサイズされてるっぽいんですが、どこの処理か分かりますか？」&lt;/p&gt;

&lt;p&gt;すると、同僚から&lt;/p&gt;

&lt;p&gt;「ああ、その処理は別のインフラ用リポジトリにあるLambda関数がやってるんですよ。ただ、そのリポジトリを直接編集しても意味なくて、今のアプリケーション用リポジトリにコードを移行して、CI/CDを再設定する必要があるんですわこりゃ」&lt;/p&gt;

&lt;p&gt;わしの頭の中は「あーLambda聞いたことあるわー、awsのやつじゃわー」て感じでしたが、とりあえずやってみました。&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP1: Lambdaてそもそもなんですの？
&lt;/h2&gt;

&lt;p&gt;これは「特定のイベントをきっかけに、自動で動いてくれる小さなプログラム」で、サーバーの管理を自分でする必要がない「サーバーレス」という仕組みの一つで、例えば以下のようなイベントをきっかけに処理を実行できます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon S3にファイルがアップロードされた時&lt;/li&gt;
&lt;li&gt;API Gatewayにリクエストが来た時&lt;/li&gt;
&lt;li&gt;特定の時間になった時 (cronのように)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;今回のケースでは、「S3バケットにオリジナル画像がアップロードされたら、Lambda関数が起動してサムネイル画像を生成し、別のS3バケットに保存する」という処理が行われていたのです。これなら、画像が引き伸ばされている原因も、Lambda関数のコードを修正すれば解決できそうです&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: Lambda関数の移行とCI/CDの構築
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;旧インフラリポジトリから、Lambda関数のソースコードを現在のアプリケーションリポジトリに持ってくる。&lt;/li&gt;
&lt;li&gt;持ってきたコードを修正し、画像が引き伸ばされないようにする。（今回はリサイズのロジックを修正、resize methodをthumbnail methodに変）&lt;/li&gt;
&lt;li&gt;コードが更新されたら、自動でAWS上のLambda関数にデプロイされる仕組み（CI/CD）をGitHub Actionsで構築する。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;▼ GitHub ActionsでLambdaに自動デプロイ&lt;br&gt;
GitHub Actionsのワークフローを使い、特定のブランチ（例: main）にコードがプッシュされたら、自動でAWS Lambdaにデプロイされるように設定します。&lt;code&gt;.github/workflows&lt;/code&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="c1"&gt;# sample code&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to AWS Lambda&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;デプロイ先の環境を選択してください'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;release-candidate&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.environment }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="c1"&gt;# ... (Node.jsやPythonのセットアップ、依存パッケージのインストールなど) ...&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS Credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ap-northeast-1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Zip&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# ビルドやパッケージングの処理&lt;/span&gt;
          &lt;span class="s"&gt;zip -r function.zip .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Lambda&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;aws lambda update-function-code \&lt;/span&gt;
            &lt;span class="s"&gt;--function-name ${{ secrets.YOUR_LAMBDA_FUNCTION_NAME }} \&lt;/span&gt;
            &lt;span class="s"&gt;--zip-file fileb://function.zip&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;このワークフローのポイントは2つです&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 環境に応じたSecretsの利用
&lt;/h3&gt;

&lt;p&gt;GitHubリポジトリの Settings &amp;gt; Environments で release-candidate, staging, production の各環境がありました。なのでそれぞれの環境のSecretsに &lt;code&gt;YOUR_LAMBDA_FUNCTION_NAME&lt;/code&gt;というキーで、対応するLambda関数名（例: rc-YourLambdaFunctionName）を登録しておきます。&lt;br&gt;
ワークフローが実行されると、選択された環境に応じて適切な関数名が自動で読み込まれるため、デプロイ先を間違えるリスクを低減できます。&lt;/p&gt;

&lt;h3&gt;
  
  
  2. 手動トリガーによる安全なデプロイ
&lt;/h3&gt;

&lt;p&gt;on: push を使わず on: workflow_dispatch を使うことで、意図しない自動デプロイを防ぎます。開発者はGitHubのActionsタブから「Run workflow」ボタンを押し、デプロイしたい環境を選択するだけで、安全かつ確実にリリース作業を行えます。&lt;/p&gt;

&lt;h3&gt;
  
  
  🔥 &lt;strong&gt;つまずきポイント：GitHub Actionsの「Run workflow」が表示されない！&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;ワークフローを作成し、いざ手動で実行テストをしようとした時のことです。GitHubリポジトリのActionsタブを見ても、手動実行ボタンである「Run workflow」が表示されません。&lt;/p&gt;

&lt;p&gt;原因は単純で、&lt;code&gt;workflow_dispatch&lt;/code&gt; を含むワークフローファイルが、リポジポジトリのデフォルトブランチ（&lt;code&gt;main&lt;/code&gt;や&lt;code&gt;master&lt;/code&gt;）にマージされていないと、手動実行のUIは表示されないという仕様でした。&lt;br&gt;
開発ブランチでいくら試行錯誤してもボタンは現れません。この仕様を知らずに、githubと戦っておりました&lt;/p&gt;

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

&lt;p&gt;たった1枚の引き伸ばされた画像でしたが、AWS Lambda、そしてCI/CDの世界に飛び込めました&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;表示の不具合は、必ずしもフロントエンド、バックエンドが原因とは限らない。&lt;/li&gt;
&lt;li&gt;AWS Lambdaは、イベント駆動で動く便利な自動処理プログラム。&lt;/li&gt;
&lt;li&gt;ワークフローの手動実行（&lt;code&gt;workflow_dispatch&lt;/code&gt;）は、デフォルトブランチでしか有効にならない。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;てなわけでバグも修正され、気持ちよくなりましたわ〜&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
    </item>
    <item>
      <title>&lt;ChatGPT vs me&gt; A better way to call functions in a child component from a parent component in React</title>
      <dc:creator>Kaziu</dc:creator>
      <pubDate>Thu, 06 Mar 2025 07:49:18 +0000</pubDate>
      <link>https://dev.to/kaziusan/a-better-way-to-call-functions-in-a-child-component-from-a-parent-component-in-react-7m7</link>
      <guid>https://dev.to/kaziusan/a-better-way-to-call-functions-in-a-child-component-from-a-parent-component-in-react-7m7</guid>
      <description>&lt;h1&gt;
  
  
  ❓Question
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// should define in child component (&amp;lt;Body&amp;gt;) &lt;/span&gt;
&lt;span class="c1"&gt;//   to follow the Container-Presenter pattern, but how?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;hoge&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Footer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;onCliCk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Footer&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  ChatGPT's answer
&lt;/h1&gt;

&lt;p&gt;In this situation, ChatGPT might suggest using &lt;code&gt;useRef&lt;/code&gt; and &lt;code&gt;useImperativeHandle&lt;/code&gt;, which works, but there is a better way, I think.&lt;/p&gt;

&lt;p&gt;That approach often adds complicated code, and some engineers may not be familiar with them&lt;/p&gt;

&lt;h1&gt;
  
  
  My answer
&lt;/h1&gt;

&lt;p&gt;set a useState in parent component to store function states, and assign functions themselves in initial useEffect in child component&lt;/p&gt;

&lt;p&gt;▼ Parent component&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;....},&lt;/span&gt; &lt;span class="nx"&gt;setHandlers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hoge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt; &lt;span class="nx"&gt;setHandlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setHandlers&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Footer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;onCliCk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Footer&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;▼ Child component &amp;lt;Body&amp;gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setHandlers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hoge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onHoge&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="p"&gt;},[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would be simpler than using useRef and useImperativeHandle.&lt;br&gt;
This pattern is kind of &lt;strong&gt;Function Injection&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Your answer
&lt;/h1&gt;

&lt;p&gt;what do you think?&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
