[{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:29\"\u003emodern-normalize を使う。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"3:1-5:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm install modern-normalize\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"7:1-7:33\"\u003eそして例えば main.js で、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"8:1-10:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eimport 'modern-normalize'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"12:1-12:29\"\u003eとしておけばOKです。\u003c/p\u003e\n","body":"modern-normalize を使う。\n\n```\nnpm install modern-normalize\n```\n\nそして例えば main.js で、\n```\nimport 'modern-normalize'\n```\n\nとしておけばOKです。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T20:42:55+09:00","group":null,"id":"1236f0f4ccbea2e06052","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"CSS","versions":[]},{"name":"Vue.js","versions":[]}],"title":"Vue.jsでCSSをノーマライズする方法","updated_at":"2026-09-27T20:42:55+09:00","url":"https://qiita.com/latte00/items/1236f0f4ccbea2e06052","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":0,"github_login_name":null,"id":"latte00","items_count":26,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4089961,"profile_image_url":"https://secure.gravatar.com/avatar/48cb1ae701a2f15108ea13476e06ef6f","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:253\"\u003ecomputed() は関数を受け取り、その関数が return した値を「計算結果」として持つ。そして、その関数の中で使われているリアクティブな値を追跡して、値が変化すると自動的に再計算する。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"3:1-7:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003econst totalAmount = computed(() =\u0026gt; {\n  return 返却する値\n})\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"8:1-14:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003econst totalAmount = computed(() =\u0026gt; {\n  return expenses.value.reduce((total, expense) =\u0026gt; {\n    return total + Number(expense.amount)\n  }, 0)\n})\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n","body":"computed() は関数を受け取り、その関数が return した値を「計算結果」として持つ。そして、その関数の中で使われているリアクティブな値を追跡して、値が変化すると自動的に再計算する。\n\n```\nconst totalAmount = computed(() =\u003e {\n  return 返却する値\n})\n```\n```\nconst totalAmount = computed(() =\u003e {\n  return expenses.value.reduce((total, expense) =\u003e {\n    return total + Number(expense.amount)\n  }, 0)\n})\n```\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T20:38:29+09:00","group":null,"id":"dbef3206e3d1950f9136","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]}],"title":"computedの使い方　Vue.js","updated_at":"2026-09-27T20:38:29+09:00","url":"https://qiita.com/latte00/items/dbef3206e3d1950f9136","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":0,"github_login_name":null,"id":"latte00","items_count":26,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4089961,"profile_image_url":"https://secure.gravatar.com/avatar/48cb1ae701a2f15108ea13476e06ef6f","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-2:87\"\u003ereduceには「処理する関数」と「最初の値」を渡す。\u003cbr\u003e\nその関数には基本的に「累積値」と「現在の要素」が渡される。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"4:1-7:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003earray.reduce(コールバック関数, 初期値)\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"9:1-9:69\"\u003eという形です。例えば、このような変数を用意し、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"10:1-12:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003econst expenses = ref([])\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"13:1-13:42\"\u003eこのようなデータがあった場合\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"14:1-38:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eexpenses.value = [\n  {\n    id: 0,\n    date: '2026-09-27',\n    title: 'スーパー',\n    amount: 3000,\n    category: 'food'\n  },\n  {\n    id: 1,\n    date: '2026-09-27',\n    title: '美容院',\n    amount: 5000,\n    category: 'beauty'\n  },\n  {\n    id: 2,\n    date: '2026-09-27',\n    title: '子どもの文房具',\n    amount: 2000,\n    category: 'child'\n  }\n]\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"39:1-40:120\"\u003eこのように使う。\u003cbr\u003e\n『expenses配列の中にある「それぞれの支出の金額」を全部足して、1つの合計金額にする』\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"42:1-49:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e配列.reduce(\n  (累計する値, 今処理している要素) =\u0026gt; {\n    return 累計する値 + 今処理している要素\n  },\n  最初の値\n)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"51:1-58:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eexpenses.value.reduce(\n  (total, expense) =\u0026gt; {\n    return total + Number(expense.amount)\n  },\n  0\n)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"61:1-61:12\"\u003eつまり、\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-68:39\"\u003ereduce()\u003cbr\u003e\n├─ 第1引数 → コールバック関数\u003cbr\u003e\n│              ├─ total\u003cbr\u003e\n│              └─ expense\u003cbr\u003e\n│\u003cbr\u003e\n└─ 第2引数 → 初期値「0」\u003c/p\u003e\n\u003cp data-sourcepos=\"70:2-70:114\"\u003ereduce()は配列の中にオブジェクトが入っている形だけでなく、配列のみでも使える。\u003c/p\u003e\n","body":"reduceには「処理する関数」と「最初の値」を渡す。\nその関数には基本的に「累積値」と「現在の要素」が渡される。\n\n```\narray.reduce(コールバック関数, 初期値)\n\n```\n\nという形です。例えば、このような変数を用意し、\n```\nconst expenses = ref([])\n```\nこのようなデータがあった場合\n```\nexpenses.value = [\n  {\n    id: 0,\n    date: '2026-09-27',\n    title: 'スーパー',\n    amount: 3000,\n    category: 'food'\n  },\n  {\n    id: 1,\n    date: '2026-09-27',\n    title: '美容院',\n    amount: 5000,\n    category: 'beauty'\n  },\n  {\n    id: 2,\n    date: '2026-09-27',\n    title: '子どもの文房具',\n    amount: 2000,\n    category: 'child'\n  }\n]\n```\nこのように使う。\n『expenses配列の中にある「それぞれの支出の金額」を全部足して、1つの合計金額にする』\n\n```\n配列.reduce(\n  (累計する値, 今処理している要素) =\u003e {\n    return 累計する値 + 今処理している要素\n  },\n  最初の値\n)\n```\n\n```\nexpenses.value.reduce(\n  (total, expense) =\u003e {\n    return total + Number(expense.amount)\n  },\n  0\n)\n```\n\n\nつまり、\n\nreduce()\n ├─ 第1引数 → コールバック関数\n │              ├─ total\n │              └─ expense\n │\n └─ 第2引数 → 初期値「0」\n\n reduce()は配列の中にオブジェクトが入っている形だけでなく、配列のみでも使える。\n\n\n\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T20:17:57+09:00","group":null,"id":"b6e476773b7976e8c5ce","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"JavaScript","versions":[]},{"name":"Vue.js","versions":[]}],"title":"reduce()の使い方　JavaScript　Vue.js","updated_at":"2026-09-27T20:17:57+09:00","url":"https://qiita.com/latte00/items/b6e476773b7976e8c5ce","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":0,"github_login_name":null,"id":"latte00","items_count":26,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4089961,"profile_image_url":"https://secure.gravatar.com/avatar/48cb1ae701a2f15108ea13476e06ef6f","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"2:1-5:54\"\u003eLaravel + Inertia.js + Vue 3(TypeScript)の構成を学習していて、\u003cbr\u003e\n\u003ccode\u003einterface Props\u003c/code\u003e と \u003ccode\u003edefineProps\u0026lt;Props\u0026gt;()\u003c/code\u003e の意味がいまいち\u003cbr\u003e\nつかめなかったので、\u003cbr\u003e\nMVCの流れと合わせて整理してみました。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"7:1-7:23\"\u003e\n\u003cspan id=\"1inertia環境とmvc\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1inertia%E7%92%B0%E5%A2%83%E3%81%A8mvc\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1:Inertia環境とMVC\u003c/h1\u003e\n\u003cp data-sourcepos=\"9:1-11:65\"\u003e従来のMVCでは「ControllerがView(HTML)をレンダリングして返す」形でした。\u003cbr\u003e\nInertia.jsを使うと、\u003cstrong\u003eVueコンポーネントがそのままViewの役割\u003c/strong\u003eを持ち、\u003cbr\u003e\nLaravelのControllerと直接データをやり取りします。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"13:1-15:76\"\u003e\n\u003cp data-sourcepos=\"13:3-15:76\"\u003eInertia.jsって何だっけ\u003cbr\u003e\nLaravelとVueを「APIを作らずに」つなげてくれる仕組み。\u003cbr\u003e\nControllerからVueコンポーネントへ、直接データを渡せる。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"17:1-17:52\"\u003e\u003ciframe id=\"qiita-embed-content__4c19f263514885f676594a7edd123e3b\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__4c19f263514885f676594a7edd123e3b\" data-content=\"https%3A%2F%2Fqiita.com%2Fmetappi%2Fitems%2Fb4ec1f2cd60e1f449c72\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:66\"\u003eデータの流れをMVCに当てはめるとこうなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-21:37\"\u003e\u003cstrong\u003eM(Model):データを取得する\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"23:1-24:66\"\u003eLaravel: Product.php\u003cbr\u003e\nproductsテーブル(id, name, price, stock, description)を扱う\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"26:1-51:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e// Laravel: Product.php\n// productsテーブル(id, name, price, stock, description)その他を扱う\n\n\u003cspan class=\"cp\"\u003e\u0026lt;?php\u003c/span\u003e\n\n\u003cspan class=\"kn\"\u003enamespace\u003c/span\u003e \u003cspan class=\"nn\"\u003eApp\\Models\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Eloquent\\Model\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Eloquent\\SoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProduct\u003c/span\u003e \u003cspan class=\"kd\"\u003eextends\u003c/span\u003e \u003cspan class=\"nc\"\u003eModel\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eSoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eprotected\u003c/span\u003e \u003cspan class=\"nv\"\u003e$fillable\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'name'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'price'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'stock'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'description'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'created_at'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'updated_at'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"53:1-53:57\"\u003e\u003cstrong\u003eC(Controller):ViewへデータをPropsとして渡す\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"54:1-63:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// Laravel: ProductController.php\u003c/span\u003e\n\u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eindex\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eInertia\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003erender\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'Products/Index'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e// このキー名が、Vue側の props.products になる\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'products'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nc\"\u003eProduct\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"65:1-65:39\"\u003e\u003cstrong\u003eV(View):受け取って表示する\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"66:1-80:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eproducts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003eprice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003estock\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003edescription\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eprops\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"82:1-82:15\"\u003eまとめると\u003c/p\u003e\n\u003cp data-sourcepos=\"84:1-86:53\"\u003eModel      → DBのデータ\u003cbr\u003e\nController → Inertia::render() でPropsとして渡す\u003cbr\u003e\nView(Vue)  → defineProps() で受け取って表示\u003c/p\u003e\n\u003cp data-sourcepos=\"88:1-89:55\"\u003eControllerの \u003ccode\u003e'products'\u003c/code\u003e とVueの \u003ccode\u003eprops.products\u003c/code\u003e が\u003cbr\u003e\n\u003cstrong\u003e同じ名前で直接つながる\u003c/strong\u003eのがポイント\u003c/p\u003e\n\u003ch1 data-sourcepos=\"91:1-91:36\"\u003e\n\u003cspan id=\"2interface-props---の意味\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2interface-props---%E3%81%AE%E6%84%8F%E5%91%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2:interface Props { ... }の意味\u003c/h1\u003e\n\u003cp data-sourcepos=\"93:1-93:102\"\u003eこれは「このコンポーネントが受け取るデータの\u003cstrong\u003e説明書(設計図)\u003c/strong\u003e」です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"95:1-99:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eproducts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"p\"\u003e...\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ctable data-sourcepos=\"101:1-105:64\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"101:1-101:19\"\u003e\n\u003cth data-sourcepos=\"101:2-101:9\"\u003e部分\u003c/th\u003e\n\u003cth data-sourcepos=\"101:11-101:18\"\u003e意味\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"103:1-103:64\"\u003e\n\u003ctd data-sourcepos=\"103:2-103:10\"\u003eproducts\u003c/td\u003e\n\u003ctd data-sourcepos=\"103:12-103:63\"\u003e受け取るデータの名前(プロパティ名)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"104:1-104:55\"\u003e\n\u003ctd data-sourcepos=\"104:2-104:13\"\u003eArray\u0026lt;...\u0026gt;\u003c/td\u003e\n\u003ctd data-sourcepos=\"104:15-104:54\"\u003eデータが配列(リスト)で届く\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"105:1-105:64\"\u003e\n\u003ctd data-sourcepos=\"105:2-105:21\"\u003e{ id: number; ... }\u003c/td\u003e\n\u003ctd data-sourcepos=\"105:23-105:63\"\u003e配列の中身(商品1個分)の構造\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"107:1-108:84\"\u003e\u003ccode\u003eProduct::all()\u003c/code\u003e や \u003ccode\u003eProduct::get()\u003c/code\u003e は\u003cbr\u003e\n複数の商品の集まりを返すため \u003ccode\u003eArray\u003c/code\u003e にする必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"110:1-110:15\"\u003e各項目の型\u003c/p\u003e\n\u003cul data-sourcepos=\"111:1-116:0\"\u003e\n\u003cli data-sourcepos=\"111:1-111:17\"\u003e\n\u003ccode\u003eid\u003c/code\u003e → number\u003c/li\u003e\n\u003cli data-sourcepos=\"112:1-112:19\"\u003e\n\u003ccode\u003ename\u003c/code\u003e → string\u003c/li\u003e\n\u003cli data-sourcepos=\"113:1-113:20\"\u003e\n\u003ccode\u003eprice\u003c/code\u003e → number\u003c/li\u003e\n\u003cli data-sourcepos=\"114:1-114:20\"\u003e\n\u003ccode\u003estock\u003c/code\u003e → number\u003c/li\u003e\n\u003cli data-sourcepos=\"115:1-116:0\"\u003e\n\u003ccode\u003edescription\u003c/code\u003e → string\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"117:1-118:53\"\u003eこんな感じ。　descriptionは\u003cbr\u003e\n文字数が多いけどstring型で定義してOK。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"120:1-120:48\"\u003e\n\u003cspan id=\"3const-props--definepropsの意味\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3const-props--defineprops%E3%81%AE%E6%84%8F%E5%91%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3:const props = defineProps();の意味\u003c/h1\u003e\n\u003cp data-sourcepos=\"122:1-122:81\"\u003eさっき作った説明書を、実際にVueに適用している部分です。\u003c/p\u003e\n\u003cul data-sourcepos=\"124:1-128:0\"\u003e\n\u003cli data-sourcepos=\"124:1-125:43\"\u003e\n\u003ccode\u003edefineProps\u003c/code\u003e:「親からデータ(Props)を受け取ります」と\u003cbr\u003e\nいう合図(Vue 3の \u003ccode\u003e\u0026lt;script setup\u0026gt;\u003c/code\u003e 用)\u003c/li\u003e\n\u003cli data-sourcepos=\"126:1-126:99\"\u003e\n\u003ccode\u003e\u0026lt;Props\u0026gt;\u003c/code\u003e:さっきの \u003ccode\u003einterface Props\u003c/code\u003e のルールを適用する(ジェネリクスという)\u003c/li\u003e\n\u003cli data-sourcepos=\"127:1-128:0\"\u003e\n\u003ccode\u003econst props =\u003c/code\u003e:受け取ったデータを \u003ccode\u003eprops\u003c/code\u003e に入れて使えるようにする\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"129:1-129:90\"\u003eこれで、テンプレート内で \u003ccode\u003eprops.products\u003c/code\u003e としてアクセスできます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"131:1-139:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;ul\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;li\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"product in props.products\"\u003c/span\u003e \u003cspan class=\"na\"\u003e:key=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"product.id\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n      \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e:\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eprice\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e円\n    \u003cspan class=\"nt\"\u003e\u0026lt;/li\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/ul\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch1 data-sourcepos=\"141:1-141:43\"\u003e\n\u003cspan id=\"4型定義をすると何が良いの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%82%92%E3%81%99%E3%82%8B%E3%81%A8%E4%BD%95%E3%81%8C%E8%89%AF%E3%81%84%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4:型定義をすると何が良いの？\u003c/h1\u003e\n\u003cp data-sourcepos=\"143:1-145:66\"\u003e\u003cstrong\u003e① エディタが補完してくれる\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003ev-for=\"product in props.products\"\u003c/code\u003e の後で \u003ccode\u003eproduct.〜\u003c/code\u003e と打つと、\u003cbr\u003e\n\u003ccode\u003eid / name / price / stock / description\u003c/code\u003e が候補に出ます。\u003c/p\u003e\n\u003cp data-sourcepos=\"147:1-150:90\"\u003e\u003cstrong\u003e② タイポにすぐ気づける\u003c/strong\u003e\u003cbr\u003e\nLaravel側は \u003ccode\u003eprice\u003c/code\u003e で送っているのに、Vue側で \u003ccode\u003eproduct.kakaku\u003c/code\u003e と\u003cbr\u003e\n書いてしまった場合、\u003cbr\u003e\nブラウザで動かす前にエディタが赤線でエラーを教えてくれます。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"152:1-152:74\"\u003e\n\u003cspan id=\"5productは切り出すべきそれともpropsで定義するべき\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5product%E3%81%AF%E5%88%87%E3%82%8A%E5%87%BA%E3%81%99%E3%81%B9%E3%81%8D%E3%81%9D%E3%82%8C%E3%81%A8%E3%82%82props%E3%81%A7%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B%E3%81%B9%E3%81%8D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5:Productは切り出すべき?それともPropsで定義するべき？\u003c/h1\u003e\n\u003cp data-sourcepos=\"154:1-154:30\"\u003e結論：どっちでもいい\u003c/p\u003e\n\u003cp data-sourcepos=\"156:1-157:98\"\u003e今の書き方(インライン定義)は\u003cstrong\u003eそのままでOK\u003c/strong\u003eだが、\u003cbr\u003e\n切り出すのは「ダメだから」ではなく「あとで楽をしたいから(再利用)」\u003c/p\u003e\n\u003cp data-sourcepos=\"160:1-160:73\"\u003e\u003cstrong\u003e① インライン定義するパターン(今採用してるやつ)\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"161:1-171:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eproducts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003eprice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003estock\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"nl\"\u003edescription\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cblockquote data-sourcepos=\"172:1-174:89\"\u003e\n\u003cp data-sourcepos=\"172:2-174:89\"\u003eインライン定義とは\u003cbr\u003e\nv-on ディレクティブを使用することで、 DOM イベントの購読やイベント発火時にいくつかの JavaScript を実行します。これは通常 @ に省略することができます。使い方は v-on:click=\"handler\"、あるいは省略して @click=\"handler\" として使用します。\u003cbr\u003e\n参考：vue公式ドキュメント \u003ca href=\"https://ja.vuejs.org/guide/essentials/event-handling\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://ja.vuejs.org/guide/essentials/event-handling\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"176:1-176:51\"\u003e\u003cstrong\u003e② \u003ccode\u003eProduct\u003c/code\u003e を外に切り出すパターン\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"177:1-189:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eProduct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eprice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003estock\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003edescription\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eproducts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e[];\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Array\u0026lt;Product\u0026gt; でも同じ意味\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ctable data-sourcepos=\"191:1-194:76\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"191:1-191:36\"\u003e\n\u003cth data-sourcepos=\"191:2-191:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"191:4-191:20\"\u003eインライン\u003c/th\u003e\n\u003cth data-sourcepos=\"191:22-191:35\"\u003e切り出し\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"193:1-193:121\"\u003e\n\u003ctd data-sourcepos=\"193:2-193:15\"\u003eメリット\u003c/td\u003e\n\u003ctd data-sourcepos=\"193:17-193:78\"\u003eこのファイルだけで「何が届くか」が分かる\u003c/td\u003e\n\u003ctd data-sourcepos=\"193:80-193:120\"\u003e他の場所でも型を使い回せる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"194:1-194:76\"\u003e\n\u003ctd data-sourcepos=\"194:2-194:18\"\u003eデメリット\u003c/td\u003e\n\u003ctd data-sourcepos=\"194:20-194:48\"\u003e他で使い回しにくい\u003c/td\u003e\n\u003ctd data-sourcepos=\"194:50-194:75\"\u003e行数が少し増える\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"196:1-196:108\"\u003e例えば、「商品をクリックしたら詳細をモーダルで表示したい」機能を足すとき\u003c/p\u003e\n\u003cp data-sourcepos=\"198:1-198:69\"\u003e切り出していれば関数の引数にそのまま使えます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"200:1-205:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eopenModal\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eselectedProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eisModalOpen\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"207:1-209:40\"\u003e切り出していないと、引数の型を長々と書き直すか\u003cbr\u003e\n\u003ccode\u003eany\u003c/code\u003e に頼ることになってしまいます。\u003cbr\u003e\n(でもanyだとTSエラー出ちゃう)\u003c/p\u003e\n\u003cp data-sourcepos=\"211:1-211:52\"\u003e\u003ciframe id=\"qiita-embed-content__dd7f3eb098ddf9555483b618a22512bb\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__dd7f3eb098ddf9555483b618a22512bb\" data-content=\"https%3A%2F%2Fqiita.com%2Fmetappi%2Fitems%2F091bbe93431699cb6b48\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cul data-sourcepos=\"213:1-216:0\"\u003e\n\u003cli data-sourcepos=\"213:1-213:94\"\u003e今のファイル以外で商品の構造を使う予定がない → \u003cstrong\u003e今のままでOK\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"214:1-216:0\"\u003e別の関数・子コンポーネント・編集画面でも使う\u003cbr\u003e\n→ \u003cstrong\u003e\u003ccode\u003einterface Product\u003c/code\u003e として切り出す\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"217:1-217:11\"\u003e\n\u003cspan id=\"注意点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%B3%A8%E6%84%8F%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e注意点\u003c/h1\u003e\n\u003cp data-sourcepos=\"219:1-221:90\"\u003e\u003cstrong\u003e型定義は「Laravelの中身」を自動でチェックしてくれるわけではありません。\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003einterface\u003c/code\u003e はあくまでVue側に自分で書いた「期待するデータの形」で\u003cbr\u003e\nLaravel側のカラム(型)を変えたときは、こちらも手直しが必要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"223:1-225:48\"\u003eたとえばDBの \u003ccode\u003edecimal\u003c/code\u003e カラムは、実際には文字列で届くことがあります。\u003cbr\u003e\nその場合 \u003ccode\u003eprice: number\u003c/code\u003e と書いていても、実データは\u003cbr\u003e\n\u003ccode\u003estring\u003c/code\u003e というズレが起こりえます。\u003c/p\u003e\n\u003cp data-sourcepos=\"227:1-228:58\"\u003eまた、\u003ccode\u003einterface\u003c/code\u003e を別ファイルに置いて \u003ccode\u003eimport\u003c/code\u003e する場合は、\u003cbr\u003e\nVue 3.3以降であれば \u003ccode\u003edefineProps\u003c/code\u003e で使えます。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"230:1-230:11\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h1\u003e\n\u003cul data-sourcepos=\"232:1-241:0\"\u003e\n\u003cli data-sourcepos=\"232:1-233:48\"\u003eInertiaでは**Vueコンポーネントが「MVCのV」**になり、\u003cbr\u003e\nControllerからPropsで直接データが届く\u003c/li\u003e\n\u003cli data-sourcepos=\"234:1-235:57\"\u003e\n\u003ccode\u003einterface Props\u003c/code\u003e は受け取るデータの\u003cstrong\u003e説明書\u003c/strong\u003e、\u003cbr\u003e\n\u003ccode\u003edefineProps\u0026lt;Props\u0026gt;()\u003c/code\u003e はそれを\u003cstrong\u003e適用する処理\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"236:1-236:69\"\u003e型定義のおかげで、\u003cstrong\u003e補完とタイポの検出\u003c/strong\u003eが効く\u003c/li\u003e\n\u003cli data-sourcepos=\"237:1-238:35\"\u003e他でも使い回すなら \u003ccode\u003eProduct\u003c/code\u003e を切り出す\u003cbr\u003e\n使わないなら今のままでOK\u003c/li\u003e\n\u003cli data-sourcepos=\"239:1-241:0\"\u003eただし型はあくまで「自分で書いた約束」のため\u003cbr\u003e\nLaravel側の変更とはズレる可能性がある\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"242:1-242:102\"\u003eMVCの全体像とつなげて考えると、型定義の意味がだいぶスッキリしました。\u003c/p\u003e\n","body":"\nLaravel + Inertia.js + Vue 3(TypeScript)の構成を学習していて、\n`interface Props` と `defineProps\u003cProps\u003e()` の意味がいまいち\nつかめなかったので、\nMVCの流れと合わせて整理してみました。\n\n# 1:Inertia環境とMVC\n\n従来のMVCでは「ControllerがView(HTML)をレンダリングして返す」形でした。\nInertia.jsを使うと、**VueコンポーネントがそのままViewの役割**を持ち、\nLaravelのControllerと直接データをやり取りします。\n\n\u003e Inertia.jsって何だっけ\nLaravelとVueを「APIを作らずに」つなげてくれる仕組み。\nControllerからVueコンポーネントへ、直接データを渡せる。\n\nhttps://qiita.com/metappi/items/b4ec1f2cd60e1f449c72\n\nデータの流れをMVCに当てはめるとこうなります。\n\n**M(Model):データを取得する**\n\nLaravel: Product.php\nproductsテーブル(id, name, price, stock, description)を扱う\n\n~~~php\n// Laravel: Product.php\n// productsテーブル(id, name, price, stock, description)その他を扱う\n\n\u003c?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass Product extends Model\n{\n    use SoftDeletes;\n\n    protected $fillable = [\n        'name',\n        'price',\n        'stock',\n        'description',\n        'created_at',\n        'updated_at',\n    ];\n}\n\n~~~\n\n**C(Controller):ViewへデータをPropsとして渡す**\n~~~php\n// Laravel: ProductController.php\npublic function index()\n{\n    return Inertia::render('Products/Index', [\n        // このキー名が、Vue側の props.products になる\n        'products' =\u003e Product::all(),\n    ]);\n}\n~~~\n\n**V(View):受け取って表示する**\n~~~vue\n\u003cscript setup lang=\"ts\"\u003e\ninterface Props {\n    products: Array\u003c{\n        id: number;\n        name: string;\n        price: number;\n        stock: number;\n        description: string;\n    }\u003e;\n}\n\nconst props = defineProps\u003cProps\u003e();\n\u003c/script\u003e\n~~~\n\nまとめると\n\nModel      → DBのデータ\nController → Inertia::render() でPropsとして渡す\nView(Vue)  → defineProps() で受け取って表示\n\nControllerの `'products'` とVueの `props.products` が\n**同じ名前で直接つながる**のがポイント\n\n# 2:interface Props { ... }の意味\n\nこれは「このコンポーネントが受け取るデータの**説明書(設計図)**」です。\n\n~~~ts\ninterface Props {\n    products: Array\u003c{ ... }\u003e;\n}\n~~~\n\n| 部分 | 意味 |\n|---|---|\n| products| 受け取るデータの名前(プロパティ名) |\n| Array\u003c...\u003e | データが配列(リスト)で届く |\n| { id: number; ... }| 配列の中身(商品1個分)の構造 |\n\n`Product::all()` や `Product::get()` は\n複数の商品の集まりを返すため `Array` にする必要があります。\n\n各項目の型\n* `id` → number\n* `name` → string\n* `price` → number\n* `stock` → number\n* `description` → string\n\nこんな感じ。　descriptionは\n文字数が多いけどstring型で定義してOK。\n\n# 3:const props = defineProps\u003cProps\u003e();の意味\n\nさっき作った説明書を、実際にVueに適用している部分です。\n\n* `defineProps`:「親からデータ(Props)を受け取ります」と\nいう合図(Vue 3の `\u003cscript setup\u003e` 用)\n* `\u003cProps\u003e`:さっきの `interface Props` のルールを適用する(ジェネリクスという)\n* `const props =`:受け取ったデータを `props` に入れて使えるようにする\n\nこれで、テンプレート内で `props.products` としてアクセスできます。\n\n~~~vue\n\u003ctemplate\u003e\n  \u003cul\u003e\n    \u003cli v-for=\"product in props.products\" :key=\"product.id\"\u003e\n      {{ product.name }}:{{ product.price }}円\n    \u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/template\u003e\n~~~\n\n# 4:型定義をすると何が良いの？\n\n**① エディタが補完してくれる**\n`v-for=\"product in props.products\"` の後で `product.〜` と打つと、\n`id / name / price / stock / description` が候補に出ます。\n\n**② タイポにすぐ気づける**\nLaravel側は `price` で送っているのに、Vue側で `product.kakaku` と\n書いてしまった場合、\nブラウザで動かす前にエディタが赤線でエラーを教えてくれます。\n\n# 5:Productは切り出すべき?それともPropsで定義するべき？\n\n結論：どっちでもいい\n\n今の書き方(インライン定義)は**そのままでOK**だが、\n切り出すのは「ダメだから」ではなく「あとで楽をしたいから(再利用)」\n\n\n**① インライン定義するパターン(今採用してるやつ)**\n~~~ts\ninterface Props {\n    products: Array\u003c{\n        id: number;\n        name: string;\n        price: number;\n        stock: number;\n        description: string;\n    }\u003e;\n}\n~~~\n\u003eインライン定義とは\n\u003ev-on ディレクティブを使用することで、 DOM イベントの購読やイベント発火時にいくつかの JavaScript を実行します。これは通常 @ に省略することができます。使い方は v-on:click=\"handler\"、あるいは省略して @click=\"handler\" として使用します。\n参考：vue公式ドキュメント https://ja.vuejs.org/guide/essentials/event-handling\n\n**② `Product` を外に切り出すパターン**\n~~~ts\ninterface Product {\n    id: number;\n    name: string;\n    price: number;\n    stock: number;\n    description: string;\n}\n\ninterface Props {\n    products: Product[]; // Array\u003cProduct\u003e でも同じ意味\n}\n~~~\n\n| | インライン | 切り出し |\n|---|---|---|\n| メリット | このファイルだけで「何が届くか」が分かる | 他の場所でも型を使い回せる |\n| デメリット | 他で使い回しにくい | 行数が少し増える |\n\n例えば、「商品をクリックしたら詳細をモーダルで表示したい」機能を足すとき\n\n切り出していれば関数の引数にそのまま使えます。\n\n~~~ts\nconst openModal = (product: Product) =\u003e {\n    selectedProduct.value = product;\n    isModalOpen.value = true;\n};\n~~~\n\n切り出していないと、引数の型を長々と書き直すか \n`any` に頼ることになってしまいます。\n(でもanyだとTSエラー出ちゃう)\n\nhttps://qiita.com/metappi/items/091bbe93431699cb6b48\n\n* 今のファイル以外で商品の構造を使う予定がない → **今のままでOK**\n* 別の関数・子コンポーネント・編集画面でも使う \n→ **`interface Product` として切り出す**\n\n# 注意点\n\n**型定義は「Laravelの中身」を自動でチェックしてくれるわけではありません。**\n`interface` はあくまでVue側に自分で書いた「期待するデータの形」で\nLaravel側のカラム(型)を変えたときは、こちらも手直しが必要です。\n\nたとえばDBの `decimal` カラムは、実際には文字列で届くことがあります。\nその場合 `price: number` と書いていても、実データは \n`string` というズレが起こりえます。\n\nまた、`interface` を別ファイルに置いて `import` する場合は、\nVue 3.3以降であれば `defineProps` で使えます。\n\n# まとめ\n\n* Inertiaでは**Vueコンポーネントが「MVCのV」**になり、\nControllerからPropsで直接データが届く\n* `interface Props` は受け取るデータの**説明書**、\n`defineProps\u003cProps\u003e()` はそれを**適用する処理**\n* 型定義のおかげで、**補完とタイポの検出**が効く\n* 他でも使い回すなら `Product` を切り出す\n使わないなら今のままでOK\n* ただし型はあくまで「自分で書いた約束」のため\nLaravel側の変更とはズレる可能性がある\n\nMVCの全体像とつなげて考えると、型定義の意味がだいぶスッキリしました。\n","coediting":false,"comments_count":0,"created_at":"2026-09-24T09:23:22+09:00","group":null,"id":"24fa08104b7ba17044c3","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Laravel","versions":[]},{"name":"Vue.js","versions":[]},{"name":"inertia","versions":[]}],"title":"【Laravel × Inertia × Vue 3】Propsの型定義で「MVC」のつながりを整理してみた","updated_at":"2026-09-24T11:28:15+09:00","url":"https://qiita.com/metappi/items/24fa08104b7ba17044c3","user":{"description":"プログラミングスクールRUNTEQ卒業後、\r\n受託企業に就職しました。\r\nPHP Laravel Vueをメインに学習中。\r\n楽しみながら学んでいきたいと考えております。\r\n夢は所さんになることです。\r\nまだまだエンジニアのタマゴなので間違い、ご意見等ございましたら\r\nぜひ頂戴したいと思っております。よろしくお願いいたします。","facebook_id":"","followees_count":21,"followers_count":32,"github_login_name":"Zundabyon","id":"metappi","items_count":79,"linkedin_id":"","location":"Japan","name":"(めたっぴ72期A) 小笠原","organization":"","permanent_id":4155281,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/profile-images/1789107820","team_only":false,"twitter_screen_name":"zundabyon","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:350\"\u003eVue 3やNuxtで多言語対応（i18n）アプリを構築する際、長年にわたりデファクトスタンダードとして君臨してきたのが \u003ccode\u003evue-i18n\u003c/code\u003e です。Kazupon氏をはじめとするコミュニティによって成熟したエコシステムが築かれ、Vue開発者にとって最初の選択肢であり続けました。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-3:154\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F78ec8fa9-2e38-477d-82ec-63dcee1c91ff.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e7a0214dff8175daace50aa3fc63814f\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F78ec8fa9-2e38-477d-82ec-63dcee1c91ff.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e7a0214dff8175daace50aa3fc63814f\" alt=\"Screenshot 2026-09-22 at 9.54.50 PM.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F78ec8fa9-2e38-477d-82ec-63dcee1c91ff.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=69ec5881fc7df4a2c5c397d4120b2f8e 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/78ec8fa9-2e38-477d-82ec-63dcee1c91ff.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:558\"\u003eしかし、なぜ \u003ccode\u003evue-i18n\u003c/code\u003e が現在の挙動になっているのかを理解するには、その設計時期を振り返る必要があります。このアーキテクチャが作られたのは、動的インポート（\u003ccode\u003eimport()\u003c/code\u003e）やルート単位のコード分割がまだ標準化されていなかった、古き良きSPA（Single Page Application）の全盛期でした。当時は、アプリ起動時に巨大なグローバル翻訳オブジェクトを丸ごとメモリに読み込むアプローチが最も自然だったのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-8:432\"\u003eところが現在のフロントエンド開発は、Nuxtや静的サイト生成（SSG）、きめ細やかな動的ページ読み込みを駆使した「ページ容量（初期ロードJS）の徹底的な最適化」が前提となっています。この現代的なアーキテクチャにおいて、中央集約型のグローバルプロバイダーモデルは極めて深刻なボトルネックと化しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-10:856\"\u003eそもそも、なぜ \u003ccode\u003evue-i18n\u003c/code\u003e のライブラリ本体はここまで重いのでしょうか？ 単に \u003ccode\u003evue-i18n\u003c/code\u003e をインポートしただけの空コンポーネントでも、文字を1つも描画しない段階で \u003cstrong\u003egzip約24.3 KB（非圧縮83.2 KB）\u003c/strong\u003e もの容量を消費します。その理由は、動的な変数挿入（\u003ccode\u003e{{number}}\u003c/code\u003e や \u003ccode\u003e{name}\u003c/code\u003e）の評価、複雑な複数形（Pluralization）ルール、リスト整形などをブラウザ側で実行時にパースするための巨大なランタイムエンジンを丸ごと同梱しているからです。さらに多言語ルーティングやURL管理を行おうとすると、Cookieの保存やロケール自動判定、URLプレフィックスの書き換えといった大量のクライアント側ロジックが追加で読み込まれることになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"12:1-12:122\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2Fa2e6d18a-e22a-47cf-92f3-dff226622caa.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=51261d7f32f0b1d08f8619e90f316346\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2Fa2e6d18a-e22a-47cf-92f3-dff226622caa.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=51261d7f32f0b1d08f8619e90f316346\" alt=\"image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2Fa2e6d18a-e22a-47cf-92f3-dff226622caa.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=6f9214d3fe578bfaf9d69def56cbf1d5 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/a2e6d18a-e22a-47cf-92f3-dff226622caa.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:144\"\u003eそして、それ以上に深刻なのが、画面間での「翻訳文言の大量漏洩（Copy Leakage）」という構造的欠陥です。\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:932\"\u003e一般的なVueアプリでは、\u003ccode\u003ecreateI18n({ messages })\u003c/code\u003e が全言語・全ページの翻訳キーを抱えたモノリシックなグローバルツリーを生成します。その結果、ユーザーがシンプルな \u003ccode\u003e/contact\u003c/code\u003e ページを開いただけでも、ブラウザは \u003ccode\u003e/dashboard\u003c/code\u003e や \u003ccode\u003e/pricing\u003c/code\u003e、\u003ccode\u003e/settings\u003c/code\u003e など、アプリ全体のすべてのテキストをダウンロードせざるを得ません。標準的な10画面・10言語のVite + Vue 3アプリでベンチマークを実施したところ、\u003cstrong\u003eある1画面でダウンロードされた翻訳データの実に90%が、その画面とは全く関係のない他画面のテキスト\u003c/strong\u003eでした。さらに \u003ccode\u003euseI18n()\u003c/code\u003e がこのグローバルインスタンスに直接バインドされるため、1つのコンポーネントを単体でコンパイルするだけでも平均 \u003cstrong\u003e196 KB\u003c/strong\u003e ものJSを引きずり込んでしまいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:438\"\u003e加えて、\u003ccode\u003et(\"key.path\")\u003c/code\u003e は実行時に動的に文字列を評価するため、ViteやRollupといったバンドラは実際にどのキーが使われているかを静的に追跡できません。そのため未使用の翻訳をツリーシェイキングで削除することができず、キーのタイポもTypeScriptのビルドエラーにならず本番環境でサイレントに表示崩れを引き起こします。\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-21:122\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F4e21aab0-579a-4895-8731-7242f8bb8db2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9c690d0b68b4f9fac99475cc6ac63850\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F4e21aab0-579a-4895-8731-7242f8bb8db2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9c690d0b68b4f9fac99475cc6ac63850\" alt=\"image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3963127%2F4e21aab0-579a-4895-8731-7242f8bb8db2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0ecb64f05b98826155ee68a842b22e5c 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/4e21aab0-579a-4895-8731-7242f8bb8db2.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"24:1-24:50\"\u003e\n\u003cspan id=\"intlayerはこれをどう解決するのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#intlayer%E3%81%AF%E3%81%93%E3%82%8C%E3%82%92%E3%81%A9%E3%81%86%E8%A7%A3%E6%B1%BA%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eIntlayerはこれをどう解決するのか？\u003c/h2\u003e\n\u003cp data-sourcepos=\"26:1-26:318\"\u003eIntlayerは、肥大化したランタイムパーサーやプロバイダーに頼るのではなく、\u003cstrong\u003eこれらの余分な処理をすべてビルド時に排除し、静的コード変換によってコンテンツをコンポーネントへ直接接続する\u003c/strong\u003eことでこの構造的課題を解決します。\u003c/p\u003e\n\u003cp data-sourcepos=\"28:1-29:533\"\u003e\u003cstrong\u003e1. ビルド時プリコンパイル（ランタイムパースのオーバーヘッドをゼロに）\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003e{{number}}\u003c/code\u003e などの変数挿入をブラウザ上でパースしたり、実行時にCookieやURLプレフィックスを処理する重厚なロジックをクライアントへ送り込む代わりに、Intlayerはこれらすべてをビルド段階で事前に解決・最適化します。ブラウザに届くのは余計な処理が削ぎ落とされた極めて軽量なコードのみであり、ランタイムフットプリントはわずか \u003cstrong\u003e3.9 KB\u003c/strong\u003e（互換アダプターでも7.9 KB）まで削減されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"31:1-32:524\"\u003e\u003cstrong\u003e2. コンポーネントとコンテンツの直接バインディング\u003c/strong\u003e\u003cbr\u003e\n無秩序に肥大化する \u003ccode\u003elocales/en.json\u003c/code\u003e にすべての文言を詰め込む代わりに、Intlayerは辞書ファイルをそれを消費するコンポーネントのすぐ隣（例: \u003ccode\u003eFooter.vue\u003c/code\u003e の隣の \u003ccode\u003eFooter.content.ts\u003c/code\u003e）にコロケーション（同居）させます。その結果、インポートされていないコンポーネントの文言がロードされることは一切ありません。未使用のデッドコードが翻訳データを道連れにバンドルされるのを防ぎます。\u003c/p\u003e\n\u003cp data-sourcepos=\"34:1-35:585\"\u003e\u003cstrong\u003e3. Viteによるウォーターフォールなしの動的ローディング\u003c/strong\u003e\u003cbr\u003e\nさらに強力なのが、Viteの高度なモジュール変換の活用です。動的ページローディングやコード分割を使用している場合でも、Intlayerはそのチャンク内で実際に消費されるローカライズデータだけを、対象チャンクに直接インラインで統合します。描画前にリモートのJSON辞書を待ち受けるような\u003cstrong\u003e余計なネットワークリクエストやウォーターフォールはゼロ\u003c/strong\u003e。コンポーネントと必要な翻訳データが1つのチャンクとして同時に届きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-38:306\"\u003e\u003cstrong\u003e4. ビルド時の厳格な型安全性\u003c/strong\u003e\u003cbr\u003e\nIntlayerはコンテンツ定義からTypeScriptの型定義を自動生成します。IDEでのキーの自動補完はもちろん、翻訳の欠落やタイポがあれば即座にビルドエラーとして検出されるため、本番環境でのフォールバック事故を未然に防げます。\u003c/p\u003e\n\u003cp data-sourcepos=\"40:1-41:487\"\u003e\u003cstrong\u003e5. 漏洩0%と3倍小さなバンドルサイズ\u003c/strong\u003e\u003cbr\u003e\nビルド時、Intlayerコンパイラは呼び出し元を静的解析し、各ルートが実際に描画するテキストのみを含むように辞書を分割します。実測ベンチマークでは、画面間の文言漏洩が \u003cstrong\u003e90%から0%\u003c/strong\u003e へと完全に消滅し、ページあたりのJavaScriptサイズが \u003cstrong\u003e134.9 KBから47.0 KB\u003c/strong\u003e（gzip）へと激減しました（参考として、i18nを全く導入していないベースアプリの容量は41.3 KBです）。\u003c/p\u003e\n\u003cp data-sourcepos=\"43:1-44:667\"\u003e\u003cstrong\u003e6. \u003ccode\u003e@intlayer/vue-i18n\u003c/code\u003e によるDrop-in移行\u003c/strong\u003e\u003cbr\u003e\nすでに運用中のVueコードベースがある場合でも、コンポーネントを書き直す必要はありません。互換アダプターである \u003ccode\u003e@intlayer/vue-i18n\u003c/code\u003e は、既存の \u003ccode\u003evue-i18n\u003c/code\u003e と全く同じAPI（\u003ccode\u003euseI18n\u003c/code\u003e, \u003ccode\u003et()\u003c/code\u003e, \u003ccode\u003ed()\u003c/code\u003e, \u003ccode\u003en()\u003c/code\u003e, \u003ccode\u003e$t\u003c/code\u003e, \u003ccode\u003ev-t\u003c/code\u003e）を提供します。Viteプラグインを導入し、巨大な \u003ccode\u003emessages\u003c/code\u003e インポートを削除するだけで、既存の \u003ccode\u003et(\"key\")\u003c/code\u003e の呼び出し先がコンパイル・分割済みの辞書へと自動的に差し替わります。\u003ccode\u003e.vue\u003c/code\u003e ファイルを1行も書き換えることなく、ランタイムは3分の1、コンポーネントサイズは23分の1に縮小します。\u003c/p\u003e\n\u003cp data-sourcepos=\"46:1-46:375\"\u003eもし本番環境で多言語対応のVueまたはNuxtアプリケーションを運用しているなら、ぜひブラウザのNetworkタブを開いてサブページを確認してみてください。ダウンロードされているJavaScriptの大半が、ユーザーが一度も目にすることのない翻訳テキストであることに気づくはずです。\u003c/p\u003e\n\u003cp data-sourcepos=\"48:1-50:58\"\u003e詳細なベンチマーク数値や移行ガイド、アーキテクチャの解説は以下の記事で公開しています：\u003cbr\u003e\n\u003ca href=\"https://intlayer.org/ja/blog/vue-i18n-vs-intlayer\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://intlayer.org/ja/blog/vue-i18n-vs-intlayer\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://intlayer.org/ja/blog/vue-i18n-vs-intlayer-vue-i18n\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://intlayer.org/ja/blog/vue-i18n-vs-intlayer-vue-i18n\u003c/a\u003e\u003c/p\u003e\n","body":"Vue 3やNuxtで多言語対応（i18n）アプリを構築する際、長年にわたりデファクトスタンダードとして君臨してきたのが `vue-i18n` です。Kazupon氏をはじめとするコミュニティによって成熟したエコシステムが築かれ、Vue開発者にとって最初の選択肢であり続けました。\n\n![Screenshot 2026-09-22 at 9.54.50 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/78ec8fa9-2e38-477d-82ec-63dcee1c91ff.png)\n\n\nしかし、なぜ `vue-i18n` が現在の挙動になっているのかを理解するには、その設計時期を振り返る必要があります。このアーキテクチャが作られたのは、動的インポート（`import()`）やルート単位のコード分割がまだ標準化されていなかった、古き良きSPA（Single Page Application）の全盛期でした。当時は、アプリ起動時に巨大なグローバル翻訳オブジェクトを丸ごとメモリに読み込むアプローチが最も自然だったのです。\n\nところが現在のフロントエンド開発は、Nuxtや静的サイト生成（SSG）、きめ細やかな動的ページ読み込みを駆使した「ページ容量（初期ロードJS）の徹底的な最適化」が前提となっています。この現代的なアーキテクチャにおいて、中央集約型のグローバルプロバイダーモデルは極めて深刻なボトルネックと化しています。\n\nそもそも、なぜ `vue-i18n` のライブラリ本体はここまで重いのでしょうか？ 単に `vue-i18n` をインポートしただけの空コンポーネントでも、文字を1つも描画しない段階で **gzip約24.3 KB（非圧縮83.2 KB）** もの容量を消費します。その理由は、動的な変数挿入（`{{number}}` や `{name}`）の評価、複雑な複数形（Pluralization）ルール、リスト整形などをブラウザ側で実行時にパースするための巨大なランタイムエンジンを丸ごと同梱しているからです。さらに多言語ルーティングやURL管理を行おうとすると、Cookieの保存やロケール自動判定、URLプレフィックスの書き換えといった大量のクライアント側ロジックが追加で読み込まれることになります。\n\n![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/a2e6d18a-e22a-47cf-92f3-dff226622caa.png)\n\n\nそして、それ以上に深刻なのが、画面間での「翻訳文言の大量漏洩（Copy Leakage）」という構造的欠陥です。\n\n一般的なVueアプリでは、`createI18n({ messages })` が全言語・全ページの翻訳キーを抱えたモノリシックなグローバルツリーを生成します。その結果、ユーザーがシンプルな `/contact` ページを開いただけでも、ブラウザは `/dashboard` や `/pricing`、`/settings` など、アプリ全体のすべてのテキストをダウンロードせざるを得ません。標準的な10画面・10言語のVite + Vue 3アプリでベンチマークを実施したところ、**ある1画面でダウンロードされた翻訳データの実に90%が、その画面とは全く関係のない他画面のテキスト**でした。さらに `useI18n()` がこのグローバルインスタンスに直接バインドされるため、1つのコンポーネントを単体でコンパイルするだけでも平均 **196 KB** ものJSを引きずり込んでしまいます。\n\n加えて、`t(\"key.path\")` は実行時に動的に文字列を評価するため、ViteやRollupといったバンドラは実際にどのキーが使われているかを静的に追跡できません。そのため未使用の翻訳をツリーシェイキングで削除することができず、キーのタイポもTypeScriptのビルドエラーにならず本番環境でサイレントに表示崩れを引き起こします。\n\n![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/4e21aab0-579a-4895-8731-7242f8bb8db2.png)\n\n\n## Intlayerはこれをどう解決するのか？\n\nIntlayerは、肥大化したランタイムパーサーやプロバイダーに頼るのではなく、**これらの余分な処理をすべてビルド時に排除し、静的コード変換によってコンテンツをコンポーネントへ直接接続する**ことでこの構造的課題を解決します。\n\n**1. ビルド時プリコンパイル（ランタイムパースのオーバーヘッドをゼロに）**  \n`{{number}}` などの変数挿入をブラウザ上でパースしたり、実行時にCookieやURLプレフィックスを処理する重厚なロジックをクライアントへ送り込む代わりに、Intlayerはこれらすべてをビルド段階で事前に解決・最適化します。ブラウザに届くのは余計な処理が削ぎ落とされた極めて軽量なコードのみであり、ランタイムフットプリントはわずか **3.9 KB**（互換アダプターでも7.9 KB）まで削減されます。\n\n**2. コンポーネントとコンテンツの直接バインディング**  \n無秩序に肥大化する `locales/en.json` にすべての文言を詰め込む代わりに、Intlayerは辞書ファイルをそれを消費するコンポーネントのすぐ隣（例: `Footer.vue` の隣の `Footer.content.ts`）にコロケーション（同居）させます。その結果、インポートされていないコンポーネントの文言がロードされることは一切ありません。未使用のデッドコードが翻訳データを道連れにバンドルされるのを防ぎます。\n\n**3. Viteによるウォーターフォールなしの動的ローディング**  \nさらに強力なのが、Viteの高度なモジュール変換の活用です。動的ページローディングやコード分割を使用している場合でも、Intlayerはそのチャンク内で実際に消費されるローカライズデータだけを、対象チャンクに直接インラインで統合します。描画前にリモートのJSON辞書を待ち受けるような**余計なネットワークリクエストやウォーターフォールはゼロ**。コンポーネントと必要な翻訳データが1つのチャンクとして同時に届きます。\n\n**4. ビルド時の厳格な型安全性**  \nIntlayerはコンテンツ定義からTypeScriptの型定義を自動生成します。IDEでのキーの自動補完はもちろん、翻訳の欠落やタイポがあれば即座にビルドエラーとして検出されるため、本番環境でのフォールバック事故を未然に防げます。\n\n**5. 漏洩0%と3倍小さなバンドルサイズ**  \nビルド時、Intlayerコンパイラは呼び出し元を静的解析し、各ルートが実際に描画するテキストのみを含むように辞書を分割します。実測ベンチマークでは、画面間の文言漏洩が **90%から0%** へと完全に消滅し、ページあたりのJavaScriptサイズが **134.9 KBから47.0 KB**（gzip）へと激減しました（参考として、i18nを全く導入していないベースアプリの容量は41.3 KBです）。\n\n**6. `@intlayer/vue-i18n` によるDrop-in移行**  \nすでに運用中のVueコードベースがある場合でも、コンポーネントを書き直す必要はありません。互換アダプターである `@intlayer/vue-i18n` は、既存の `vue-i18n` と全く同じAPI（`useI18n`, `t()`, `d()`, `n()`, `$t`, `v-t`）を提供します。Viteプラグインを導入し、巨大な `messages` インポートを削除するだけで、既存の `t(\"key\")` の呼び出し先がコンパイル・分割済みの辞書へと自動的に差し替わります。`.vue` ファイルを1行も書き換えることなく、ランタイムは3分の1、コンポーネントサイズは23分の1に縮小します。\n\nもし本番環境で多言語対応のVueまたはNuxtアプリケーションを運用しているなら、ぜひブラウザのNetworkタブを開いてサブページを確認してみてください。ダウンロードされているJavaScriptの大半が、ユーザーが一度も目にすることのない翻訳テキストであることに気づくはずです。\n\n詳細なベンチマーク数値や移行ガイド、アーキテクチャの解説は以下の記事で公開しています：\nhttps://intlayer.org/ja/blog/vue-i18n-vs-intlayer\nhttps://intlayer.org/ja/blog/vue-i18n-vs-intlayer-vue-i18n\n","coediting":false,"comments_count":0,"created_at":"2026-09-23T13:07:06+09:00","group":null,"id":"49630875e04c1adeb481","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"I18n","versions":[]},{"name":"Vue.js","versions":[]},{"name":"vue-i18n","versions":[]},{"name":"JavaScript","versions":[]}],"title":"vue-i18nが抱える問題と、Intlayerがそれを根本から解決する理由","updated_at":"2026-09-23T13:07:06+09:00","url":"https://qiita.com/aymericzip/items/49630875e04c1adeb481","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":2,"github_login_name":null,"id":"aymericzip","items_count":14,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":3963127,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocKeHS3ltJBSyCzUhaR46XTV6at3LNqmJVPZoQdyZ-S5GPEnYoY0=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-2:109\"\u003eこんにちは、Hilavaです。\u003cbr\u003e\nマイキャラ管理ツール「MyChara-Zukan」というWebアプリを開発・リリースしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"4:1-4:210\"\u003eこれは、自分のオリジナルキャラクターをブラウザで管理するという趣旨のWebアプリです。データはブラウザに保存され、完全無料でご利用いただけます。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:85\"\u003e記事はZennにまとめましたので、見ていただけると嬉しいです。\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-8:32\"\u003eMyChara-Zukanはこちら👇️\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-10:31\"\u003e\u003ciframe id=\"qiita-embed-content__da664e004454d782df3ab83ec8971ab3\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__da664e004454d782df3ab83ec8971ab3\" data-content=\"https%3A%2F%2Fmychara-tool.pages.dev%2F\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"12:1-12:37\"\u003e詳細記事はこちらです👇️\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-14:47\"\u003e\u003ciframe id=\"qiita-embed-content__cb1866e38627032707e6222b255c5ca5\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__cb1866e38627032707e6222b255c5ca5\" data-content=\"https%3A%2F%2Fzenn.dev%2Fhilava%2Farticles%2F7746b3143d757a\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003ch3 data-sourcepos=\"16:1-16:31\"\u003e\n\u003cspan id=\"スクリーンショット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eスクリーンショット\u003c/h3\u003e\n\u003cp data-sourcepos=\"17:1-17:162\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F467f73a0-e696-4f10-8e00-72728aa51b35.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b1efb7c6612e9c6732e28ac93880e333\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F467f73a0-e696-4f10-8e00-72728aa51b35.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b1efb7c6612e9c6732e28ac93880e333\" alt=\"スクリーンショット 2026-09-23 080826.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F467f73a0-e696-4f10-8e00-72728aa51b35.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=a774d6dff0cefa679c05dfcfc4ec3b94 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/467f73a0-e696-4f10-8e00-72728aa51b35.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:162\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F4ee33523-4bb9-4416-89d8-7ecc035c8ea8.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=ed03f1ed148625594e36795e5b9beb3a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F4ee33523-4bb9-4416-89d8-7ecc035c8ea8.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=ed03f1ed148625594e36795e5b9beb3a\" alt=\"スクリーンショット 2026-09-23 112604.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F4ee33523-4bb9-4416-89d8-7ecc035c8ea8.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=d5b13a69ec41e65b12d2de6e9d597f51 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/4ee33523-4bb9-4416-89d8-7ecc035c8ea8.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-21:54\"\u003e※ヘッダーは任意の画像を指定できます\u003c/p\u003e\n\u003cp data-sourcepos=\"23:1-23:162\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F394ce57d-17a8-40e0-95c5-4bd953e04988.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1f2aa64c43ea327f937cd35e0e562cac\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F394ce57d-17a8-40e0-95c5-4bd953e04988.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1f2aa64c43ea327f937cd35e0e562cac\" alt=\"スクリーンショット 2026-09-23 095218.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4506057%2F394ce57d-17a8-40e0-95c5-4bd953e04988.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=cc42d9191bad3afcf2c26eff6658da9d 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/394ce57d-17a8-40e0-95c5-4bd953e04988.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"25:1-26:39\"\u003e短いですが以上となります。\u003cbr\u003e\nよろしくお願いいたします。\u003c/p\u003e\n","body":"こんにちは、Hilavaです。\nマイキャラ管理ツール「MyChara-Zukan」というWebアプリを開発・リリースしました。\n\nこれは、自分のオリジナルキャラクターをブラウザで管理するという趣旨のWebアプリです。データはブラウザに保存され、完全無料でご利用いただけます。\n\n記事はZennにまとめましたので、見ていただけると嬉しいです。\n\nMyChara-Zukanはこちら👇️\n\nhttps://mychara-tool.pages.dev/\n\n詳細記事はこちらです👇️\n\nhttps://zenn.dev/hilava/articles/7746b3143d757a\n\n### スクリーンショット\n![スクリーンショット 2026-09-23 080826.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/467f73a0-e696-4f10-8e00-72728aa51b35.png)\n\n![スクリーンショット 2026-09-23 112604.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/4ee33523-4bb9-4416-89d8-7ecc035c8ea8.png)\n\n※ヘッダーは任意の画像を指定できます\n\n![スクリーンショット 2026-09-23 095218.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4506057/394ce57d-17a8-40e0-95c5-4bd953e04988.png)\n\n短いですが以上となります。\nよろしくお願いいたします。\n","coediting":false,"comments_count":0,"created_at":"2026-09-23T11:33:10+09:00","group":null,"id":"e22977de186af80a26af","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"vite","versions":[]},{"name":"Gemini","versions":[]},{"name":"個人開発","versions":[]},{"name":"Webアプリケーション","versions":[]}],"title":"【Vue+Vite+Gemini】マイキャラ管理ツールを開発した話","updated_at":"2026-09-23T11:33:10+09:00","url":"https://qiita.com/Hilava/items/e22977de186af80a26af","user":{"description":"しがない絵描き。","facebook_id":"","followees_count":1,"followers_count":0,"github_login_name":null,"id":"Hilava","items_count":1,"linkedin_id":"","location":"","name":"Hilava","organization":"","permanent_id":4506057,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4506057/2afd5859ab971217a7f024041d87c9a33ebe8b5a/large.png?1790129353","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:29\"\u003e\n\u003cspan id=\"こんなエラーが出た\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%82%93%E3%81%AA%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこんなエラーが出た\u003c/h1\u003e\n\u003cp data-sourcepos=\"2:1-3:45\"\u003eLaravel x Vue x Inertiaの開発環境において\u003cbr\u003e\n以下のようなエラーが出ました。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:33\"\u003esail npm run types:checkを実行\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-9:65\"\u003e(Laravel Sail（Docker）のコンテナ内で、\u003cbr\u003e\nTypeScriptの型チェックを実行するためのコマンド)\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"10:1-17:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\nresources/js/pages/Products/Index.vue:18:24 - error TS7006: Parameter \u003cspan class=\"s1\"\u003e'product'\u003c/span\u003e implicitly has an \u003cspan class=\"s1\"\u003e'any'\u003c/span\u003e type.\n\n18 const deleteProduct \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eproduct\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\nFound 1 error \u003cspan class=\"k\"\u003ein \u003c/span\u003eresources/js/pages/Products/Index.vue:18\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"19:1-22:64\"\u003e日本語訳すると\u003cbr\u003e\nパラメーター 'product' の型は暗黙的に 'any' になります。\u003cbr\u003e\nここがヒント（というか答え？）\u003cbr\u003e\nそしてそれは Index.vue の18行目にあるとのこと。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:69\"\u003eこの辺りを紐解きつつ、修正and学習していきます。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"26:1-26:8\"\u003e\n\u003cspan id=\"結論\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B5%90%E8%AB%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e結論\u003c/h1\u003e\n\u003cp data-sourcepos=\"27:1-28:12\"\u003eIndex.vueの script の型定義をちゃんとしていなかった\u003cbr\u003e\nのが原因\u003c/p\u003e\n\u003cp data-sourcepos=\"30:1-31:73\"\u003e引数に : number と書いたことで、「この関数には必ず数字（ID）を渡される」 と\u003cbr\u003e\nTypeScriptに教えることができたため、エラーが消えた。\u003c/p\u003e\n\u003cp data-sourcepos=\"33:1-34:78\"\u003e以下に仮説検証した記録を書いておきます。\u003cbr\u003e\nもし同様のエラーがあった際に参考になればと思います。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"36:1-36:8\"\u003e\n\u003cspan id=\"仮説\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BB%AE%E8%AA%AC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e仮説\u003c/h1\u003e\n\u003cp data-sourcepos=\"37:1-41:69\"\u003e１：TypeScriptの型定義がしっかりできていない\u003cbr\u003e\nIndex.vueを見に行けばそこが少なくともエラー\u003cbr\u003e\n箇所なのでそこを解消する必要がある\u003cbr\u003e\n２：deleteproductという削除処理に関するコードを書いた際に\u003cbr\u003e\n発生していたのでその関数周りに原因がある可能性\u003c/p\u003e\n\u003ch1 data-sourcepos=\"43:1-43:8\"\u003e\n\u003cspan id=\"検証\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A4%9C%E8%A8%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e検証\u003c/h1\u003e\n\u003cp data-sourcepos=\"45:1-46:36\"\u003e該当するであろう箇所です。\u003cbr\u003e\n上から確認していきます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"47:1-72:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e//index.vue\u003c/span\u003e\n\n\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"n\"\u003esetup\u003c/span\u003e \u003cspan class=\"n\"\u003elang\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"n\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nc\"\u003eHead\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nc\"\u003eLink\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erouter\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003efrom\u003c/span\u003e \u003cspan class=\"s1\"\u003e'@inertiajs/vue3'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eproducts\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eprice\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003estock\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003edescription\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// propsに型を適用する\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003eProps\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 削除処理とconfirmによる確認\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003edeleteProduct\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003econfirm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'本当に削除しますか？'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003erouter\u003c/span\u003e\u003cspan class=\"mf\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`/products/${product}`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"74:1-74:33\"\u003e\n\u003cspan id=\"interfaceは問題なさそう\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#interface%E3%81%AF%E5%95%8F%E9%A1%8C%E3%81%AA%E3%81%95%E3%81%9D%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003einterfaceは問題なさそう\u003c/h2\u003e\n\u003cp data-sourcepos=\"75:1-75:60\"\u003e型定義できてないとのことだったので念の為\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"76:1-86:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eproducts\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eprice\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003estock\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n        \u003cspan class=\"n\"\u003edescription\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"87:1-90:30\"\u003eこのinterface 〇〇は\u003cbr\u003e\n「このコンポーネントが受け取るデータの『説明書（設計図）』」を作っていてTypeScriptに対し\u003cbr\u003e\n「この画面は、必ず Props という名前のルールに沿ったデータを受け取りますよ」\u003cbr\u003e\nと宣言しているところ\u003c/p\u003e\n\u003cp data-sourcepos=\"92:1-92:72\"\u003eしっかり必要な要素は定義されているので大丈夫そう\u003c/p\u003e\n\u003ch2 data-sourcepos=\"94:1-94:56\"\u003e\n\u003cspan id=\"const-props--definepropsも大丈夫そう\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#const-props--defineprops%E3%82%82%E5%A4%A7%E4%B8%88%E5%A4%AB%E3%81%9D%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003econst Props = defineProps();も大丈夫そう\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"96:1-99:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// propsに型を適用する\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003eProps\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"100:1-102:39\"\u003eここは\u003cbr\u003e\n先ほど作った説明書（ルール）を実際にVueのシステムに適用し、データを使える状態にしていて、\u003cbr\u003e\nその定義も特に問題なさそう\u003c/p\u003e\n\u003cp data-sourcepos=\"104:1-104:65\"\u003e\u003ccode\u003einterface Props{略}\u003c/code\u003e を\u003ccode\u003e\u0026lt;Props\u0026gt;\u003c/code\u003eでちゃんと使っている\u003c/p\u003e\n\u003ch2 data-sourcepos=\"106:1-106:48\"\u003e\n\u003cspan id=\"削除処理の型定義がやはり怪しい\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8A%E9%99%A4%E5%87%A6%E7%90%86%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%81%8C%E3%82%84%E3%81%AF%E3%82%8A%E6%80%AA%E3%81%97%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e削除処理の型定義がやはり怪しい\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"107:1-114:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// 削除処理とconfirmによる確認\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003edeleteProduct\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproduct\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003econfirm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'本当に削除しますか？'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003erouter\u003c/span\u003e\u003cspan class=\"mf\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`/products/${product}`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"116:1-118:51\"\u003eTypeScriptエラーによると当該箇所は\u003cbr\u003e\nこの辺りでした。\u003cbr\u003e\n理解のためにめっちゃ噛み砕きます。\u003c/p\u003e\n\u003cul data-sourcepos=\"120:1-128:0\"\u003e\n\u003cli data-sourcepos=\"120:1-120:90\"\u003e\n\u003cp data-sourcepos=\"120:3-120:90\"\u003edeleteProductという「削除処理」をするための関数を定数 constで定義\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"121:1-125:0\"\u003e\n\u003cp data-sourcepos=\"121:3-124:41\"\u003eその際に引数は productという名前\u003cbr\u003e\n(関数が処理を行うために外部から受け取るデータを入れるための専用の変数)\u003cbr\u003e\nIDが５だったとして、外部からその５が入るための\u003cbr\u003e\n受け皿、箱　最悪 hogeでも動く\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"126:1-128:0\"\u003e\n\u003cp data-sourcepos=\"126:3-127:43\"\u003eifより下は恐らく関係なさそう\u003cbr\u003e\n（confirmの処理そのものだから）\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"129:1-129:74\"\u003e\n\u003cspan id=\"productに型定義すればいいのではこれで解決した\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#product%E3%81%AB%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8C%E3%81%B0%E3%81%84%E3%81%84%E3%81%AE%E3%81%A7%E3%81%AF%E3%81%93%E3%82%8C%E3%81%A7%E8%A7%A3%E6%B1%BA%E3%81%97%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eproductに型定義すればいいのでは？？(これで解決した)\u003c/h1\u003e\n\u003cp data-sourcepos=\"131:1-132:81\"\u003e調べたところ、 引数を入れる場所に型定義できることが判明。\u003cbr\u003e\n早速このように変えたところ、エラーが出なくなりました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"134:1-140:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003edeleteProduct\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproductId\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003econfirm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'本当に削除しますか？'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003erouter\u003c/span\u003e\u003cspan class=\"mf\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`/products/${productId}`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"141:1-142:79\"\u003e引数に : number と書いたことで、「この関数には必ず数字（ID）を渡される」 と\u003cbr\u003e\nTypeScriptに教えることができたため、エラーが消えました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"144:1-148:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ehogehogehogehoge@q112 vue-laravel-training % sail npm run types:check\nnpm notice run types:check\nnpm notice run vue-tsc --noEmit\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch1 data-sourcepos=\"152:1-152:59\"\u003e\n\u003cspan id=\"その他リファクタすれば良さそうな箇所\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%9D%E3%81%AE%E4%BB%96%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%81%99%E3%82%8C%E3%81%B0%E8%89%AF%E3%81%95%E3%81%9D%E3%81%86%E3%81%AA%E7%AE%87%E6%89%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eその他リファクタすれば良さそうな箇所　\u003c/h1\u003e\n\u003ch2 data-sourcepos=\"154:1-154:53\"\u003e\n\u003cspan id=\"propsの型定義の大文字小文字使い分け\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#props%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%81%AE%E5%A4%A7%E6%96%87%E5%AD%97%E5%B0%8F%E6%96%87%E5%AD%97%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003epropsの型定義の大文字小文字使い分け\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"156:1-159:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// propsに型を適用する\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003eProps\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"160:1-160:30\"\u003eここの部分について、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"161:1-163:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003eprops\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"164:1-164:63\"\u003eとした方がVueの規則的に良いとのことで修正。\u003c/p\u003e\n\u003cp data-sourcepos=\"166:1-166:51\"\u003e参考：vue公式サイト　スタイルガイド\u003c/p\u003e\n\u003cp data-sourcepos=\"168:1-168:33\"\u003e\u003ciframe id=\"qiita-embed-content__8f39bd3e98f4efba4016801b31d4bbfe\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__8f39bd3e98f4efba4016801b31d4bbfe\" data-content=\"https%3A%2F%2Fja.vuejs.org%2Fstyle-guide%2F\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003ctable data-sourcepos=\"170:1-174:187\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"170:1-170:76\"\u003e\n\u003cth data-sourcepos=\"170:2-170:9\"\u003e要素\u003c/th\u003e\n\u003cth data-sourcepos=\"170:11-170:33\"\u003e表記（ケース）\u003c/th\u003e\n\u003cth data-sourcepos=\"170:35-170:51\"\u003e役割の種類\u003c/th\u003e\n\u003cth data-sourcepos=\"170:53-170:75\"\u003e命名規則の理由\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"172:1-172:134\"\u003e\n\u003ctd data-sourcepos=\"172:2-172:10\"\u003e\u003ccode\u003eprops\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"172:12-172:43\"\u003ecamelCase (小文字始まり)\u003c/td\u003e\n\u003ctd data-sourcepos=\"172:45-172:70\"\u003eオブジェクト変数\u003c/td\u003e\n\u003ctd data-sourcepos=\"172:72-172:133\"\u003eスクリプト内でデータを読み出すための変数\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"173:1-173:157\"\u003e\n\u003ctd data-sourcepos=\"173:2-173:16\"\u003e\u003ccode\u003edefineProps\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"173:18-173:49\"\u003ecamelCase (小文字始まり)\u003c/td\u003e\n\u003ctd data-sourcepos=\"173:51-173:73\"\u003e関数（マクロ）\u003c/td\u003e\n\u003ctd data-sourcepos=\"173:75-173:156\"\u003eJS/TSにおける関数・メソッドの命名ルールに従っているため\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"174:1-174:187\"\u003e\n\u003ctd data-sourcepos=\"174:2-174:11\"\u003e\u003ccode\u003e\u0026lt;Props\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"174:13-174:45\"\u003ePascalCase (大文字始まり)\u003c/td\u003e\n\u003ctd data-sourcepos=\"174:47-174:73\"\u003e型（Type / Interface）\u003c/td\u003e\n\u003ctd data-sourcepos=\"174:75-174:186\"\u003eTSにおける「型やインターフェース」は必ず大文字で始める絶対ルールがあるため\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote data-sourcepos=\"176:1-178:51\"\u003e\n\u003cp data-sourcepos=\"176:2-178:51\"\u003e念の為補足\u003cbr\u003e\nconst は値を基本的に変えることのできない「定数」\u003cbr\u003e\nだがlet同様「変数」として区別される\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"181:1-183:48\"\u003eTypeScriptに触れて間もないのもあり、だいぶ苦戦しましたが、\u003cbr\u003e\nしっかりとエラーを読み解くことで解決につながることを学びました。\u003cbr\u003e\nエラーに対する苦手意識なくそう。\u003c/p\u003e\n","body":"# こんなエラーが出た\nLaravel x Vue x Inertiaの開発環境において\n以下のようなエラーが出ました。\n\n\nsail npm run types:checkを実行\n\n(Laravel Sail（Docker）のコンテナ内で、\nTypeScriptの型チェックを実行するためのコマンド)\n~~~bash\n\nresources/js/pages/Products/Index.vue:18:24 - error TS7006: Parameter 'product' implicitly has an 'any' type.\n\n18 const deleteProduct = (product) =\u003e {\n\nFound 1 error in resources/js/pages/Products/Index.vue:18\n~~~\n\n日本語訳すると\nパラメーター 'product' の型は暗黙的に 'any' になります。\nここがヒント（というか答え？）\nそしてそれは Index.vue の18行目にあるとのこと。\n\nこの辺りを紐解きつつ、修正and学習していきます。\n\n# 結論\nIndex.vueの script の型定義をちゃんとしていなかった\nのが原因\n\n引数に : number と書いたことで、「この関数には必ず数字（ID）を渡される」 と\nTypeScriptに教えることができたため、エラーが消えた。\n\n以下に仮説検証した記録を書いておきます。\nもし同様のエラーがあった際に参考になればと思います。\n\n# 仮説\n１：TypeScriptの型定義がしっかりできていない\nIndex.vueを見に行けばそこが少なくともエラー\n箇所なのでそこを解消する必要がある\n２：deleteproductという削除処理に関するコードを書いた際に\n発生していたのでその関数周りに原因がある可能性\n\n# 検証\n\n該当するであろう箇所です。\n上から確認していきます。\n~~~php\n//index.vue\n\n\u003cscript setup lang=\"ts\"\u003e\nimport { Head, Link, router } from '@inertiajs/vue3';\n\ninterface Props {\n    products: Array\u003c{\n        id: number;\n        name: string;\n        price: number;\n        stock: number;\n        description: string;\n    }\u003e;\n}\n// propsに型を適用する\nconst Props = defineProps\u003cProps\u003e();\n\n// 削除処理とconfirmによる確認\nconst deleteProduct = (product) =\u003e {\n    if (confirm('本当に削除しますか？')) {\n        router.delete(`/products/${product}`);\n    }\n};\n\n~~~\n\n## interfaceは問題なさそう\n型定義できてないとのことだったので念の為\n~~~php\ninterface Props {\n    products: Array\u003c{\n        id: number;\n        name: string;\n        price: number;\n        stock: number;\n        description: string;\n    }\u003e;\n}\n~~~\nこのinterface 〇〇は\n「このコンポーネントが受け取るデータの『説明書（設計図）』」を作っていてTypeScriptに対し\n「この画面は、必ず Props という名前のルールに沿ったデータを受け取りますよ」\nと宣言しているところ\n\nしっかり必要な要素は定義されているので大丈夫そう\n\n## const Props = defineProps\u003cProps\u003e();も大丈夫そう\n\n~~~php\n// propsに型を適用する\nconst Props = defineProps\u003cProps\u003e();\n~~~\nここは\n先ほど作った説明書（ルール）を実際にVueのシステムに適用し、データを使える状態にしていて、\nその定義も特に問題なさそう\n\n`interface Props{略}` を`\u003cProps\u003e`でちゃんと使っている\n\n## 削除処理の型定義がやはり怪しい\n~~~php\n// 削除処理とconfirmによる確認\nconst deleteProduct = (product) =\u003e {\n    if (confirm('本当に削除しますか？')) {\n        router.delete(`/products/${product}`);\n    }\n};\n~~~\n\nTypeScriptエラーによると当該箇所は\nこの辺りでした。\n理解のためにめっちゃ噛み砕きます。\n\n- deleteProductという「削除処理」をするための関数を定数 constで定義\n- その際に引数は productという名前\n(関数が処理を行うために外部から受け取るデータを入れるための専用の変数)\nIDが５だったとして、外部からその５が入るための\n受け皿、箱　最悪 hogeでも動く\n\n- ifより下は恐らく関係なさそう\n（confirmの処理そのものだから）\n\n# productに型定義すればいいのでは？？(これで解決した)\n\n調べたところ、 引数を入れる場所に型定義できることが判明。\n早速このように変えたところ、エラーが出なくなりました。\n\n~~~php\nconst deleteProduct = (productId: number) =\u003e {\n    if (confirm('本当に削除しますか？')) {\n        router.delete(`/products/${productId}`);\n    }\n};\n~~~\n引数に : number と書いたことで、「この関数には必ず数字（ID）を渡される」 と\nTypeScriptに教えることができたため、エラーが消えました。\n\n~~~\nhogehogehogehoge@q112 vue-laravel-training % sail npm run types:check\nnpm notice run types:check\nnpm notice run vue-tsc --noEmit\n~~~\n\n\n\n# その他リファクタすれば良さそうな箇所　\n\n## propsの型定義の大文字小文字使い分け\n\n~~~php\n// propsに型を適用する\nconst Props = defineProps\u003cProps\u003e();\n~~~\nここの部分について、\n~~~php\nconst props = defineProps\u003cProps\u003e();\n~~~\nとした方がVueの規則的に良いとのことで修正。\n\n参考：vue公式サイト　スタイルガイド\n\nhttps://ja.vuejs.org/style-guide/\n\n| 要素 | 表記（ケース） | 役割の種類 | 命名規則の理由 |\n|---|---|---|---|\n| `props` | camelCase (小文字始まり) | オブジェクト変数 | スクリプト内でデータを読み出すための変数 |\n| `defineProps` | camelCase (小文字始まり) | 関数（マクロ） | JS/TSにおける関数・メソッドの命名ルールに従っているため |\n| `\u003cProps\u003e`| PascalCase (大文字始まり) | 型（Type / Interface） | TSにおける「型やインターフェース」は必ず大文字で始める絶対ルールがあるため |\n\n\u003e念の為補足\n\u003econst は値を基本的に変えることのできない「定数」\nだがlet同様「変数」として区別される\n\n\nTypeScriptに触れて間もないのもあり、だいぶ苦戦しましたが、\nしっかりとエラーを読み解くことで解決につながることを学びました。\nエラーに対する苦手意識なくそう。\n","coediting":false,"comments_count":0,"created_at":"2026-09-22T09:27:27+09:00","group":null,"id":"091bbe93431699cb6b48","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"TypeScript","versions":[]},{"name":"Laravel","versions":[]},{"name":"Vue.js","versions":[]},{"name":"inertia","versions":[]}],"title":"TS7006エラーを解読して修正した話 Laravel x Vue x Inertia","updated_at":"2026-09-24T15:36:10+09:00","url":"https://qiita.com/metappi/items/091bbe93431699cb6b48","user":{"description":"プログラミングスクールRUNTEQ卒業後、\r\n受託企業に就職しました。\r\nPHP Laravel Vueをメインに学習中。\r\n楽しみながら学んでいきたいと考えております。\r\n夢は所さんになることです。\r\nまだまだエンジニアのタマゴなので間違い、ご意見等ございましたら\r\nぜひ頂戴したいと思っております。よろしくお願いいたします。","facebook_id":"","followees_count":21,"followers_count":32,"github_login_name":"Zundabyon","id":"metappi","items_count":79,"linkedin_id":"","location":"Japan","name":"(めたっぴ72期A) 小笠原","organization":"","permanent_id":4155281,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/profile-images/1789107820","team_only":false,"twitter_screen_name":"zundabyon","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:30\"\u003e\n\u003cspan id=\"この記事は後編です\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AF%E5%BE%8C%E7%B7%A8%E3%81%A7%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事は後編です\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-4:128\"\u003e\u003ca href=\"https://qiita.com/mukku928/items/2ddb6a8274c55ebfac62\" id=\"reference-9bee35fda88a7dd3de38\"\u003e前編\u003c/a\u003eでは、ホストに Node / Python / Java を一切入れず、Docker だけで SonarQube を立て、\u003cbr\u003e\nVue3 + Django REST Framework のプロジェクトを解析してカバレッジを取り込むところまでやりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:36\"\u003e前編の到達点はここです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"8:1-11:43\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"8:1-8:37\"\u003e\n\u003cth data-sourcepos=\"8:2-8:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"8:4-8:18\"\u003ebackend (DRF)\u003c/th\u003e\n\u003cth data-sourcepos=\"8:20-8:36\"\u003efrontend (Vue3)\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"10:1-10:28\"\u003e\n\u003ctd data-sourcepos=\"10:2-10:11\"\u003eCoverage\u003c/td\u003e\n\u003ctd data-sourcepos=\"10:13-10:19\"\u003e47.7%\u003c/td\u003e\n\u003ctd data-sourcepos=\"10:21-10:27\"\u003e52.0%\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"11:1-11:43\"\u003e\n\u003ctd data-sourcepos=\"11:2-11:24\"\u003e検出された指摘\u003c/td\u003e\n\u003ctd data-sourcepos=\"11:26-11:33\"\u003e14 件\u003c/td\u003e\n\u003ctd data-sourcepos=\"11:35-11:42\"\u003e26 件\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"13:1-14:48\"\u003e\u003cstrong\u003eところが、この状態で Quality Gate を見ると「Passed」と表示されます。\u003c/strong\u003e\u003cbr\u003e\nこれだけ指摘が出ているのにです。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-17:87\"\u003e後編はその正体から入り、\u003cstrong\u003e指摘をどう読み、どう運用するか\u003c/strong\u003eを扱います。\u003cbr\u003e\n数値・ログ・エンドポイントはすべて実際に動かした結果です。\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:90\"\u003e検証環境: SonarQube Community Build \u003cstrong\u003e26.9.0.129388\u003c/strong\u003e / Docker 28.2.2 / macOS (x86_64)\u003c/p\u003e\n\u003ch2 data-sourcepos=\"21:1-21:24\"\u003e\n\u003cspan id=\"この記事の構成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E6%A7%8B%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事の構成\u003c/h2\u003e\n\u003ctable data-sourcepos=\"23:1-31:79\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"23:1-23:16\"\u003e\n\u003cth data-sourcepos=\"23:2-23:6\"\u003e章\u003c/th\u003e\n\u003cth data-sourcepos=\"23:8-23:15\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"25:1-25:62\"\u003e\n\u003ctd data-sourcepos=\"25:2-25:4\"\u003e5\u003c/td\u003e\n\u003ctd data-sourcepos=\"25:6-25:61\"\u003e\n\u003cstrong\u003e検出結果の一覧\u003c/strong\u003e — 実際に何が出たか\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"26:1-26:117\"\u003e\n\u003ctd data-sourcepos=\"26:2-26:4\"\u003e6\u003c/td\u003e\n\u003ctd data-sourcepos=\"26:6-26:116\"\u003e\n\u003cstrong\u003e出なかった指摘の原因\u003c/strong\u003e — 3 パターンの切り分けと Quality Profile のカスタマイズ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"27:1-27:81\"\u003e\n\u003ctd data-sourcepos=\"27:2-27:4\"\u003e7\u003c/td\u003e\n\u003ctd data-sourcepos=\"27:6-27:80\"\u003e\n\u003cstrong\u003eQuality Gate が初回素通りする正体\u003c/strong\u003e — New Code とは何か\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"28:1-28:92\"\u003e\n\u003ctd data-sourcepos=\"28:2-28:4\"\u003e8\u003c/td\u003e\n\u003ctd data-sourcepos=\"28:6-28:91\"\u003e\n\u003cstrong\u003eFail → 修正 → Pass の実演\u003c/strong\u003e — 誤検知（False Positive）の扱いも\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"29:1-29:33\"\u003e\n\u003ctd data-sourcepos=\"29:2-29:4\"\u003e9\u003c/td\u003e\n\u003ctd data-sourcepos=\"29:6-29:32\"\u003e\u003cstrong\u003eGitHub Actions 連携\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"30:1-30:90\"\u003e\n\u003ctd data-sourcepos=\"30:2-30:5\"\u003e10\u003c/td\u003e\n\u003ctd data-sourcepos=\"30:7-30:89\"\u003e\n\u003cstrong\u003e静的解析の深さ\u003c/strong\u003e — どこまで見るのか、脆弱性検出の限界\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"31:1-31:79\"\u003e\n\u003ctd data-sourcepos=\"31:2-31:5\"\u003e11\u003c/td\u003e\n\u003ctd data-sourcepos=\"31:7-31:78\"\u003e\n\u003cstrong\u003eCommunity Build の線引き\u003c/strong\u003e — 有償版が必要になる境界\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"33:1-34:94\"\u003e\u003ccode\u003eS106\u003c/code\u003e のような個別ルールの解説は、分量が多いので\u003cbr\u003e\n**別記事「SonarQube ルール早見表（Vue3 + DRF 編）」**にまとめています。\u003c/p\u003e\n\u003chr data-sourcepos=\"36:1-37:0\"\u003e\n\u003ch2 data-sourcepos=\"38:1-38:30\"\u003e\n\u003cspan id=\"5-何が検出されたか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E4%BD%95%E3%81%8C%E6%A4%9C%E5%87%BA%E3%81%95%E3%82%8C%E3%81%9F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 何が検出されたか\u003c/h2\u003e\n\u003cp data-sourcepos=\"40:1-40:112\"\u003e検証アプリには典型的な問題コードを仕込みました。修正前の backend で 14 件です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"42:1-56:74\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"42:1-42:31\"\u003e\n\u003cth data-sourcepos=\"42:2-42:12\"\u003eルール\u003c/th\u003e\n\u003cth data-sourcepos=\"42:14-42:21\"\u003e種別\u003c/th\u003e\n\u003cth data-sourcepos=\"42:23-42:30\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"44:1-44:82\"\u003e\n\u003ctd data-sourcepos=\"44:2-44:18\"\u003e\u003ccode\u003esecrets:S6687\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"44:20-44:44\"\u003eVulnerability (BLOCKER)\u003c/td\u003e\n\u003ctd data-sourcepos=\"44:46-44:81\"\u003e\n\u003ccode\u003eSECRET_KEY\u003c/code\u003e のハードコード\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"45:1-45:51\"\u003e\n\u003ctd data-sourcepos=\"45:2-45:17\"\u003e\u003ccode\u003epython:S4507\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"45:19-45:33\"\u003eVulnerability\u003c/td\u003e\n\u003ctd data-sourcepos=\"45:35-45:50\"\u003e\u003ccode\u003eDEBUG = True\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"46:1-46:120\"\u003e\n\u003ctd data-sourcepos=\"46:2-46:21\"\u003e\n\u003ccode\u003epython:S4502\u003c/code\u003e ×2\u003c/td\u003e\n\u003ctd data-sourcepos=\"46:23-46:48\"\u003eVulnerability (CRITICAL)\u003c/td\u003e\n\u003ctd data-sourcepos=\"46:50-46:119\"\u003eCSRF 保護の無効化（\u003ccode\u003e@csrf_exempt\u003c/code\u003e と middleware 未設定）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"47:1-47:73\"\u003e\n\u003ctd data-sourcepos=\"47:2-47:17\"\u003e\u003ccode\u003epython:S4790\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"47:19-47:44\"\u003eVulnerability (CRITICAL)\u003c/td\u003e\n\u003ctd data-sourcepos=\"47:46-47:72\"\u003eMD5 でのハッシュ化\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"48:1-48:82\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:17\"\u003e\u003ccode\u003epython:S2245\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:19-48:33\"\u003eVulnerability\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:35-48:81\"\u003e\n\u003ccode\u003erandom\u003c/code\u003e をセキュリティ用途に使用\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"49:1-49:90\"\u003e\n\u003ctd data-sourcepos=\"49:2-49:17\"\u003e\u003ccode\u003epython:S4830\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:19-49:44\"\u003eVulnerability (CRITICAL)\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:46-49:89\"\u003e\n\u003ccode\u003everify=False\u003c/code\u003e（TLS 検証の無効化）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:66\"\u003e\n\u003ctd data-sourcepos=\"50:2-50:17\"\u003e\u003ccode\u003epython:S3752\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:19-50:33\"\u003eVulnerability\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:35-50:65\"\u003eHTTP メソッドの未指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:63\"\u003e\n\u003ctd data-sourcepos=\"51:2-51:17\"\u003e\u003ccode\u003epython:S1763\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:19-51:23\"\u003eBug\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:25-51:62\"\u003e\n\u003ccode\u003ereturn\u003c/code\u003e 後の到達不能コード\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"52:1-52:74\"\u003e\n\u003ctd data-sourcepos=\"52:2-52:17\"\u003e\u003ccode\u003epython:S1862\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:19-52:23\"\u003eBug\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:25-52:73\"\u003e同じ条件の \u003ccode\u003eelif\u003c/code\u003e で到達しない分岐\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"53:1-53:72\"\u003e\n\u003ctd data-sourcepos=\"53:2-53:17\"\u003e\u003ccode\u003epython:S1192\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"53:19-53:30\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"53:32-53:71\"\u003e3 回重複した文字列リテラル\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"54:1-54:53\"\u003e\n\u003ctd data-sourcepos=\"54:2-54:17\"\u003e\u003ccode\u003epython:S1135\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"54:19-54:30\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"54:32-54:52\"\u003e\n\u003ccode\u003eTODO\u003c/code\u003e コメント\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"55:1-55:64\"\u003e\n\u003ctd data-sourcepos=\"55:2-55:17\"\u003e\u003ccode\u003epython:S1481\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"55:19-55:30\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"55:32-55:63\"\u003e未使用のローカル変数\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"56:1-56:74\"\u003e\n\u003ctd data-sourcepos=\"56:2-56:17\"\u003e\u003ccode\u003epython:S3776\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:19-56:30\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:32-56:73\"\u003e認知的複雑度 \u003cstrong\u003e28\u003c/strong\u003e（上限 15）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"58:1-58:63\"\u003efrontend は 26 件。Vue3 特有のものを抜粋します。\u003c/p\u003e\n\u003ctable data-sourcepos=\"60:1-71:81\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"60:1-60:31\"\u003e\n\u003cth data-sourcepos=\"60:2-60:12\"\u003eルール\u003c/th\u003e\n\u003cth data-sourcepos=\"60:14-60:21\"\u003e種別\u003c/th\u003e\n\u003cth data-sourcepos=\"60:23-60:30\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"62:1-62:64\"\u003e\n\u003ctd data-sourcepos=\"62:2-62:21\"\u003e\u003ccode\u003etypescript:S1656\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"62:23-62:27\"\u003eBug\u003c/td\u003e\n\u003ctd data-sourcepos=\"62:29-62:63\"\u003e自己代入（\u003ccode\u003etitle = title\u003c/code\u003e）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"63:1-63:56\"\u003e\n\u003ctd data-sourcepos=\"63:2-63:21\"\u003e\u003ccode\u003etypescript:S1862\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"63:23-63:27\"\u003eBug\u003c/td\u003e\n\u003ctd data-sourcepos=\"63:29-63:55\"\u003e同じ条件の \u003ccode\u003eelse if\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"64:1-64:100\"\u003e\n\u003ctd data-sourcepos=\"64:2-64:31\"\u003e\u003ccode\u003eWeb:InputWithoutLabelCheck\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"64:33-64:37\"\u003eBug\u003c/td\u003e\n\u003ctd data-sourcepos=\"64:39-64:99\"\u003e\n\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003e に label が無い（アクセシビリティ）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"65:1-65:56\"\u003e\n\u003ctd data-sourcepos=\"65:2-65:21\"\u003e\u003ccode\u003etypescript:S2245\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"65:23-65:37\"\u003eVulnerability\u003c/td\u003e\n\u003ctd data-sourcepos=\"65:39-65:55\"\u003e\u003ccode\u003eMath.random()\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"66:1-66:87\"\u003e\n\u003ctd data-sourcepos=\"66:2-66:25\"\u003e\n\u003ccode\u003etypescript:S4144\u003c/code\u003e ×2\u003c/td\u003e\n\u003ctd data-sourcepos=\"66:27-66:38\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"66:40-66:86\"\u003e実装が同一の関数（コピペ検出）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"67:1-67:59\"\u003e\n\u003ctd data-sourcepos=\"67:2-67:21\"\u003e\u003ccode\u003etypescript:S1854\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"67:23-67:34\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"67:36-67:58\"\u003e使われない代入\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"68:1-68:52\"\u003e\n\u003ctd data-sourcepos=\"68:2-68:21\"\u003e\u003ccode\u003etypescript:S2486\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"68:23-68:34\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"68:36-68:51\"\u003e空の \u003ccode\u003ecatch\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"69:1-69:88\"\u003e\n\u003ctd data-sourcepos=\"69:2-69:21\"\u003e\u003ccode\u003etypescript:S2699\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"69:23-69:44\"\u003eCode Smell (BLOCKER)\u003c/td\u003e\n\u003ctd data-sourcepos=\"69:46-69:87\"\u003e\u003cstrong\u003eアサーションが無いテスト\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"70:1-70:59\"\u003e\n\u003ctd data-sourcepos=\"70:2-70:21\"\u003e\u003ccode\u003etypescript:S3776\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"70:23-70:34\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"70:36-70:58\"\u003e認知的複雑度 22\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"71:1-71:81\"\u003e\n\u003ctd data-sourcepos=\"71:2-71:21\"\u003e\u003ccode\u003etypescript:S7781\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"71:23-71:34\"\u003eCode Smell\u003c/td\u003e\n\u003ctd data-sourcepos=\"71:36-71:80\"\u003e\n\u003ccode\u003ereplace()\u003c/code\u003e より \u003ccode\u003ereplaceAll()\u003c/code\u003e を使う\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"73:1-74:66\"\u003e\u003ccode\u003eS2699\u003c/code\u003e（アサーション無しテスト）が BLOCKER で出るのは実用的だと感じました。\u003cbr\u003e\n「通っているつもりのテスト」を検出できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"76:1-76:39\"\u003e\n\u003cspan id=\"security-hotspot-は-0-件だった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#security-hotspot-%E3%81%AF-0-%E4%BB%B6%E3%81%A0%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSecurity Hotspot は 0 件だった\u003c/h3\u003e\n\u003cp data-sourcepos=\"78:1-81:66\"\u003e意外だったのですが、\u003ccode\u003esecurity_hotspots = 0\u003c/code\u003e で、\u003ccode\u003e/api/hotspots/search\u003c/code\u003e も空でした。\u003cbr\u003e\n従来 Security Hotspot に分類されていたルール（MD5、擬似乱数、TLS 検証無効化など。\u003cbr\u003e\nメッセージが \"Make sure ... is safe here\" になっているもの）が、\u003cbr\u003e\n\u003cstrong\u003e26.9 では Vulnerability として集計されています\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"83:1-84:93\"\u003e先行記事にある「Overview に Security Hotspots が出る」という説明とは\u003cbr\u003e\n挙動が変わっているので、バージョンによって読み替えが必要です。\u003c/p\u003e\n\u003chr data-sourcepos=\"86:1-87:0\"\u003e\n\u003ch2 data-sourcepos=\"88:1-88:73\"\u003e\n\u003cspan id=\"6-つまずき-仕込んだのに検出されない指摘がある\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D-%E4%BB%95%E8%BE%BC%E3%82%93%E3%81%A0%E3%81%AE%E3%81%AB%E6%A4%9C%E5%87%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E6%8C%87%E6%91%98%E3%81%8C%E3%81%82%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. つまずき④ 仕込んだのに検出されない指摘がある\u003c/h2\u003e\n\u003cp data-sourcepos=\"90:1-90:100\"\u003eここが一番の発見でした。\u003cstrong\u003e仕込んだのに出ないものが複数ありました。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"92:1-92:108\"\u003e原因は 3 種類に分かれます。\u003ccode\u003e/api/rules/show\u003c/code\u003e と \u003ccode\u003e/api/rules/search\u003c/code\u003e で切り分けました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"94:1-104:106\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"94:1-94:40\"\u003e\n\u003cth data-sourcepos=\"94:2-94:21\"\u003e仕込んだもの\u003c/th\u003e\n\u003cth data-sourcepos=\"94:23-94:30\"\u003e結果\u003c/th\u003e\n\u003cth data-sourcepos=\"94:32-94:39\"\u003e原因\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"96:1-96:88\"\u003e\n\u003ctd data-sourcepos=\"96:2-96:26\"\u003e\u003cstrong\u003e\u003ccode\u003ev-html\u003c/code\u003e での XSS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"96:28-96:47\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"96:49-96:87\"\u003e\u003cstrong\u003eルール自体が存在しない\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"97:1-97:84\"\u003e\n\u003ctd data-sourcepos=\"97:2-97:26\"\u003e\u003ccode\u003eALLOWED_HOSTS = [\"*\"]\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"97:28-97:47\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"97:49-97:83\"\u003eルール自体が存在しない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"98:1-98:95\"\u003e\n\u003ctd data-sourcepos=\"98:2-98:30\"\u003e\n\u003ccode\u003eeval()\u003c/code\u003e での動的実行\u003c/td\u003e\n\u003ctd data-sourcepos=\"98:32-98:51\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"98:53-98:94\"\u003ePython 用のルールが存在しない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"99:1-99:105\"\u003e\n\u003ctd data-sourcepos=\"99:2-99:26\"\u003e\n\u003ccode\u003econsole.log\u003c/code\u003e の残留\u003c/td\u003e\n\u003ctd data-sourcepos=\"99:28-99:47\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"99:49-99:104\"\u003e\n\u003ccode\u003etypescript:S106\u003c/code\u003e はあるが \u003cstrong\u003eSonar way で無効\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"100:1-100:76\"\u003e\n\u003ctd data-sourcepos=\"100:2-100:23\"\u003e\n\u003ccode\u003e==\u003c/code\u003e による比較\u003c/td\u003e\n\u003ctd data-sourcepos=\"100:25-100:44\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"100:46-100:75\"\u003e\n\u003ccode\u003etypescript:S1440\u003c/code\u003e が無効\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"101:1-101:71\"\u003e\n\u003ctd data-sourcepos=\"101:2-101:18\"\u003e\n\u003ccode\u003eany\u003c/code\u003e の使用\u003c/td\u003e\n\u003ctd data-sourcepos=\"101:20-101:39\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"101:41-101:70\"\u003e\n\u003ccode\u003etypescript:S4204\u003c/code\u003e が無効\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"102:1-102:85\"\u003e\n\u003ctd data-sourcepos=\"102:2-102:36\"\u003e未使用の \u003ccode\u003eimport\u003c/code\u003e（Python）\u003c/td\u003e\n\u003ctd data-sourcepos=\"102:38-102:57\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"102:59-102:84\"\u003e\n\u003ccode\u003epython:S1128\u003c/code\u003e が無効\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"103:1-103:123\"\u003e\n\u003ctd data-sourcepos=\"103:2-103:40\"\u003eAPI トークンのハードコード\u003c/td\u003e\n\u003ctd data-sourcepos=\"103:42-103:61\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"103:63-103:122\"\u003e\n\u003ccode\u003etypescript:S2068\u003c/code\u003e は有効だが変数名が合致せず\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"104:1-104:106\"\u003e\n\u003ctd data-sourcepos=\"104:2-104:34\"\u003eSQL インジェクション等\u003c/td\u003e\n\u003ctd data-sourcepos=\"104:36-104:55\"\u003e検出されない\u003c/td\u003e\n\u003ctd data-sourcepos=\"104:57-104:105\"\u003etaint analysis は \u003cstrong\u003eDeveloper Edition 以上\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"106:1-106:30\"\u003e確認方法はこうです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"108:1-115:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# ルールが存在するか・有効かを調べる\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/rules/show?key=typescript:S106\u0026amp;actives=true\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# → rule は返るが actives が空 = 存在するが無効\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# ルールキーが分からないときはキーワードで検索する\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/rules/search?q=console\u0026amp;languages=js,ts\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"117:1-117:37\"\u003e\n\u003cspan id=\"ここから導ける運用方針\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%93%E3%81%8B%E3%82%89%E5%B0%8E%E3%81%91%E3%82%8B%E9%81%8B%E7%94%A8%E6%96%B9%E9%87%9D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eここから導ける運用方針\u003c/h3\u003e\n\u003cp data-sourcepos=\"119:1-119:83\"\u003e\u003cstrong\u003eSonarQube だけで Vue3 / DRF の品質を担保しようとしないこと。\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"121:1-124:0\"\u003e\n\u003cli data-sourcepos=\"121:1-121:100\"\u003e\n\u003ccode\u003ev-html\u003c/code\u003e の XSS は SonarQube では拾えないので、\u003cstrong\u003eESLint の \u003ccode\u003evue/no-v-html\u003c/code\u003e\u003c/strong\u003e が必要\u003c/li\u003e\n\u003cli data-sourcepos=\"122:1-122:88\"\u003e\n\u003ccode\u003econsole.log\u003c/code\u003e、\u003ccode\u003e==\u003c/code\u003e、\u003ccode\u003eany\u003c/code\u003e は ESLint / TypeScript の設定で止めるのが素直\u003c/li\u003e\n\u003cli data-sourcepos=\"123:1-124:0\"\u003ePython の未使用 import は \u003cstrong\u003eruff / flake8\u003c/strong\u003e の担当\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"125:1-127:102\"\u003eSonarQube の強みは、\u003cstrong\u003e認知的複雑度・重複・カバレッジ・セキュリティ設定の指摘を\u003cbr\u003e\nダッシュボードで時系列に追えること\u003c/strong\u003eでした。リンターの置き換えではなく、\u003cbr\u003e\nリンターが見ないレイヤを埋めるツールと考えるのが実態に合っています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"129:1-129:58\"\u003e\n\u003cspan id=\"無効になっているルールは有効化できる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AF%E6%9C%89%E5%8A%B9%E5%8C%96%E3%81%A7%E3%81%8D%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e無効になっているルールは有効化できる\u003c/h3\u003e\n\u003cp data-sourcepos=\"131:1-132:33\"\u003eパターン B（存在するが無効）は、\u003cstrong\u003eQuality Profile をカスタマイズすれば検出できます。\u003c/strong\u003e\u003cbr\u003e\n実際にやってみました。\u003c/p\u003e\n\u003col data-sourcepos=\"134:1-145:0\"\u003e\n\u003cli data-sourcepos=\"134:1-137:0\"\u003e\n\u003cp data-sourcepos=\"134:4-136:167\"\u003e\u003ccode\u003eQuality Profiles\u003c/code\u003e → \u003cstrong\u003eTypeScript\u003c/strong\u003e の \u003ccode\u003eSonar way\u003c/code\u003e を \u003cstrong\u003e\u003ccode\u003eCopy\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\n（\u003ccode\u003eSonar way\u003c/code\u003e は \u003ccode\u003eBuilt-in\u003c/code\u003e なので\u003cstrong\u003e直接編集できません\u003c/strong\u003e。コピーが必須）\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb2e333e0-ca6e-4bb1-9f3f-47cdb14a0c7e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=172b69c385c2626565f258681e87818f\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb2e333e0-ca6e-4bb1-9f3f-47cdb14a0c7e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=172b69c385c2626565f258681e87818f\" alt=\"スクリーンショット 2026-09-21 23.00.50.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb2e333e0-ca6e-4bb1-9f3f-47cdb14a0c7e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0d47d96a82e47dfd999ec8ec424ab12c 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/b2e333e0-ca6e-4bb1-9f3f-47cdb14a0c7e.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"138:1-140:0\"\u003e\n\u003cp data-sourcepos=\"138:4-139:168\"\u003e作ったプロファイルで \u003ccode\u003eActivate More Rules\u003c/code\u003e → \u003ccode\u003eS106\u003c/code\u003e と \u003ccode\u003eS4204\u003c/code\u003e を \u003ccode\u003eActivate\u003c/code\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe0bebd04-aad4-44c3-915e-f1fa5cabade1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=cb9f0723ccab236541b7bccd896fc27e\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe0bebd04-aad4-44c3-915e-f1fa5cabade1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=cb9f0723ccab236541b7bccd896fc27e\" alt=\"スクリーンショット 2026-09-21 21.04.20.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe0bebd04-aad4-44c3-915e-f1fa5cabade1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0864c88159dc9507858899420c8a808b 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e0bebd04-aad4-44c3-915e-f1fa5cabade1.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"141:1-143:0\"\u003e\n\u003cp data-sourcepos=\"141:4-142:165\"\u003e\u003ccode\u003eProjects\u003c/code\u003e タブで対象プロジェクトに適用\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Ff61531e5-71ed-4e8b-8028-355c76c084fe.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0d43874939a94da95eb432c7f7d88b8e\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Ff61531e5-71ed-4e8b-8028-355c76c084fe.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0d43874939a94da95eb432c7f7d88b8e\" alt=\"スクリーンショット 2026-09-21 21.07.34.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Ff61531e5-71ed-4e8b-8028-355c76c084fe.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=29dede7cef36b435bf32df7eecd69476 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/f61531e5-71ed-4e8b-8028-355c76c084fe.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"144:1-145:0\"\u003e\n\u003cp data-sourcepos=\"144:4-144:125\"\u003e\u003cstrong\u003e再解析\u003c/strong\u003e（Quality Gate と同じで、プロファイルを変えても再解析しないと反映されません）\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"146:1-146:61\"\u003e結果、\u003cstrong\u003e指摘が 26 件 → 30 件に増えました。\u003c/strong\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"148:1-151:86\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"148:1-148:55\"\u003e\n\u003cth data-sourcepos=\"148:2-148:12\"\u003eルール\u003c/th\u003e\n\u003cth data-sourcepos=\"148:14-148:30\"\u003e増えた件数\u003c/th\u003e\n\u003cth data-sourcepos=\"148:32-148:54\"\u003e検出された場所\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"150:1-150:97\"\u003e\n\u003ctd data-sourcepos=\"150:2-150:42\"\u003e\n\u003ccode\u003etypescript:S4204\u003c/code\u003e（\u003ccode\u003eany\u003c/code\u003e の使用）\u003c/td\u003e\n\u003ctd data-sourcepos=\"150:44-150:50\"\u003e3 件\u003c/td\u003e\n\u003ctd data-sourcepos=\"150:52-150:96\"\u003e\n\u003ccode\u003ecomposables/useTodos.ts\u003c/code\u003e、\u003ccode\u003eapi/client.ts\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"151:1-151:86\"\u003e\n\u003ctd data-sourcepos=\"151:2-151:49\"\u003e\n\u003ccode\u003etypescript:S106\u003c/code\u003e（\u003ccode\u003econsole.log\u003c/code\u003e の残留）\u003c/td\u003e\n\u003ctd data-sourcepos=\"151:51-151:57\"\u003e1 件\u003c/td\u003e\n\u003ctd data-sourcepos=\"151:59-151:85\"\u003e\u003ccode\u003ecomposables/useTodos.ts\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"153:1-154:103\"\u003e\u003cstrong\u003e「デフォルトが緩いなら締められる」\u003c/strong\u003e ということです。ただし ESLint で止めれば済む内容も\u003cbr\u003e\n多いので、\u003cstrong\u003eどちらの層で止めるかをチームで決めておく\u003c/strong\u003eのが現実的です。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"156:1-180:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"157:1-157:43\"\u003e\n\u003cspan id=\"有効化するときの注意点-2-つ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%89%E5%8A%B9%E5%8C%96%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9-2-%E3%81%A4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e有効化するときの注意点 2 つ\u003c/h3\u003e\n\u003cp data-sourcepos=\"159:1-159:65\"\u003e\u003cstrong\u003e① 同じルールが言語ごとに別キーで存在する\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"161:1-164:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e[無効] javascript:S106   Standard outputs should not be used...\n[無効] typescript:S106   Standard outputs should not be used...\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"166:1-167:100\"\u003e\u003cstrong\u003eプロファイルは言語単位\u003c/strong\u003eです。\u003ccode\u003e.js\u003c/code\u003e と \u003ccode\u003e.ts\u003c/code\u003e が混在するプロジェクトでは、\u003cbr\u003e\n\u003cstrong\u003e両方の言語のプロファイルで有効にしないと片方だけ検出されません。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"169:1-169:77\"\u003e\u003cstrong\u003e② 同じ問題でも言語によって検出できるルールが違う\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"171:1-171:96\"\u003e「空の例外ハンドラ」を両方に仕込んだところ、結果が非対称でした。\u003c/p\u003e\n\u003ctable data-sourcepos=\"173:1-176:83\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"173:1-173:36\"\u003e\n\u003cth data-sourcepos=\"173:2-173:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"173:4-173:26\"\u003e仕込んだコード\u003c/th\u003e\n\u003cth data-sourcepos=\"173:28-173:35\"\u003e結果\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"175:1-175:69\"\u003e\n\u003ctd data-sourcepos=\"175:2-175:11\"\u003efrontend\u003c/td\u003e\n\u003ctd data-sourcepos=\"175:13-175:33\"\u003e\u003ccode\u003ecatch (error) { }\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"175:35-175:68\"\u003e✅ \u003ccode\u003etypescript:S2486\u003c/code\u003e で検出\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"176:1-176:83\"\u003e\n\u003ctd data-sourcepos=\"176:2-176:10\"\u003ebackend\u003c/td\u003e\n\u003ctd data-sourcepos=\"176:12-176:37\"\u003e\u003ccode\u003eexcept Exception: pass\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"176:39-176:82\"\u003e❌ 該当ルールが Sonar way に無い\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"178:1-179:102\"\u003e\u003cstrong\u003e「Python で出たから TypeScript でも出る」とは限りません。\u003c/strong\u003e 言語ごとにルールセットの\u003cbr\u003e\n成熟度が違うので、両方のプロファイルを個別に確認する必要があります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"181:1-182:0\"\u003e\n\u003ch2 data-sourcepos=\"183:1-183:81\"\u003e\n\u003cspan id=\"7-つまずき-初回スキャンで-quality-gate-が必ず素通りする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D-%E5%88%9D%E5%9B%9E%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%A7-quality-gate-%E3%81%8C%E5%BF%85%E3%81%9A%E7%B4%A0%E9%80%9A%E3%82%8A%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. つまずき⑤ 初回スキャンで Quality Gate が必ず素通りする\u003c/h2\u003e\n\u003cp data-sourcepos=\"185:1-185:105\"\u003e初回スキャン直後、あれだけ指摘が出ているのに \u003cstrong\u003eQuality Gate は \u003ccode\u003eOK\u003c/code\u003e\u003c/strong\u003e でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"187:1-187:112\"\u003e理由は条件定義を見れば分かります。デフォルトの \u003ccode\u003eSonar way\u003c/code\u003e はこうなっています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"189:1-194:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enew_violations                  GT 0\nnew_coverage                    LT 80\nnew_duplicated_lines_density    GT 3\nnew_security_hotspots_reviewed  LT 100\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"196:1-198:89\"\u003e\u003cstrong\u003e4 つ全部が \u003ccode\u003enew_*\u003c/code\u003e（New Code 側）です。\u003c/strong\u003e\u003cbr\u003e\n初回解析には比較対象の New Code が存在しないため、判定条件が 0 件になり、\u003cbr\u003e\nそのまま合格になります。実際 API で見ると conditions が空でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"200:1-202:136\"\u003eこれは設計思想で、\u003cstrong\u003eClean as You Code\u003c/strong\u003e（既存の負債は一旦置き、\u003cbr\u003e\nこれから書くコードをきれいに保つ）という考え方です。新規プロジェクトには合いますが、\u003cbr\u003e\n\u003cstrong\u003e既存プロジェクトに後から入れて「今の品質」を可視化したい場合は条件を足す必要があります。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv data-sourcepos=\"204:1-211:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"205:1-205:13\"\u003e\n\u003cspan id=\"解決策\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E6%B1%BA%E7%AD%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解決策\u003c/h3\u003e\n\u003cp data-sourcepos=\"206:1-207:164\"\u003e新しいQuality Gateを作成して、\u003cstrong\u003eConditions on Overall Code\u003c/strong\u003e に条件を設定した上でプロジェクトを紐づける\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F128ac6b6-4603-4619-a327-31db117917e9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a2d5d0eabd4be1b3e5ec67e32dc26fdf\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F128ac6b6-4603-4619-a327-31db117917e9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a2d5d0eabd4be1b3e5ec67e32dc26fdf\" alt=\"スクリーンショット 2026-09-21 22.24.02.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F128ac6b6-4603-4619-a327-31db117917e9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=2f9d9ff8ede13bbc6a433837e565721e 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/128ac6b6-4603-4619-a327-31db117917e9.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"209:1-210:164\"\u003e再度スキャンを行う\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fc1a66556-56ca-4a80-a3b4-bb83a346e95c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=566fbeb4b9ae2758c68d86974d35ffa5\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fc1a66556-56ca-4a80-a3b4-bb83a346e95c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=566fbeb4b9ae2758c68d86974d35ffa5\" alt=\"スクリーンショット 2026-09-21 20.41.50.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fc1a66556-56ca-4a80-a3b4-bb83a346e95c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=6cc99af91733285ed485a255b5e2e559 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/c1a66556-56ca-4a80-a3b4-bb83a346e95c.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"213:1-294:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"214:1-214:65\"\u003e\n\u003cspan id=\"何回スキャンしても-new-code-が空のままだった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%95%E5%9B%9E%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%97%E3%81%A6%E3%82%82-new-code-%E3%81%8C%E7%A9%BA%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%A0%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e何回スキャンしても New Code が空のままだった\u003c/h3\u003e\n\u003cp data-sourcepos=\"215:1-215:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb561fcff-f9c3-4e92-8790-b41d8ebe0958.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b0859a8a2d97c5551ad058b90bdf7b49\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb561fcff-f9c3-4e92-8790-b41d8ebe0958.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b0859a8a2d97c5551ad058b90bdf7b49\" alt=\"スクリーンショット 2026-09-21 22.28.15.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fb561fcff-f9c3-4e92-8790-b41d8ebe0958.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=72146422d09b9aec27e82b08d68c7ab1 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/b561fcff-f9c3-4e92-8790-b41d8ebe0958.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"218:1-219:94\"\u003eここで当然の疑問が出ます。\u003cstrong\u003e「2 回目・3 回目のスキャンなら、前回との差分が New Code に\u003cbr\u003e\nなるのでは？」\u003c/strong\u003e — ところが、何回スキャンしても空のままでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"221:1-221:61\"\u003e原因は \u003ccode\u003esonar-project.properties\u003c/code\u003e のこの 1 行です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"properties\" data-sourcepos=\"223:1-225:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"py\"\u003esonar.projectVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e0.1.0\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"227:1-228:55\"\u003eNew Code の既定の定義は **\u003ccode\u003ePREVIOUS_VERSION\u003c/code\u003e（前回バージョン以降）**です。\u003cbr\u003e\nAPI で確認すると理由がはっきりします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"230:1-238:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eNew Code の定義:\n  {\"projectKey\":\"qube-test-backend\",\"type\":\"PREVIOUS_VERSION\",\"inherited\":true}\n\n解析履歴:\n  11:22:33  projectVersion = \"0.1.0\"\n  11:39:04  projectVersion = \"0.1.0\"    ← 2 回スキャンしたが同じ\n  VERSION イベントは \"0.1.0\" の 1 つだけ\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"240:1-241:132\"\u003e\u003cstrong\u003e区切りはスキャン回数ではなく「バージョンが変わったかどうか」で決まります。\u003c/strong\u003e\u003cbr\u003e\nバージョンを固定していたため「前回のバージョン」が存在せず、基準点が定まりませんでした。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"243:1-243:49\"\u003e\n\u003cspan id=\"new-code-が機能する条件は-2-つある\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#new-code-%E3%81%8C%E6%A9%9F%E8%83%BD%E3%81%99%E3%82%8B%E6%9D%A1%E4%BB%B6%E3%81%AF-2-%E3%81%A4%E3%81%82%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eNew Code が機能する条件は 2 つある\u003c/h3\u003e\n\u003ctable data-sourcepos=\"245:1-249:55\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"245:1-245:46\"\u003e\n\u003cth data-sourcepos=\"245:2-245:15\"\u003eやること\u003c/th\u003e\n\u003cth data-sourcepos=\"245:17-245:45\"\u003eNew Code はどうなるか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"247:1-247:101\"\u003e\n\u003ctd data-sourcepos=\"247:2-247:36\"\u003eバージョンを上げるだけ\u003c/td\u003e\n\u003ctd data-sourcepos=\"247:38-247:100\"\u003e基準点はできるが、\u003cstrong\u003e変更行が無いので 0 件\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"248:1-248:98\"\u003e\n\u003ctd data-sourcepos=\"248:2-248:30\"\u003eコードを変えるだけ\u003c/td\u003e\n\u003ctd data-sourcepos=\"248:32-248:97\"\u003e\n\u003cstrong\u003e基準点が無いので 0 件のまま\u003c/strong\u003e（今回の状態）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"249:1-249:55\"\u003e\n\u003ctd data-sourcepos=\"249:2-249:13\"\u003e\u003cstrong\u003e両方\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"249:15-249:54\"\u003e\u003cstrong\u003e変更分が New Code に現れる\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"251:1-251:46\"\u003e\u003cstrong\u003eどちらか一方では動きません。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 data-sourcepos=\"253:1-253:67\"\u003e\n\u003cspan id=\"実プロジェクトでは-projectversion-を固定しない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A7%E3%81%AF-projectversion-%E3%82%92%E5%9B%BA%E5%AE%9A%E3%81%97%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実プロジェクトでは \u003ccode\u003eprojectVersion\u003c/code\u003e を固定しない\u003c/h3\u003e\n\u003cp data-sourcepos=\"255:1-255:91\"\u003e検証用に固定で書いたのが原因なので、実務では CI で注入します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"257:1-263:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# package.json から取る\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e-Dsonar\u003c/span\u003e.projectVersion\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e$(\u003c/span\u003enode \u003cspan class=\"nt\"\u003e-p\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"require('./package.json').version\"\u003c/span\u003e\u003cspan class=\"si\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# git tag から取る\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e-Dsonar\u003c/span\u003e.projectVersion\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e$(\u003c/span\u003egit describe \u003cspan class=\"nt\"\u003e--tags\u003c/span\u003e \u003cspan class=\"nt\"\u003e--abbrev\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e0\u003cspan class=\"si\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"265:1-265:118\"\u003eリリースごとに区切りができ、コードは日々変わるので New Code は自動的に機能します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"267:1-267:70\"\u003e\n\u003cspan id=\"バージョン運用をしたくない場合は定義を変える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E9%81%8B%E7%94%A8%E3%82%92%E3%81%97%E3%81%9F%E3%81%8F%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E5%AE%9A%E7%BE%A9%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eバージョン運用をしたくない場合は定義を変える\u003c/h3\u003e\n\u003cp data-sourcepos=\"269:1-269:65\"\u003e\u003ccode\u003eAdministration\u003c/code\u003e → \u003ccode\u003eNew Code\u003c/code\u003e で 4 種類から選べます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"271:1-276:84\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"271:1-271:43\"\u003e\n\u003cth data-sourcepos=\"271:2-271:9\"\u003e定義\u003c/th\u003e\n\u003cth data-sourcepos=\"271:11-271:18\"\u003e基準\u003c/th\u003e\n\u003cth data-sourcepos=\"271:20-271:42\"\u003e向いている場面\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"273:1-273:123\"\u003e\n\u003ctd data-sourcepos=\"273:2-273:35\"\u003e\n\u003cstrong\u003ePrevious version\u003c/strong\u003e（既定）\u003c/td\u003e\n\u003ctd data-sourcepos=\"273:37-273:89\"\u003e前回 \u003ccode\u003eprojectVersion\u003c/code\u003e が変わった解析以降\u003c/td\u003e\n\u003ctd data-sourcepos=\"273:91-273:122\"\u003eリリース単位で区切る\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"274:1-274:131\"\u003e\n\u003ctd data-sourcepos=\"274:2-274:21\"\u003e\u003cstrong\u003eNumber of days\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"274:23-274:60\"\u003e過去 N 日間に変更された行\u003c/td\u003e\n\u003ctd data-sourcepos=\"274:62-274:130\"\u003e\u003cstrong\u003eリリースを切らない継続開発。一番扱いやすい\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"275:1-275:101\"\u003e\n\u003ctd data-sourcepos=\"275:2-275:24\"\u003e\u003cstrong\u003eSpecific analysis\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"275:26-275:51\"\u003e指定した解析以降\u003c/td\u003e\n\u003ctd data-sourcepos=\"275:53-275:100\"\u003e\u003cstrong\u003e既存プロジェクトへの導入直後\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"276:1-276:84\"\u003e\n\u003ctd data-sourcepos=\"276:2-276:19\"\u003eReference branch\u003c/td\u003e\n\u003ctd data-sourcepos=\"276:21-276:52\"\u003e指定ブランチとの差分\u003c/td\u003e\n\u003ctd data-sourcepos=\"276:54-276:83\"\u003e\u003cstrong\u003eDeveloper Edition 以上\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"278:1-279:96\"\u003e\u003cstrong\u003e既存プロジェクトに後から入れるなら \u003ccode\u003eSpecific analysis\u003c/code\u003e で導入日を基準にする\u003c/strong\u003eのが\u003cbr\u003e\n実用的です。「今日以降に書いたコードだけを見る」状態が作れます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"281:1-281:47\"\u003e\n\u003cspan id=\"git-管理下でないと精度が落ちる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#git-%E7%AE%A1%E7%90%86%E4%B8%8B%E3%81%A7%E3%81%AA%E3%81%84%E3%81%A8%E7%B2%BE%E5%BA%A6%E3%81%8C%E8%90%BD%E3%81%A1%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003egit 管理下でないと精度が落ちる\u003c/h3\u003e\n\u003cp data-sourcepos=\"283:1-284:60\"\u003eもう 1 つ要因があります。この検証環境は git リポジトリではなかったため、\u003cbr\u003e\nスキャンのたびにこの警告が出ていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"286:1-289:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eWARN  SCM provider autodetection failed. Please use \"sonar.scm.provider\" to define\n      SCM of your project, or disable the SCM Sensor in the project settings.\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"291:1-293:99\"\u003e\u003cstrong\u003eSonarQube は git の blame から行ごとの変更日を取得して New Code を判定します。\u003c/strong\u003e\u003cbr\u003e\nSCM が無いと行レベルの精度が落ちるため、実プロジェクトでは必ず git 管理下で\u003cbr\u003e\nスキャンしてください（CI で \u003ccode\u003efetch-depth: 0\u003c/code\u003e が必要なのも同じ理由です）。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"297:1-298:108\"\u003e\u003cstrong\u003e「既存の負債は据え置き、今回入れた指摘だけで落とす」\u003c/strong\u003e — これが SonarQube が\u003cbr\u003e\n本来想定している運用の姿です。導入時にはご自身の環境で確認してください。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"300:1-300:47\"\u003e\n\u003cspan id=\"overall-code-を見る条件を追加する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#overall-code-%E3%82%92%E8%A6%8B%E3%82%8B%E6%9D%A1%E4%BB%B6%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eOverall Code を見る条件を追加する\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"302:1-319:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/qualitygates/create\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"name=qube-test strict\"\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# rating 系は 1=A, 2=B, ... 5=E なので「GT 1」で A 以外を落とす\u003c/span\u003e\n\u003cspan class=\"k\"\u003efor \u003c/span\u003econd \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"coverage:LT:70\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"duplicated_lines_density:GT:3\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n            \u003cspan class=\"s2\"\u003e\"reliability_rating:GT:1\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"security_rating:GT:1\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\n  \u003c/span\u003e\u003cspan class=\"nv\"\u003eIFS\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e: \u003cspan class=\"nb\"\u003eread\u003c/span\u003e \u003cspan class=\"nt\"\u003e-r\u003c/span\u003e metric op err \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cond\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n  curl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/qualitygates/create_condition\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"gateName=qube-test strict\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"metric=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$metric\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"op=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$op\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"error=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$err\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"k\"\u003edone\n\n\u003c/span\u003ecurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/qualitygates/select\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"gateName=qube-test strict\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"projectKey=qube-test-backend\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"321:1-323:104\"\u003eなお、\u003cstrong\u003e新規作成した Quality Gate にはデフォルトの \u003ccode\u003enew_*\u003c/code\u003e 条件が最初から入っていました。\u003c/strong\u003e\u003cbr\u003e\n追加した 4 条件と合わせて 8 条件になります。\u003cbr\u003e\n※画面上での設定の仕方は、先述した\u003ca href=\"#%E8%A7%A3%E6%B1%BA%E7%AD%96\"\u003e解決策\u003c/a\u003eを参照してください。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"325:1-348:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"326:1-326:88\"\u003e\n\u003cspan id=\"重要ゲートを差し替えても再解析するまで判定は変わらない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%87%8D%E8%A6%81%E3%82%B2%E3%83%BC%E3%83%88%E3%82%92%E5%B7%AE%E3%81%97%E6%9B%BF%E3%81%88%E3%81%A6%E3%82%82%E5%86%8D%E8%A7%A3%E6%9E%90%E3%81%99%E3%82%8B%E3%81%BE%E3%81%A7%E5%88%A4%E5%AE%9A%E3%81%AF%E5%A4%89%E3%82%8F%E3%82%89%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e重要：ゲートを差し替えても再解析するまで判定は変わらない\u003c/h3\u003e\n\u003cp data-sourcepos=\"328:1-328:69\"\u003e差し替えた直後に判定を見たら \u003ccode\u003eOK\u003c/code\u003e のままでした。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"330:1-333:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eqube-test-backend   適用ゲート: qube-test strict\n --- 判定: OK ---          ← 再解析していないので古い判定のまま\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"335:1-335:54\"\u003e再スキャンしたら期待通り落ちました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"337:1-344:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e --- 判定: ERROR ---\n   [NG!] coverage                                 actual=52.0 (閾値 LT 70)\n   [NG!] duplicated_lines                         actual=51 (閾値 GT 3)\n   [OK ] new_violations                           actual=0 (閾値 GT 0)← New Code が無いので 0 件扱い\n   [NG!] software_quality_reliability_rating      actual=3 (閾値 GT 1)\n   [NG!] software_quality_security_rating         actual=3 (閾値 GT 1)    \n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"346:1-347:99\"\u003e\u003cstrong\u003eQuality Gate の判定は解析時に評価される\u003c/strong\u003eので、CI で運用する場合は\u003cbr\u003e\n「ゲートを変えたら次の解析から効く」と理解しておく必要があります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"349:1-350:0\"\u003e\n\u003ch2 data-sourcepos=\"351:1-351:49\"\u003e\n\u003cspan id=\"8-fail--修正--pass-をやってみる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-fail--%E4%BF%AE%E6%AD%A3--pass-%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. Fail → 修正 → Pass をやってみる\u003c/h2\u003e\n\u003cp data-sourcepos=\"353:1-353:74\"\u003ebackend の指摘を実際に直しました。主な修正内容です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"355:1-366:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 修正前\n\u003c/span\u003e\u003cspan class=\"n\"\u003eSECRET_KEY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edjango-insecure-3k9v2m8x7q1w...\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# secrets:S6687\n\u003c/span\u003e\u003cspan class=\"n\"\u003eDEBUG\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e                                      \u003cspan class=\"c1\"\u003e# python:S4507\n\u003c/span\u003e\u003cspan class=\"n\"\u003eALLOWED_HOSTS\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e*\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 修正後\n\u003c/span\u003e\u003cspan class=\"n\"\u003eSECRET_KEY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eenviron\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDJANGO_SECRET_KEY\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"ow\"\u003eor\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_random_secret_key\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"n\"\u003eDEBUG\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eenviron\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDJANGO_DEBUG\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003efalse\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003elower\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003etrue\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"n\"\u003eALLOWED_HOSTS\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eh\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estrip\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eh\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e\n                 \u003cspan class=\"n\"\u003eos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eenviron\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDJANGO_ALLOWED_HOSTS\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003elocalhost,127.0.0.1\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003esplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e,\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"368:1-378:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 修正前: python:S4790 / S2245 / S4830\n\u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ehashlib\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003emd5\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eraw_password\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"nf\"\u003ehexdigest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"n\"\u003esuffix\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ejoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erandom\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003echoice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ealphabet\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"nf\"\u003erange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003cspan class=\"n\"\u003erequests\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003everify\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etimeout\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 修正後\n\u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003emake_password\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eraw_password\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e              \u003cspan class=\"c1\"\u003e# Django 標準（PBKDF2）\n\u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003etodo_id\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s\"\u003e-\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003esecrets\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003etoken_urlsafe\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e \u003cspan class=\"c1\"\u003e# secrets モジュール\n\u003c/span\u003e\u003cspan class=\"n\"\u003erequests\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etimeout\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e                   \u003cspan class=\"c1\"\u003e# verify は既定（有効）のまま\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"380:1-382:95\"\u003e認知的複雑度 28 の関数は、役割ごとに 3 つの関数へ分割しました\u003cbr\u003e\n（\u003ccode\u003e_count_status\u003c/code\u003e / \u003ccode\u003e_count_priority\u003c/code\u003e / \u003ccode\u003e_collect_warnings\u003c/code\u003e）。\u003cbr\u003e\nコピペされていた 3 つの CSV エクスポート関数は 1 つに統合しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"384:1-385:164\"\u003eそして\u003cstrong\u003eテストを追加してカバレッジを 96%（SonarQube 表示 78.9%）まで上げました。\u003c/strong\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F61219f18-dc0f-4d2c-be53-4840c775e0bc.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=fc85d760cdcedf7bf677f55266082169\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F61219f18-dc0f-4d2c-be53-4840c775e0bc.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=fc85d760cdcedf7bf677f55266082169\" alt=\"スクリーンショット 2026-09-21 22.35.21.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F61219f18-dc0f-4d2c-be53-4840c775e0bc.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5ee6bc071d5e60de9aaccb472150e395 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/61219f18-dc0f-4d2c-be53-4840c775e0bc.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"388:1-390:72\"\u003e\n\u003cp data-sourcepos=\"388:3-390:72\"\u003ecoverage.py の 96% と SonarQube の 78.9% がずれるのは、集計対象が違うためです。\u003cbr\u003e\n\u003ccode\u003e.coveragerc\u003c/code\u003e の \u003ccode\u003eomit\u003c/code\u003e で除外したファイルも SonarQube 側では\u003cbr\u003e\n「カバレッジ 0% のファイル」として計上されます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv data-sourcepos=\"392:1-401:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"393:1-393:52\"\u003e\n\u003cspan id=\"画面上から問題箇所を特定するには\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%94%BB%E9%9D%A2%E4%B8%8A%E3%81%8B%E3%82%89%E5%95%8F%E9%A1%8C%E7%AE%87%E6%89%80%E3%82%92%E7%89%B9%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e画面上から問題箇所を特定するには\u003c/h3\u003e\n\u003cp data-sourcepos=\"394:1-395:164\"\u003eプロジェクト画面上の\u003cstrong\u003eIssues\u003c/strong\u003e を選択して表示されているものを選択\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe1c5dd68-b203-41c0-abe3-3d754de31fa1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=28c7733848e4f26775327c07807cb46b\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe1c5dd68-b203-41c0-abe3-3d754de31fa1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=28c7733848e4f26775327c07807cb46b\" alt=\"スクリーンショット 2026-09-21 22.48.11.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe1c5dd68-b203-41c0-abe3-3d754de31fa1.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=1766fa1ab20bae27e3f8a6c9ac937df3 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e1c5dd68-b203-41c0-abe3-3d754de31fa1.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"397:1-399:164\"\u003e詳細画面上には、その問題の該当箇所となぜ問題なのかを教えてくれます。\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F77fa5cf6-664f-4994-9114-2055d6d5aa87.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1ce775bcfe50657c85aa6149e6d3c96e\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F77fa5cf6-664f-4994-9114-2055d6d5aa87.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1ce775bcfe50657c85aa6149e6d3c96e\" alt=\"スクリーンショット 2026-09-21 22.48.30.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F77fa5cf6-664f-4994-9114-2055d6d5aa87.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=783e40ad73893940a8aa0cd97f950920 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/77fa5cf6-664f-4994-9114-2055d6d5aa87.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9673c6ce-fdf3-4548-a49e-956568d5d924.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=ce20594c3a47395795027cd49e7c8f2a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9673c6ce-fdf3-4548-a49e-956568d5d924.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=ce20594c3a47395795027cd49e7c8f2a\" alt=\"スクリーンショット 2026-09-21 22.48.48.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9673c6ce-fdf3-4548-a49e-956568d5d924.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=df35299bada9c3f55db91206e50ef2f8 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/9673c6ce-fdf3-4548-a49e-956568d5d924.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"403:1-403:46\"\u003e\n\u003cspan id=\"修正の結果新しい指摘が出た\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BF%AE%E6%AD%A3%E3%81%AE%E7%B5%90%E6%9E%9C%E6%96%B0%E3%81%97%E3%81%84%E6%8C%87%E6%91%98%E3%81%8C%E5%87%BA%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e修正の結果、新しい指摘が出た\u003c/h3\u003e\n\u003cp data-sourcepos=\"406:1-406:84\"\u003e再スキャンしたら、直したはずが今度は別の 4 件が出ました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"408:1-412:88\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"408:1-408:31\"\u003e\n\u003cth data-sourcepos=\"408:2-408:12\"\u003eルール\u003c/th\u003e\n\u003cth data-sourcepos=\"408:14-408:21\"\u003e場所\u003c/th\u003e\n\u003cth data-sourcepos=\"408:23-408:30\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"410:1-410:89\"\u003e\n\u003ctd data-sourcepos=\"410:2-410:21\"\u003e\n\u003ccode\u003epython:S5863\u003c/code\u003e ×2\u003c/td\u003e\n\u003ctd data-sourcepos=\"410:23-410:48\"\u003e\u003ccode\u003etests/test_services.py\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"410:50-410:88\"\u003e同じ式を左右に置いた assert\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"411:1-411:100\"\u003e\n\u003ctd data-sourcepos=\"411:2-411:17\"\u003e\u003ccode\u003epython:S5443\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"411:19-411:39\"\u003e\u003ccode\u003etodos/services.py\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"411:41-411:99\"\u003e\n\u003ccode\u003e/tmp\u003c/code\u003e（誰でも書けるディレクトリ）の使用\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"412:1-412:88\"\u003e\n\u003ctd data-sourcepos=\"412:2-412:16\"\u003e\u003ccode\u003epython:S125\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"412:18-412:39\"\u003e\u003ccode\u003econfig/settings.py\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"412:41-412:87\"\u003e「コメントアウトされたコード」\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"414:1-414:62\"\u003e\u003ccode\u003eS5863\u003c/code\u003e は自分のテストの書き方の問題でした。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"416:1-424:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 指摘される\n\u003c/span\u003e\u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"nf\"\u003ehash_password\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esame\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"nf\"\u003ehash_password\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esame\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 一度変数に受ける\n\u003c/span\u003e\u003cspan class=\"n\"\u003efirst\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ehash_password\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esame\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"n\"\u003esecond\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ehash_password\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esame\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"n\"\u003efirst\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003esecond\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"426:1-426:108\"\u003e\u003ccode\u003eS5443\u003c/code\u003e は \u003ccode\u003e/tmp/todo-archive.log\u003c/code\u003e を既定値にしていたのが原因で、アプリ配下に変更。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"428:1-428:40\"\u003e\n\u003cspan id=\"最後の-1-件は誤検知だった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E5%BE%8C%E3%81%AE-1-%E4%BB%B6%E3%81%AF%E8%AA%A4%E6%A4%9C%E7%9F%A5%E3%81%A0%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e最後の 1 件は誤検知だった\u003c/h3\u003e\n\u003cp data-sourcepos=\"430:1-430:47\"\u003e\u003ccode\u003epython:S125\u003c/code\u003e が指した行はこれです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"432:1-434:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 環境変数から読み、無ければ実行ごとに使い捨ての鍵を生成する。\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"436:1-437:108\"\u003e\u003cstrong\u003e日本語のコメントを「コメントアウトされたコード」と誤検知していました。\u003c/strong\u003e\u003cbr\u003e\n日本語のコメントを書く日本語圏のプロジェクトでは、これは避けられません。\u003c/p\u003e\n\u003cp data-sourcepos=\"439:1-440:115\"\u003eコードを歪めて回避するのではなく、\u003cstrong\u003e理由を添えて False Positive として解決する\u003c/strong\u003eのが\u003cbr\u003e\n正しい運用です。画面なら指摘を開いて \u003ccode\u003eFalse Positive\u003c/code\u003e を選ぶだけ。API ならこうです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"442:1-451:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 理由をコメントとして残す（後から判断を追えるように）\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/issues/add_comment\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"issue=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$ISSUE_KEY\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"text=日本語コメントをコードと誤認した誤検知のため\"\u003c/span\u003e\n\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/issues/do_transition\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"issue=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$ISSUE_KEY\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"transition=falsepositive\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"452:1-457:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"453:1-455:164\"\u003e画面操作からなら\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F94d3e211-d46f-4c84-8aa0-c6c0974f4037.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=367a35e06f9848f97d3a13c11a21bb25\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F94d3e211-d46f-4c84-8aa0-c6c0974f4037.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=367a35e06f9848f97d3a13c11a21bb25\" alt=\"スクリーンショット 2026-09-21 21.15.19.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F94d3e211-d46f-4c84-8aa0-c6c0974f4037.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=31ad61057e4b9735ae9cd754d04da919 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/94d3e211-d46f-4c84-8aa0-c6c0974f4037.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe10cd096-5793-4d02-9441-1a188b43796e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a8c57e4e69936fc382f46b9d7985d8f6\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe10cd096-5793-4d02-9441-1a188b43796e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a8c57e4e69936fc382f46b9d7985d8f6\" alt=\"スクリーンショット 2026-09-21 22.52.10.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2Fe10cd096-5793-4d02-9441-1a188b43796e.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=840881704e354fb59a637271d897e334 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e10cd096-5793-4d02-9441-1a188b43796e.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"459:1-460:72\"\u003e\u003cstrong\u003eQuality Gate の差し替えと違い、False Positive のマークは即座に反映されました。\u003c/strong\u003e\u003cbr\u003e\nこの操作だけで判定が \u003ccode\u003eERROR\u003c/code\u003e → \u003ccode\u003eOK\u003c/code\u003e に変わりました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"462:1-465:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eqube-test-backend   適用ゲート: qube-test strict\n --- 判定: OK ---\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"467:1-467:47\"\u003e\n\u003cspan id=\"余談git-管理下でスキャンする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%99%E8%AB%87git-%E7%AE%A1%E7%90%86%E4%B8%8B%E3%81%A7%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e余談：git 管理下でスキャンする\u003c/h3\u003e\n\u003cp data-sourcepos=\"469:1-469:54\"\u003e検証中ずっとこの警告が出ていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"471:1-474:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eWARN  SCM provider autodetection failed. Please use \"sonar.scm.provider\" to define\n      SCM of your project, or disable the SCM Sensor in the project settings.\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"476:1-480:72\"\u003e解析対象を git リポジトリにしていなかったためです。解析自体は成功しますが、\u003cbr\u003e\nSonarQube は \u003cstrong\u003egit の blame 情報から「その行がいつ書かれたか」を判断して\u003cbr\u003e\nNew Code を決めます\u003c/strong\u003e。SCM 情報が無いと New Code の判定が正しく働かないので、\u003cbr\u003e\n実プロジェクトでは必ず git 管理下でスキャンしてください\u003cbr\u003e\n（CI では \u003ccode\u003efetch-depth: 0\u003c/code\u003e が必要なのも同じ理由です）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"482:1-482:69\"\u003e\n\u003cspan id=\"余談指摘の件数が合わないときは-resolvedfalse\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%99%E8%AB%87%E6%8C%87%E6%91%98%E3%81%AE%E4%BB%B6%E6%95%B0%E3%81%8C%E5%90%88%E3%82%8F%E3%81%AA%E3%81%84%E3%81%A8%E3%81%8D%E3%81%AF-resolvedfalse\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e余談：指摘の件数が合わないときは \u003ccode\u003eresolved=false\u003c/code\u003e\n\u003c/h3\u003e\n\u003cp data-sourcepos=\"484:1-485:111\"\u003e修正後に API で指摘を数えたら 18 件返ってきて「直ってない？」と焦りました。\u003cbr\u003e\n\u003ccode\u003e/api/issues/search\u003c/code\u003e は \u003cstrong\u003e\u003ccode\u003eresolved\u003c/code\u003e を指定しないと修正済み（FIXED / CLOSED）も返します。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"487:1-490:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 未解決のみ\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/issues/search?components=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$KEY\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026amp;resolved=false\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"492:1-492:87\"\u003e未解決だけに絞ったら 4 件で、ダッシュボードと一致しました。\u003c/p\u003e\n\u003chr data-sourcepos=\"494:1-495:1\"\u003e\n\u003ch2 data-sourcepos=\"496:1-496:38\"\u003e\n\u003cspan id=\"未検証9-github-actions-連携\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%AA%E6%A4%9C%E8%A8%BC9-github-actions-%E9%80%A3%E6%90%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e(未検証)9. GitHub Actions 連携\u003c/h2\u003e\n\u003cp data-sourcepos=\"498:1-499:59\"\u003e\u003ccode\u003eSONAR_TOKEN\u003c/code\u003e と \u003ccode\u003eSONAR_HOST_URL\u003c/code\u003e を Secrets に入れて使います（先行記事と同じ）。\u003cbr\u003e\n\u003ccode\u003eprojectBaseDir\u003c/code\u003e を変えて 2 ジョブに分けます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"501:1-547:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eSonarQube\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# Community Build はブランチ解析・PR デコレーションに非対応。\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e# PR ごとに解析を投げても結果が分離されず、同じプロジェクトを上書きしてしまう。\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e# そのため既定では main への push のみで動かす。\u003c/span\u003e\n\u003cspan class=\"na\"\u003eon\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003epush\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebranches\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003emain\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eworkflow_dispatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003ejobs\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ebackend\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eubuntu-latest\u003c/span\u003e\n    \u003cspan class=\"na\"\u003esteps\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003euses\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eactions/checkout@v5\u003c/span\u003e\n        \u003cspan class=\"na\"\u003ewith\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003efetch-depth\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e     \u003cspan class=\"c1\"\u003e# SCM 情報を使うので浅いクローンにしない\u003c/span\u003e\n\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003euses\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eactions/setup-python@v6\u003c/span\u003e\n        \u003cspan class=\"na\"\u003ewith\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003epython-version\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s1\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003e3.13'\u003c/span\u003e\n\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003epip install -r backend/requirements.txt\u003c/span\u003e\n\n      \u003cspan class=\"c1\"\u003e# カバレッジ生成は「スキャンより前」。逆にすると Coverage が 0% になる\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eテストとカバレッジ生成\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eworking-directory\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ebackend\u003c/span\u003e\n        \u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e|\u003c/span\u003e\n          \u003cspan class=\"s\"\u003ecoverage run -m pytest\u003c/span\u003e\n          \u003cspan class=\"s\"\u003ecoverage xml\u003c/span\u003e\n\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003euses\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eSonarSource/sonarqube-scan-action@v8\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eSONAR_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.SONAR_TOKEN }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eSONAR_HOST_URL\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.SONAR_HOST_URL }}\u003c/span\u003e\n        \u003cspan class=\"na\"\u003ewith\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eprojectBaseDir\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ebackend\u003c/span\u003e\n\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003euses\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonarsource/sonarqube-quality-gate-action@master\u003c/span\u003e\n        \u003cspan class=\"na\"\u003etimeout-minutes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eSONAR_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.SONAR_TOKEN }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eSONAR_HOST_URL\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.SONAR_HOST_URL }}\u003c/span\u003e\n        \u003cspan class=\"na\"\u003ewith\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003epollingTimeoutSec\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e600\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"549:1-549:99\"\u003efrontend 側は \u003ccode\u003esetup-node\u003c/code\u003e + \u003ccode\u003enpm ci\u003c/code\u003e + \u003ccode\u003enpm run test:coverage\u003c/code\u003e に置き換えるだけです。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"551:1-560:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"552:1-552:30\"\u003e注意点を整理します。\u003c/p\u003e\n\u003cul data-sourcepos=\"554:1-559:92\"\u003e\n\u003cli data-sourcepos=\"554:1-554:94\"\u003e\n\u003ccode\u003esonarqube-scan-action\u003c/code\u003e は \u003cstrong\u003ev8\u003c/strong\u003e が最新（先行記事の頃より進んでいます）\u003c/li\u003e\n\u003cli data-sourcepos=\"555:1-556:87\"\u003eQuality Gate で CI を落とすには \u003ccode\u003esonarqube-quality-gate-action\u003c/code\u003e を併用。\u003cbr\u003e\n\u003ccode\u003escan-action\u003c/code\u003e と組み合わせる場合 \u003ccode\u003escanMetadataReportFile\u003c/code\u003e の指定は不要\u003c/li\u003e\n\u003cli data-sourcepos=\"557:1-558:63\"\u003e\n\u003cstrong\u003eセルフホストの SonarQube は GitHub Actions ランナーから到達できる必要があります。\u003c/strong\u003e\u003cbr\u003e\n社内 LAN に置くなら self-hosted runner が必要です\u003c/li\u003e\n\u003cli data-sourcepos=\"559:1-559:92\"\u003eCommunity Build で PR 単位の結果を見たいなら Developer Edition 以上が必要\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"561:1-562:0\"\u003e\n\u003ch2 data-sourcepos=\"563:1-563:56\"\u003e\n\u003cspan id=\"10-sonarqube-はどこまで見ているのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10-sonarqube-%E3%81%AF%E3%81%A9%E3%81%93%E3%81%BE%E3%81%A7%E8%A6%8B%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. SonarQube は「どこまで」見ているのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"565:1-566:98\"\u003e「静的解析」と一括りにされますが、実際には深さに段階があります。ここを理解しておくと、\u003cbr\u003e\n何を SonarQube に任せられて、何は別のツールが必要かの判断ができます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"568:1-568:31\"\u003e\n\u003cspan id=\"解析の深さは-4-段階\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E6%9E%90%E3%81%AE%E6%B7%B1%E3%81%95%E3%81%AF-4-%E6%AE%B5%E9%9A%8E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解析の深さは 4 段階\u003c/h3\u003e\n\u003ctable data-sourcepos=\"570:1-575:126\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"570:1-570:67\"\u003e\n\u003cth data-sourcepos=\"570:2-570:9\"\u003e深さ\u003c/th\u003e\n\u003cth data-sourcepos=\"570:11-570:30\"\u003e見ているもの\u003c/th\u003e\n\u003cth data-sourcepos=\"570:32-570:48\"\u003e今回の実例\u003c/th\u003e\n\u003cth data-sourcepos=\"570:50-570:66\"\u003eCommunity Build\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"572:1-572:88\"\u003e\n\u003ctd data-sourcepos=\"572:2-572:17\"\u003e① \u003cstrong\u003e構文\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"572:19-572:35\"\u003eコードの形\u003c/td\u003e\n\u003ctd data-sourcepos=\"572:37-572:81\"\u003e\n\u003ccode\u003eTODO\u003c/code\u003e コメント、\u003ccode\u003eexcept: pass\u003c/code\u003e の形\u003c/td\u003e\n\u003ctd data-sourcepos=\"572:83-572:87\"\u003e✅\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"573:1-573:84\"\u003e\n\u003ctd data-sourcepos=\"573:2-573:14\"\u003e② \u003cstrong\u003e型\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"573:16-573:62\"\u003e型チェッカーを動かして型を解決\u003c/td\u003e\n\u003ctd data-sourcepos=\"573:64-573:77\"\u003e下記参照\u003c/td\u003e\n\u003ctd data-sourcepos=\"573:79-573:83\"\u003e✅\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"574:1-574:151\"\u003e\n\u003ctd data-sourcepos=\"574:2-574:32\"\u003e③ \u003cstrong\u003e関数内のフロー\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"574:34-574:71\"\u003e制御フロー・データフロー\u003c/td\u003e\n\u003ctd data-sourcepos=\"574:73-574:144\"\u003e\n\u003ccode\u003eS1763\u003c/code\u003e 到達不能コード、\u003ccode\u003eS1854\u003c/code\u003e 代入したが読まれない\u003c/td\u003e\n\u003ctd data-sourcepos=\"574:146-574:150\"\u003e✅\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"575:1-575:126\"\u003e\n\u003ctd data-sourcepos=\"575:2-575:48\"\u003e④ \u003cstrong\u003e関数/ファイルを越えた追跡\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"575:50-575:65\"\u003etaint analysis\u003c/td\u003e\n\u003ctd data-sourcepos=\"575:67-575:98\"\u003e（検出されなかった）\u003c/td\u003e\n\u003ctd data-sourcepos=\"575:100-575:125\"\u003e❌ \u003cstrong\u003eDeveloper 以上\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"577:1-577:66\"\u003e②については、スキャンログが証拠になります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"579:1-583:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eINFO  Found 1 tsconfig.json file(s): [/usr/src/frontend/tsconfig.json]\nINFO  Creating TypeScript(6.0.3) program with configuration file .../tsconfig.json\nINFO  Analyzing 16 file(s) from tsconfig ... (76 total files in program)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"585:1-586:45\"\u003e\u003cstrong\u003e\u003ccode\u003etsconfig.json\u003c/code\u003e を読んで TypeScript の型チェッカーを動かしています\u003c/strong\u003e（\u003ccode\u003e76 total files\u003c/code\u003e は\u003cbr\u003e\n\u003ccode\u003enode_modules\u003c/code\u003e の型定義を含む数）。\u003c/p\u003e\n\u003cp data-sourcepos=\"588:1-588:126\"\u003eそのおかげで面白い挙動がありました。\u003ccode\u003e==\u003c/code\u003e をわざと書いたのに\u003cstrong\u003e指摘されなかった\u003c/strong\u003eのです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"590:1-592:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etodo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etitle\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"dl\"\u003e''\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 指摘なし\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"594:1-595:92\"\u003e型情報から \u003ccode\u003etodo.title\u003c/code\u003e が \u003ccode\u003estring\u003c/code\u003e と分かるため、\u003cstrong\u003e\u003ccode\u003e==\u003c/code\u003e でも \u003ccode\u003e===\u003c/code\u003e と同じ結果になると\u003cbr\u003e\n判断して黙った\u003c/strong\u003eわけです。単純な構文マッチではこうなりません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"597:1-597:46\"\u003e\n\u003cspan id=\"脆弱性は-3-種類に分けて考える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%84%86%E5%BC%B1%E6%80%A7%E3%81%AF-3-%E7%A8%AE%E9%A1%9E%E3%81%AB%E5%88%86%E3%81%91%E3%81%A6%E8%80%83%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e脆弱性は 3 種類に分けて考える\u003c/h3\u003e\n\u003cp data-sourcepos=\"599:1-599:94\"\u003e「SonarQube は脆弱性を見るのか」への答えは \u003cstrong\u003e「種類による」\u003c/strong\u003e です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"601:1-605:161\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"601:1-601:46\"\u003e\n\u003cth data-sourcepos=\"601:2-601:6\"\u003e型\u003c/th\u003e\n\u003cth data-sourcepos=\"601:8-601:12\"\u003e例\u003c/th\u003e\n\u003cth data-sourcepos=\"601:14-601:30\"\u003eCommunity Build\u003c/th\u003e\n\u003cth data-sourcepos=\"601:32-601:45\"\u003e補う手段\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"603:1-603:145\"\u003e\n\u003ctd data-sourcepos=\"603:2-603:38\"\u003e\u003cstrong\u003e危険な API / 設定の誤用\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"603:40-603:109\"\u003eMD5、\u003ccode\u003eDEBUG=True\u003c/code\u003e、\u003ccode\u003everify=False\u003c/code\u003e、秘密鍵のハードコード\u003c/td\u003e\n\u003ctd data-sourcepos=\"603:111-603:138\"\u003e✅ \u003cstrong\u003e今回 8 件検出\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"603:140-603:144\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"604:1-604:146\"\u003e\n\u003ctd data-sourcepos=\"604:2-604:34\"\u003e\u003cstrong\u003eインジェクション系\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"604:36-604:95\"\u003eSQLi、XSS、コマンド注入、パストラバーサル\u003c/td\u003e\n\u003ctd data-sourcepos=\"604:97-604:101\"\u003e❌\u003c/td\u003e\n\u003ctd data-sourcepos=\"604:103-604:145\"\u003eSemgrep、bandit、eslint-plugin-security\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"605:1-605:161\"\u003e\n\u003ctd data-sourcepos=\"605:2-605:55\"\u003e\u003cstrong\u003e依存ライブラリの既知脆弱性（CVE）\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"605:57-605:101\"\u003e\n\u003ccode\u003erequirements.txt\u003c/code\u003e の古いパッケージ\u003c/td\u003e\n\u003ctd data-sourcepos=\"605:103-605:107\"\u003e❌\u003c/td\u003e\n\u003ctd data-sourcepos=\"605:109-605:160\"\u003e\n\u003cstrong\u003eDependabot\u003c/strong\u003e、\u003ccode\u003epip-audit\u003c/code\u003e、\u003ccode\u003enpm audit\u003c/code\u003e、Trivy\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"607:1-608:42\"\u003e1 つ目が検出できるのは「1 箇所を見れば判断できる」からです。\u003ccode\u003ehashlib.md5(\u003c/code\u003e と書いてあれば、\u003cbr\u003e\nそれだけで危険と分かります。\u003c/p\u003e\n\u003cp data-sourcepos=\"610:1-610:110\"\u003e2 つ目は入力から出力までの経路を追う必要があるため、taint analysis が要ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"612:1-612:110\"\u003e3 つ目は特に見落としやすいので注意してください。スキャンログにこう出ます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"614:1-617:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eINFO  ------------- Gather SCA dependencies on project\nINFO  Dependency analysis skipped        ← スキップされている\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"618:1-621:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"619:1-620:111\"\u003eSCA（Software Composition Analysis）は \u003cstrong\u003eAdvanced Security アドオンで Enterprise Edition\u003cbr\u003e\n以上\u003c/strong\u003eが必要です。\u003cstrong\u003eCommunity Build では依存ライブラリの CVE を一切検出しません。\u003c/strong\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"622:1-629:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"623:1-623:19\"\u003e\n\u003cspan id=\"見ないもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A6%8B%E3%81%AA%E3%81%84%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e見ないもの\u003c/h3\u003e\n\u003cul data-sourcepos=\"625:1-628:74\"\u003e\n\u003cli data-sourcepos=\"625:1-625:80\"\u003e\n\u003cstrong\u003e実行時の動作\u003c/strong\u003e — 静的解析なのでコードは動かしません\u003c/li\u003e\n\u003cli data-sourcepos=\"626:1-627:107\"\u003e\n\u003cstrong\u003eテストが正しいか\u003c/strong\u003e — テストは実行せず、カバレッジレポートを読むだけです\u003cbr\u003e\n（だから「アサーションの無いテスト」は構文から \u003ccode\u003eS2699\u003c/code\u003e で指摘されました）\u003c/li\u003e\n\u003cli data-sourcepos=\"628:1-628:74\"\u003e\n\u003cstrong\u003e性能\u003c/strong\u003e — 実測しないので遅いコードは分かりません\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"630:1-631:0\"\u003e\n\u003ch2 data-sourcepos=\"632:1-632:50\"\u003e\n\u003cspan id=\"11-community-build-でどこまでできるか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#11-community-build-%E3%81%A7%E3%81%A9%E3%81%93%E3%81%BE%E3%81%A7%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e11. Community Build でどこまでできるか\u003c/h2\u003e\n\u003cp data-sourcepos=\"634:1-634:48\"\u003e実際に触って分かった線引きです。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"636:1-645:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"637:1-637:19\"\u003e\u003cstrong\u003eできたこと\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"639:1-644:73\"\u003e\n\u003cli data-sourcepos=\"639:1-639:55\"\u003ePython / TypeScript / Vue(SFC) / CSS / HTML の解析\u003c/li\u003e\n\u003cli data-sourcepos=\"640:1-640:58\"\u003eカバレッジの取り込み（Cobertura XML / LCOV）\u003c/li\u003e\n\u003cli data-sourcepos=\"641:1-641:91\"\u003e重複コードの検出（コピペ関数を \u003ccode\u003eS4144\u003c/code\u003e と Duplications 10.9% で検出）\u003c/li\u003e\n\u003cli data-sourcepos=\"642:1-642:76\"\u003e認知的複雑度の定量化（28 → 上限 15 という形で出る）\u003c/li\u003e\n\u003cli data-sourcepos=\"643:1-643:55\"\u003eQuality Gate のカスタマイズと CI での判定\u003c/li\u003e\n\u003cli data-sourcepos=\"644:1-644:73\"\u003eWeb API による初期化・結果取得・誤検知処理の自動化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"646:1-652:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"647:1-647:67\"\u003e\u003cstrong\u003eできなかったこと（Developer Edition 以上が必要）\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"649:1-651:107\"\u003e\n\u003cli data-sourcepos=\"649:1-649:47\"\u003eブランチ解析・PR デコレーション\u003c/li\u003e\n\u003cli data-sourcepos=\"650:1-650:82\"\u003eモノレポとしての一元管理（2 プロジェクトに分けて回避）\u003c/li\u003e\n\u003cli data-sourcepos=\"651:1-651:107\"\u003etaint analysis（SQL インジェクション、パストラバーサル等のデータフロー追跡）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"653:1-659:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"654:1-654:96\"\u003e\u003cstrong\u003eできなかったこと（Enterprise Edition ＋ Advanced Security アドオンが必要）\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"656:1-658:50\"\u003e\n\u003cli data-sourcepos=\"656:1-658:50\"\u003e\n\u003cstrong\u003eSCA（依存ライブラリの既知脆弱性・ライセンス違反の検出）\u003c/strong\u003e\u003cbr\u003e\n… ログに \u003ccode\u003eDependency analysis skipped\u003c/code\u003e と出て、\u003ccode\u003erequirements.txt\u003c/code\u003e /\u003cbr\u003e\n\u003ccode\u003epackage.json\u003c/code\u003e は一切チェックされない\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"660:1-662:48\"\u003e社内展開では、まず Community Build で「複雑度・重複・カバレッジの可視化」を回し、\u003cbr\u003e\nPR 連携やインジェクション検出が必要になった段階で有償版を検討する、\u003cbr\u003e\nという順序が現実的だと思います。\u003c/p\u003e\n\u003cp data-sourcepos=\"664:1-666:69\"\u003eなお日本語化は、クレスコさんの \u003ca href=\"https://qiita.com/amoriki/items/b512c0ced7b7cb34fef2\" id=\"reference-c2db7a25cc774e953e7c\"\u003eSonarQube Japanese Pack\u003c/a\u003e を\u003cbr\u003e\n\u003ccode\u003eextensions/plugins\u003c/code\u003e に置いて再起動する形です（今回は英語 UI のまま検証しました。\u003cbr\u003e\nプラグインはバージョン対応の確認が必要です）。\u003c/p\u003e\n\u003chr data-sourcepos=\"668:1-669:0\"\u003e\n\u003ch2 data-sourcepos=\"670:1-670:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"672:1-672:36\"\u003e後編で押さえた要点です。\u003c/p\u003e\n\u003col data-sourcepos=\"674:1-696:0\"\u003e\n\u003cli data-sourcepos=\"674:1-675:111\"\u003e\n\u003cp data-sourcepos=\"674:4-675:111\"\u003e\u003cstrong\u003e初回スキャンで Quality Gate は素通りする。\u003c/strong\u003e \u003ccode\u003eSonar way\u003c/code\u003e の条件は 4 つ全部が\u003cbr\u003e\nNew Code 側。既存コードを評価したいなら \u003cstrong\u003eOverall Code の条件を足して再解析する\u003c/strong\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"676:1-679:100\"\u003e\n\u003cp data-sourcepos=\"676:4-679:100\"\u003e\u003cstrong\u003e\u003ccode\u003esonar.projectVersion\u003c/code\u003e を固定すると New Code は永久に空のまま。\u003c/strong\u003e\u003cbr\u003e\n区切りはスキャン回数ではなく\u003cstrong\u003eバージョンが変わったかどうか\u003c/strong\u003eで決まる。\u003cbr\u003e\nCI で注入するか、New Code の定義を \u003ccode\u003eNumber of days\u003c/code\u003e /\u003cbr\u003e\n\u003ccode\u003eSpecific analysis\u003c/code\u003e に変える。\u003cstrong\u003egit 管理下でないと行レベルの精度も落ちる\u003c/strong\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"680:1-687:0\"\u003e\n\u003cp data-sourcepos=\"680:4-680:58\"\u003e\u003cstrong\u003e反映のタイミングが操作によって違う\u003c/strong\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"682:4-686:66\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"682:4-682:37\"\u003e\n\u003cth data-sourcepos=\"682:5-682:12\"\u003e操作\u003c/th\u003e\n\u003cth data-sourcepos=\"682:14-682:36\"\u003e再解析が必要か\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"684:4-684:51\"\u003e\n\u003ctd data-sourcepos=\"684:5-684:37\"\u003eQuality Gate の条件を変更\u003c/td\u003e\n\u003ctd data-sourcepos=\"684:39-684:50\"\u003e\u003cstrong\u003e必要\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"685:4-685:57\"\u003e\n\u003ctd data-sourcepos=\"685:5-685:43\"\u003eQuality Profile のルールを変更\u003c/td\u003e\n\u003ctd data-sourcepos=\"685:45-685:56\"\u003e\u003cstrong\u003e必要\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"686:4-686:66\"\u003e\n\u003ctd data-sourcepos=\"686:5-686:40\"\u003e指摘を False Positive にする\u003c/td\u003e\n\u003ctd data-sourcepos=\"686:42-686:65\"\u003e\u003cstrong\u003e不要（即時）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"688:1-689:116\"\u003e\n\u003cp data-sourcepos=\"688:4-689:116\"\u003e\u003cstrong\u003e\u003ccode\u003ev-html\u003c/code\u003e や \u003ccode\u003econsole.log\u003c/code\u003e は検出されない。\u003c/strong\u003e 原因は「ルールが存在しない」と\u003cbr\u003e\n「存在するが無効」の 2 パターンで、後者は Quality Profile をコピーして有効化できる\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"690:1-691:88\"\u003e\n\u003cp data-sourcepos=\"690:4-691:88\"\u003e\u003cstrong\u003eESLint / ruff と併用する前提で設計する。\u003c/strong\u003e SonarQube の強みは、リンターが見ない\u003cbr\u003e\n\u003cstrong\u003e認知的複雑度・重複・カバレッジの定量化と、推移の可視化\u003c/strong\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"692:1-693:54\"\u003e\n\u003cp data-sourcepos=\"692:4-693:54\"\u003e日本語コメントは \u003ccode\u003eS125\u003c/code\u003e で誤検知される。\u003cstrong\u003eコードを歪めて回避せず、\u003cbr\u003e\n理由を添えて False Positive で解決する\u003c/strong\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"694:1-696:0\"\u003e\n\u003cp data-sourcepos=\"694:4-695:103\"\u003e\u003cstrong\u003e脆弱性は「設定・API の誤用」までしか見ない。\u003c/strong\u003e インジェクション系は\u003cbr\u003e\nDeveloper Edition 以上、依存ライブラリの CVE は Enterprise + Advanced Security が必要\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 data-sourcepos=\"697:1-697:49\"\u003e\n\u003cspan id=\"社内に入れるときの現実的な順序\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%A4%BE%E5%86%85%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E7%8F%BE%E5%AE%9F%E7%9A%84%E3%81%AA%E9%A0%86%E5%BA%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e社内に入れるときの現実的な順序\u003c/h3\u003e\n\u003cp data-sourcepos=\"699:1-699:72\"\u003e前後編を通して検証した結果、この順序を勧めます。\u003c/p\u003e\n\u003col data-sourcepos=\"701:1-709:0\"\u003e\n\u003cli data-sourcepos=\"701:1-702:81\"\u003e\n\u003cstrong\u003eまず Community Build で「可視化」だけ始める\u003c/strong\u003e — Quality Gate は既定の \u003ccode\u003eSonar way\u003c/code\u003e\u003cbr\u003e\n（New Code のみ）のままにして、既存の負債では落とさない\u003c/li\u003e\n\u003cli data-sourcepos=\"703:1-704:72\"\u003e\n\u003cstrong\u003e新規に書くコードだけきれいに保つ\u003c/strong\u003e（Clean as You Code）。これが SonarQube の\u003cbr\u003e\n設計思想に沿った使い方で、チームの抵抗も少ない\u003c/li\u003e\n\u003cli data-sourcepos=\"705:1-706:30\"\u003e\n\u003cstrong\u003e既存コードの現状を把握したいときだけ\u003c/strong\u003e Overall Code 条件を足した別ゲートを作り、\u003cbr\u003e\nレポート目的で使う\u003c/li\u003e\n\u003cli data-sourcepos=\"707:1-707:115\"\u003e\n\u003cstrong\u003e足りない部分は他ツールで埋める\u003c/strong\u003e — \u003ccode\u003ev-html\u003c/code\u003e は ESLint、依存の CVE は DependabotやTrivy\u003c/li\u003e\n\u003cli data-sourcepos=\"708:1-709:0\"\u003ePR 連携やインジェクション検出が必要になった段階で\u003cstrong\u003e有償版を検討する\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 data-sourcepos=\"710:1-710:81\"\u003e\n\u003cspan id=\"検証環境一式\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A4%9C%E8%A8%BC%E7%92%B0%E5%A2%83%E4%B8%80%E5%BC%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ca href=\"https://github.com/Mutsuki-Y/sonarqube-vue3-drf-sandbox\" rel=\"nofollow noopener\" target=\"_blank\"\u003e検証環境一式\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp data-sourcepos=\"712:1-713:125\"\u003eDocker Compose / Makefile / 自動化スクリプト / 指摘を仕込んだアプリは、\u003cbr\u003e\nそのまま動く形でまとめてあります。\u003ccode\u003emake all\u003c/code\u003e だけで起動から解析結果の表示まで通ります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"715:1-718:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nb\"\u003ecp\u003c/span\u003e .env.example .env\nmake all\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n","body":"## この記事は後編です\n\n[前編](https://qiita.com/mukku928/items/2ddb6a8274c55ebfac62)では、ホストに Node / Python / Java を一切入れず、Docker だけで SonarQube を立て、\nVue3 + Django REST Framework のプロジェクトを解析してカバレッジを取り込むところまでやりました。\n\n前編の到達点はここです。\n\n| | backend (DRF) | frontend (Vue3) |\n|---|---|---|\n| Coverage | 47.7% | 52.0% |\n| 検出された指摘 | 14 件 | 26 件 |\n\n**ところが、この状態で Quality Gate を見ると「Passed」と表示されます。**\nこれだけ指摘が出ているのにです。\n\n後編はその正体から入り、**指摘をどう読み、どう運用するか**を扱います。\n数値・ログ・エンドポイントはすべて実際に動かした結果です。\n\n検証環境: SonarQube Community Build **26.9.0.129388** / Docker 28.2.2 / macOS (x86_64)\n\n## この記事の構成\n\n| 章 | 内容 |\n|---|---|\n| 5 | **検出結果の一覧** — 実際に何が出たか |\n| 6 | **出なかった指摘の原因** — 3 パターンの切り分けと Quality Profile のカスタマイズ |\n| 7 | **Quality Gate が初回素通りする正体** — New Code とは何か |\n| 8 | **Fail → 修正 → Pass の実演** — 誤検知（False Positive）の扱いも |\n| 9 | **GitHub Actions 連携** |\n| 10 | **静的解析の深さ** — どこまで見るのか、脆弱性検出の限界 |\n| 11 | **Community Build の線引き** — 有償版が必要になる境界 |\n\n`S106` のような個別ルールの解説は、分量が多いので\n**別記事「SonarQube ルール早見表（Vue3 + DRF 編）」**にまとめています。\n\n---\n\n## 5. 何が検出されたか\n\n検証アプリには典型的な問題コードを仕込みました。修正前の backend で 14 件です。\n\n| ルール | 種別 | 内容 |\n|---|---|---|\n| `secrets:S6687` | Vulnerability (BLOCKER) | `SECRET_KEY` のハードコード |\n| `python:S4507` | Vulnerability | `DEBUG = True` |\n| `python:S4502` ×2 | Vulnerability (CRITICAL) | CSRF 保護の無効化（`@csrf_exempt` と middleware 未設定） |\n| `python:S4790` | Vulnerability (CRITICAL) | MD5 でのハッシュ化 |\n| `python:S2245` | Vulnerability | `random` をセキュリティ用途に使用 |\n| `python:S4830` | Vulnerability (CRITICAL) | `verify=False`（TLS 検証の無効化） |\n| `python:S3752` | Vulnerability | HTTP メソッドの未指定 |\n| `python:S1763` | Bug | `return` 後の到達不能コード |\n| `python:S1862` | Bug | 同じ条件の `elif` で到達しない分岐 |\n| `python:S1192` | Code Smell | 3 回重複した文字列リテラル |\n| `python:S1135` | Code Smell | `TODO` コメント |\n| `python:S1481` | Code Smell | 未使用のローカル変数 |\n| `python:S3776` | Code Smell | 認知的複雑度 **28**（上限 15） |\n\nfrontend は 26 件。Vue3 特有のものを抜粋します。\n\n| ルール | 種別 | 内容 |\n|---|---|---|\n| `typescript:S1656` | Bug | 自己代入（`title = title`） |\n| `typescript:S1862` | Bug | 同じ条件の `else if` |\n| `Web:InputWithoutLabelCheck` | Bug | `\u003cinput\u003e` に label が無い（アクセシビリティ） |\n| `typescript:S2245` | Vulnerability | `Math.random()` |\n| `typescript:S4144` ×2 | Code Smell | 実装が同一の関数（コピペ検出） |\n| `typescript:S1854` | Code Smell | 使われない代入 |\n| `typescript:S2486` | Code Smell | 空の `catch` |\n| `typescript:S2699` | Code Smell (BLOCKER) | **アサーションが無いテスト** |\n| `typescript:S3776` | Code Smell | 認知的複雑度 22 |\n| `typescript:S7781` | Code Smell | `replace()` より `replaceAll()` を使う |\n\n`S2699`（アサーション無しテスト）が BLOCKER で出るのは実用的だと感じました。\n「通っているつもりのテスト」を検出できます。\n\n### Security Hotspot は 0 件だった\n\n意外だったのですが、`security_hotspots = 0` で、`/api/hotspots/search` も空でした。\n従来 Security Hotspot に分類されていたルール（MD5、擬似乱数、TLS 検証無効化など。\nメッセージが \"Make sure ... is safe here\" になっているもの）が、\n**26.9 では Vulnerability として集計されています**。\n\n先行記事にある「Overview に Security Hotspots が出る」という説明とは\n挙動が変わっているので、バージョンによって読み替えが必要です。\n\n---\n\n## 6. つまずき④ 仕込んだのに検出されない指摘がある\n\nここが一番の発見でした。**仕込んだのに出ないものが複数ありました。**\n\n原因は 3 種類に分かれます。`/api/rules/show` と `/api/rules/search` で切り分けました。\n\n| 仕込んだもの | 結果 | 原因 |\n|---|---|---|\n| **`v-html` での XSS** | 検出されない | **ルール自体が存在しない** |\n| `ALLOWED_HOSTS = [\"*\"]` | 検出されない | ルール自体が存在しない |\n| `eval()` での動的実行 | 検出されない | Python 用のルールが存在しない |\n| `console.log` の残留 | 検出されない | `typescript:S106` はあるが **Sonar way で無効** |\n| `==` による比較 | 検出されない | `typescript:S1440` が無効 |\n| `any` の使用 | 検出されない | `typescript:S4204` が無効 |\n| 未使用の `import`（Python） | 検出されない | `python:S1128` が無効 |\n| API トークンのハードコード | 検出されない | `typescript:S2068` は有効だが変数名が合致せず |\n| SQL インジェクション等 | 検出されない | taint analysis は **Developer Edition 以上** |\n\n確認方法はこうです。\n\n```bash\n# ルールが存在するか・有効かを調べる\ncurl -s -u \"admin:$PASS\" \"$SONAR_URL/api/rules/show?key=typescript:S106\u0026actives=true\"\n# → rule は返るが actives が空 = 存在するが無効\n\n# ルールキーが分からないときはキーワードで検索する\ncurl -s -u \"admin:$PASS\" \"$SONAR_URL/api/rules/search?q=console\u0026languages=js,ts\"\n```\n\n### ここから導ける運用方針\n\n**SonarQube だけで Vue3 / DRF の品質を担保しようとしないこと。**\n\n- `v-html` の XSS は SonarQube では拾えないので、**ESLint の `vue/no-v-html`** が必要\n- `console.log`、`==`、`any` は ESLint / TypeScript の設定で止めるのが素直\n- Python の未使用 import は **ruff / flake8** の担当\n\nSonarQube の強みは、**認知的複雑度・重複・カバレッジ・セキュリティ設定の指摘を\nダッシュボードで時系列に追えること**でした。リンターの置き換えではなく、\nリンターが見ないレイヤを埋めるツールと考えるのが実態に合っています。\n\n### 無効になっているルールは有効化できる\n\nパターン B（存在するが無効）は、**Quality Profile をカスタマイズすれば検出できます。**\n実際にやってみました。\n\n1. `Quality Profiles` → **TypeScript** の `Sonar way` を **`Copy`**\n   （`Sonar way` は `Built-in` なので**直接編集できません**。コピーが必須）\n   ![スクリーンショット 2026-09-21 23.00.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/b2e333e0-ca6e-4bb1-9f3f-47cdb14a0c7e.png)\n\n2. 作ったプロファイルで `Activate More Rules` → `S106` と `S4204` を `Activate`\n    ![スクリーンショット 2026-09-21 21.04.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e0bebd04-aad4-44c3-915e-f1fa5cabade1.png)\n\n3. `Projects` タブで対象プロジェクトに適用\n ![スクリーンショット 2026-09-21 21.07.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/f61531e5-71ed-4e8b-8028-355c76c084fe.png)\n\n4. **再解析**（Quality Gate と同じで、プロファイルを変えても再解析しないと反映されません）\n\n結果、**指摘が 26 件 → 30 件に増えました。**\n\n| ルール | 増えた件数 | 検出された場所 |\n|---|---|---|\n| `typescript:S4204`（`any` の使用） | 3 件 | `composables/useTodos.ts`、`api/client.ts` |\n| `typescript:S106`（`console.log` の残留） | 1 件 | `composables/useTodos.ts` |\n\n**「デフォルトが緩いなら締められる」** ということです。ただし ESLint で止めれば済む内容も\n多いので、**どちらの層で止めるかをチームで決めておく**のが現実的です。\n\n:::note warn\n### 有効化するときの注意点 2 つ\n\n**① 同じルールが言語ごとに別キーで存在する**\n\n```\n[無効] javascript:S106   Standard outputs should not be used...\n[無効] typescript:S106   Standard outputs should not be used...\n```\n\n**プロファイルは言語単位**です。`.js` と `.ts` が混在するプロジェクトでは、\n**両方の言語のプロファイルで有効にしないと片方だけ検出されません。**\n\n**② 同じ問題でも言語によって検出できるルールが違う**\n\n「空の例外ハンドラ」を両方に仕込んだところ、結果が非対称でした。\n\n| | 仕込んだコード | 結果 |\n|---|---|---|\n| frontend | `catch (error) { }` | ✅ `typescript:S2486` で検出 |\n| backend | `except Exception: pass` | ❌ 該当ルールが Sonar way に無い |\n\n**「Python で出たから TypeScript でも出る」とは限りません。** 言語ごとにルールセットの\n成熟度が違うので、両方のプロファイルを個別に確認する必要があります。\n:::\n---\n\n## 7. つまずき⑤ 初回スキャンで Quality Gate が必ず素通りする\n\n初回スキャン直後、あれだけ指摘が出ているのに **Quality Gate は `OK`** でした。\n\n理由は条件定義を見れば分かります。デフォルトの `Sonar way` はこうなっています。\n\n```\nnew_violations                  GT 0\nnew_coverage                    LT 80\nnew_duplicated_lines_density    GT 3\nnew_security_hotspots_reviewed  LT 100\n```\n\n**4 つ全部が `new_*`（New Code 側）です。**\n初回解析には比較対象の New Code が存在しないため、判定条件が 0 件になり、\nそのまま合格になります。実際 API で見ると conditions が空でした。\n\nこれは設計思想で、**Clean as You Code**（既存の負債は一旦置き、\nこれから書くコードをきれいに保つ）という考え方です。新規プロジェクトには合いますが、\n**既存プロジェクトに後から入れて「今の品質」を可視化したい場合は条件を足す必要があります。**\n\n:::note\n### 解決策\n新しいQuality Gateを作成して、**Conditions on Overall Code** に条件を設定した上でプロジェクトを紐づける\n![スクリーンショット 2026-09-21 22.24.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/128ac6b6-4603-4619-a327-31db117917e9.png)\n\n再度スキャンを行う\n![スクリーンショット 2026-09-21 20.41.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/c1a66556-56ca-4a80-a3b4-bb83a346e95c.png)\n:::\n\n:::note warn\n### 何回スキャンしても New Code が空のままだった\n![スクリーンショット 2026-09-21 22.28.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/b561fcff-f9c3-4e92-8790-b41d8ebe0958.png)\n\n\nここで当然の疑問が出ます。**「2 回目・3 回目のスキャンなら、前回との差分が New Code に\nなるのでは？」** — ところが、何回スキャンしても空のままでした。\n\n原因は `sonar-project.properties` のこの 1 行です。\n\n```properties\nsonar.projectVersion=0.1.0\n```\n\nNew Code の既定の定義は **`PREVIOUS_VERSION`（前回バージョン以降）**です。\nAPI で確認すると理由がはっきりします。\n\n```\nNew Code の定義:\n  {\"projectKey\":\"qube-test-backend\",\"type\":\"PREVIOUS_VERSION\",\"inherited\":true}\n\n解析履歴:\n  11:22:33  projectVersion = \"0.1.0\"\n  11:39:04  projectVersion = \"0.1.0\"    ← 2 回スキャンしたが同じ\n  VERSION イベントは \"0.1.0\" の 1 つだけ\n```\n\n**区切りはスキャン回数ではなく「バージョンが変わったかどうか」で決まります。**\nバージョンを固定していたため「前回のバージョン」が存在せず、基準点が定まりませんでした。\n\n### New Code が機能する条件は 2 つある\n\n| やること | New Code はどうなるか |\n|---|---|\n| バージョンを上げるだけ | 基準点はできるが、**変更行が無いので 0 件** |\n| コードを変えるだけ | **基準点が無いので 0 件のまま**（今回の状態） |\n| **両方** | **変更分が New Code に現れる** |\n\n**どちらか一方では動きません。**\n\n### 実プロジェクトでは `projectVersion` を固定しない\n\n検証用に固定で書いたのが原因なので、実務では CI で注入します。\n\n```bash\n# package.json から取る\n-Dsonar.projectVersion=$(node -p \"require('./package.json').version\")\n\n# git tag から取る\n-Dsonar.projectVersion=$(git describe --tags --abbrev=0)\n```\n\nリリースごとに区切りができ、コードは日々変わるので New Code は自動的に機能します。\n\n### バージョン運用をしたくない場合は定義を変える\n\n`Administration` → `New Code` で 4 種類から選べます。\n\n| 定義 | 基準 | 向いている場面 |\n|---|---|---|\n| **Previous version**（既定） | 前回 `projectVersion` が変わった解析以降 | リリース単位で区切る |\n| **Number of days** | 過去 N 日間に変更された行 | **リリースを切らない継続開発。一番扱いやすい** |\n| **Specific analysis** | 指定した解析以降 | **既存プロジェクトへの導入直後** |\n| Reference branch | 指定ブランチとの差分 | **Developer Edition 以上** |\n\n**既存プロジェクトに後から入れるなら `Specific analysis` で導入日を基準にする**のが\n実用的です。「今日以降に書いたコードだけを見る」状態が作れます。\n\n### git 管理下でないと精度が落ちる\n\nもう 1 つ要因があります。この検証環境は git リポジトリではなかったため、\nスキャンのたびにこの警告が出ていました。\n\n```\nWARN  SCM provider autodetection failed. Please use \"sonar.scm.provider\" to define\n      SCM of your project, or disable the SCM Sensor in the project settings.\n```\n\n**SonarQube は git の blame から行ごとの変更日を取得して New Code を判定します。**\nSCM が無いと行レベルの精度が落ちるため、実プロジェクトでは必ず git 管理下で\nスキャンしてください（CI で `fetch-depth: 0` が必要なのも同じ理由です）。\n:::\n\n\n**「既存の負債は据え置き、今回入れた指摘だけで落とす」** — これが SonarQube が\n本来想定している運用の姿です。導入時にはご自身の環境で確認してください。\n\n### Overall Code を見る条件を追加する\n\n```bash\ncurl -s -u \"admin:$PASS\" -X POST \"$SONAR_URL/api/qualitygates/create\" \\\n  --data-urlencode \"name=qube-test strict\"\n\n# rating 系は 1=A, 2=B, ... 5=E なので「GT 1」で A 以外を落とす\nfor cond in \"coverage:LT:70\" \"duplicated_lines_density:GT:3\" \\\n            \"reliability_rating:GT:1\" \"security_rating:GT:1\"; do\n  IFS=: read -r metric op err \u003c\u003c\u003c \"$cond\"\n  curl -s -u \"admin:$PASS\" -X POST \"$SONAR_URL/api/qualitygates/create_condition\" \\\n    --data-urlencode \"gateName=qube-test strict\" \\\n    --data-urlencode \"metric=$metric\" --data-urlencode \"op=$op\" \\\n    --data-urlencode \"error=$err\"\ndone\n\ncurl -s -u \"admin:$PASS\" -X POST \"$SONAR_URL/api/qualitygates/select\" \\\n  --data-urlencode \"gateName=qube-test strict\" \\\n  --data-urlencode \"projectKey=qube-test-backend\"\n```\n\nなお、**新規作成した Quality Gate にはデフォルトの `new_*` 条件が最初から入っていました。**\n追加した 4 条件と合わせて 8 条件になります。\n※画面上での設定の仕方は、先述した[解決策](#解決策)を参照してください。\n\n::: note\n### 重要：ゲートを差し替えても再解析するまで判定は変わらない\n\n差し替えた直後に判定を見たら `OK` のままでした。\n\n```\nqube-test-backend   適用ゲート: qube-test strict\n --- 判定: OK ---          ← 再解析していないので古い判定のまま\n```\n\n再スキャンしたら期待通り落ちました。\n\n```\n --- 判定: ERROR ---\n   [NG!] coverage                                 actual=52.0 (閾値 LT 70)\n   [NG!] duplicated_lines                         actual=51 (閾値 GT 3)\n   [OK ] new_violations                           actual=0 (閾値 GT 0)← New Code が無いので 0 件扱い\n   [NG!] software_quality_reliability_rating      actual=3 (閾値 GT 1)\n   [NG!] software_quality_security_rating         actual=3 (閾値 GT 1)    \n```\n\n**Quality Gate の判定は解析時に評価される**ので、CI で運用する場合は\n「ゲートを変えたら次の解析から効く」と理解しておく必要があります。\n:::\n---\n\n## 8. Fail → 修正 → Pass をやってみる\n\nbackend の指摘を実際に直しました。主な修正内容です。\n\n```python\n# 修正前\nSECRET_KEY = \"django-insecure-3k9v2m8x7q1w...\"   # secrets:S6687\nDEBUG = True                                      # python:S4507\nALLOWED_HOSTS = [\"*\"]\n\n# 修正後\nSECRET_KEY = os.environ.get(\"DJANGO_SECRET_KEY\") or get_random_secret_key()\nDEBUG = os.environ.get(\"DJANGO_DEBUG\", \"false\").lower() == \"true\"\nALLOWED_HOSTS = [h.strip() for h in\n                 os.environ.get(\"DJANGO_ALLOWED_HOSTS\", \"localhost,127.0.0.1\").split(\",\")]\n```\n\n```python\n# 修正前: python:S4790 / S2245 / S4830\nreturn hashlib.md5(raw_password.encode()).hexdigest()\nsuffix = \"\".join(random.choice(alphabet) for _ in range(16))\nrequests.post(url, verify=False, timeout=5)\n\n# 修正後\nreturn make_password(raw_password)              # Django 標準（PBKDF2）\nreturn f\"{todo_id}-{secrets.token_urlsafe(12)}\" # secrets モジュール\nrequests.post(url, timeout=5)                   # verify は既定（有効）のまま\n```\n\n認知的複雑度 28 の関数は、役割ごとに 3 つの関数へ分割しました\n（`_count_status` / `_count_priority` / `_collect_warnings`）。\nコピペされていた 3 つの CSV エクスポート関数は 1 つに統合しました。\n\nそして**テストを追加してカバレッジを 96%（SonarQube 表示 78.9%）まで上げました。**\n![スクリーンショット 2026-09-21 22.35.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/61219f18-dc0f-4d2c-be53-4840c775e0bc.png)\n\n\n\u003e coverage.py の 96% と SonarQube の 78.9% がずれるのは、集計対象が違うためです。\n\u003e `.coveragerc` の `omit` で除外したファイルも SonarQube 側では\n\u003e 「カバレッジ 0% のファイル」として計上されます。\n\n:::note\n### 画面上から問題箇所を特定するには\nプロジェクト画面上の**Issues** を選択して表示されているものを選択\n![スクリーンショット 2026-09-21 22.48.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e1c5dd68-b203-41c0-abe3-3d754de31fa1.png)\n\n詳細画面上には、その問題の該当箇所となぜ問題なのかを教えてくれます。\n![スクリーンショット 2026-09-21 22.48.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/77fa5cf6-664f-4994-9114-2055d6d5aa87.png)\n![スクリーンショット 2026-09-21 22.48.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/9673c6ce-fdf3-4548-a49e-956568d5d924.png)\n\n:::\n\n### 修正の結果、新しい指摘が出た\n\n\n再スキャンしたら、直したはずが今度は別の 4 件が出ました。\n\n| ルール | 場所 | 内容 |\n|---|---|---|\n| `python:S5863` ×2 | `tests/test_services.py` | 同じ式を左右に置いた assert |\n| `python:S5443` | `todos/services.py` | `/tmp`（誰でも書けるディレクトリ）の使用 |\n| `python:S125` | `config/settings.py` | 「コメントアウトされたコード」 |\n\n`S5863` は自分のテストの書き方の問題でした。\n\n```python\n# 指摘される\nassert hash_password(\"same\") != hash_password(\"same\")\n\n# 一度変数に受ける\nfirst = hash_password(\"same\")\nsecond = hash_password(\"same\")\nassert first != second\n```\n\n`S5443` は `/tmp/todo-archive.log` を既定値にしていたのが原因で、アプリ配下に変更。\n\n### 最後の 1 件は誤検知だった\n\n`python:S125` が指した行はこれです。\n\n```python\n# 環境変数から読み、無ければ実行ごとに使い捨ての鍵を生成する。\n```\n\n**日本語のコメントを「コメントアウトされたコード」と誤検知していました。**\n日本語のコメントを書く日本語圏のプロジェクトでは、これは避けられません。\n\nコードを歪めて回避するのではなく、**理由を添えて False Positive として解決する**のが\n正しい運用です。画面なら指摘を開いて `False Positive` を選ぶだけ。API ならこうです。\n\n```bash\n# 理由をコメントとして残す（後から判断を追えるように）\ncurl -s -u \"admin:$PASS\" -X POST \"$SONAR_URL/api/issues/add_comment\" \\\n  --data-urlencode \"issue=$ISSUE_KEY\" \\\n  --data-urlencode \"text=日本語コメントをコードと誤認した誤検知のため\"\n\ncurl -s -u \"admin:$PASS\" -X POST \"$SONAR_URL/api/issues/do_transition\" \\\n  --data-urlencode \"issue=$ISSUE_KEY\" \\\n  --data-urlencode \"transition=falsepositive\"\n```\n:::note\n画面操作からなら\n![スクリーンショット 2026-09-21 21.15.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/94d3e211-d46f-4c84-8aa0-c6c0974f4037.png)\n![スクリーンショット 2026-09-21 22.52.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/e10cd096-5793-4d02-9441-1a188b43796e.png)\n\n:::\n\n**Quality Gate の差し替えと違い、False Positive のマークは即座に反映されました。**\nこの操作だけで判定が `ERROR` → `OK` に変わりました。\n\n```\nqube-test-backend   適用ゲート: qube-test strict\n --- 判定: OK ---\n```\n\n### 余談：git 管理下でスキャンする\n\n検証中ずっとこの警告が出ていました。\n\n```\nWARN  SCM provider autodetection failed. Please use \"sonar.scm.provider\" to define\n      SCM of your project, or disable the SCM Sensor in the project settings.\n```\n\n解析対象を git リポジトリにしていなかったためです。解析自体は成功しますが、\nSonarQube は **git の blame 情報から「その行がいつ書かれたか」を判断して\nNew Code を決めます**。SCM 情報が無いと New Code の判定が正しく働かないので、\n実プロジェクトでは必ず git 管理下でスキャンしてください\n（CI では `fetch-depth: 0` が必要なのも同じ理由です）。\n\n### 余談：指摘の件数が合わないときは `resolved=false`\n\n修正後に API で指摘を数えたら 18 件返ってきて「直ってない？」と焦りました。\n`/api/issues/search` は **`resolved` を指定しないと修正済み（FIXED / CLOSED）も返します。**\n\n```bash\n# 未解決のみ\ncurl -s -u \"admin:$PASS\" \"$SONAR_URL/api/issues/search?components=$KEY\u0026resolved=false\"\n```\n\n未解決だけに絞ったら 4 件で、ダッシュボードと一致しました。\n\n---\n \n## (未検証)9. GitHub Actions 連携\n\n`SONAR_TOKEN` と `SONAR_HOST_URL` を Secrets に入れて使います（先行記事と同じ）。\n`projectBaseDir` を変えて 2 ジョブに分けます。\n\n```yaml\nname: SonarQube\n\n# Community Build はブランチ解析・PR デコレーションに非対応。\n# PR ごとに解析を投げても結果が分離されず、同じプロジェクトを上書きしてしまう。\n# そのため既定では main への push のみで動かす。\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  backend:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0     # SCM 情報を使うので浅いクローンにしない\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.13'\n\n      - run: pip install -r backend/requirements.txt\n\n      # カバレッジ生成は「スキャンより前」。逆にすると Coverage が 0% になる\n      - name: テストとカバレッジ生成\n        working-directory: backend\n        run: |\n          coverage run -m pytest\n          coverage xml\n\n      - uses: SonarSource/sonarqube-scan-action@v8\n        env:\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}\n          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}\n        with:\n          projectBaseDir: backend\n\n      - uses: sonarsource/sonarqube-quality-gate-action@master\n        timeout-minutes: 10\n        env:\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}\n          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}\n        with:\n          pollingTimeoutSec: 600\n```\n\nfrontend 側は `setup-node` + `npm ci` + `npm run test:coverage` に置き換えるだけです。\n\n:::note warn\n注意点を整理します。\n\n- `sonarqube-scan-action` は **v8** が最新（先行記事の頃より進んでいます）\n- Quality Gate で CI を落とすには `sonarqube-quality-gate-action` を併用。\n  `scan-action` と組み合わせる場合 `scanMetadataReportFile` の指定は不要\n- **セルフホストの SonarQube は GitHub Actions ランナーから到達できる必要があります。**\n  社内 LAN に置くなら self-hosted runner が必要です\n- Community Build で PR 単位の結果を見たいなら Developer Edition 以上が必要\n:::\n---\n\n## 10. SonarQube は「どこまで」見ているのか\n\n「静的解析」と一括りにされますが、実際には深さに段階があります。ここを理解しておくと、\n何を SonarQube に任せられて、何は別のツールが必要かの判断ができます。\n\n### 解析の深さは 4 段階\n\n| 深さ | 見ているもの | 今回の実例 | Community Build |\n|---|---|---|---|\n| ① **構文** | コードの形 | `TODO` コメント、`except: pass` の形 | ✅ |\n| ② **型** | 型チェッカーを動かして型を解決 | 下記参照 | ✅ |\n| ③ **関数内のフロー** | 制御フロー・データフロー | `S1763` 到達不能コード、`S1854` 代入したが読まれない | ✅ |\n| ④ **関数/ファイルを越えた追跡** | taint analysis | （検出されなかった） | ❌ **Developer 以上** |\n\n②については、スキャンログが証拠になります。\n\n```\nINFO  Found 1 tsconfig.json file(s): [/usr/src/frontend/tsconfig.json]\nINFO  Creating TypeScript(6.0.3) program with configuration file .../tsconfig.json\nINFO  Analyzing 16 file(s) from tsconfig ... (76 total files in program)\n```\n\n**`tsconfig.json` を読んで TypeScript の型チェッカーを動かしています**（`76 total files` は\n`node_modules` の型定義を含む数）。\n\nそのおかげで面白い挙動がありました。`==` をわざと書いたのに**指摘されなかった**のです。\n\n```ts\nif (todo.title == '') {   // 指摘なし\n```\n\n型情報から `todo.title` が `string` と分かるため、**`==` でも `===` と同じ結果になると\n判断して黙った**わけです。単純な構文マッチではこうなりません。\n\n### 脆弱性は 3 種類に分けて考える\n\n「SonarQube は脆弱性を見るのか」への答えは **「種類による」** です。\n\n| 型 | 例 | Community Build | 補う手段 |\n|---|---|---|---|\n| **危険な API / 設定の誤用** | MD5、`DEBUG=True`、`verify=False`、秘密鍵のハードコード | ✅ **今回 8 件検出** | — |\n| **インジェクション系** | SQLi、XSS、コマンド注入、パストラバーサル | ❌ | Semgrep、bandit、eslint-plugin-security |\n| **依存ライブラリの既知脆弱性（CVE）** | `requirements.txt` の古いパッケージ | ❌ | **Dependabot**、`pip-audit`、`npm audit`、Trivy |\n\n1 つ目が検出できるのは「1 箇所を見れば判断できる」からです。`hashlib.md5(` と書いてあれば、\nそれだけで危険と分かります。\n\n2 つ目は入力から出力までの経路を追う必要があるため、taint analysis が要ります。\n\n3 つ目は特に見落としやすいので注意してください。スキャンログにこう出ます。\n\n```\nINFO  ------------- Gather SCA dependencies on project\nINFO  Dependency analysis skipped        ← スキップされている\n```\n:::note alert\nSCA（Software Composition Analysis）は **Advanced Security アドオンで Enterprise Edition\n以上**が必要です。**Community Build では依存ライブラリの CVE を一切検出しません。**\n:::\n:::note warn\n### 見ないもの\n\n- **実行時の動作** — 静的解析なのでコードは動かしません\n- **テストが正しいか** — テストは実行せず、カバレッジレポートを読むだけです\n  （だから「アサーションの無いテスト」は構文から `S2699` で指摘されました）\n- **性能** — 実測しないので遅いコードは分かりません\n:::\n---\n\n## 11. Community Build でどこまでできるか\n\n実際に触って分かった線引きです。\n\n:::note\n**できたこと**\n\n- Python / TypeScript / Vue(SFC) / CSS / HTML の解析\n- カバレッジの取り込み（Cobertura XML / LCOV）\n- 重複コードの検出（コピペ関数を `S4144` と Duplications 10.9% で検出）\n- 認知的複雑度の定量化（28 → 上限 15 という形で出る）\n- Quality Gate のカスタマイズと CI での判定\n- Web API による初期化・結果取得・誤検知処理の自動化\n:::\n:::note alert\n**できなかったこと（Developer Edition 以上が必要）**\n\n- ブランチ解析・PR デコレーション\n- モノレポとしての一元管理（2 プロジェクトに分けて回避）\n- taint analysis（SQL インジェクション、パストラバーサル等のデータフロー追跡）\n:::\n:::note alert\n**できなかったこと（Enterprise Edition ＋ Advanced Security アドオンが必要）**\n\n- **SCA（依存ライブラリの既知脆弱性・ライセンス違反の検出）**\n  … ログに `Dependency analysis skipped` と出て、`requirements.txt` /\n  `package.json` は一切チェックされない\n:::\n社内展開では、まず Community Build で「複雑度・重複・カバレッジの可視化」を回し、\nPR 連携やインジェクション検出が必要になった段階で有償版を検討する、\nという順序が現実的だと思います。\n\nなお日本語化は、クレスコさんの [SonarQube Japanese Pack](https://qiita.com/amoriki/items/b512c0ced7b7cb34fef2) を\n`extensions/plugins` に置いて再起動する形です（今回は英語 UI のまま検証しました。\nプラグインはバージョン対応の確認が必要です）。\n\n---\n\n## まとめ\n\n後編で押さえた要点です。\n\n1. **初回スキャンで Quality Gate は素通りする。** `Sonar way` の条件は 4 つ全部が\n   New Code 側。既存コードを評価したいなら **Overall Code の条件を足して再解析する**\n2. **`sonar.projectVersion` を固定すると New Code は永久に空のまま。**\n   区切りはスキャン回数ではなく**バージョンが変わったかどうか**で決まる。\n   CI で注入するか、New Code の定義を `Number of days` /\n   `Specific analysis` に変える。**git 管理下でないと行レベルの精度も落ちる**\n3. **反映のタイミングが操作によって違う**\n\n   | 操作 | 再解析が必要か |\n   |---|---|\n   | Quality Gate の条件を変更 | **必要** |\n   | Quality Profile のルールを変更 | **必要** |\n   | 指摘を False Positive にする | **不要（即時）** |\n\n3. **`v-html` や `console.log` は検出されない。** 原因は「ルールが存在しない」と\n   「存在するが無効」の 2 パターンで、後者は Quality Profile をコピーして有効化できる\n4. **ESLint / ruff と併用する前提で設計する。** SonarQube の強みは、リンターが見ない\n   **認知的複雑度・重複・カバレッジの定量化と、推移の可視化**\n5. 日本語コメントは `S125` で誤検知される。**コードを歪めて回避せず、\n   理由を添えて False Positive で解決する**\n6. **脆弱性は「設定・API の誤用」までしか見ない。** インジェクション系は\n   Developer Edition 以上、依存ライブラリの CVE は Enterprise + Advanced Security が必要\n\n### 社内に入れるときの現実的な順序\n\n前後編を通して検証した結果、この順序を勧めます。\n\n1. **まず Community Build で「可視化」だけ始める** — Quality Gate は既定の `Sonar way`\n   （New Code のみ）のままにして、既存の負債では落とさない\n2. **新規に書くコードだけきれいに保つ**（Clean as You Code）。これが SonarQube の\n   設計思想に沿った使い方で、チームの抵抗も少ない\n3. **既存コードの現状を把握したいときだけ** Overall Code 条件を足した別ゲートを作り、\n   レポート目的で使う\n4. **足りない部分は他ツールで埋める** — `v-html` は ESLint、依存の CVE は DependabotやTrivy\n5. PR 連携やインジェクション検出が必要になった段階で**有償版を検討する**\n\n### [検証環境一式](https://github.com/Mutsuki-Y/sonarqube-vue3-drf-sandbox)\n\nDocker Compose / Makefile / 自動化スクリプト / 指摘を仕込んだアプリは、\nそのまま動く形でまとめてあります。`make all` だけで起動から解析結果の表示まで通ります。\n\n```bash\ncp .env.example .env\nmake all\n```\n","coediting":false,"comments_count":0,"created_at":"2026-09-21T23:32:09+09:00","group":null,"id":"b234a01ea1dbbd07fd11","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"SonarQube","versions":[]},{"name":"Docker","versions":[]},{"name":"Vue.js","versions":[]},{"name":"django-rest-framework","versions":[]}],"title":"Vue3 + DRF に SonarQube を入れる（後編）— 指摘の読み方と Quality Gate の落とし穴","updated_at":"2026-09-21T23:32:09+09:00","url":"https://qiita.com/mukku928/items/b234a01ea1dbbd07fd11","user":{"description":null,"facebook_id":null,"followees_count":3,"followers_count":1,"github_login_name":null,"id":"mukku928","items_count":11,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":3884367,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocITD4x93F7dmTULUMUig_yiEmzTkHDU6aw2AQPuW_CCsjiYF29C=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:45\"\u003e\n\u003cspan id=\"この記事の対象と書いた理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E5%AF%BE%E8%B1%A1%E3%81%A8%E6%9B%B8%E3%81%84%E3%81%9F%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事の対象と、書いた理由\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-4:109\"\u003eVue3 + Django REST Framework の構成でアプリを作る案件によく入るのですが、\u003cbr\u003e\n静的解析は ESLint / ruff で止まっていて、SonarQube は名前しか知らない状態でした。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"6:1-13:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"7:1-7:69\"\u003e先行記事を読むと、次のところまでは分かります。\u003c/p\u003e\n\u003cul data-sourcepos=\"9:1-12:116\"\u003e\n\u003cli data-sourcepos=\"9:1-10:103\"\u003e\n\u003ca href=\"https://zenn.dev/siva_dev/articles/sonarqube-onboarding\" rel=\"nofollow noopener\" target=\"_blank\"\u003eSonarQube をオンボーディングする（Zenn）\u003c/a\u003e\u003cbr\u003e\n… \u003ccode\u003edocker run\u003c/code\u003e での起動、GitHub App 連携、Secrets 名（\u003ccode\u003eSONAR_TOKEN\u003c/code\u003e / \u003ccode\u003eSONAR_HOST_URL\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-12:116\"\u003e\n\u003ca href=\"https://qiita.com/amoriki/items/b512c0ced7b7cb34fef2\" id=\"reference-fe0749cd696c0ada9f6c\"\u003eSonarQube とは（Qiita）\u003c/a\u003e\u003cbr\u003e\n… 製品ラインナップ、Community Build / Developer / Enterprise の機能差、日本語化プラグイン\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"15:1-15:121\"\u003e一方で、\u003cstrong\u003e自分のプロジェクトに当てはめるときに必要な情報\u003c/strong\u003eが見つかりませんでした。\u003c/p\u003e\n\u003cul data-sourcepos=\"17:1-20:0\"\u003e\n\u003cli data-sourcepos=\"17:1-17:74\"\u003e\n\u003ccode\u003esonar-project.properties\u003c/code\u003e に Vue3 / DRF それぞれ何を書くのか\u003c/li\u003e\n\u003cli data-sourcepos=\"18:1-18:74\"\u003e\n\u003ccode\u003evitest\u003c/code\u003e と \u003ccode\u003ecoverage.py\u003c/code\u003e のカバレッジをどう食わせるのか\u003c/li\u003e\n\u003cli data-sourcepos=\"19:1-20:0\"\u003e出てきた指摘をどう読み、Quality Gate をどう運用するのか\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"21:1-23:102\"\u003eそこで、\u003cstrong\u003eホストに Node / Python / Java / sonar-scanner を一切入れず\u003c/strong\u003e、\u003cbr\u003e\nDocker だけで検証環境を作って一通り動かしました。この記事はその一次情報です。\u003cbr\u003e\n数値・ログ・エンドポイントはすべて実際に動かした結果を載せています。\u003c/p\u003e\n\u003cp data-sourcepos=\"25:1-25:108\"\u003e検証環境: SonarQube Community Build \u003cstrong\u003e26.9.0.129388\u003c/strong\u003e / Docker 28.2.2 / Compose v2.37.1 / macOS (x86_64)\u003c/p\u003e\n\u003ch4 data-sourcepos=\"26:1-26:82\"\u003e\n\u003cspan id=\"ソースコード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ca href=\"https://github.com/Mutsuki-Y/sonarqube-vue3-drf-sandbox\" rel=\"nofollow noopener\" target=\"_blank\"\u003eソースコード\u003c/a\u003e\n\u003c/h4\u003e\n\u003ch2 data-sourcepos=\"28:1-28:52\"\u003e\n\u003cspan id=\"前提sonarqube-とは何をするツールか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E6%8F%90sonarqube-%E3%81%A8%E3%81%AF%E4%BD%95%E3%82%92%E3%81%99%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前提：SonarQube とは何をするツールか\u003c/h2\u003e\n\u003cp data-sourcepos=\"30:1-30:138\"\u003e一言でいうと \u003cstrong\u003e「コードを読んで問題を指摘し、品質を数値にしてサーバーに溜めるツール」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"32:1-32:62\"\u003eESLint / ruff と比べると役割の違いが見えます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"34:1-40:79\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"34:1-34:31\"\u003e\n\u003cth data-sourcepos=\"34:2-34:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"34:4-34:18\"\u003eESLint / ruff\u003c/th\u003e\n\u003cth data-sourcepos=\"34:20-34:30\"\u003eSonarQube\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"36:1-36:116\"\u003e\n\u003ctd data-sourcepos=\"36:2-36:15\"\u003e動く場所\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:17-36:57\"\u003e手元（保存時・コミット時）\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:59-36:115\"\u003e\n\u003cstrong\u003eサーバー\u003c/strong\u003e（解析結果を送信して蓄積）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"37:1-37:137\"\u003e\n\u003ctd data-sourcepos=\"37:2-37:15\"\u003e見るもの\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:17-37:57\"\u003e文法・スタイル・一部のバグ\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:59-37:136\"\u003e左記＋\u003cstrong\u003eカバレッジ・重複・複雑度・セキュリティ設定\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"38:1-38:110\"\u003e\n\u003ctd data-sourcepos=\"38:2-38:15\"\u003e結果の形\u003c/td\u003e\n\u003ctd data-sourcepos=\"38:17-38:45\"\u003eその場のエラー一覧\u003c/td\u003e\n\u003ctd data-sourcepos=\"38:47-38:109\"\u003e\u003cstrong\u003e時系列のグラフ、プロジェクト横断の比較\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"39:1-39:106\"\u003e\n\u003ctd data-sourcepos=\"39:2-39:9\"\u003e強み\u003c/td\u003e\n\u003ctd data-sourcepos=\"39:11-39:42\"\u003e即時、自動修正できる\u003c/td\u003e\n\u003ctd data-sourcepos=\"39:44-39:105\"\u003e推移が見える、CI で合否判定、チームで共有\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"40:1-40:79\"\u003e\n\u003ctd data-sourcepos=\"40:2-40:9\"\u003e弱み\u003c/td\u003e\n\u003ctd data-sourcepos=\"40:11-40:33\"\u003e履歴が残らない\u003c/td\u003e\n\u003ctd data-sourcepos=\"40:35-40:78\"\u003e即時性がない、サーバーが必要\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"42:1-44:105\"\u003e\u003cstrong\u003e置き換えではなく併用\u003c/strong\u003eです。実際この検証でも、\u003ccode\u003ev-html\u003c/code\u003e の XSS は SonarQube では\u003cbr\u003e\n拾えず ESLint の担当でした（後編で詳述）。逆に「認知的複雑度が 28 で上限 15 を超えている」\u003cbr\u003e\n「重複が 10.9%」のような\u003cstrong\u003e定量化と推移の可視化\u003c/strong\u003eは SonarQube の得意分野です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"46:1-46:40\"\u003e\n\u003cspan id=\"覚えておく用語はこれだけ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A6%9A%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8F%E7%94%A8%E8%AA%9E%E3%81%AF%E3%81%93%E3%82%8C%E3%81%A0%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e覚えておく用語はこれだけ\u003c/h3\u003e\n\u003cp data-sourcepos=\"48:1-48:55\"\u003e\u003cstrong\u003eIssue（指摘）\u003c/strong\u003e は 3 種類に分かれます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"50:1-54:106\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"50:1-50:37\"\u003e\n\u003cth data-sourcepos=\"50:2-50:9\"\u003e種別\u003c/th\u003e\n\u003cth data-sourcepos=\"50:11-50:18\"\u003e意味\u003c/th\u003e\n\u003cth data-sourcepos=\"50:20-50:36\"\u003e今回出た例\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"52:1-52:110\"\u003e\n\u003ctd data-sourcepos=\"52:2-52:10\"\u003e\u003cstrong\u003eBug\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:12-52:55\"\u003e動作が壊れる・意図通りでない\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:57-52:109\"\u003e\n\u003ccode\u003ereturn\u003c/code\u003e 後の到達不能コード、自己代入\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"53:1-53:116\"\u003e\n\u003ctd data-sourcepos=\"53:2-53:20\"\u003e\u003cstrong\u003eVulnerability\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"53:22-53:53\"\u003eセキュリティ上の問題\u003c/td\u003e\n\u003ctd data-sourcepos=\"53:55-53:115\"\u003eMD5 でハッシュ化、\u003ccode\u003eSECRET_KEY\u003c/code\u003e のハードコード\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"54:1-54:106\"\u003e\n\u003ctd data-sourcepos=\"54:2-54:17\"\u003e\u003cstrong\u003eCode Smell\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"54:19-54:47\"\u003e動くが保守しづらい\u003c/td\u003e\n\u003ctd data-sourcepos=\"54:49-54:105\"\u003e複雑度が高い、\u003ccode\u003eTODO\u003c/code\u003e の放置、コピペ関数\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"56:1-57:90\"\u003e\u003cstrong\u003eRating\u003c/strong\u003e は A〜E の 5 段階評価で、上の 3 種類にそれぞれ対応します\u003cbr\u003e\n（Reliability ← Bug、Security ← Vulnerability、Maintainability ← Code Smell）。\u003c/p\u003e\n\u003cp data-sourcepos=\"59:1-59:98\"\u003e\u003cstrong\u003eQuality Gate\u003c/strong\u003e は合否判定のルールセットで、CI を止める根拠になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"61:1-63:117\"\u003e\u003cstrong\u003eNew Code\u003c/strong\u003e は「最近書いたコード」。SonarQube の設計思想は \u003cem\u003eClean as You Code\u003c/em\u003e\u003cbr\u003e\n（既存の負債は据え置き、\u003cstrong\u003eこれから書く分だけ\u003c/strong\u003eきれいに保つ）で、\u003cbr\u003e\n\u003cstrong\u003e既定の判定条件は全部この New Code 側を見ています。\u003c/strong\u003e ここが後編の山場になります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"65:1-65:27\"\u003e\n\u003cspan id=\"結論先に成果\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B5%90%E8%AB%96%E5%85%88%E3%81%AB%E6%88%90%E6%9E%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e結論（先に成果）\u003c/h2\u003e\n\u003cp data-sourcepos=\"67:1-68:54\"\u003e同一リポジトリに 2 プロジェクトを登録し、片方は指摘を修正、もう片方は未修正のまま\u003cbr\u003e\n並べました。最終的にこうなりました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"70:1-78:42\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"70:1-70:61\"\u003e\n\u003cth data-sourcepos=\"70:2-70:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"70:4-70:30\"\u003ebackend (DRF・修正後)\u003c/th\u003e\n\u003cth data-sourcepos=\"70:32-70:60\"\u003efrontend (Vue3・未修正)\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"72:1-72:37\"\u003e\n\u003ctd data-sourcepos=\"72:2-72:15\"\u003eQuality Gate\u003c/td\u003e\n\u003ctd data-sourcepos=\"72:17-72:24\"\u003e\u003cstrong\u003eOK\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"72:26-72:36\"\u003e\u003cstrong\u003eERROR\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"73:1-73:16\"\u003e\n\u003ctd data-sourcepos=\"73:2-73:7\"\u003eBugs\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:9-73:11\"\u003e0\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:13-73:15\"\u003e3\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"74:1-74:27\"\u003e\n\u003ctd data-sourcepos=\"74:2-74:18\"\u003eVulnerabilities\u003c/td\u003e\n\u003ctd data-sourcepos=\"74:20-74:22\"\u003e0\u003c/td\u003e\n\u003ctd data-sourcepos=\"74:24-74:26\"\u003e1\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"75:1-75:24\"\u003e\n\u003ctd data-sourcepos=\"75:2-75:14\"\u003eCode Smells\u003c/td\u003e\n\u003ctd data-sourcepos=\"75:16-75:18\"\u003e0\u003c/td\u003e\n\u003ctd data-sourcepos=\"75:20-75:23\"\u003e22\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"76:1-76:28\"\u003e\n\u003ctd data-sourcepos=\"76:2-76:11\"\u003eCoverage\u003c/td\u003e\n\u003ctd data-sourcepos=\"76:13-76:19\"\u003e78.9%\u003c/td\u003e\n\u003ctd data-sourcepos=\"76:21-76:27\"\u003e52.0%\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"77:1-77:31\"\u003e\n\u003ctd data-sourcepos=\"77:2-77:15\"\u003eDuplications\u003c/td\u003e\n\u003ctd data-sourcepos=\"77:17-77:22\"\u003e0.0%\u003c/td\u003e\n\u003ctd data-sourcepos=\"77:24-77:30\"\u003e10.9%\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"78:1-78:42\"\u003e\n\u003ctd data-sourcepos=\"78:2-78:25\"\u003eReliability / Security\u003c/td\u003e\n\u003ctd data-sourcepos=\"78:27-78:33\"\u003eA / A\u003c/td\u003e\n\u003ctd data-sourcepos=\"78:35-78:41\"\u003eC / C\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"80:1-80:85\"\u003eそして、\u003cstrong\u003eつまずいた点が 5 つ\u003c/strong\u003eありました。ここが本題です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"82:1-88:114\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"82:1-82:35\"\u003e\n\u003cth data-sourcepos=\"82:2-82:4\"\u003e#\u003c/th\u003e\n\u003cth data-sourcepos=\"82:6-82:19\"\u003eつまずき\u003c/th\u003e\n\u003cth data-sourcepos=\"82:21-82:34\"\u003e扱う記事\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"84:1-84:130\"\u003e\n\u003ctd data-sourcepos=\"84:2-84:4\"\u003e1\u003c/td\u003e\n\u003ctd data-sourcepos=\"84:6-84:98\"\u003eディスク使用率 95% で SonarQube が起動しない（Elasticsearch の watermark）\u003c/td\u003e\n\u003ctd data-sourcepos=\"84:100-84:129\"\u003e\u003cstrong\u003e前編（この記事）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"85:1-85:114\"\u003e\n\u003ctd data-sourcepos=\"85:2-85:4\"\u003e2\u003c/td\u003e\n\u003ctd data-sourcepos=\"85:6-85:100\"\u003eパスワード変更 API のパスが先行記事と違う（\u003ccode\u003e/api/users/change_password\u003c/code\u003e）\u003c/td\u003e\n\u003ctd data-sourcepos=\"85:102-85:113\"\u003e\u003cstrong\u003e前編\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"86:1-86:119\"\u003e\n\u003ctd data-sourcepos=\"86:2-86:4\"\u003e3\u003c/td\u003e\n\u003ctd data-sourcepos=\"86:6-86:105\"\u003e\n\u003cstrong\u003eカバレッジが 0% になる罠\u003c/strong\u003e（レポートのパスと \u003ccode\u003eprojectBaseDir\u003c/code\u003e の不一致）\u003c/td\u003e\n\u003ctd data-sourcepos=\"86:107-86:118\"\u003e\u003cstrong\u003e前編\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"87:1-87:113\"\u003e\n\u003ctd data-sourcepos=\"87:2-87:4\"\u003e4\u003c/td\u003e\n\u003ctd data-sourcepos=\"87:6-87:103\"\u003e初回スキャンでは Quality Gate が必ず素通りする（条件が全部 New Code 側）\u003c/td\u003e\n\u003ctd data-sourcepos=\"87:105-87:112\"\u003e後編\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"88:1-88:114\"\u003e\n\u003ctd data-sourcepos=\"88:2-88:4\"\u003e5\u003c/td\u003e\n\u003ctd data-sourcepos=\"88:6-88:104\"\u003e仕込んだのに検出されない指摘がある（\u003ccode\u003ev-html\u003c/code\u003e、\u003ccode\u003econsole.log\u003c/code\u003e、\u003ccode\u003eeval\u003c/code\u003e など）\u003c/td\u003e\n\u003ctd data-sourcepos=\"88:106-88:113\"\u003e後編\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"90:1-90:24\"\u003e\n\u003cspan id=\"この記事の構成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E6%A7%8B%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事の構成\u003c/h2\u003e\n\u003cp data-sourcepos=\"92:1-93:75\"\u003e\u003cstrong\u003e前編（この記事）で「動いて数字が出る状態」まで到達します。\u003c/strong\u003e\u003cbr\u003e\n必要なところだけ読んでも分かるように書いています。\u003c/p\u003e\n\u003ctable data-sourcepos=\"95:1-100:92\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"95:1-95:16\"\u003e\n\u003cth data-sourcepos=\"95:2-95:6\"\u003e章\u003c/th\u003e\n\u003cth data-sourcepos=\"95:8-95:15\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"97:1-97:105\"\u003e\n\u003ctd data-sourcepos=\"97:2-97:4\"\u003e1\u003c/td\u003e\n\u003ctd data-sourcepos=\"97:6-97:104\"\u003e\n\u003cstrong\u003e環境構築\u003c/strong\u003e — Compose 全文、PostgreSQL が必要な理由、\u003ccode\u003escanner\u003c/code\u003e / \u003ccode\u003eapi\u003c/code\u003e の役割\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"98:1-98:97\"\u003e\n\u003ctd data-sourcepos=\"98:2-98:4\"\u003e2\u003c/td\u003e\n\u003ctd data-sourcepos=\"98:6-98:96\"\u003e\n\u003cstrong\u003e初期セットアップの自動化\u003c/strong\u003e — パスワード変更 API のパスに注意\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"99:1-99:106\"\u003e\n\u003ctd data-sourcepos=\"99:2-99:4\"\u003e3\u003c/td\u003e\n\u003ctd data-sourcepos=\"99:6-99:105\"\u003e\n\u003cstrong\u003e解析設定\u003c/strong\u003e — Vue3 / DRF それぞれの \u003ccode\u003esonar-project.properties\u003c/code\u003e、スキャンの中身\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"100:1-100:92\"\u003e\n\u003ctd data-sourcepos=\"100:2-100:4\"\u003e4\u003c/td\u003e\n\u003ctd data-sourcepos=\"100:6-100:91\"\u003e\n\u003cstrong\u003eカバレッジ連携\u003c/strong\u003e — 0% になる罠、手元の数字と合わない理由\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"102:1-102:150\"\u003e\u003ca href=\"https://qiita.com/mukku928/items/b234a01ea1dbbd07fd11\" id=\"reference-2d5345fd1618b9aeb467\"\u003e後編\u003c/a\u003eでは「出てきた結果をどう読み、どう運用するか」を扱います。\u003c/p\u003e\n\u003ctable data-sourcepos=\"104:1-112:79\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"104:1-104:16\"\u003e\n\u003cth data-sourcepos=\"104:2-104:6\"\u003e章\u003c/th\u003e\n\u003cth data-sourcepos=\"104:8-104:15\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"106:1-106:62\"\u003e\n\u003ctd data-sourcepos=\"106:2-106:4\"\u003e5\u003c/td\u003e\n\u003ctd data-sourcepos=\"106:6-106:61\"\u003e\n\u003cstrong\u003e検出結果の一覧\u003c/strong\u003e — 実際に何が出たか\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"107:1-107:117\"\u003e\n\u003ctd data-sourcepos=\"107:2-107:4\"\u003e6\u003c/td\u003e\n\u003ctd data-sourcepos=\"107:6-107:116\"\u003e\n\u003cstrong\u003e出なかった指摘の原因\u003c/strong\u003e — 3 パターンの切り分けと Quality Profile のカスタマイズ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"108:1-108:81\"\u003e\n\u003ctd data-sourcepos=\"108:2-108:4\"\u003e7\u003c/td\u003e\n\u003ctd data-sourcepos=\"108:6-108:80\"\u003e\n\u003cstrong\u003eQuality Gate が初回素通りする正体\u003c/strong\u003e — New Code とは何か\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"109:1-109:92\"\u003e\n\u003ctd data-sourcepos=\"109:2-109:4\"\u003e8\u003c/td\u003e\n\u003ctd data-sourcepos=\"109:6-109:91\"\u003e\n\u003cstrong\u003eFail → 修正 → Pass の実演\u003c/strong\u003e — 誤検知（False Positive）の扱いも\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"110:1-110:33\"\u003e\n\u003ctd data-sourcepos=\"110:2-110:4\"\u003e9\u003c/td\u003e\n\u003ctd data-sourcepos=\"110:6-110:32\"\u003e\u003cstrong\u003eGitHub Actions 連携\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"111:1-111:90\"\u003e\n\u003ctd data-sourcepos=\"111:2-111:5\"\u003e10\u003c/td\u003e\n\u003ctd data-sourcepos=\"111:7-111:89\"\u003e\n\u003cstrong\u003e静的解析の深さ\u003c/strong\u003e — どこまで見るのか、脆弱性検出の限界\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"112:1-112:79\"\u003e\n\u003ctd data-sourcepos=\"112:2-112:5\"\u003e11\u003c/td\u003e\n\u003ctd data-sourcepos=\"112:7-112:78\"\u003e\n\u003cstrong\u003eCommunity Build の線引き\u003c/strong\u003e — 有償版が必要になる境界\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"114:1-115:168\"\u003e\u003ccode\u003eS106\u003c/code\u003e のような個別ルールの解説（何を問題としているのか・どう直すのか）は、\u003cbr\u003e\n分量が多いので\u003ca href=\"https://qiita.com/mukku928/items/8195667ef112d54b6e0b\" id=\"reference-92fbbba05c62ef5fdb2b\"\u003e別記事「SonarQube ルール早見表（Vue3 + DRF 編）」\u003c/a\u003eにまとめています。\u003c/p\u003e\n\u003chr data-sourcepos=\"117:1-118:0\"\u003e\n\u003ch2 data-sourcepos=\"119:1-119:38\"\u003e\n\u003cspan id=\"1-環境構築docker-compose\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89docker-compose\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 環境構築（Docker Compose）\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"121:1-121:53\"\u003e\n\u003cspan id=\"イメージタグは-community-を明示する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%82%BF%E3%82%B0%E3%81%AF-community-%E3%82%92%E6%98%8E%E7%A4%BA%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eイメージタグは \u003ccode\u003ecommunity\u003c/code\u003e を明示する\u003c/h3\u003e\n\u003cp data-sourcepos=\"123:1-123:118\"\u003e先行記事では \u003ccode\u003esonarqube:latest\u003c/code\u003e を使っていますが、\u003cstrong\u003e今は \u003ccode\u003elatest\u003c/code\u003e を避けた方が安全\u003c/strong\u003eです。\u003c/p\u003e\n\u003cp data-sourcepos=\"125:1-126:75\"\u003e調べた時点で \u003ccode\u003elatest\u003c/code\u003e と \u003ccode\u003ecommunity\u003c/code\u003e は同じイメージ（\u003ccode\u003e26.9.0.129388-community\u003c/code\u003e、LGPL v3）を\u003cbr\u003e\n指していましたが、バージョン体系が分岐しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"128:1-130:0\"\u003e\n\u003cli data-sourcepos=\"128:1-128:67\"\u003eCommunity Build（無償）: \u003ccode\u003e26.x\u003c/code\u003e 系 → \u003ccode\u003esonarqube:community\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"129:1-130:0\"\u003e商用エディション: \u003ccode\u003e2026.x\u003c/code\u003e 系 → \u003ccode\u003esonarqube:developer\u003c/code\u003e / \u003ccode\u003eenterprise\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"131:1-131:103\"\u003e将来 \u003ccode\u003elatest\u003c/code\u003e がどちらを指すか保証がないので、\u003ccode\u003ecommunity\u003c/code\u003e と書いておきます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"133:1-133:22\"\u003e\n\u003cspan id=\"docker-composeyml\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#docker-composeyml\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003edocker-compose.yml\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"135:1-196:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eqube-test\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003eservices\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edb\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eimage\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003epostgres:17-alpine\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_USER\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonar\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_PASSWORD\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonar\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_DB\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonar\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003epostgres_data:/var/lib/postgresql/data\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ehealthcheck\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etest\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eCMD-SHELL\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003epg_isready\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e-U\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003esonar\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e-d\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003esonar\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n      \u003cspan class=\"na\"\u003einterval\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e10s\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etimeout\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e5s\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eretries\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\n\n  \u003cspan class=\"na\"\u003esonarqube\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eimage\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonarqube:community\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edepends_on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003edb\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"na\"\u003econdition\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eservice_healthy\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_JDBC_URL\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ejdbc:postgresql://db:5432/sonar\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_JDBC_USERNAME\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonar\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_JDBC_PASSWORD\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonar\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_ES_BOOTSTRAP_CHECKS_DISABLE\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003etrue\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eports\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e127.0.0.1:9000:9000\"\u003c/span\u003e     \u003cspan class=\"c1\"\u003e# LAN に晒さない\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003esonarqube_data:/opt/sonarqube/data\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003esonarqube_extensions:/opt/sonarqube/extensions\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003esonarqube_logs:/opt/sonarqube/logs\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003esonarqube_temp:/opt/sonarqube/temp\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etmpfs\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e/tmp:rw,noexec,nosuid,size=256m\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eread_only\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eulimits\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003enofile\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e{\u003c/span\u003e \u003cspan class=\"nv\"\u003esoft\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"nv\"\u003e65536\u003c/span\u003e\u003cspan class=\"pi\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003ehard\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"nv\"\u003e65536\u003c/span\u003e \u003cspan class=\"pi\"\u003e}\u003c/span\u003e\n\n  \u003cspan class=\"c1\"\u003e# 以下は profiles 付き。`up` では起動せず `run --rm` で使い捨て実行する\u003c/span\u003e\n  \u003cspan class=\"na\"\u003escanner\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eimage\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003esonarsource/sonar-scanner-cli:latest\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eprofiles\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003etools\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_HOST_URL\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ehttp://sonarqube:9000\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eSONAR_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${SONAR_TOKEN:-}\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e.:/usr/src\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003escanner_cache:/opt/sonar-scanner/.sonar/cache\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eworking_dir\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e/usr/src\u003c/span\u003e\n\n  \u003cspan class=\"na\"\u003efrontend\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eimage\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003enode:22-alpine\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eprofiles\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003etools\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e.:/usr/src\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003efrontend_node_modules:/usr/src/frontend/node_modules\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# ホストに出さない\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003enpm_cache:/root/.npm\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eworking_dir\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e/usr/src/frontend\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"198:1-198:30\"\u003eポイントは 3 つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"200:1-203:0\"\u003e\n\u003cli data-sourcepos=\"200:1-200:93\"\u003e\n\u003ccode\u003eread_only: true\u003c/code\u003e + \u003ccode\u003etmpfs\u003c/code\u003e で書き込み先を限定（公式の Compose 例に準拠）\u003c/li\u003e\n\u003cli data-sourcepos=\"201:1-201:42\"\u003e公開ポートを \u003ccode\u003e127.0.0.1\u003c/code\u003e に固定\u003c/li\u003e\n\u003cli data-sourcepos=\"202:1-203:0\"\u003e\n\u003cstrong\u003e全サービスでリポジトリルートを \u003ccode\u003e/usr/src\u003c/code\u003e にマウント\u003c/strong\u003e（後述のカバレッジ問題対策）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv data-sourcepos=\"204:1-241:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"205:1-205:89\"\u003e\n\u003cspan id=\"q-なぜ-postgresql-が必要なのかdocker-run-単発では駄目なのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q-%E3%81%AA%E3%81%9C-postgresql-%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%8Bdocker-run-%E5%8D%98%E7%99%BA%E3%81%A7%E3%81%AF%E9%A7%84%E7%9B%AE%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ. なぜ PostgreSQL が必要なのか（\u003ccode\u003edocker run\u003c/code\u003e 単発では駄目なのか）\u003c/h3\u003e\n\u003cp data-sourcepos=\"207:1-208:91\"\u003e先行記事のように \u003ccode\u003edocker run\u003c/code\u003e だけで起動すると、\u003cstrong\u003e組み込みの H2 データベース\u003c/strong\u003eが使われます。\u003cbr\u003e\n動くことは動きますが、\u003cstrong\u003e公式が「評価目的のみ」としています。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"210:1-210:61\"\u003eSonarQube は 2 種類のデータストアを使います。\u003c/p\u003e\n\u003ctable data-sourcepos=\"212:1-215:102\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"212:1-212:61\"\u003e\n\u003cth data-sourcepos=\"212:2-212:21\"\u003eデータストア\u003c/th\u003e\n\u003cth data-sourcepos=\"212:23-212:39\"\u003e何を持つか\u003c/th\u003e\n\u003cth data-sourcepos=\"212:41-212:60\"\u003eどこにあるか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"214:1-214:187\"\u003e\n\u003ctd data-sourcepos=\"214:2-214:31\"\u003e\u003cstrong\u003eリレーショナル DB\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"214:33-214:155\"\u003eプロジェクト、指摘の一覧、メトリクス、\u003cstrong\u003e解析履歴\u003c/strong\u003e、ユーザー、Quality Gate/Profile の設定\u003c/td\u003e\n\u003ctd data-sourcepos=\"214:157-214:186\"\u003e\u003cstrong\u003e外部に用意が必要\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"215:1-215:102\"\u003e\n\u003ctd data-sourcepos=\"215:2-215:16\"\u003eElasticsearch\u003c/td\u003e\n\u003ctd data-sourcepos=\"215:18-215:55\"\u003e検索・集計用インデックス\u003c/td\u003e\n\u003ctd data-sourcepos=\"215:57-215:101\"\u003e\n\u003cstrong\u003eコンテナに同梱\u003c/strong\u003e（用意不要）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"217:1-217:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F8bcb01d2-31ea-4ed8-aa06-7dfefda0a22a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b828cdfe13051f65931acb78723177b0\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F8bcb01d2-31ea-4ed8-aa06-7dfefda0a22a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b828cdfe13051f65931acb78723177b0\" alt=\"スクリーンショット 2026-09-21 19.52.09.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F8bcb01d2-31ea-4ed8-aa06-7dfefda0a22a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=6563d58366d6043e07d6234c96a04e62 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/8bcb01d2-31ea-4ed8-aa06-7dfefda0a22a.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"220:1-220:135\"\u003e前述のディスク不足で落ちたのが Elasticsearch だったのは、後者がコンテナ内で動いているからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"222:1-222:51\"\u003eH2 で運用した場合の問題はこれです。\u003c/p\u003e\n\u003col data-sourcepos=\"224:1-226:0\"\u003e\n\u003cli data-sourcepos=\"224:1-224:102\"\u003e\n\u003cstrong\u003eバージョンアップ時にデータを引き継げない\u003c/strong\u003e — 解析履歴が全部消える\u003c/li\u003e\n\u003cli data-sourcepos=\"225:1-226:0\"\u003e性能が出ない（プロジェクトが増えると顕著）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"227:1-229:30\"\u003e\u003cstrong\u003e「とりあえず触る」なら H2 で十分。「社内に置いて履歴を溜める」なら PostgreSQL が必須\u003c/strong\u003eです。\u003cbr\u003e\n繋ぎ方は環境変数 3 つだけで、対応 DB は PostgreSQL / Oracle / SQL Server です\u003cbr\u003e\n（MySQL は廃止済み）。\u003c/p\u003e\n\u003cp data-sourcepos=\"231:1-231:108\"\u003eボリューム 5 つの役割も押さえておくと、どれを消すと何を失うか分かります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"233:1-239:50\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"233:1-233:52\"\u003e\n\u003cth data-sourcepos=\"233:2-233:18\"\u003eボリューム\u003c/th\u003e\n\u003cth data-sourcepos=\"233:20-233:27\"\u003e中身\u003c/th\u003e\n\u003cth data-sourcepos=\"233:29-233:51\"\u003e消すと失うもの\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"235:1-235:93\"\u003e\n\u003ctd data-sourcepos=\"235:2-235:18\"\u003e\u003ccode\u003epostgres_data\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"235:20-235:58\"\u003e\u003cstrong\u003e解析結果・履歴・全設定\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"235:60-235:92\"\u003e\u003cstrong\u003e全部（これが本体）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"236:1-236:79\"\u003e\n\u003ctd data-sourcepos=\"236:2-236:19\"\u003e\u003ccode\u003esonarqube_data\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"236:21-236:57\"\u003eElasticsearch のインデックス\u003c/td\u003e\n\u003ctd data-sourcepos=\"236:59-236:78\"\u003e再構築される\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"237:1-237:107\"\u003e\n\u003ctd data-sourcepos=\"237:2-237:25\"\u003e\u003ccode\u003esonarqube_extensions\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"237:27-237:88\"\u003eプラグイン（日本語 Pack を入れるならここ）\u003c/td\u003e\n\u003ctd data-sourcepos=\"237:90-237:106\"\u003eプラグイン\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"238:1-238:38\"\u003e\n\u003ctd data-sourcepos=\"238:2-238:19\"\u003e\u003ccode\u003esonarqube_logs\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"238:21-238:28\"\u003eログ\u003c/td\u003e\n\u003ctd data-sourcepos=\"238:30-238:37\"\u003eログ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"239:1-239:50\"\u003e\n\u003ctd data-sourcepos=\"239:2-239:19\"\u003e\u003ccode\u003esonarqube_temp\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"239:21-239:40\"\u003e一時ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"239:42-239:49\"\u003eなし\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"243:1-286:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"244:1-244:63\"\u003e\n\u003cspan id=\"q-scanner-と-api-は何をするコンテナなのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q-scanner-%E3%81%A8-api-%E3%81%AF%E4%BD%95%E3%82%92%E3%81%99%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ. \u003ccode\u003escanner\u003c/code\u003e と \u003ccode\u003eapi\u003c/code\u003e は何をするコンテナなのか\u003c/h3\u003e\n\u003cp data-sourcepos=\"246:1-247:84\"\u003e\u003cstrong\u003eここは SonarQube の構造理解に直結します。\u003c/strong\u003e\u003cbr\u003e\nSonarQube は \u003cstrong\u003e「サーバー」と「スキャナ」に分かれています。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"249:1-257:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e  ソースコード\n      │ 読む\n      ▼\n┌──────────────┐   解析結果を送信   ┌──────────────┐        ┌──────┐\n│   scanner    │ ─────────────────\u0026gt; │  sonarqube   │ ─────\u0026gt; │  db  │\n│  (解析する)   │    HTTP + Token    │ (保存・表示)  │        └──────┘\n└──────────────┘                    └──────────────┘\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"259:1-259:109\"\u003e\u003cstrong\u003e解析処理はスキャナ側で走ります。サーバーはソースコードを解析しません。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"261:1-263:52\"\u003eこれが分かると CI 連携が繋がります。GitHub Actions で \u003ccode\u003esonarqube-scan-action\u003c/code\u003e を\u003cbr\u003e\n動かすのは、\u003cstrong\u003eCI ランナー上でスキャナを実行してサーバーに送っている\u003c/strong\u003eだけで、\u003cbr\u003e\nローカルの \u003ccode\u003emake scan\u003c/code\u003e と同じことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"265:1-266:51\"\u003e\u003ccode\u003esonar-scanner-cli\u003c/code\u003e は \u003cstrong\u003eJava 製\u003c/strong\u003eです。コンテナにする最大の理由がこれで、\u003cbr\u003e\n\u003cstrong\u003eホストに JDK を入れずに済みます。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"268:1-269:128\"\u003e一方 \u003ccode\u003eapi\u003c/code\u003e（curl コンテナ）は \u003cstrong\u003eSonarQube の仕組みとは無関係で、こちらの便宜のための箱\u003c/strong\u003eです。\u003cbr\u003e\nWeb API を叩くのに使っており、ホストに curl があるかどうかにも依存させないためのものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"271:1-271:51\"\u003eサービスは 2 種類に分かれています。\u003c/p\u003e\n\u003ctable data-sourcepos=\"273:1-276:133\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"273:1-273:34\"\u003e\n\u003cth data-sourcepos=\"273:2-273:15\"\u003eサービス\u003c/th\u003e\n\u003cth data-sourcepos=\"273:17-273:24\"\u003e役割\u003c/th\u003e\n\u003cth data-sourcepos=\"273:26-273:33\"\u003e種類\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"275:1-275:74\"\u003e\n\u003ctd data-sourcepos=\"275:2-275:21\"\u003e\n\u003ccode\u003edb\u003c/code\u003e / \u003ccode\u003esonarqube\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"275:23-275:60\"\u003eサーバーとして動き続ける\u003c/td\u003e\n\u003ctd data-sourcepos=\"275:62-275:73\"\u003e\u003cstrong\u003e常駐\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"276:1-276:133\"\u003e\n\u003ctd data-sourcepos=\"276:2-276:45\"\u003e\n\u003ccode\u003escanner\u003c/code\u003e / \u003ccode\u003ebackend\u003c/code\u003e / \u003ccode\u003efrontend\u003c/code\u003e / \u003ccode\u003eapi\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"276:47-276:90\"\u003eコマンドを 1 回実行して消える\u003c/td\u003e\n\u003ctd data-sourcepos=\"276:92-276:132\"\u003e使い捨て（\u003ccode\u003eprofiles: [\"tools\"]\u003c/code\u003e）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"278:1-280:101\"\u003e\u003ccode\u003eprofiles: [\"tools\"]\u003c/code\u003e が付いていると \u003cstrong\u003e\u003ccode\u003edocker compose up\u003c/code\u003e では起動しません。\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003edocker compose run --rm scanner ...\u003c/code\u003e と明示的に呼んだときだけ動きます。\u003cbr\u003e\n\u003cstrong\u003eSonarQube はサーバーだが、スキャナやテスト実行はコマンドだから\u003c/strong\u003eです。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"282:1-284:61\"\u003e\n\u003cp data-sourcepos=\"282:3-284:61\"\u003e\u003cstrong\u003e後片付けの注意\u003c/strong\u003e: \u003ccode\u003eprofiles\u003c/code\u003e 付きサービスのボリュームは \u003ccode\u003edocker compose down -v\u003c/code\u003e の\u003cbr\u003e\n対象外です。\u003ccode\u003enode_modules\u003c/code\u003e や scanner キャッシュまで消すには\u003cbr\u003e\n\u003ccode\u003edocker compose --profile tools down -v\u003c/code\u003e が必要です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"288:1-320:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"289:1-289:55\"\u003e\n\u003cspan id=\"つまずき-ディスク-95-で起動しない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D-%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF-95-%E3%81%A7%E8%B5%B7%E5%8B%95%E3%81%97%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまずき① ディスク 95% で起動しない\u003c/h3\u003e\n\u003cp data-sourcepos=\"291:1-291:101\"\u003e起動したのにコンテナが \u003ccode\u003eExited (0)\u003c/code\u003e になりました。ログを追うとこれです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"293:1-300:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eflood stage disk watermark [95%] exceeded on [.../opt/sonarqube/data/es9]\nfree: 9.3gb[4.9%], all indices on this node will be marked read-only\n...\nCaused by: ElasticsearchException: [es/get] failed:\n  [no_shard_available_action_exception] No shard available for [get [metadatas][dbVendor]]\nERROR web[][o.s.s.p.Platform] Background initialization failed. Stopping SonarQube\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"302:1-304:57\"\u003eSonarQube の設定ミスではなく、\u003cstrong\u003eDocker のディスクが埋まっていた\u003c/strong\u003eだけでした。\u003cbr\u003e\nElasticsearch は使用率 95% を超えると全インデックスを read-only にするので、\u003cbr\u003e\n起動時のインデックス作成が失敗します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"306:1-313:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003edocker run \u003cspan class=\"nt\"\u003e--rm\u003c/span\u003e alpine \u003cspan class=\"nb\"\u003edf\u003c/span\u003e \u003cspan class=\"nt\"\u003e-h\u003c/span\u003e /\n\u003cspan class=\"c\"\u003e# overlay  188.7G  170.3G  8.8G  95% /   ← これが原因\u003c/span\u003e\n\ndocker system \u003cspan class=\"nb\"\u003edf\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# Images        97.15GB  (77.15GB reclaimable)\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# Build Cache   27.02GB  (27.02GB reclaimable)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"315:1-317:101\"\u003e私は \u003ccode\u003edocker system prune -af\u003c/code\u003e で \u003cstrong\u003e118.2GB\u003c/strong\u003e 回収し、95% → 8% になって起動しました。\u003cbr\u003e\nただしこれは未使用イメージも全部消えるので、まずは影響の小さい\u003cbr\u003e\n\u003ccode\u003edocker builder prune -af\u003c/code\u003e（ビルドキャッシュのみ）から試すのがおすすめです。\u003c/p\u003e\n\u003cp data-sourcepos=\"319:1-319:92\"\u003e\u003cstrong\u003eSonarQube は最低 15GB 程度の空きを確保してから起動してください。\u003c/strong\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"322:1-323:0\"\u003e\n\u003ch2 data-sourcepos=\"324:1-324:56\"\u003e\n\u003cspan id=\"2-初期セットアップを-api-で自動化する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E5%88%9D%E6%9C%9F%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97%E3%82%92-api-%E3%81%A7%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 初期セットアップを API で自動化する\u003c/h2\u003e\n\u003cp data-sourcepos=\"326:1-330:164\"\u003e画面で \u003ccode\u003eadmin\u003c/code\u003e / \u003ccode\u003eadmin\u003c/code\u003e でログインするとパスワード変更を求められ、\u003cbr\u003e\nそのあとトークンを発行してコピーする、という手順になります。\u003cbr\u003e\nチームに配る手順書としては、ここはスクリプト化したいところです。\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F6295e025-b744-41f0-aa8a-d471cc4d0752.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=47121b1614b3044f88e3ae91fcb9ce79\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F6295e025-b744-41f0-aa8a-d471cc4d0752.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=47121b1614b3044f88e3ae91fcb9ce79\" alt=\"スクリーンショット 2026-09-21 19.48.14.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F6295e025-b744-41f0-aa8a-d471cc4d0752.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=fc9d961762c59853529462ead0c33cf0 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/6295e025-b744-41f0-aa8a-d471cc4d0752.png\" loading=\"lazy\"\u003e\u003c/a\u003e \u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F3fa53025-e687-447f-807c-b8119c72309d.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b3c8705060b2e46e69642ba50fe3e235\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F3fa53025-e687-447f-807c-b8119c72309d.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b3c8705060b2e46e69642ba50fe3e235\" alt=\"スクリーンショット 2026-09-21 19.50.10.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F3fa53025-e687-447f-807c-b8119c72309d.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=488b6eeda0f9a6d787eec08fd386ed41 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/3fa53025-e687-447f-807c-b8119c72309d.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9ca44fee-0c42-43a7-b818-bd778dd1c210.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=65dc2470e91bdbfc6f0b0af84e8430cf\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9ca44fee-0c42-43a7-b818-bd778dd1c210.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=65dc2470e91bdbfc6f0b0af84e8430cf\" alt=\"スクリーンショット 2026-09-21 19.54.49.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F9ca44fee-0c42-43a7-b818-bd778dd1c210.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=a8d235bb48c213ff389b71b7a6a094d7 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/9ca44fee-0c42-43a7-b818-bd778dd1c210.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv data-sourcepos=\"334:1-355:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"335:1-335:56\"\u003e\n\u003cspan id=\"つまずき-change_password-のパスが違う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D-change_password-%E3%81%AE%E3%83%91%E3%82%B9%E3%81%8C%E9%81%95%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまずき② \u003ccode\u003echange_password\u003c/code\u003e のパスが違う\u003c/h3\u003e\n\u003cp data-sourcepos=\"337:1-337:32\"\u003eまず 404 になりました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"339:1-342:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ePOST /api/authentication/change_password\n→ {\"errors\":[{\"msg\":\"Unknown url : /api/authentication/change_password\"}]}\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"344:1-346:89\"\u003e正しくは \u003cstrong\u003e\u003ccode\u003e/api/users/change_password\u003c/code\u003e\u003c/strong\u003e でした。\u003cbr\u003e\nバージョンで変わるので、実機の Web API 一覧から探すのが確実です。\u003cbr\u003e\nなお \u003ccode\u003e/api/webservices/list\u003c/code\u003e は\u003cstrong\u003e認証必須\u003c/strong\u003eで、匿名だと空が返ります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"348:1-354:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e admin:admin \u003cspan class=\"s1\"\u003e'http://localhost:9000/api/webservices/list'\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  | \u003cspan class=\"nb\"\u003etr\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{'\u003c/span\u003e \u003cspan class=\"s1\"\u003e'\\n'\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-oE\u003c/span\u003e \u003cspan class=\"s1\"\u003e'\"path\":\"[^\"]*\"|\"key\":\"change_password\"'\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-B1\u003c/span\u003e change_password\n\u003cspan class=\"c\"\u003e# \"path\":\"api/users\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# \"key\":\"change_password\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"357:1-357:37\"\u003e\n\u003cspan id=\"自動化スクリプトの中身\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%87%AA%E5%8B%95%E5%8C%96%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E4%B8%AD%E8%BA%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e自動化スクリプトの中身\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sh\" data-sourcepos=\"359:1-380:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 1. 初期パスワードの変更（冪等にするため、まず現状を確認する）\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#    SonarQube は認証失敗でも 200 で {\"valid\":false} を返す点に注意\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$NEW_PASSWORD\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/authentication/validate\"\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-q\u003c/span\u003e \u003cspan class=\"s1\"\u003e'\"valid\":true'\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"o\"\u003e||\u003c/span\u003e curl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:admin\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/users/change_password\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n       \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"login=admin\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n       \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"previousPassword=admin\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n       \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"password=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$NEW_PASSWORD\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# 2. 解析用トークンの発行\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#    同名トークンがあると失敗するので、先に失効させておくと冪等になる\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$NEW_PASSWORD\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/user_tokens/revoke\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"name=qube-test-analysis\"\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$NEW_PASSWORD\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/user_tokens/generate\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"name=qube-test-analysis\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"type=GLOBAL_ANALYSIS_TOKEN\"\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# 3. プロジェクトの作成\u003c/span\u003e\ncurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$NEW_PASSWORD\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e-X\u003c/span\u003e POST \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/projects/create\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"project=qube-test-backend\"\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e--data-urlencode\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"name=qube-test (backend)\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"382:1-382:77\"\u003eトークンの \u003ccode\u003etype\u003c/code\u003e は実機で確認すると 3 種類ありました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"384:1-388:70\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"384:1-384:17\"\u003e\n\u003cth data-sourcepos=\"384:2-384:7\"\u003etype\u003c/th\u003e\n\u003cth data-sourcepos=\"384:9-384:16\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"386:1-386:65\"\u003e\n\u003ctd data-sourcepos=\"386:2-386:15\"\u003e\u003ccode\u003eUSER_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"386:17-386:64\"\u003eユーザーの代理。API 全般に使える\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"387:1-387:97\"\u003e\n\u003ctd data-sourcepos=\"387:2-387:26\"\u003e\u003ccode\u003eGLOBAL_ANALYSIS_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"387:28-387:96\"\u003e解析専用。\u003cstrong\u003e全プロジェクトを 1 本で解析できる\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"388:1-388:70\"\u003e\n\u003ctd data-sourcepos=\"388:2-388:27\"\u003e\u003ccode\u003ePROJECT_ANALYSIS_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"388:29-388:69\"\u003e解析専用。プロジェクト単位\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"390:1-390:103\"\u003eモノレポで 2 プロジェクトを解析するので \u003ccode\u003eGLOBAL_ANALYSIS_TOKEN\u003c/code\u003e を選びました。\u003c/p\u003e\n\u003chr data-sourcepos=\"392:1-393:0\"\u003e\n\u003ch2 data-sourcepos=\"394:1-394:50\"\u003e\n\u003cspan id=\"3-解析設定sonar-projectproperties\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E8%A7%A3%E6%9E%90%E8%A8%AD%E5%AE%9Asonar-projectproperties\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 解析設定（\u003ccode\u003esonar-project.properties\u003c/code\u003e）\u003c/h2\u003e\n\u003cp data-sourcepos=\"396:1-397:109\"\u003eCommunity Build には\u003cstrong\u003eモノレポ機能がありません\u003c/strong\u003e。1 キーにまとめると言語ごとの\u003cbr\u003e\nカバレッジや Quality Gate を分けられないので、2 プロジェクトとして登録します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"399:1-399:29\"\u003e\n\u003cspan id=\"backenddjango--drf\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#backenddjango--drf\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003ebackend（Django / DRF）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"properties\" data-sourcepos=\"401:1-422:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"py\"\u003esonar.projectKey\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eqube-test-backend\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.projectName\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eqube-test (backend / Django REST Framework)\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# New Code の基準になる重要な設定。\n# 検証用にここでは固定しているが、実務では CI でリリースバージョンを注入する。\n# 固定したままだと New Code が永久に空になる（理由は後編で詳説）\n\u003c/span\u003e\u003cspan class=\"py\"\u003esonar.projectVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e0.1.0\u003c/span\u003e\n\n\u003cspan class=\"py\"\u003esonar.sources\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e.\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# tests は「テストコード」として登録する（指摘の重み付けが変わる）\n\u003c/span\u003e\u003cspan class=\"py\"\u003esonar.tests\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003etests\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.test.inclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003etests/**/*.py\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# migrations は自動生成なので対象外\n\u003c/span\u003e\u003cspan class=\"py\"\u003esonar.exclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003etests/**,**/migrations/**,coverage.xml,**/__pycache__/**,db.sqlite3\u003c/span\u003e\n\n\u003cspan class=\"py\"\u003esonar.python.version\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e3.13\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.python.coverage.reportPaths\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ecoverage.xml\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.sourceEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eUTF-8\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"424:1-424:19\"\u003e\n\u003cspan id=\"各行の意味\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%90%84%E8%A1%8C%E3%81%AE%E6%84%8F%E5%91%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e各行の意味\u003c/h3\u003e\n\u003ctable data-sourcepos=\"426:1-437:111\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"426:1-426:31\"\u003e\n\u003cth data-sourcepos=\"426:2-426:9\"\u003e設定\u003c/th\u003e\n\u003cth data-sourcepos=\"426:11-426:21\"\u003e必須度\u003c/th\u003e\n\u003cth data-sourcepos=\"426:23-426:30\"\u003e意味\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"428:1-428:103\"\u003e\n\u003ctd data-sourcepos=\"428:2-428:15\"\u003e\u003ccode\u003eprojectKey\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"428:17-428:28\"\u003e\u003cstrong\u003e必須\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"428:30-428:102\"\u003eSonarQube 上の一意な ID。\u003cstrong\u003eこれが無いと解析できない\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"429:1-429:100\"\u003e\n\u003ctd data-sourcepos=\"429:2-429:16\"\u003e\u003ccode\u003eprojectName\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"429:18-429:25\"\u003e任意\u003c/td\u003e\n\u003ctd data-sourcepos=\"429:27-429:99\"\u003e画面の表示名。省略すると key がそのまま表示される\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"430:1-430:84\"\u003e\n\u003ctd data-sourcepos=\"430:2-430:19\"\u003e\u003ccode\u003eprojectVersion\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"430:21-430:28\"\u003e任意\u003c/td\u003e\n\u003ctd data-sourcepos=\"430:30-430:83\"\u003e\n\u003cstrong\u003eNew Code の基準になる\u003c/strong\u003e（後編で詳説）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"431:1-431:59\"\u003e\n\u003ctd data-sourcepos=\"431:2-431:12\"\u003e\u003ccode\u003esources\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"431:14-431:25\"\u003e\u003cstrong\u003e必須\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"431:27-431:58\"\u003e解析するディレクトリ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"432:1-432:50\"\u003e\n\u003ctd data-sourcepos=\"432:2-432:10\"\u003e\u003ccode\u003etests\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"432:12-432:19\"\u003e任意\u003c/td\u003e\n\u003ctd data-sourcepos=\"432:21-432:49\"\u003eテストコードの場所\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"433:1-433:75\"\u003e\n\u003ctd data-sourcepos=\"433:2-433:20\"\u003e\u003ccode\u003etest.inclusions\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"433:22-433:29\"\u003e任意\u003c/td\u003e\n\u003ctd data-sourcepos=\"433:31-433:74\"\u003eその中で何をテストとみなすか\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"434:1-434:52\"\u003e\n\u003ctd data-sourcepos=\"434:2-434:15\"\u003e\u003ccode\u003eexclusions\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"434:17-434:24\"\u003e任意\u003c/td\u003e\n\u003ctd data-sourcepos=\"434:26-434:51\"\u003e解析から外すもの\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"435:1-435:83\"\u003e\n\u003ctd data-sourcepos=\"435:2-435:19\"\u003e\u003ccode\u003epython.version\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"435:21-435:28\"\u003e推奨\u003c/td\u003e\n\u003ctd data-sourcepos=\"435:30-435:82\"\u003eバージョン依存ルールの精度が上がる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"436:1-436:117\"\u003e\n\u003ctd data-sourcepos=\"436:2-436:32\"\u003e\u003ccode\u003epython.coverage.reportPaths\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"436:34-436:71\"\u003eカバレッジを使うなら必須\u003c/td\u003e\n\u003ctd data-sourcepos=\"436:73-436:116\"\u003e\u003cstrong\u003e\u003ccode\u003eprojectBaseDir\u003c/code\u003e からの相対パス\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"437:1-437:111\"\u003e\n\u003ctd data-sourcepos=\"437:2-437:19\"\u003e\u003ccode\u003esourceEncoding\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"437:21-437:56\"\u003e\u003cstrong\u003e日本語があるなら必須\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"437:58-437:110\"\u003e指定しないとコメントが文字化けする\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"439:1-440:65\"\u003eなお \u003ccode\u003eprojectKey\u003c/code\u003e を指定してスキャンすると、\u003cstrong\u003eプロジェクトは自動で作られます。\u003c/strong\u003e\u003cbr\u003e\n事前に画面や API で作成する必要はありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"442:1-442:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F7062036f-b60f-4ef4-b84c-2a862edb1235.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0e648d9100dcebf5673553983bf7cc0a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F7062036f-b60f-4ef4-b84c-2a862edb1235.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0e648d9100dcebf5673553983bf7cc0a\" alt=\"スクリーンショット 2026-09-21 20.26.16.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3884367%2F7062036f-b60f-4ef4-b84c-2a862edb1235.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=cee90f9fe825f8ee8ec53a61a4ede70e 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/7062036f-b60f-4ef4-b84c-2a862edb1235.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv data-sourcepos=\"444:1-456:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"445:1-445:44\"\u003e\n\u003cspan id=\"sources-と-tests-を分ける理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#sources-%E3%81%A8-tests-%E3%82%92%E5%88%86%E3%81%91%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003esources\u003c/code\u003e と \u003ccode\u003etests\u003c/code\u003e を分ける理由\u003c/h3\u003e\n\u003cp data-sourcepos=\"447:1-447:91\"\u003eテストコードは\u003cstrong\u003e別扱いにする必要があります\u003c/strong\u003e。理由は 3 つです。\u003c/p\u003e\n\u003col data-sourcepos=\"449:1-453:0\"\u003e\n\u003cli data-sourcepos=\"449:1-450:69\"\u003e\n\u003cstrong\u003eテスト専用のルールが適用される\u003c/strong\u003e — 例：\u003ccode\u003eS2699\u003c/code\u003e「アサーションが無いテスト」は、\u003cbr\u003e\nテストとして認識されたファイルにだけ効きます\u003c/li\u003e\n\u003cli data-sourcepos=\"451:1-451:105\"\u003e\n\u003cstrong\u003eカバレッジの分母から外れる\u003c/strong\u003e — テストコード自体のカバレッジは無意味\u003c/li\u003e\n\u003cli data-sourcepos=\"452:1-453:0\"\u003e\n\u003cstrong\u003e複雑度の指摘が緩む\u003c/strong\u003e — テストは多少長くても問題視されない\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"454:1-455:69\"\u003e\u003ccode\u003etests\u003c/code\u003e を指定しないと、テストコードが「本番コード」として扱われ、\u003cbr\u003e\n\u003cstrong\u003eテストの中の \u003ccode\u003eTODO\u003c/code\u003e や複雑度まで指摘されます。\u003c/strong\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"458:1-458:35\"\u003e\n\u003cspan id=\"frontendvue3--typescript\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#frontendvue3--typescript\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003efrontend（Vue3 / TypeScript）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"properties\" data-sourcepos=\"460:1-476:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"py\"\u003esonar.projectKey\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eqube-test-frontend\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.projectName\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eqube-test (frontend / Vue3)\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# .vue も JS/TS アナライザが解析してくれる\n\u003c/span\u003e\u003cspan class=\"py\"\u003esonar.sources\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# sonar.sources と sonar.tests が同じ src を指すので、\n# exclusions と test.inclusions で役割を分ける。\n# この分離が無いと「同じファイルが source と test の両方に属する」エラーになる\n\u003c/span\u003e\u003cspan class=\"py\"\u003esonar.tests\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.test.inclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc/**/*.spec.ts\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.exclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc/**/*.spec.ts,node_modules/**,coverage/**,dist/**\u003c/span\u003e\n\n\u003cspan class=\"py\"\u003esonar.javascript.lcov.reportPaths\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ecoverage/lcov.info\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.sourceEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003eUTF-8\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"477:1-496:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"478:1-478:40\"\u003e\n\u003cspan id=\"frontend-はここがトリッキー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#frontend-%E3%81%AF%E3%81%93%E3%81%93%E3%81%8C%E3%83%88%E3%83%AA%E3%83%83%E3%82%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003efrontend はここがトリッキー\u003c/h3\u003e\n\u003cp data-sourcepos=\"480:1-482:27\"\u003e\u003cstrong\u003eVue や React では、テストがソースと同じディレクトリに同居します\u003c/strong\u003e\u003cbr\u003e\n（\u003ccode\u003eformat.ts\u003c/code\u003e と \u003ccode\u003eformat.spec.ts\u003c/code\u003e が隣同士）。Django のように \u003ccode\u003etests/\u003c/code\u003e ディレクトリで\u003cbr\u003e\n分かれていません。\u003c/p\u003e\n\u003cp data-sourcepos=\"484:1-484:111\"\u003eそのため \u003ccode\u003esources\u003c/code\u003e と \u003ccode\u003etests\u003c/code\u003e の\u003cstrong\u003e両方に \u003ccode\u003esrc\u003c/code\u003e を指定\u003c/strong\u003eし、2 行で役割を切り分けます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"properties\" data-sourcepos=\"486:1-489:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"py\"\u003esonar.test.inclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc/**/*.spec.ts       # 「*.spec.ts はテストです」\u003c/span\u003e\n\u003cspan class=\"py\"\u003esonar.exclusions\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esrc/**/*.spec.ts,...        # 「*.spec.ts は本番コードから除外」\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"491:1-492:125\"\u003e\u003cstrong\u003eこの 2 行のどちらかが欠けると、「同じファイルが source と test の両方に属している」\u003cbr\u003e\nというエラーでスキャンが失敗します。\u003c/strong\u003e Vue3 プロジェクトで最初にぶつかる設定ミスです。\u003c/p\u003e\n\u003cp data-sourcepos=\"494:1-495:21\"\u003e\u003ccode\u003enode_modules/**\u003c/code\u003e の除外も必須です。入れ忘れると数万ファイルを解析しようとして\u003cbr\u003e\n終わりません。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"498:1-498:64\"\u003e\n\u003cspan id=\"カバレッジのプロパティ名は言語ごとに違う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AB%E3%83%90%E3%83%AC%E3%83%83%E3%82%B8%E3%81%AE%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E5%90%8D%E3%81%AF%E8%A8%80%E8%AA%9E%E3%81%94%E3%81%A8%E3%81%AB%E9%81%95%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eカバレッジのプロパティ名は言語ごとに違う\u003c/h3\u003e\n\u003ctable data-sourcepos=\"500:1-503:56\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"500:1-500:42\"\u003e\n\u003cth data-sourcepos=\"500:2-500:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"500:4-500:20\"\u003eプロパティ\u003c/th\u003e\n\u003cth data-sourcepos=\"500:22-500:41\"\u003eレポート形式\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"502:1-502:64\"\u003e\n\u003ctd data-sourcepos=\"502:2-502:9\"\u003ePython\u003c/td\u003e\n\u003ctd data-sourcepos=\"502:11-502:47\"\u003e\u003ccode\u003esonar.python.coverage.reportPaths\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"502:49-502:63\"\u003eCobertura XML\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"503:1-503:56\"\u003e\n\u003ctd data-sourcepos=\"503:2-503:10\"\u003eJS / TS\u003c/td\u003e\n\u003ctd data-sourcepos=\"503:12-503:48\"\u003e\u003ccode\u003esonar.javascript.lcov.reportPaths\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"503:50-503:55\"\u003eLCOV\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"505:1-506:76\"\u003e\u003cstrong\u003e\u003ccode\u003esonar.typescript.lcov.reportPaths\u003c/code\u003e は非推奨です。\u003c/strong\u003e TypeScript でも\u003cbr\u003e\n\u003ccode\u003esonar.javascript.lcov.reportPaths\u003c/code\u003e を使います（\u003ccode\u003e.vue\u003c/code\u003e も同じ）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"508:1-508:25\"\u003e\n\u003cspan id=\"スキャンの実行\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A1%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eスキャンの実行\u003c/h3\u003e\n\u003cp data-sourcepos=\"510:1-510:59\"\u003e\u003ccode\u003eprojectBaseDir\u003c/code\u003e を切り替えて 2 回走らせます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"512:1-515:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003edocker compose run \u003cspan class=\"nt\"\u003e--rm\u003c/span\u003e scanner sonar-scanner \u003cspan class=\"nt\"\u003e-Dsonar\u003c/span\u003e.projectBaseDir\u003cspan class=\"o\"\u003e=\u003c/span\u003e/usr/src/backend\ndocker compose run \u003cspan class=\"nt\"\u003e--rm\u003c/span\u003e scanner sonar-scanner \u003cspan class=\"nt\"\u003e-Dsonar\u003c/span\u003e.projectBaseDir\u003cspan class=\"o\"\u003e=\u003c/span\u003e/usr/src/frontend\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"517:1-517:64\"\u003e\n\u003cspan id=\"q-スキャンは具体的に何をしているのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q-%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%AF%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AB%E4%BD%95%E3%82%92%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ. 「スキャン」は具体的に何をしているのか\u003c/h3\u003e\n\u003cp data-sourcepos=\"519:1-519:33\"\u003e入力は 2 種類だけです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"521:1-524:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003esonar.sources で指定した範囲のファイル（exclusions を引いたもの）   ← コード本体\ncoverage.xml / lcov.info                                  ← テストツールの出力\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"526:1-527:36\"\u003eそして単一の処理ではなく、\u003cstrong\u003e目的別の「センサー」が順番に走ります。\u003c/strong\u003e\u003cbr\u003e\nログを見ると分かります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"529:1-536:75\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"529:1-529:40\"\u003e\n\u003cth data-sourcepos=\"529:2-529:15\"\u003eセンサー\u003c/th\u003e\n\u003cth data-sourcepos=\"529:17-529:39\"\u003e何をしているか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"531:1-531:115\"\u003e\n\u003ctd data-sourcepos=\"531:2-531:53\"\u003e\n\u003ccode\u003ePython Sensor\u003c/code\u003e / \u003ccode\u003eJavaScript/TypeScript analysis\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"531:55-531:114\"\u003e構文解析してルールに照合。\u003cstrong\u003e指摘の本体\u003c/strong\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"532:1-532:75\"\u003e\n\u003ctd data-sourcepos=\"532:2-532:41\"\u003e\u003ccode\u003eCobertura Sensor for Python coverage\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"532:43-532:74\"\u003e\n\u003ccode\u003ecoverage.xml\u003c/code\u003e を読み込む\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"533:1-533:150\"\u003e\n\u003ctd data-sourcepos=\"533:2-533:29\"\u003e\u003cstrong\u003e\u003ccode\u003eTextAndSecretsSensor\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"533:31-533:149\"\u003e\u003cstrong\u003e全ファイルを「ただのテキスト」として走査し、鍵や API トークンのパターンを探す\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"534:1-534:80\"\u003e\n\u003ctd data-sourcepos=\"534:2-534:23\"\u003e\u003ccode\u003eIaC Project Sensor\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"534:25-534:79\"\u003eDockerfile / docker-compose.yml / Terraform を解析\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"535:1-535:117\"\u003e\n\u003ctd data-sourcepos=\"535:2-535:29\"\u003e\u003cstrong\u003e\u003ccode\u003eZero Coverage Sensor\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"535:31-535:116\"\u003e\u003cstrong\u003eカバレッジ情報が無かったファイルを「0%」として記録する\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"536:1-536:75\"\u003e\n\u003ctd data-sourcepos=\"536:2-536:17\"\u003e\u003ccode\u003eCPD Executor\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"536:19-536:74\"\u003eトークン列を比較して重複コードを検出\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"538:1-540:112\"\u003e\u003cstrong\u003e\u003ccode\u003eTextAndSecretsSensor\u003c/code\u003e が独立しているのが重要です。\u003c/strong\u003e \u003ccode\u003eSECRET_KEY\u003c/code\u003e を検出したルールは\u003cbr\u003e\n\u003ccode\u003esecrets:S6687\u003c/code\u003e で、\u003cstrong\u003e\u003ccode\u003epython:\u003c/code\u003e ではありません\u003c/strong\u003e。Python の構文解析ではなく\u003cbr\u003e\nテキストのパターンマッチなので、\u003ccode\u003e.py\u003c/code\u003e に限らず \u003ccode\u003e.env\u003c/code\u003e や \u003ccode\u003e.yml\u003c/code\u003e も対象になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"542:1-542:95\"\u003e\u003ccode\u003eZero Coverage Sensor\u003c/code\u003e は後述の「数字が合わない問題」の原因になります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"544:1-544:25\"\u003e\n\u003cspan id=\"送信されるもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%80%81%E4%BF%A1%E3%81%95%E3%82%8C%E3%82%8B%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e送信されるもの\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"546:1-551:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eINFO  Analysis report generated in 136ms, dir size=348.4 kB\nINFO  Analysis report compressed in 40ms, zip size=80.0 kB\nINFO  Analysis report uploaded in 136ms\nINFO  EXECUTION SUCCESS\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"553:1-554:116\"\u003e\u003cstrong\u003e\u003ccode\u003eEXECUTION SUCCESS\u003c/code\u003e は「送信完了」の意味でしかありません。\u003c/strong\u003e\u003cbr\u003e\nサーバー側の Compute Engine が\u003cstrong\u003e非同期で\u003c/strong\u003e処理してからダッシュボードに反映されます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"556:1-559:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eINFO  Note that you will be able to access the updated dashboard\n      once the server has processed the submitted analysis report\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"561:1-562:105\"\u003e\u003cstrong\u003eスキャン直後に画面を見ても数字が変わっていないことがあります。\u003c/strong\u003e\u003cbr\u003e\n処理状況は画面の \u003ccode\u003eAdministration\u003c/code\u003e → \u003ccode\u003eBackground Tasks\u003c/code\u003e、または API で確認できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"564:1-567:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-u\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"admin:\u003c/span\u003e\u003cspan class=\"nv\"\u003e$PASS\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SONAR_URL\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/ce/activity_status\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# → {\"pending\":0,\"inProgress\":0,...} になれば完了\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"569:1-571:39\"\u003eなお、表示用にソースコード自体もサーバーに送られます（\u003ccode\u003eCode\u003c/code\u003e タブで読めるため）。\u003cbr\u003e\n\u003cstrong\u003e社内の SonarQube に解析を投げる＝そのサーバーにコードが渡る\u003c/strong\u003e、という認識は\u003cbr\u003e\n持っておいた方がいいです。\u003c/p\u003e\n\u003chr data-sourcepos=\"573:1-574:0\"\u003e\n\u003ch2 data-sourcepos=\"575:1-575:56\"\u003e\n\u003cspan id=\"4-つまずき-カバレッジが-0-になる罠\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D-%E3%82%AB%E3%83%90%E3%83%AC%E3%83%83%E3%82%B8%E3%81%8C-0-%E3%81%AB%E3%81%AA%E3%82%8B%E7%BD%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. つまずき③ カバレッジが 0% になる罠\u003c/h2\u003e\n\u003cdiv data-sourcepos=\"576:1-606:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"577:1-577:40\"\u003e\n\u003cspan id=\"前提カバレッジとは何か\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E6%8F%90%E3%82%AB%E3%83%90%E3%83%AC%E3%83%83%E3%82%B8%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前提：カバレッジとは何か\u003c/h3\u003e\n\u003cp data-sourcepos=\"579:1-579:118\"\u003e**「テストを実行したとき、ソースコードのうち何割が実際に通ったか」**の割合です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"581:1-588:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ecalculate_score\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etodo\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003etodo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003epriority\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ehigh\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# ①\n\u003c/span\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e30\u003c/span\u003e                 \u003cspan class=\"c1\"\u003e# ②\n\u003c/span\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003etodo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003epriority\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003emid\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# ③\n\u003c/span\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e20\u003c/span\u003e                 \u003cspan class=\"c1\"\u003e# ④\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e                     \u003cspan class=\"c1\"\u003e# ⑤\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"590:1-590:83\"\u003e\u003ccode\u003epriority=\"high\"\u003c/code\u003e のテストだけ書くと、通るのは ①② だけです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"592:1-594:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e実行された行 2 / 実行可能な行 5 = カバレッジ 40%\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"596:1-597:109\"\u003e\u003cstrong\u003e「テストが通っている＝正しい」ではなく、「テストが触ってすらいない箇所がどれだけあるか」\u003c/strong\u003e\u003cbr\u003e\nを測る指標です。40% なら、残り 60% は\u003cstrong\u003e壊れていても誰も気づかない\u003c/strong\u003e状態です。\u003c/p\u003e\n\u003cp data-sourcepos=\"599:1-599:109\"\u003e測り方は 3 種類あり、\u003cstrong\u003eSonarQube は Line と Branch を合成した独自の値\u003c/strong\u003eを使います。\u003c/p\u003e\n\u003ctable data-sourcepos=\"601:1-605:57\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"601:1-601:28\"\u003e\n\u003cth data-sourcepos=\"601:2-601:9\"\u003e種類\u003c/th\u003e\n\u003cth data-sourcepos=\"601:11-601:27\"\u003e何を見るか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"603:1-603:54\"\u003e\n\u003ctd data-sourcepos=\"603:2-603:20\"\u003e\u003cstrong\u003eLine（行）\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"603:22-603:53\"\u003eその行が実行されたか\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"604:1-604:87\"\u003e\n\u003ctd data-sourcepos=\"604:2-604:25\"\u003e\u003cstrong\u003eBranch（分岐）\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"604:27-604:86\"\u003e\n\u003ccode\u003eif\u003c/code\u003e の true 側と false 側の\u003cstrong\u003e両方\u003c/strong\u003eを通ったか\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"605:1-605:57\"\u003e\n\u003ctd data-sourcepos=\"605:2-605:23\"\u003eFunction（関数）\u003c/td\u003e\n\u003ctd data-sourcepos=\"605:25-605:56\"\u003eその関数が呼ばれたか\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv data-sourcepos=\"607:1-632:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"608:1-608:79\"\u003e\n\u003cspan id=\"q-テストを書いていないプロジェクトでは使えないのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q-%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A7%E3%81%AF%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ. テストを書いていないプロジェクトでは使えないのか\u003c/h3\u003e\n\u003cp data-sourcepos=\"610:1-610:76\"\u003e\u003cstrong\u003e使えます。テストが必要なのはカバレッジだけです。\u003c/strong\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"612:1-617:111\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"612:1-612:49\"\u003e\n\u003cth data-sourcepos=\"612:2-612:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"612:11-612:33\"\u003eテストが必要か\u003c/th\u003e\n\u003cth data-sourcepos=\"612:35-612:48\"\u003e対象範囲\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"614:1-614:111\"\u003e\n\u003ctd data-sourcepos=\"614:2-614:60\"\u003e\n\u003cstrong\u003e指摘の検出\u003c/strong\u003e（Bug / Vulnerability / Code Smell）\u003c/td\u003e\n\u003ctd data-sourcepos=\"614:62-614:73\"\u003e\u003cstrong\u003e不要\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"614:75-614:110\"\u003e\n\u003ccode\u003esonar.sources\u003c/code\u003e の全ファイル\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"615:1-615:42\"\u003e\n\u003ctd data-sourcepos=\"615:2-615:19\"\u003e\u003cstrong\u003e重複検出\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"615:21-615:32\"\u003e\u003cstrong\u003e不要\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"615:34-615:41\"\u003e同じ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"616:1-616:48\"\u003e\n\u003ctd data-sourcepos=\"616:2-616:25\"\u003e\u003cstrong\u003e複雑度・行数\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"616:27-616:38\"\u003e\u003cstrong\u003e不要\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"616:40-616:47\"\u003e同じ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"617:1-617:111\"\u003e\n\u003ctd data-sourcepos=\"617:2-617:22\"\u003e\u003cstrong\u003eカバレッジ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"617:24-617:35\"\u003e\u003cstrong\u003e必須\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"617:37-617:110\"\u003eテストが触った範囲＋触っていない範囲（0% として）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"619:1-620:115\"\u003e検証でも、テストを 1 行も書いていない \u003ccode\u003eutils/insecure.ts\u003c/code\u003e と \u003ccode\u003eutils/legacy.ts\u003c/code\u003e から\u003cbr\u003e\n指摘がちゃんと出ています（\u003ccode\u003eS2245\u003c/code\u003e、\u003ccode\u003eS1135\u003c/code\u003e、\u003ccode\u003eS4144\u003c/code\u003e）。\u003cstrong\u003eカバレッジだけが 0.0%\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"622:1-623:90\"\u003eそして \u003cstrong\u003e0% であること自体が有用な情報\u003c/strong\u003eです。「複雑度 22 なのにテスト 0%」という\u003cbr\u003e\n組み合わせが見えれば、そこが最も危険な箇所だと判断できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"625:1-627:75\"\u003eカバレッジ連携をしない場合は \u003ccode\u003esonar.*.coverage.reportPaths\u003c/code\u003e を書かなければよく、\u003cbr\u003e\nその際は \u003cstrong\u003eQuality Gate からカバレッジ条件を外してください\u003c/strong\u003e。外さないと\u003cbr\u003e\n「レポートが無い → 0% → 永久に不合格」になります。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"629:1-631:83\"\u003e\n\u003cp data-sourcepos=\"629:3-631:83\"\u003e\u003cstrong\u003e導入の順序としては、まず指摘と複雑度だけ見る → テストを書き始めたら\u003cbr\u003e\nカバレッジを足す\u003c/strong\u003e、という段階的な入れ方が現実的です。\u003cbr\u003e\nテストが無いことを理由に導入を諦める必要はありません。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"634:1-634:25\"\u003e\n\u003cspan id=\"ここからが本題\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%93%E3%81%8B%E3%82%89%E3%81%8C%E6%9C%AC%E9%A1%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eここからが本題\u003c/h3\u003e\n\u003cp data-sourcepos=\"636:1-637:106\"\u003e\u003cstrong\u003eカバレッジ連携で一番ハマりやすいのがここです。\u003c/strong\u003e 原因はほぼ、\u003cbr\u003e\n\u003cstrong\u003eレポート内のファイルパスと \u003ccode\u003esonar.projectBaseDir\u003c/code\u003e が一致していない\u003c/strong\u003eことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"639:1-642:76\"\u003eコンテナでテストを走らせると、レポートには \u003ccode\u003e/usr/src/backend/todos/views.py\u003c/code\u003e のような\u003cbr\u003e\nコンテナ内の絶対パスが書かれます。一方 SonarQube は \u003ccode\u003eprojectBaseDir\u003c/code\u003e からの相対パスで\u003cbr\u003e\nファイルを識別するので、一致せず「カバレッジ情報の無いファイル」として扱われます。\u003cbr\u003e\nエラーにならず静かに 0% になるので気づきにくいです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"644:1-644:28\"\u003e\n\u003cspan id=\"対策構成で防ぐ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AF%BE%E7%AD%96%E6%A7%8B%E6%88%90%E3%81%A7%E9%98%B2%E3%81%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e対策：構成で防ぐ\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"646:1-650:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eリポジトリルート → /usr/src （全サービス共通）\n  backend のテスト実行 workdir : /usr/src/backend  →  projectBaseDir と一致\n  frontend のテスト実行 workdir: /usr/src/frontend →  projectBaseDir と一致\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"652:1-652:75\"\u003eそのうえで、\u003cstrong\u003ePython は \u003ccode\u003erelative_files = True\u003c/code\u003e が必須\u003c/strong\u003eです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ini\" data-sourcepos=\"654:1-668:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# backend/.coveragerc\n\u003c/span\u003e\u003cspan class=\"nn\"\u003e[run]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"py\"\u003erelative_files\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003eTrue\u003c/span\u003e\n\u003cspan class=\"py\"\u003esource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e.\u003c/span\u003e\n\u003cspan class=\"py\"\u003eomit\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\n\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"na\"\u003emanage.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003econfig/wsgi.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003econfig/settings.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003e*/migrations/*\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003etests/*\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\n\u003c/span\u003e\u003cspan class=\"nn\"\u003e[xml]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"py\"\u003eoutput\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003ecoverage.xml\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"670:1-670:92\"\u003eJS/TS は vitest を \u003ccode\u003efrontend/\u003c/code\u003e 起点で動かし、reporter に \u003ccode\u003elcov\u003c/code\u003e を含めます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"672:1-686:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// frontend/vite.config.ts\u003c/span\u003e\n\u003cspan class=\"nx\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ejsdom\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003ecoverage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eprovider\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ev8\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003ereporter\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-summary\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elcov\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// lcov が SonarQube 用\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003ereportsDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ecoverage\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// テストが 1 つも無いファイルも 0% として集計させる。\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// ここを絞りすぎると「カバレッジが高く見えるだけ」になる\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003einclude\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esrc/**/*.ts\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esrc/**/*.vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eexclude\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esrc/main.ts\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esrc/**/*.spec.ts\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"688:1-688:40\"\u003e\n\u003cspan id=\"スキャン前に必ず確認する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E5%89%8D%E3%81%AB%E5%BF%85%E3%81%9A%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eスキャン前に必ず確認する\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"690:1-698:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nv\"\u003e$ \u003c/span\u003e\u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-m3\u003c/span\u003e \u003cspan class=\"nv\"\u003efilename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e backend/coverage.xml\n\u003cspan class=\"nv\"\u003efilename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"config/urls.py\"\u003c/span\u003e\n\u003cspan class=\"nv\"\u003efilename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"todos/legacy.py\"\u003c/span\u003e      \u003cspan class=\"c\"\u003e# ← 相対パスならOK\u003c/span\u003e\n\n\u003cspan class=\"nv\"\u003e$ \u003c/span\u003e\u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-m3\u003c/span\u003e \u003cspan class=\"s1\"\u003e'^SF:'\u003c/span\u003e frontend/coverage/lcov.info\nSF:src/App.vue\nSF:src/api/client.ts            \u003cspan class=\"c\"\u003e# ← 相対パスならOK\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"700:1-700:92\"\u003eここが絶対パスになっていたら、SonarQube に渡しても 0% になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"702:1-702:99\"\u003eこの設計で、実際に \u003cstrong\u003ebackend 47.7% / frontend 52.0%\u003c/strong\u003e が一発で取り込めました。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"704:1-747:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch3 data-sourcepos=\"705:1-705:60\"\u003e\n\u003cspan id=\"q-手元の数字と-sonarqube-の数字が合わない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q-%E6%89%8B%E5%85%83%E3%81%AE%E6%95%B0%E5%AD%97%E3%81%A8-sonarqube-%E3%81%AE%E6%95%B0%E5%AD%97%E3%81%8C%E5%90%88%E3%82%8F%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ. 手元の数字と SonarQube の数字が合わない\u003c/h3\u003e\n\u003cp data-sourcepos=\"707:1-707:131\"\u003e\u003cstrong\u003e取り込みは成功しているのに、数字が一致しません。\u003c/strong\u003e これは不具合ではなく定義の違いです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"709:1-712:66\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"709:1-709:36\"\u003e\n\u003cth data-sourcepos=\"709:2-709:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"709:4-709:23\"\u003e手元のツール\u003c/th\u003e\n\u003cth data-sourcepos=\"709:25-709:35\"\u003eSonarQube\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"711:1-711:67\"\u003e\n\u003ctd data-sourcepos=\"711:2-711:10\"\u003ebackend\u003c/td\u003e\n\u003ctd data-sourcepos=\"711:12-711:37\"\u003e\n\u003cstrong\u003e96%\u003c/strong\u003e（coverage.py）\u003c/td\u003e\n\u003ctd data-sourcepos=\"711:39-711:66\"\u003e\n\u003cstrong\u003e78.9%\u003c/strong\u003e ↓ 下がった\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"712:1-712:66\"\u003e\n\u003ctd data-sourcepos=\"712:2-712:11\"\u003efrontend\u003c/td\u003e\n\u003ctd data-sourcepos=\"712:13-712:36\"\u003e\n\u003cstrong\u003e46.18%\u003c/strong\u003e（vitest）\u003c/td\u003e\n\u003ctd data-sourcepos=\"712:38-712:65\"\u003e\n\u003cstrong\u003e52.0%\u003c/strong\u003e ↑ 上がった\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"714:1-714:69\"\u003e\u003cstrong\u003ebackend が下がる理由\u003c/strong\u003e — \u003ccode\u003e.coveragerc\u003c/code\u003e の \u003ccode\u003eomit\u003c/code\u003e です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ini\" data-sourcepos=\"716:1-721:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"py\"\u003eomit\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\n\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"na\"\u003emanage.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003econfig/wsgi.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"na\"\u003econfig/settings.py\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"723:1-726:68\"\u003ecoverage.py は「除外したので集計対象外」として\u003cstrong\u003e分母から外します\u003c/strong\u003e。\u003cbr\u003e\n一方 SonarQube にとってこれらは \u003ccode\u003esonar.sources=.\u003c/code\u003e の範囲内にある普通のソースファイルで、\u003cbr\u003e\n\u003ccode\u003ecoverage.xml\u003c/code\u003e に情報が無いため \u003cstrong\u003e\u003ccode\u003eZero Coverage Sensor\u003c/code\u003e が「0% のファイル」として\u003cbr\u003e\n記録します\u003c/strong\u003e。分母に入るので全体が下がります。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"728:1-731:63\"\u003e\n\u003cp data-sourcepos=\"728:3-731:63\"\u003e\u003cstrong\u003eこれは実務で重要です。\u003c/strong\u003e\u003cbr\u003e\n\u003cstrong\u003e\u003ccode\u003e.coveragerc\u003c/code\u003e の \u003ccode\u003eomit\u003c/code\u003e だけでは、SonarQube 側の数字は逆に下がります。\u003c/strong\u003e\u003cbr\u003e\nSonarQube 側でも無視させたいなら \u003ccode\u003esonar-project.properties\u003c/code\u003e に\u003cbr\u003e\n\u003ccode\u003esonar.coverage.exclusions\u003c/code\u003e を書く必要があります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"733:1-733:63\"\u003e\u003cstrong\u003efrontend が上がる理由\u003c/strong\u003e — 計算式の違いです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"735:1-741:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eSonarQube の Coverage = (カバーされた行 + カバーされた分岐) / (実行可能行 + 全分岐)\n\nLines    : 121 / 262\nBranches :  42 /  53\n(121 + 42) / (262 + 53) = 163 / 315 = 51.7%  ≒ SonarQube の 52.0%\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"743:1-744:33\"\u003evitest の \u003ccode\u003eStatements 46.18%\u003c/code\u003e は\u003cstrong\u003e行だけ\u003c/strong\u003eの値です。SonarQube は分岐（79.24%）も\u003cbr\u003e\n混ぜるので上がります。\u003c/p\u003e\n\u003cp data-sourcepos=\"746:1-746:118\"\u003e\u003cstrong\u003e記事やレポートで数字を出すときは「どのツールの値か」を明示した方が安全です。\u003c/strong\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"749:1-750:0\"\u003e\n\u003ch2 data-sourcepos=\"751:1-751:21\"\u003e\n\u003cspan id=\"前編のまとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E7%B7%A8%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前編のまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"753:1-754:54\"\u003eここまでで、\u003cstrong\u003eVue3 + DRF のプロジェクトが解析され、カバレッジまで取り込まれた状態\u003c/strong\u003eに\u003cbr\u003e\nなりました。押さえた要点は 6 つです。\u003c/p\u003e\n\u003col data-sourcepos=\"756:1-764:0\"\u003e\n\u003cli data-sourcepos=\"756:1-756:95\"\u003eイメージタグは \u003ccode\u003esonarqube:community\u003c/code\u003e を明示する（\u003ccode\u003elatest\u003c/code\u003e は将来ブレる）\u003c/li\u003e\n\u003cli data-sourcepos=\"757:1-757:100\"\u003e\n\u003cstrong\u003eDocker に 15GB 以上の空きを確保する\u003c/strong\u003e（ES の watermark で起動に失敗する）\u003c/li\u003e\n\u003cli data-sourcepos=\"758:1-758:121\"\u003e永続運用するなら PostgreSQL を用意する（組み込み H2 はアップグレードでデータを失う）\u003c/li\u003e\n\u003cli data-sourcepos=\"759:1-760:62\"\u003eCommunity Build はモノレポ非対応。\u003cstrong\u003eプロジェクトを 2 つに分け\u003c/strong\u003e、\u003cbr\u003e\n\u003ccode\u003eprojectBaseDir\u003c/code\u003e を切り替えて 2 回スキャンする\u003c/li\u003e\n\u003cli data-sourcepos=\"761:1-762:85\"\u003e\n\u003cstrong\u003eカバレッジはレポート内のパスと \u003ccode\u003eprojectBaseDir\u003c/code\u003e を一致させる。\u003c/strong\u003e\u003cbr\u003e\nPython は \u003ccode\u003erelative_files = True\u003c/code\u003e、JS/TS は \u003ccode\u003esonar.javascript.lcov.reportPaths\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"763:1-764:0\"\u003e\n\u003cstrong\u003eテストが無いプロジェクトでも導入できる\u003c/strong\u003e（カバレッジ条件を外すだけ）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"765:1-766:101\"\u003e検証に使った一式（Docker Compose / Makefile / 自動化スクリプト / 仕込み入りアプリ）は\u003cbr\u003e\n\u003ccode\u003emake all\u003c/code\u003e だけで起動から解析結果の表示まで通るようにまとめてあります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"768:1-771:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nb\"\u003ecp\u003c/span\u003e .env.example .env\nmake all\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"773:1-774:0\"\u003e\n\u003ch2 data-sourcepos=\"775:1-775:24\"\u003e\n\u003cspan id=\"後編に続きます\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%BE%8C%E7%B7%A8%E3%81%AB%E7%B6%9A%E3%81%8D%E3%81%BE%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e後編に続きます\u003c/h2\u003e\n\u003cp data-sourcepos=\"777:1-777:88\"\u003eただし、\u003cstrong\u003eこの時点でダッシュボードを見ると拍子抜けします。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"779:1-780:73\"\u003eあれだけ指摘が出ているのに、\u003cstrong\u003eQuality Gate は「Passed」と表示される\u003c/strong\u003eのです。\u003cbr\u003e\nバグではなく、SonarQube の設計思想によるものでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"782:1-782:106\"\u003e後編ではその正体（\u003cstrong\u003eNew Code\u003c/strong\u003e という概念）から始めて、次の内容を扱います。\u003c/p\u003e\n\u003cul data-sourcepos=\"784:1-789:0\"\u003e\n\u003cli data-sourcepos=\"784:1-784:62\"\u003e実際に検出された指摘の一覧と、その読み方\u003c/li\u003e\n\u003cli data-sourcepos=\"785:1-785:94\"\u003e\n\u003cstrong\u003e仕込んだのに検出されなかった指摘\u003c/strong\u003e — \u003ccode\u003ev-html\u003c/code\u003e の XSS が出ない理由\u003c/li\u003e\n\u003cli data-sourcepos=\"786:1-786:81\"\u003eQuality Gate を意図的に不合格にし、修正して合格させるまで\u003c/li\u003e\n\u003cli data-sourcepos=\"787:1-787:68\"\u003e日本語コメントが誤検知されたときの正しい対処\u003c/li\u003e\n\u003cli data-sourcepos=\"788:1-789:0\"\u003eGitHub Actions 連携と、Community Build の限界\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"790:1-790:180\"\u003e→ \u003cstrong\u003e後編\u003ca href=\"https://qiita.com/mukku928/items/b234a01ea1dbbd07fd11\"\u003e「Vue3 + DRF に SonarQube を入れる（後編）— 指摘の読み方と Quality Gate の落とし穴」\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"792:1-793:0\"\u003e\n","body":"## この記事の対象と、書いた理由\n\nVue3 + Django REST Framework の構成でアプリを作る案件によく入るのですが、\n静的解析は ESLint / ruff で止まっていて、SonarQube は名前しか知らない状態でした。\n\n:::note\n先行記事を読むと、次のところまでは分かります。\n\n- [SonarQube をオンボーディングする（Zenn）](https://zenn.dev/siva_dev/articles/sonarqube-onboarding)\n  … `docker run` での起動、GitHub App 連携、Secrets 名（`SONAR_TOKEN` / `SONAR_HOST_URL`）\n- [SonarQube とは（Qiita）](https://qiita.com/amoriki/items/b512c0ced7b7cb34fef2)\n  … 製品ラインナップ、Community Build / Developer / Enterprise の機能差、日本語化プラグイン\n:::\n\n一方で、**自分のプロジェクトに当てはめるときに必要な情報**が見つかりませんでした。\n\n- `sonar-project.properties` に Vue3 / DRF それぞれ何を書くのか\n- `vitest` と `coverage.py` のカバレッジをどう食わせるのか\n- 出てきた指摘をどう読み、Quality Gate をどう運用するのか\n\nそこで、**ホストに Node / Python / Java / sonar-scanner を一切入れず**、\nDocker だけで検証環境を作って一通り動かしました。この記事はその一次情報です。\n数値・ログ・エンドポイントはすべて実際に動かした結果を載せています。\n\n検証環境: SonarQube Community Build **26.9.0.129388** / Docker 28.2.2 / Compose v2.37.1 / macOS (x86_64)\n#### [ソースコード](https://github.com/Mutsuki-Y/sonarqube-vue3-drf-sandbox)\n\n## 前提：SonarQube とは何をするツールか\n\n一言でいうと **「コードを読んで問題を指摘し、品質を数値にしてサーバーに溜めるツール」** です。\n\nESLint / ruff と比べると役割の違いが見えます。\n\n| | ESLint / ruff | SonarQube |\n|---|---|---|\n| 動く場所 | 手元（保存時・コミット時） | **サーバー**（解析結果を送信して蓄積） |\n| 見るもの | 文法・スタイル・一部のバグ | 左記＋**カバレッジ・重複・複雑度・セキュリティ設定** |\n| 結果の形 | その場のエラー一覧 | **時系列のグラフ、プロジェクト横断の比較** |\n| 強み | 即時、自動修正できる | 推移が見える、CI で合否判定、チームで共有 |\n| 弱み | 履歴が残らない | 即時性がない、サーバーが必要 |\n\n**置き換えではなく併用**です。実際この検証でも、`v-html` の XSS は SonarQube では\n拾えず ESLint の担当でした（後編で詳述）。逆に「認知的複雑度が 28 で上限 15 を超えている」\n「重複が 10.9%」のような**定量化と推移の可視化**は SonarQube の得意分野です。\n\n### 覚えておく用語はこれだけ\n\n**Issue（指摘）** は 3 種類に分かれます。\n\n| 種別 | 意味 | 今回出た例 |\n|---|---|---|\n| **Bug** | 動作が壊れる・意図通りでない | `return` 後の到達不能コード、自己代入 |\n| **Vulnerability** | セキュリティ上の問題 | MD5 でハッシュ化、`SECRET_KEY` のハードコード |\n| **Code Smell** | 動くが保守しづらい | 複雑度が高い、`TODO` の放置、コピペ関数 |\n\n**Rating** は A〜E の 5 段階評価で、上の 3 種類にそれぞれ対応します\n（Reliability ← Bug、Security ← Vulnerability、Maintainability ← Code Smell）。\n\n**Quality Gate** は合否判定のルールセットで、CI を止める根拠になります。\n\n**New Code** は「最近書いたコード」。SonarQube の設計思想は *Clean as You Code*\n（既存の負債は据え置き、**これから書く分だけ**きれいに保つ）で、\n**既定の判定条件は全部この New Code 側を見ています。** ここが後編の山場になります。\n\n## 結論（先に成果）\n\n同一リポジトリに 2 プロジェクトを登録し、片方は指摘を修正、もう片方は未修正のまま\n並べました。最終的にこうなりました。\n\n| | backend (DRF・修正後) | frontend (Vue3・未修正) |\n|---|---|---|\n| Quality Gate | **OK** | **ERROR** |\n| Bugs | 0 | 3 |\n| Vulnerabilities | 0 | 1 |\n| Code Smells | 0 | 22 |\n| Coverage | 78.9% | 52.0% |\n| Duplications | 0.0% | 10.9% |\n| Reliability / Security | A / A | C / C |\n\nそして、**つまずいた点が 5 つ**ありました。ここが本題です。\n\n| # | つまずき | 扱う記事 |\n|---|---|---|\n| 1 | ディスク使用率 95% で SonarQube が起動しない（Elasticsearch の watermark） | **前編（この記事）** |\n| 2 | パスワード変更 API のパスが先行記事と違う（`/api/users/change_password`） | **前編** |\n| 3 | **カバレッジが 0% になる罠**（レポートのパスと `projectBaseDir` の不一致） | **前編** |\n| 4 | 初回スキャンでは Quality Gate が必ず素通りする（条件が全部 New Code 側） | 後編 |\n| 5 | 仕込んだのに検出されない指摘がある（`v-html`、`console.log`、`eval` など） | 後編 |\n\n## この記事の構成\n\n**前編（この記事）で「動いて数字が出る状態」まで到達します。**\n必要なところだけ読んでも分かるように書いています。\n\n| 章 | 内容 |\n|---|---|\n| 1 | **環境構築** — Compose 全文、PostgreSQL が必要な理由、`scanner` / `api` の役割 |\n| 2 | **初期セットアップの自動化** — パスワード変更 API のパスに注意 |\n| 3 | **解析設定** — Vue3 / DRF それぞれの `sonar-project.properties`、スキャンの中身 |\n| 4 | **カバレッジ連携** — 0% になる罠、手元の数字と合わない理由 |\n\n[後編](https://qiita.com/mukku928/items/b234a01ea1dbbd07fd11)では「出てきた結果をどう読み、どう運用するか」を扱います。\n\n| 章 | 内容 |\n|---|---|\n| 5 | **検出結果の一覧** — 実際に何が出たか |\n| 6 | **出なかった指摘の原因** — 3 パターンの切り分けと Quality Profile のカスタマイズ |\n| 7 | **Quality Gate が初回素通りする正体** — New Code とは何か |\n| 8 | **Fail → 修正 → Pass の実演** — 誤検知（False Positive）の扱いも |\n| 9 | **GitHub Actions 連携** |\n| 10 | **静的解析の深さ** — どこまで見るのか、脆弱性検出の限界 |\n| 11 | **Community Build の線引き** — 有償版が必要になる境界 |\n\n`S106` のような個別ルールの解説（何を問題としているのか・どう直すのか）は、\n分量が多いので[別記事「SonarQube ルール早見表（Vue3 + DRF 編）」](https://qiita.com/mukku928/items/8195667ef112d54b6e0b)にまとめています。\n\n---\n\n## 1. 環境構築（Docker Compose）\n\n### イメージタグは `community` を明示する\n\n先行記事では `sonarqube:latest` を使っていますが、**今は `latest` を避けた方が安全**です。\n\n調べた時点で `latest` と `community` は同じイメージ（`26.9.0.129388-community`、LGPL v3）を\n指していましたが、バージョン体系が分岐しています。\n\n- Community Build（無償）: `26.x` 系 → `sonarqube:community`\n- 商用エディション: `2026.x` 系 → `sonarqube:developer` / `enterprise`\n\n将来 `latest` がどちらを指すか保証がないので、`community` と書いておきます。\n\n### docker-compose.yml\n\n```yaml\nname: qube-test\n\nservices:\n  db:\n    image: postgres:17-alpine\n    environment:\n      POSTGRES_USER: sonar\n      POSTGRES_PASSWORD: sonar\n      POSTGRES_DB: sonar\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U sonar -d sonar\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  sonarqube:\n    image: sonarqube:community\n    depends_on:\n      db:\n        condition: service_healthy\n    environment:\n      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar\n      SONAR_JDBC_USERNAME: sonar\n      SONAR_JDBC_PASSWORD: sonar\n      SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: \"true\"\n    ports:\n      - \"127.0.0.1:9000:9000\"     # LAN に晒さない\n    volumes:\n      - sonarqube_data:/opt/sonarqube/data\n      - sonarqube_extensions:/opt/sonarqube/extensions\n      - sonarqube_logs:/opt/sonarqube/logs\n      - sonarqube_temp:/opt/sonarqube/temp\n    tmpfs:\n      - /tmp:rw,noexec,nosuid,size=256m\n    read_only: true\n    ulimits:\n      nofile: { soft: 65536, hard: 65536 }\n\n  # 以下は profiles 付き。`up` では起動せず `run --rm` で使い捨て実行する\n  scanner:\n    image: sonarsource/sonar-scanner-cli:latest\n    profiles: [\"tools\"]\n    environment:\n      SONAR_HOST_URL: http://sonarqube:9000\n      SONAR_TOKEN: ${SONAR_TOKEN:-}\n    volumes:\n      - .:/usr/src\n      - scanner_cache:/opt/sonar-scanner/.sonar/cache\n    working_dir: /usr/src\n\n  frontend:\n    image: node:22-alpine\n    profiles: [\"tools\"]\n    volumes:\n      - .:/usr/src\n      - frontend_node_modules:/usr/src/frontend/node_modules  # ホストに出さない\n      - npm_cache:/root/.npm\n    working_dir: /usr/src/frontend\n```\n\nポイントは 3 つです。\n\n- `read_only: true` + `tmpfs` で書き込み先を限定（公式の Compose 例に準拠）\n- 公開ポートを `127.0.0.1` に固定\n- **全サービスでリポジトリルートを `/usr/src` にマウント**（後述のカバレッジ問題対策）\n\n:::note\n### Q. なぜ PostgreSQL が必要なのか（`docker run` 単発では駄目なのか）\n\n先行記事のように `docker run` だけで起動すると、**組み込みの H2 データベース**が使われます。\n動くことは動きますが、**公式が「評価目的のみ」としています。**\n\nSonarQube は 2 種類のデータストアを使います。\n\n| データストア | 何を持つか | どこにあるか |\n|---|---|---|\n| **リレーショナル DB** | プロジェクト、指摘の一覧、メトリクス、**解析履歴**、ユーザー、Quality Gate/Profile の設定 | **外部に用意が必要** |\n| Elasticsearch | 検索・集計用インデックス | **コンテナに同梱**（用意不要） |\n\n![スクリーンショット 2026-09-21 19.52.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/8bcb01d2-31ea-4ed8-aa06-7dfefda0a22a.png)\n\n\n前述のディスク不足で落ちたのが Elasticsearch だったのは、後者がコンテナ内で動いているからです。\n\nH2 で運用した場合の問題はこれです。\n\n1. **バージョンアップ時にデータを引き継げない** — 解析履歴が全部消える\n2. 性能が出ない（プロジェクトが増えると顕著）\n\n**「とりあえず触る」なら H2 で十分。「社内に置いて履歴を溜める」なら PostgreSQL が必須**です。\n繋ぎ方は環境変数 3 つだけで、対応 DB は PostgreSQL / Oracle / SQL Server です\n（MySQL は廃止済み）。\n\nボリューム 5 つの役割も押さえておくと、どれを消すと何を失うか分かります。\n\n| ボリューム | 中身 | 消すと失うもの |\n|---|---|---|\n| `postgres_data` | **解析結果・履歴・全設定** | **全部（これが本体）** |\n| `sonarqube_data` | Elasticsearch のインデックス | 再構築される |\n| `sonarqube_extensions` | プラグイン（日本語 Pack を入れるならここ） | プラグイン |\n| `sonarqube_logs` | ログ | ログ |\n| `sonarqube_temp` | 一時ファイル | なし |\n\n:::\n\n:::note\n### Q. `scanner` と `api` は何をするコンテナなのか\n\n**ここは SonarQube の構造理解に直結します。**\nSonarQube は **「サーバー」と「スキャナ」に分かれています。**\n\n```\n  ソースコード\n      │ 読む\n      ▼\n┌──────────────┐   解析結果を送信   ┌──────────────┐        ┌──────┐\n│   scanner    │ ─────────────────\u003e │  sonarqube   │ ─────\u003e │  db  │\n│  (解析する)   │    HTTP + Token    │ (保存・表示)  │        └──────┘\n└──────────────┘                    └──────────────┘\n```\n\n**解析処理はスキャナ側で走ります。サーバーはソースコードを解析しません。**\n\nこれが分かると CI 連携が繋がります。GitHub Actions で `sonarqube-scan-action` を\n動かすのは、**CI ランナー上でスキャナを実行してサーバーに送っている**だけで、\nローカルの `make scan` と同じことです。\n\n`sonar-scanner-cli` は **Java 製**です。コンテナにする最大の理由がこれで、\n**ホストに JDK を入れずに済みます。**\n\n一方 `api`（curl コンテナ）は **SonarQube の仕組みとは無関係で、こちらの便宜のための箱**です。\nWeb API を叩くのに使っており、ホストに curl があるかどうかにも依存させないためのものです。\n\nサービスは 2 種類に分かれています。\n\n| サービス | 役割 | 種類 |\n|---|---|---|\n| `db` / `sonarqube` | サーバーとして動き続ける | **常駐** |\n| `scanner` / `backend` / `frontend` / `api` | コマンドを 1 回実行して消える | 使い捨て（`profiles: [\"tools\"]`） |\n\n`profiles: [\"tools\"]` が付いていると **`docker compose up` では起動しません。**\n`docker compose run --rm scanner ...` と明示的に呼んだときだけ動きます。\n**SonarQube はサーバーだが、スキャナやテスト実行はコマンドだから**です。\n\n\u003e **後片付けの注意**: `profiles` 付きサービスのボリュームは `docker compose down -v` の\n\u003e 対象外です。`node_modules` や scanner キャッシュまで消すには\n\u003e `docker compose --profile tools down -v` が必要です。\n\n:::\n\n:::note warn\n### つまずき① ディスク 95% で起動しない\n\n起動したのにコンテナが `Exited (0)` になりました。ログを追うとこれです。\n\n```\nflood stage disk watermark [95%] exceeded on [.../opt/sonarqube/data/es9]\nfree: 9.3gb[4.9%], all indices on this node will be marked read-only\n...\nCaused by: ElasticsearchException: [es/get] failed:\n  [no_shard_available_action_exception] No shard available for [get [metadatas][dbVendor]]\nERROR web[][o.s.s.p.Platform] Background initialization failed. Stopping SonarQube\n```\n\nSonarQube の設定ミスではなく、**Docker のディスクが埋まっていた**だけでした。\nElasticsearch は使用率 95% を超えると全インデックスを read-only にするので、\n起動時のインデックス作成が失敗します。\n\n```bash\ndocker run --rm alpine df -h /\n# overlay  188.7G  170.3G  8.8G  95% /   ← これが原因\n\ndocker system df\n# Images        97.15GB  (77.15GB reclaimable)\n# Build Cache   27.02GB  (27.02GB reclaimable)\n```\n\n私は `docker system prune -af` で **118.2GB** 回収し、95% → 8% になって起動しました。\nただしこれは未使用イメージも全部消えるので、まずは影響の小さい\n`docker builder prune -af`（ビルドキャッシュのみ）から試すのがおすすめです。\n\n**SonarQube は最低 15GB 程度の空きを確保してから起動してください。**\n:::\n\n---\n\n## 2. 初期セットアップを API で自動化する\n\n画面で `admin` / `admin` でログインするとパスワード変更を求められ、\nそのあとトークンを発行してコピーする、という手順になります。\nチームに配る手順書としては、ここはスクリプト化したいところです。\n![スクリーンショット 2026-09-21 19.48.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/6295e025-b744-41f0-aa8a-d471cc4d0752.png) ![スクリーンショット 2026-09-21 19.50.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/3fa53025-e687-447f-807c-b8119c72309d.png)\n![スクリーンショット 2026-09-21 19.54.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/9ca44fee-0c42-43a7-b818-bd778dd1c210.png)\n\n\n\n:::note warn\n### つまずき② `change_password` のパスが違う\n\nまず 404 になりました。\n\n```\nPOST /api/authentication/change_password\n→ {\"errors\":[{\"msg\":\"Unknown url : /api/authentication/change_password\"}]}\n```\n\n正しくは **`/api/users/change_password`** でした。\nバージョンで変わるので、実機の Web API 一覧から探すのが確実です。\nなお `/api/webservices/list` は**認証必須**で、匿名だと空が返ります。\n\n```bash\ncurl -s -u admin:admin 'http://localhost:9000/api/webservices/list' \\\n  | tr '{' '\\n' | grep -oE '\"path\":\"[^\"]*\"|\"key\":\"change_password\"' \\\n  | grep -B1 change_password\n# \"path\":\"api/users\"\n# \"key\":\"change_password\"\n```\n:::\n\n### 自動化スクリプトの中身\n\n```sh\n# 1. 初期パスワードの変更（冪等にするため、まず現状を確認する）\n#    SonarQube は認証失敗でも 200 で {\"valid\":false} を返す点に注意\ncurl -s -u \"admin:$NEW_PASSWORD\" \"$SONAR_URL/api/authentication/validate\" | grep -q '\"valid\":true' \\\n  || curl -s -u \"admin:admin\" -X POST \"$SONAR_URL/api/users/change_password\" \\\n       --data-urlencode \"login=admin\" \\\n       --data-urlencode \"previousPassword=admin\" \\\n       --data-urlencode \"password=$NEW_PASSWORD\"\n\n# 2. 解析用トークンの発行\n#    同名トークンがあると失敗するので、先に失効させておくと冪等になる\ncurl -s -u \"admin:$NEW_PASSWORD\" -X POST \"$SONAR_URL/api/user_tokens/revoke\" \\\n  --data-urlencode \"name=qube-test-analysis\"\ncurl -s -u \"admin:$NEW_PASSWORD\" -X POST \"$SONAR_URL/api/user_tokens/generate\" \\\n  --data-urlencode \"name=qube-test-analysis\" \\\n  --data-urlencode \"type=GLOBAL_ANALYSIS_TOKEN\"\n\n# 3. プロジェクトの作成\ncurl -s -u \"admin:$NEW_PASSWORD\" -X POST \"$SONAR_URL/api/projects/create\" \\\n  --data-urlencode \"project=qube-test-backend\" \\\n  --data-urlencode \"name=qube-test (backend)\"\n```\n\nトークンの `type` は実機で確認すると 3 種類ありました。\n\n| type | 用途 |\n|---|---|\n| `USER_TOKEN` | ユーザーの代理。API 全般に使える |\n| `GLOBAL_ANALYSIS_TOKEN` | 解析専用。**全プロジェクトを 1 本で解析できる** |\n| `PROJECT_ANALYSIS_TOKEN` | 解析専用。プロジェクト単位 |\n\nモノレポで 2 プロジェクトを解析するので `GLOBAL_ANALYSIS_TOKEN` を選びました。\n\n---\n\n## 3. 解析設定（`sonar-project.properties`）\n\nCommunity Build には**モノレポ機能がありません**。1 キーにまとめると言語ごとの\nカバレッジや Quality Gate を分けられないので、2 プロジェクトとして登録します。\n\n### backend（Django / DRF）\n\n```properties\nsonar.projectKey=qube-test-backend\nsonar.projectName=qube-test (backend / Django REST Framework)\n\n# New Code の基準になる重要な設定。\n# 検証用にここでは固定しているが、実務では CI でリリースバージョンを注入する。\n# 固定したままだと New Code が永久に空になる（理由は後編で詳説）\nsonar.projectVersion=0.1.0\n\nsonar.sources=.\n\n# tests は「テストコード」として登録する（指摘の重み付けが変わる）\nsonar.tests=tests\nsonar.test.inclusions=tests/**/*.py\n\n# migrations は自動生成なので対象外\nsonar.exclusions=tests/**,**/migrations/**,coverage.xml,**/__pycache__/**,db.sqlite3\n\nsonar.python.version=3.13\nsonar.python.coverage.reportPaths=coverage.xml\nsonar.sourceEncoding=UTF-8\n```\n\n### 各行の意味\n\n| 設定 | 必須度 | 意味 |\n|---|---|---|\n| `projectKey` | **必須** | SonarQube 上の一意な ID。**これが無いと解析できない** |\n| `projectName` | 任意 | 画面の表示名。省略すると key がそのまま表示される |\n| `projectVersion` | 任意 | **New Code の基準になる**（後編で詳説） |\n| `sources` | **必須** | 解析するディレクトリ |\n| `tests` | 任意 | テストコードの場所 |\n| `test.inclusions` | 任意 | その中で何をテストとみなすか |\n| `exclusions` | 任意 | 解析から外すもの |\n| `python.version` | 推奨 | バージョン依存ルールの精度が上がる |\n| `python.coverage.reportPaths` | カバレッジを使うなら必須 | **`projectBaseDir` からの相対パス** |\n| `sourceEncoding` | **日本語があるなら必須** | 指定しないとコメントが文字化けする |\n\nなお `projectKey` を指定してスキャンすると、**プロジェクトは自動で作られます。**\n事前に画面や API で作成する必要はありません。\n\n![スクリーンショット 2026-09-21 20.26.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/7062036f-b60f-4ef4-b84c-2a862edb1235.png)\n\n:::note\n### `sources` と `tests` を分ける理由\n\nテストコードは**別扱いにする必要があります**。理由は 3 つです。\n\n1. **テスト専用のルールが適用される** — 例：`S2699`「アサーションが無いテスト」は、\n   テストとして認識されたファイルにだけ効きます\n2. **カバレッジの分母から外れる** — テストコード自体のカバレッジは無意味\n3. **複雑度の指摘が緩む** — テストは多少長くても問題視されない\n\n`tests` を指定しないと、テストコードが「本番コード」として扱われ、\n**テストの中の `TODO` や複雑度まで指摘されます。**\n:::\n\n### frontend（Vue3 / TypeScript）\n\n```properties\nsonar.projectKey=qube-test-frontend\nsonar.projectName=qube-test (frontend / Vue3)\n\n# .vue も JS/TS アナライザが解析してくれる\nsonar.sources=src\n\n# sonar.sources と sonar.tests が同じ src を指すので、\n# exclusions と test.inclusions で役割を分ける。\n# この分離が無いと「同じファイルが source と test の両方に属する」エラーになる\nsonar.tests=src\nsonar.test.inclusions=src/**/*.spec.ts\nsonar.exclusions=src/**/*.spec.ts,node_modules/**,coverage/**,dist/**\n\nsonar.javascript.lcov.reportPaths=coverage/lcov.info\nsonar.sourceEncoding=UTF-8\n```\n:::note\n### frontend はここがトリッキー\n\n**Vue や React では、テストがソースと同じディレクトリに同居します**\n（`format.ts` と `format.spec.ts` が隣同士）。Django のように `tests/` ディレクトリで\n分かれていません。\n\nそのため `sources` と `tests` の**両方に `src` を指定**し、2 行で役割を切り分けます。\n\n```properties\nsonar.test.inclusions=src/**/*.spec.ts       # 「*.spec.ts はテストです」\nsonar.exclusions=src/**/*.spec.ts,...        # 「*.spec.ts は本番コードから除外」\n```\n\n**この 2 行のどちらかが欠けると、「同じファイルが source と test の両方に属している」\nというエラーでスキャンが失敗します。** Vue3 プロジェクトで最初にぶつかる設定ミスです。\n\n`node_modules/**` の除外も必須です。入れ忘れると数万ファイルを解析しようとして\n終わりません。\n:::\n\n### カバレッジのプロパティ名は言語ごとに違う\n\n| | プロパティ | レポート形式 |\n|---|---|---|\n| Python | `sonar.python.coverage.reportPaths` | Cobertura XML |\n| JS / TS | `sonar.javascript.lcov.reportPaths` | LCOV |\n\n**`sonar.typescript.lcov.reportPaths` は非推奨です。** TypeScript でも\n`sonar.javascript.lcov.reportPaths` を使います（`.vue` も同じ）。\n\n### スキャンの実行\n\n`projectBaseDir` を切り替えて 2 回走らせます。\n\n```bash\ndocker compose run --rm scanner sonar-scanner -Dsonar.projectBaseDir=/usr/src/backend\ndocker compose run --rm scanner sonar-scanner -Dsonar.projectBaseDir=/usr/src/frontend\n```\n\n### Q. 「スキャン」は具体的に何をしているのか\n\n入力は 2 種類だけです。\n\n```\nsonar.sources で指定した範囲のファイル（exclusions を引いたもの）   ← コード本体\ncoverage.xml / lcov.info                                  ← テストツールの出力\n```\n\nそして単一の処理ではなく、**目的別の「センサー」が順番に走ります。**\nログを見ると分かります。\n\n| センサー | 何をしているか |\n|---|---|\n| `Python Sensor` / `JavaScript/TypeScript analysis` | 構文解析してルールに照合。**指摘の本体** |\n| `Cobertura Sensor for Python coverage` | `coverage.xml` を読み込む |\n| **`TextAndSecretsSensor`** | **全ファイルを「ただのテキスト」として走査し、鍵や API トークンのパターンを探す** |\n| `IaC Project Sensor` | Dockerfile / docker-compose.yml / Terraform を解析 |\n| **`Zero Coverage Sensor`** | **カバレッジ情報が無かったファイルを「0%」として記録する** |\n| `CPD Executor` | トークン列を比較して重複コードを検出 |\n\n**`TextAndSecretsSensor` が独立しているのが重要です。** `SECRET_KEY` を検出したルールは\n`secrets:S6687` で、**`python:` ではありません**。Python の構文解析ではなく\nテキストのパターンマッチなので、`.py` に限らず `.env` や `.yml` も対象になります。\n\n`Zero Coverage Sensor` は後述の「数字が合わない問題」の原因になります。\n\n### 送信されるもの\n\n```\nINFO  Analysis report generated in 136ms, dir size=348.4 kB\nINFO  Analysis report compressed in 40ms, zip size=80.0 kB\nINFO  Analysis report uploaded in 136ms\nINFO  EXECUTION SUCCESS\n```\n\n**`EXECUTION SUCCESS` は「送信完了」の意味でしかありません。**\nサーバー側の Compute Engine が**非同期で**処理してからダッシュボードに反映されます。\n\n```\nINFO  Note that you will be able to access the updated dashboard\n      once the server has processed the submitted analysis report\n```\n\n**スキャン直後に画面を見ても数字が変わっていないことがあります。**\n処理状況は画面の `Administration` → `Background Tasks`、または API で確認できます。\n\n```bash\ncurl -s -u \"admin:$PASS\" \"$SONAR_URL/api/ce/activity_status\"\n# → {\"pending\":0,\"inProgress\":0,...} になれば完了\n```\n\nなお、表示用にソースコード自体もサーバーに送られます（`Code` タブで読めるため）。\n**社内の SonarQube に解析を投げる＝そのサーバーにコードが渡る**、という認識は\n持っておいた方がいいです。\n\n---\n\n## 4. つまずき③ カバレッジが 0% になる罠\n:::note\n### 前提：カバレッジとは何か\n\n**「テストを実行したとき、ソースコードのうち何割が実際に通ったか」**の割合です。\n\n```python\ndef calculate_score(todo):\n    if todo.priority == \"high\":   # ①\n        return 30                 # ②\n    if todo.priority == \"mid\":    # ③\n        return 20                 # ④\n    return 10                     # ⑤\n```\n\n`priority=\"high\"` のテストだけ書くと、通るのは ①② だけです。\n\n```\n実行された行 2 / 実行可能な行 5 = カバレッジ 40%\n```\n\n**「テストが通っている＝正しい」ではなく、「テストが触ってすらいない箇所がどれだけあるか」**\nを測る指標です。40% なら、残り 60% は**壊れていても誰も気づかない**状態です。\n\n測り方は 3 種類あり、**SonarQube は Line と Branch を合成した独自の値**を使います。\n\n| 種類 | 何を見るか |\n|---|---|\n| **Line（行）** | その行が実行されたか |\n| **Branch（分岐）** | `if` の true 側と false 側の**両方**を通ったか |\n| Function（関数） | その関数が呼ばれたか |\n:::\n::: note\n### Q. テストを書いていないプロジェクトでは使えないのか\n\n**使えます。テストが必要なのはカバレッジだけです。**\n\n| 項目 | テストが必要か | 対象範囲 |\n|---|---|---|\n| **指摘の検出**（Bug / Vulnerability / Code Smell） | **不要** | `sonar.sources` の全ファイル |\n| **重複検出** | **不要** | 同じ |\n| **複雑度・行数** | **不要** | 同じ |\n| **カバレッジ** | **必須** | テストが触った範囲＋触っていない範囲（0% として） |\n\n検証でも、テストを 1 行も書いていない `utils/insecure.ts` と `utils/legacy.ts` から\n指摘がちゃんと出ています（`S2245`、`S1135`、`S4144`）。**カバレッジだけが 0.0%** です。\n\nそして **0% であること自体が有用な情報**です。「複雑度 22 なのにテスト 0%」という\n組み合わせが見えれば、そこが最も危険な箇所だと判断できます。\n\nカバレッジ連携をしない場合は `sonar.*.coverage.reportPaths` を書かなければよく、\nその際は **Quality Gate からカバレッジ条件を外してください**。外さないと\n「レポートが無い → 0% → 永久に不合格」になります。\n\n\u003e **導入の順序としては、まず指摘と複雑度だけ見る → テストを書き始めたら\n\u003e カバレッジを足す**、という段階的な入れ方が現実的です。\n\u003e テストが無いことを理由に導入を諦める必要はありません。\n:::\n\n### ここからが本題\n\n**カバレッジ連携で一番ハマりやすいのがここです。** 原因はほぼ、\n**レポート内のファイルパスと `sonar.projectBaseDir` が一致していない**ことです。\n\nコンテナでテストを走らせると、レポートには `/usr/src/backend/todos/views.py` のような\nコンテナ内の絶対パスが書かれます。一方 SonarQube は `projectBaseDir` からの相対パスで\nファイルを識別するので、一致せず「カバレッジ情報の無いファイル」として扱われます。\nエラーにならず静かに 0% になるので気づきにくいです。\n\n### 対策：構成で防ぐ\n\n```\nリポジトリルート → /usr/src （全サービス共通）\n  backend のテスト実行 workdir : /usr/src/backend  →  projectBaseDir と一致\n  frontend のテスト実行 workdir: /usr/src/frontend →  projectBaseDir と一致\n```\n\nそのうえで、**Python は `relative_files = True` が必須**です。\n\n```ini\n# backend/.coveragerc\n[run]\nrelative_files = True\nsource = .\nomit =\n    manage.py\n    config/wsgi.py\n    config/settings.py\n    */migrations/*\n    tests/*\n\n[xml]\noutput = coverage.xml\n```\n\nJS/TS は vitest を `frontend/` 起点で動かし、reporter に `lcov` を含めます。\n\n```ts\n// frontend/vite.config.ts\ntest: {\n  environment: 'jsdom',\n  coverage: {\n    provider: 'v8',\n    reporter: ['text-summary', 'lcov'],   // lcov が SonarQube 用\n    reportsDirectory: 'coverage',\n    // テストが 1 つも無いファイルも 0% として集計させる。\n    // ここを絞りすぎると「カバレッジが高く見えるだけ」になる\n    include: ['src/**/*.ts', 'src/**/*.vue'],\n    exclude: ['src/main.ts', 'src/**/*.spec.ts'],\n  },\n}\n```\n\n### スキャン前に必ず確認する\n\n```bash\n$ grep -m3 filename= backend/coverage.xml\nfilename=\"config/urls.py\"\nfilename=\"todos/legacy.py\"      # ← 相対パスならOK\n\n$ grep -m3 '^SF:' frontend/coverage/lcov.info\nSF:src/App.vue\nSF:src/api/client.ts            # ← 相対パスならOK\n```\n\nここが絶対パスになっていたら、SonarQube に渡しても 0% になります。\n\nこの設計で、実際に **backend 47.7% / frontend 52.0%** が一発で取り込めました。\n\n:::note\n### Q. 手元の数字と SonarQube の数字が合わない\n\n**取り込みは成功しているのに、数字が一致しません。** これは不具合ではなく定義の違いです。\n\n| | 手元のツール | SonarQube |\n|---|---|---|\n| backend | **96%**（coverage.py） | **78.9%** ↓ 下がった |\n| frontend | **46.18%**（vitest） | **52.0%** ↑ 上がった |\n\n**backend が下がる理由** — `.coveragerc` の `omit` です。\n\n```ini\nomit =\n    manage.py\n    config/wsgi.py\n    config/settings.py\n```\n\ncoverage.py は「除外したので集計対象外」として**分母から外します**。\n一方 SonarQube にとってこれらは `sonar.sources=.` の範囲内にある普通のソースファイルで、\n`coverage.xml` に情報が無いため **`Zero Coverage Sensor` が「0% のファイル」として\n記録します**。分母に入るので全体が下がります。\n\n\u003e **これは実務で重要です。**\n\u003e **`.coveragerc` の `omit` だけでは、SonarQube 側の数字は逆に下がります。**\n\u003e SonarQube 側でも無視させたいなら `sonar-project.properties` に\n\u003e `sonar.coverage.exclusions` を書く必要があります。\n\n**frontend が上がる理由** — 計算式の違いです。\n\n```\nSonarQube の Coverage = (カバーされた行 + カバーされた分岐) / (実行可能行 + 全分岐)\n\nLines    : 121 / 262\nBranches :  42 /  53\n(121 + 42) / (262 + 53) = 163 / 315 = 51.7%  ≒ SonarQube の 52.0%\n```\n\nvitest の `Statements 46.18%` は**行だけ**の値です。SonarQube は分岐（79.24%）も\n混ぜるので上がります。\n\n**記事やレポートで数字を出すときは「どのツールの値か」を明示した方が安全です。**\n:::\n\n---\n\n## 前編のまとめ\n\nここまでで、**Vue3 + DRF のプロジェクトが解析され、カバレッジまで取り込まれた状態**に\nなりました。押さえた要点は 6 つです。\n\n1. イメージタグは `sonarqube:community` を明示する（`latest` は将来ブレる）\n2. **Docker に 15GB 以上の空きを確保する**（ES の watermark で起動に失敗する）\n3. 永続運用するなら PostgreSQL を用意する（組み込み H2 はアップグレードでデータを失う）\n4. Community Build はモノレポ非対応。**プロジェクトを 2 つに分け**、\n   `projectBaseDir` を切り替えて 2 回スキャンする\n5. **カバレッジはレポート内のパスと `projectBaseDir` を一致させる。**\n   Python は `relative_files = True`、JS/TS は `sonar.javascript.lcov.reportPaths`\n6. **テストが無いプロジェクトでも導入できる**（カバレッジ条件を外すだけ）\n\n検証に使った一式（Docker Compose / Makefile / 自動化スクリプト / 仕込み入りアプリ）は\n`make all` だけで起動から解析結果の表示まで通るようにまとめてあります。\n\n```bash\ncp .env.example .env\nmake all\n```\n\n---\n\n## 後編に続きます\n\nただし、**この時点でダッシュボードを見ると拍子抜けします。**\n\nあれだけ指摘が出ているのに、**Quality Gate は「Passed」と表示される**のです。\nバグではなく、SonarQube の設計思想によるものでした。\n\n後編ではその正体（**New Code** という概念）から始めて、次の内容を扱います。\n\n- 実際に検出された指摘の一覧と、その読み方\n- **仕込んだのに検出されなかった指摘** — `v-html` の XSS が出ない理由\n- Quality Gate を意図的に不合格にし、修正して合格させるまで\n- 日本語コメントが誤検知されたときの正しい対処\n- GitHub Actions 連携と、Community Build の限界\n\n→ **後編[「Vue3 + DRF に SonarQube を入れる（後編）— 指摘の読み方と Quality Gate の落とし穴」](https://qiita.com/mukku928/items/b234a01ea1dbbd07fd11)**\n\n---\n\n","coediting":false,"comments_count":0,"created_at":"2026-09-21T23:31:20+09:00","group":null,"id":"2ddb6a8274c55ebfac62","likes_count":1,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"SonarQube","versions":[]},{"name":"Docker","versions":[]},{"name":"Vue.js","versions":[]},{"name":"django-rest-framework","versions":[]}],"title":"Vue3 + DRF に SonarQube を入れる（前編）— Docker だけで構築してカバレッジを連携する","updated_at":"2026-09-21T23:33:27+09:00","url":"https://qiita.com/mukku928/items/2ddb6a8274c55ebfac62","user":{"description":null,"facebook_id":null,"followees_count":3,"followers_count":1,"github_login_name":null,"id":"mukku928","items_count":11,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":3884367,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocITD4x93F7dmTULUMUig_yiEmzTkHDU6aw2AQPuW_CCsjiYF29C=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"2:1-2:152\"\u003eVue.jsは、JavaScriptコードをより容易に記述できるようにするために設計されたJavaScriptフレームワークの一つです。\u003c/p\u003e\n\u003cp data-sourcepos=\"4:1-4:102\"\u003eこれは手軽に小規模アプリケーションを作ることを目指して作られました。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:60\"\u003e本記事では、このことについて説明します。\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-8:98\"\u003eなお、ここではHTML、CSS、JavaScriptの基礎的な内容は既知として進めます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"10:1-10:9\"\u003e\n\u003cspan id=\"目次\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%9B%AE%E6%AC%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e目次\u003c/h2\u003e\n\u003col data-sourcepos=\"12:1-20:0\"\u003e\n\u003cli data-sourcepos=\"12:1-12:49\"\u003e\u003ca href=\"#vuejs%E3%82%92%E4%BD%BF%E3%81%86%E6%BA%96%E5%82%99\"\u003eVue.jsを使う準備\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:61\"\u003e\u003ca href=\"#vuejs%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9%E3%81%AE%E5%9F%BA%E7%A4%8E\"\u003eVue.jsの書き方の基礎\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-14:74\"\u003e\u003ca href=\"#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003eディレクティブについて\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-15:74\"\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003eコンポーネントについて\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"16:1-16:74\"\u003e\u003ca href=\"#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003eトランジションについて\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"17:1-20:0\"\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99\"\u003e参考資料\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"21:1-21:24\"\u003e\n\u003cspan id=\"vuejsを使う準備\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vuejs%E3%82%92%E4%BD%BF%E3%81%86%E6%BA%96%E5%82%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue.jsを使う準備\u003c/h2\u003e\n\u003cp data-sourcepos=\"23:1-23:61\"\u003eVue.jsを導入する方法は以下の2つがあります。\u003c/p\u003e\n\u003col data-sourcepos=\"25:1-27:0\"\u003e\n\u003cli data-sourcepos=\"25:1-25:48\"\u003eファイルを直接ダウンロードする\u003c/li\u003e\n\u003cli data-sourcepos=\"26:1-27:0\"\u003eネットワークで公開されているファイルを読み込む\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"28:1-28:78\"\u003e方法としては②が手軽なので、そちらの方法を用います。\u003c/p\u003e\n\u003cp data-sourcepos=\"30:1-30:45\"\u003eまず、こちらにアクセスします。\u003c/p\u003e\n\u003cp data-sourcepos=\"32:1-33:144\"\u003e\u003ca href=\"https://jp.vuejs.org/v2/guide/installation.html#CDN\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.js CDN\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F489516%2F708a5118-d029-467e-b668-2bb73868b9d9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=4c9275694dda8080de19aac2b8262aaa\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F489516%2F708a5118-d029-467e-b668-2bb73868b9d9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=4c9275694dda8080de19aac2b8262aaa\" alt=\"1665914262347-xrBXwOMaJy.png.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F489516%2F708a5118-d029-467e-b668-2bb73868b9d9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=b9a4ce779156d1e0a228be4347afe067 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489516/708a5118-d029-467e-b668-2bb73868b9d9.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-37:138\"\u003eここで紹介されている\u003ccode\u003escript\u003c/code\u003eタグをHTML要素内の\u003ccode\u003ehead\u003c/code\u003e要素内にコピーすることで、Vue.jsを利用できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:54\"\u003e本記事では、以下のタグを利用します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"41:1-43:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;script \u003c/span\u003e\u003cspan class=\"na\"\u003esrc=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"45:1-45:33\"\u003eこれで準備が完了です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"49:1-49:30\"\u003e\n\u003cspan id=\"vuejsの書き方の基礎\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vuejs%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9%E3%81%AE%E5%9F%BA%E7%A4%8E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue.jsの書き方の基礎\u003c/h2\u003e\n\u003cp data-sourcepos=\"51:1-51:57\"\u003eVue.jsは基本的にこのようにして書きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"53:1-53:64\"\u003eHTMLの役割となる部分は以下のように書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"55:1-59:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ID名\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  {{ プロパティ名 }}\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"61:1-61:65\"\u003eここで\u003ccode\u003e{{ }}\u003c/code\u003eを\u003cstrong\u003eマスタッシュ構文\u003c/strong\u003eといいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-63:125\"\u003eまた、このような書式でHTMLの役割となる部分を書く方法を\u003cstrong\u003eテンプレート構文\u003c/strong\u003eといいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"65:1-65:82\"\u003eJavaScriptは以下のように書き、これがVue.jsの本体となります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"67:1-71:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// Vueインスタンスの中身\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"73:1-73:102\"\u003eVueインスタンスの中身には、例として以下のようなオプションがあります。\u003c/p\u003e\n\u003cul data-sourcepos=\"75:1-78:0\"\u003e\n\u003cli data-sourcepos=\"75:1-75:76\"\u003e\n\u003ccode\u003eel\u003c/code\u003eオプション：どのHTML要素とつなげるのかを指定する\u003c/li\u003e\n\u003cli data-sourcepos=\"76:1-76:74\"\u003e\n\u003ccode\u003edata\u003c/code\u003eオプション：どんなデータがあるのかを指定する\u003c/li\u003e\n\u003cli data-sourcepos=\"77:1-78:0\"\u003e\n\u003ccode\u003emethods\u003c/code\u003eオプション：どんな処理を行うのかを指定する\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"79:1-79:99\"\u003eこの他にもオプションがあるので、必要に応じて調べる必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"81:1-81:106\"\u003e例えば、\u003ccode\u003eel\u003c/code\u003eオプションと\u003ccode\u003edata\u003c/code\u003eオプションを使う場合は、このように書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"83:1-90:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eプロパティ名\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003e値\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"92:1-92:149\"\u003eこれは、Vue.jsで\u003ccode\u003eel\u003c/code\u003eにHTMLにあるIDを指定し、そのIDにある\u003ccode\u003e{{ プロパティ名 }}\u003c/code\u003eにデータを渡すことを意味します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"94:1-94:11\"\u003e\n\u003cspan id=\"例文1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%871\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文1\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"96:1-109:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  {{ myText }}\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emyText\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello Symbol!\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"111:1-111:95\"\u003eこの場合、\u003ccode\u003emyText\u003c/code\u003eに設定されている\u003ccode\u003eHello Symbol!\u003c/code\u003eが画面に表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"113:1-113:82\"\u003eまた、HTMLの要素からVue.jsに動作を指定することもできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"115:1-115:62\"\u003eその場合は、HTML要素に\u003ccode\u003ev-〇〇\u003c/code\u003eと記述します。\u003c/p\u003e\n\u003cp data-sourcepos=\"117:1-117:72\"\u003e〇〇には、実行してほしい動作の種類を指定します。\u003c/p\u003e\n\u003cp data-sourcepos=\"119:1-119:52\"\u003eこれを\u003cstrong\u003eディレクティブ\u003c/strong\u003eといいます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"121:1-123:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-〇〇=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"プロパティ名\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"125:1-125:11\"\u003e\n\u003cspan id=\"例文2\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%872\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文2\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"127:1-140:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myText\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emyText\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello Symbol!\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"142:1-142:113\"\u003e\u003ccode\u003ev-text\u003c/code\u003eとは、プロパティで指定した値を出力することができるディレクティブです。\u003c/p\u003e\n\u003cp data-sourcepos=\"144:1-144:54\"\u003eこれは、ID名が\u003ccode\u003eapp\u003c/code\u003eである\u003ccode\u003ediv\u003c/code\u003e要素内に、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"146:1-148:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myText\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"150:1-150:164\"\u003eと記述することで、Vueインスタンスにある\u003ccode\u003edata\u003c/code\u003e内のプロパティ\u003ccode\u003emyText\u003c/code\u003eを指定し、その値である\u003ccode\u003eHello Symbol!\u003c/code\u003eを表示できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"152:1-152:120\"\u003e基本的には、この仕組みを理解することがVue.jsを学ぶ上での重要なポイントになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"154:1-154:78\"\u003eこれからは、この書き方の応用について考えていきます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"158:1-158:36\"\u003e\n\u003cspan id=\"ディレクティブについて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eディレクティブについて\u003c/h2\u003e\n\u003cp data-sourcepos=\"160:1-160:75\"\u003eここでは、ディレクティブの種類について紹介します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"162:1-162:14\"\u003e\n\u003cspan id=\"-v-bind\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-v-bind\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e① v-bind\u003c/h3\u003e\n\u003cp data-sourcepos=\"164:1-164:96\"\u003e\u003ccode\u003ev-bind\u003c/code\u003eとは、HTMLの要素の属性をデータで指定するディレクティブです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"166:1-168:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-bind:属性=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"プロパティ名\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"170:1-170:75\"\u003eプロパティは画像やURL、クラスなどにも利用できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"172:1-172:10\"\u003e\n\u003cspan id=\"例文\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"174:1-187:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;img\u003c/span\u003e \u003cspan class=\"na\"\u003ev-bind:src=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"filename\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003esymbol.png\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"189:1-189:107\"\u003eこの例では、\u003ccode\u003efilename\u003c/code\u003eに設定されている値が\u003ccode\u003eimg\u003c/code\u003e要素の\u003ccode\u003esrc\u003c/code\u003e属性に指定されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"191:1-191:55\"\u003eそのため、\u003ccode\u003efilename\u003c/code\u003eが\u003ccode\u003esymbol.png\u003c/code\u003eであれば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"193:1-195:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;img\u003c/span\u003e \u003cspan class=\"na\"\u003esrc=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"symbol.png\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"197:1-197:42\"\u003eと同じような状態になります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"201:1-201:15\"\u003e\n\u003cspan id=\"-v-model\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-v-model\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e② v-model\u003c/h3\u003e\n\u003cp data-sourcepos=\"203:1-203:90\"\u003e\u003ccode\u003ev-model\u003c/code\u003eは、入力フォームとデータをつなげるディレクティブです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"205:1-219:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"message\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u0026gt;\u003c/span\u003eMessage: {{ message }}\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello! Symbol\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"221:1-221:207\"\u003eこれは、入力タグに入力したデータがVueインスタンス内の\u003ccode\u003edata\u003c/code\u003eオプションにある\u003ccode\u003emessage\u003c/code\u003eに入り、そのデータが\u003ccode\u003e{{ message }}\u003c/code\u003eに反映されることを意味します。\u003c/p\u003e\n\u003cp data-sourcepos=\"223:1-223:51\"\u003e結果として、以下のようになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"225:1-227:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eMessage: Hello! Symbol\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"229:1-229:63\"\u003e初期状態では\u003ccode\u003eHello! Symbol\u003c/code\u003eが表示されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"231:1-231:132\"\u003eしかし、入力フォームに文字を入力すると、その内容が\u003ccode\u003eMessage\u003c/code\u003eにもリアルタイムで反映されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"233:1-233:12\"\u003eつまり、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"235:1-241:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e入力フォーム\n    ↓\n  message\n    ↓\n{{ message }}\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"243:1-243:51\"\u003eという形でデータが連動しています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"247:1-247:12\"\u003e\n\u003cspan id=\"-v-on\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-v-on\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e③ v-on\u003c/h3\u003e\n\u003cp data-sourcepos=\"249:1-249:96\"\u003e\u003ccode\u003ev-on\u003c/code\u003eは、イベントと\u003ccode\u003emethods\u003c/code\u003eオプションをつなげるディレクティブです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"251:1-266:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003ev-on:click=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"hello\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eHello\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003emethods\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ehello\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"nf\"\u003ealert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello!\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"268:1-268:184\"\u003eこれは、\u003ccode\u003eHello\u003c/code\u003eと書かれたボタンをクリックすると、\u003ccode\u003ehello\u003c/code\u003eメソッドが実行され、\u003ccode\u003eHello!\u003c/code\u003eと書かれたアラートが表示されるというものです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"272:1-272:12\"\u003e\n\u003cspan id=\"-v-if\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-v-if\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e④ v-if\u003c/h3\u003e\n\u003cp data-sourcepos=\"274:1-274:81\"\u003e\u003ccode\u003ev-if\u003c/code\u003eは、条件によって表示を制御するディレクティブです。\u003c/p\u003e\n\u003cp data-sourcepos=\"276:1-276:101\"\u003e条件によって表示要素を切り替える場合は、\u003ccode\u003ev-if\u003c/code\u003eと\u003ccode\u003ev-else\u003c/code\u003eを使い分けます。\u003c/p\u003e\n\u003cp data-sourcepos=\"278:1-278:87\"\u003e条件を満たすときだけ表示するならば、以下のように書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"280:1-284:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"条件\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  条件がtrueならば表示\n\u003cspan class=\"nt\"\u003e\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"286:1-286:10\"\u003e\n\u003cspan id=\"例文-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"288:1-308:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"checkbox\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myVisible\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    表示する\n  \u003cspan class=\"nt\"\u003e\u0026lt;/label\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myVisible\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    チェックボックスはON\n  \u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emyVisible\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"310:1-310:58\"\u003eこれは、\u003ccode\u003einput\u003c/code\u003e要素のチェックボックスに、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"312:1-314:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev-model=\"myVisible\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"316:1-316:167\"\u003eと指定することで、Vueインスタンス内の\u003ccode\u003edata\u003c/code\u003eオプションにある\u003ccode\u003emyVisible\u003c/code\u003eにチェックボックスの状態が入るようにしています。\u003c/p\u003e\n\u003cp data-sourcepos=\"318:1-318:27\"\u003eそして、\u003ccode\u003ep\u003c/code\u003e要素に、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"320:1-322:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev-if=\"myVisible\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"324:1-324:104\"\u003eと書くことで、チェックボックスがONのときだけ表示されるようになります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"327:1-327:17\"\u003e\n\u003cspan id=\"v-ifとv-else\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#v-if%E3%81%A8v-else\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003ev-ifとv-else\u003c/h3\u003e\n\u003cp data-sourcepos=\"329:1-329:95\"\u003e条件分岐を行う場合は、以下のように\u003ccode\u003ev-if\u003c/code\u003eと\u003ccode\u003ev-else\u003c/code\u003eを組み合わせます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"331:1-339:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"条件\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  条件がtrueならば表示\n\u003cspan class=\"nt\"\u003e\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-else\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  そうでないならば表示\n\u003cspan class=\"nt\"\u003e\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"341:1-341:10\"\u003e\n\u003cspan id=\"例文-2\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"343:1-367:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"checkbox\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myVisible\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    表示する\n  \u003cspan class=\"nt\"\u003e\u0026lt;/label\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"myVisible\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    チェックボックスはON\n  \u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-else\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    チェックボックスはOFF\n  \u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emyVisible\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"369:1-369:57\"\u003eチェックボックスにチェックを入れると、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"371:1-373:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eチェックボックスはON\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"375:1-375:24\"\u003eと表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"377:1-377:27\"\u003eチェックを外すと、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"379:1-381:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eチェックボックスはOFF\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"383:1-383:24\"\u003eと表示されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"385:1-385:13\"\u003e\n\u003cspan id=\"-v-for\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-v-for\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e⑤ v-for\u003c/h3\u003e\n\u003cp data-sourcepos=\"387:1-387:95\"\u003e\u003ccode\u003ev-for\u003c/code\u003eは、HTML要素を繰り返し表示するときに使うディレクティブです。\u003c/p\u003e\n\u003cp data-sourcepos=\"389:1-389:84\"\u003e繰り返し使うものには、配列やオブジェクトなどがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"391:1-391:54\"\u003e基本的な書式は以下のようになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"393:1-397:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;タグ名\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"変数 in 繰り返し使うもの\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  繰り返し表示する部分\n\u003cspan class=\"nt\"\u003e\u0026lt;/タグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"399:1-399:10\"\u003e\n\u003cspan id=\"例文-3\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"401:1-418:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;ul\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;li\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"color in colorList\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n      {{ color }}\n    \u003cspan class=\"nt\"\u003e\u0026lt;/li\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/ul\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ecolorList\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eRed\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eGreen\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eBlue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"420:1-420:108\"\u003eこの例では、\u003ccode\u003ecolorList\u003c/code\u003eに入っているデータを1つずつ取り出して表示しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"422:1-422:48\"\u003e表示結果は以下のようになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"424:1-428:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eRed\nGreen\nBlue\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"430:1-430:103\"\u003eこのように、配列などのデータを利用してHTML要素を繰り返し生成できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"433:1-433:79\"\u003e以上の5つは、代表的なディレクティブとその使い方です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"435:1-441:56\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"435:1-435:50\"\u003e\n\u003cth data-sourcepos=\"435:2-435:26\"\u003eディレクティブ\u003c/th\u003e\n\u003cth data-sourcepos=\"435:28-435:49\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"437:1-437:56\"\u003e\n\u003ctd data-sourcepos=\"437:2-437:12\"\u003e\u003ccode\u003ev-bind\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"437:14-437:55\"\u003eHTML属性とデータを結び付ける\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"438:1-438:64\"\u003e\n\u003ctd data-sourcepos=\"438:2-438:12\"\u003e\u003ccode\u003ev-model\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"438:14-438:63\"\u003e入力フォームとデータを結び付ける\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"439:1-439:58\"\u003e\n\u003ctd data-sourcepos=\"439:2-439:12\"\u003e\u003ccode\u003ev-on\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"439:14-439:57\"\u003eイベントと処理を結び付ける\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"440:1-440:60\"\u003e\n\u003ctd data-sourcepos=\"440:2-440:12\"\u003e\u003ccode\u003ev-if\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"440:14-440:59\"\u003e条件によって表示を切り替える\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"441:1-441:56\"\u003e\n\u003ctd data-sourcepos=\"441:2-441:12\"\u003e\u003ccode\u003ev-for\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"441:14-441:55\"\u003eデータを繰り返し表示する\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"443:1-443:105\"\u003e使い方は他にもありますし、ディレクティブにもさまざまな種類があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"445:1-445:111\"\u003e開発をする際は、必要に応じて公式ドキュメントなどを確認するとよいでしょう。\u003c/p\u003e\n\u003chr data-sourcepos=\"447:1-448:0\"\u003e\n\u003ch2 data-sourcepos=\"449:1-449:36\"\u003e\n\u003cspan id=\"コンポーネントについて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコンポーネントについて\u003c/h2\u003e\n\u003cp data-sourcepos=\"451:1-451:110\"\u003eコンポーネントとは、\u003cstrong\u003e同じような処理やHTMLを部品としてまとめる仕組み\u003c/strong\u003eです。\u003c/p\u003e\n\u003cp data-sourcepos=\"453:1-453:123\"\u003eVue.jsでは、コンポーネントを作ることで、同じような処理や表示を何度も再利用できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"455:1-455:141\"\u003eコンポーネントの登録方法としては、ローカルに登録する方法とグローバルに登録する方法があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"457:1-457:81\"\u003eそのうち、ここではローカルに登録する方法を紹介します。\u003c/p\u003e\n\u003cp data-sourcepos=\"459:1-459:54\"\u003eその書式は以下のようになっています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"461:1-472:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eコンポーネントのオブジェクト名\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHTML部分\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ecomponents\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eコンポーネントのタグ名\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eコンポーネントのオブジェクト名\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"474:1-474:94\"\u003eこのコンポーネントをHTMLに呼び出すときは、以下のタグを使います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"476:1-478:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;コンポーネントのタグ名\u0026gt;\u0026lt;/コンポーネントのタグ名\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"480:1-480:87\"\u003eここで、コンポーネントのタグ名は好きな名前を指定できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"482:1-482:10\"\u003e\n\u003cspan id=\"例文-4\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cp data-sourcepos=\"484:1-484:52\"\u003eJavaScriptは以下のようにして書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"486:1-497:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etestComponent\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026lt;p\u0026gt;Hello Symbol!\u0026lt;/p\u0026gt;\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ecomponents\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003emy-component\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003etestComponent\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"499:1-499:37\"\u003eこれをHTMLに呼び出します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"501:1-506:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;my-component\u0026gt;\u0026lt;/my-component\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;my-component\u0026gt;\u0026lt;/my-component\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"508:1-508:12\"\u003eすると、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"510:1-513:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eHello Symbol!\nHello Symbol!\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"515:1-515:40\"\u003eと2行に渡って表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"517:1-517:78\"\u003eこれは、コンポーネントとして定義した\u003ccode\u003etestComponent\u003c/code\u003e内で、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"519:1-521:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003eHello Symbol!\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"523:1-523:51\"\u003eという要素を定義しているためです。\u003c/p\u003e\n\u003cp data-sourcepos=\"525:1-525:84\"\u003eその\u003ccode\u003etestComponent\u003c/code\u003eをVueインスタンス内の\u003ccode\u003ecomponents\u003c/code\u003eオプションに、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"527:1-529:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003emy-component\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003etestComponent\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"531:1-531:33\"\u003eとして登録しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"533:1-533:22\"\u003eそして、HTMLの、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"535:1-537:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;my-component\u0026gt;\u0026lt;/my-component\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"539:1-539:93\"\u003eで呼び出すことで、コンポーネントに定義した内容が表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"541:1-541:114\"\u003eこのように、コンポーネントを利用することで、同じ部品を何度も再利用できます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"544:1-544:36\"\u003e\n\u003cspan id=\"トランジションについて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eトランジションについて\u003c/h2\u003e\n\u003cp data-sourcepos=\"546:1-546:72\"\u003eトランジションとは、CSSの状態変化を扱う機能です。\u003c/p\u003e\n\u003cp data-sourcepos=\"548:1-548:126\"\u003eこの機能を使うことで、要素の表示・非表示などにアニメーションを付けることができます。\u003c/p\u003e\n\u003cp data-sourcepos=\"550:1-550:167\"\u003e表示箇所の切り替えにトランジションを適用させたい場合は、\u003ccode\u003e\u0026lt;transition\u0026gt;\u003c/code\u003eと\u003ccode\u003e\u0026lt;/transition\u0026gt;\u003c/code\u003eで囲む以下のような形で記述します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"552:1-556:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;transition\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"任意の名前\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  表示内容\n\u003cspan class=\"nt\"\u003e\u0026lt;/transition\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"558:1-558:111\"\u003eここで\u003ccode\u003ename\u003c/code\u003e属性を定義しない場合、デフォルトでは\u003ccode\u003ev\u003c/code\u003eという名前が使用されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"560:1-560:10\"\u003e\n\u003cspan id=\"例文-5\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-5\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"562:1-568:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;transition\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"opacity\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003estyle=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"margin-top: 30px\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"display\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    表示箇所\n  \u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/transition\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"570:1-570:147\"\u003e次にCSSではクラスを指定することで、「どのようなタイミングで、どのように変化するのか」を指定します。\u003c/p\u003e\n\u003cp data-sourcepos=\"572:1-572:57\"\u003e例えば、以下のようなクラスがあります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"574:1-581:62\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"574:1-574:50\"\u003e\n\u003cth data-sourcepos=\"574:2-574:29\"\u003eクラス\u003c/th\u003e\n\u003cth data-sourcepos=\"574:31-574:49\"\u003e役割\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"576:1-576:66\"\u003e\n\u003ctd data-sourcepos=\"576:2-576:23\"\u003e\u003ccode\u003e.name-enter\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"576:25-576:65\"\u003e表示されるときの最初の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"577:1-577:62\"\u003e\n\u003ctd data-sourcepos=\"577:2-577:23\"\u003e\u003ccode\u003e.name-enter-active\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"577:25-577:61\"\u003e表示されている間の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"578:1-578:66\"\u003e\n\u003ctd data-sourcepos=\"578:2-578:23\"\u003e\u003ccode\u003e.name-enter-to\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"578:25-578:65\"\u003e表示されるときの最後の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"579:1-579:62\"\u003e\n\u003ctd data-sourcepos=\"579:2-579:23\"\u003e\u003ccode\u003e.name-leave\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"579:25-579:61\"\u003e消えるときの最初の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"580:1-580:58\"\u003e\n\u003ctd data-sourcepos=\"580:2-580:23\"\u003e\u003ccode\u003e.name-leave-active\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"580:25-580:57\"\u003e消えている間の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"581:1-581:62\"\u003e\n\u003ctd data-sourcepos=\"581:2-581:23\"\u003e\u003ccode\u003e.name-leave-to\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"581:25-581:61\"\u003e消えるときの最後の状態\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"583:1-583:84\"\u003e※\u003ccode\u003ename\u003c/code\u003eには\u003ccode\u003etransition\u003c/code\u003eタグで指定した\u003ccode\u003ename\u003c/code\u003e属性の値が入ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"585:1-585:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"587:1-589:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;transition\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"opacity\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"591:1-591:21\"\u003eとした場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"css\" data-sourcepos=\"593:1-600:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nc\"\u003e.opacity-enter\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.opacity-enter-active\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.opacity-enter-to\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.opacity-leave\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.opacity-leave-active\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.opacity-leave-to\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"602:1-602:42\"\u003eというクラスが使用されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"604:1-604:69\"\u003e\u003ccode\u003ename\u003c/code\u003e属性を指定しない場合は、\u003ccode\u003ev\u003c/code\u003eが使用されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"606:1-606:18\"\u003eその場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"css\" data-sourcepos=\"608:1-615:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nc\"\u003e.v-enter\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.v-enter-active\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.v-enter-to\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.v-leave\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.v-leave-active\u003c/span\u003e\n\u003cspan class=\"nc\"\u003e.v-leave-to\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"617:1-617:18\"\u003eとなります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"619:1-619:10\"\u003e\n\u003cspan id=\"例文-6\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BE%8B%E6%96%87-6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e例文\u003c/h3\u003e\n\u003ch4 data-sourcepos=\"621:1-621:12\"\u003e\n\u003cspan id=\"html側\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#html%E5%81%B4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eHTML側\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"623:1-636:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"app\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"checkbox\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isOK\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    表示する\n  \u003cspan class=\"nt\"\u003e\u0026lt;/label\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;transition\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isOK\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n      表示、非表示のアニメーション\n    \u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/transition\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"638:1-638:29\"\u003e\n\u003cspan id=\"vueインスタンス側\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E5%81%B4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVueインスタンス側\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"640:1-647:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e#app\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eisOK\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"649:1-649:11\"\u003e\n\u003cspan id=\"css側\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#css%E5%81%B4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eCSS側\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"css\" data-sourcepos=\"651:1-659:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nc\"\u003e.v-enter\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eopacity\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"nc\"\u003e.v-enter-active\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003etransition\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0.5s\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"661:1-661:19\"\u003eまず、HTMLの、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"663:1-665:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"checkbox\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isOK\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"667:1-667:42\"\u003eの情報がVueインスタンス内の、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"669:1-671:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003eisOK\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"673:1-673:24\"\u003eに反映されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"675:1-675:37\"\u003e次に、\u003ccode\u003eisOK\u003c/code\u003eの情報がHTMLの、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"677:1-679:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;p\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isOK\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"681:1-681:21\"\u003eに渡されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"683:1-683:63\"\u003e\u003ccode\u003eisOK\u003c/code\u003eが\u003ccode\u003etrue\u003c/code\u003eになると、\u003ccode\u003ep\u003c/code\u003e要素が表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"685:1-685:129\"\u003e最後に、\u003ccode\u003etransition\u003c/code\u003eタグによって表示・非表示の状態変化にCSSのトランジションが適用されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"687:1-687:30\"\u003eこのように、情報は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"689:1-699:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eチェックボックス\n      ↓\n    isOK\n      ↓\n    v-if\n      ↓\n transition\n      ↓\nCSSによるアニメーション\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"701:1-701:39\"\u003eという流れで処理されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"703:1-703:112\"\u003eVue.jsでは、このようにデータの変化とHTMLの状態変化を結び付けることができます。\u003c/p\u003e\n\u003chr data-sourcepos=\"705:1-706:0\"\u003e\n\u003ch2 data-sourcepos=\"707:1-707:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"709:1-709:75\"\u003e今回は、Vue.jsの基本的な使い方について紹介しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"711:1-711:161\"\u003eVue.jsでは、JavaScriptでデータや処理を定義し、それをHTML側と結び付けることで、動的なWebページを作ることができます。\u003c/p\u003e\n\u003cp data-sourcepos=\"713:1-713:60\"\u003e特に重要なのは、以下のような仕組みです。\u003c/p\u003e\n\u003cul data-sourcepos=\"715:1-726:0\"\u003e\n\u003cli data-sourcepos=\"715:1-715:52\"\u003e\n\u003ccode\u003enew Vue()\u003c/code\u003e：Vueインスタンスを作成する\u003c/li\u003e\n\u003cli data-sourcepos=\"716:1-716:40\"\u003e\n\u003ccode\u003eel\u003c/code\u003e：Vue.jsとHTMLを結び付ける\u003c/li\u003e\n\u003cli data-sourcepos=\"717:1-717:50\"\u003e\n\u003ccode\u003edata\u003c/code\u003e：Vue.jsで扱うデータを定義する\u003c/li\u003e\n\u003cli data-sourcepos=\"718:1-718:43\"\u003e\n\u003ccode\u003e{{ }}\u003c/code\u003e：データをHTMLに表示する\u003c/li\u003e\n\u003cli data-sourcepos=\"719:1-719:53\"\u003e\n\u003ccode\u003ev-bind\u003c/code\u003e：HTML属性とデータを結び付ける\u003c/li\u003e\n\u003cli data-sourcepos=\"720:1-720:62\"\u003e\n\u003ccode\u003ev-model\u003c/code\u003e：入力フォームとデータを結び付ける\u003c/li\u003e\n\u003cli data-sourcepos=\"721:1-721:50\"\u003e\n\u003ccode\u003ev-on\u003c/code\u003e：イベントと処理を結び付ける\u003c/li\u003e\n\u003cli data-sourcepos=\"722:1-722:53\"\u003e\n\u003ccode\u003ev-if\u003c/code\u003e：条件によって表示を切り替える\u003c/li\u003e\n\u003cli data-sourcepos=\"723:1-723:48\"\u003e\n\u003ccode\u003ev-for\u003c/code\u003e：データを繰り返し表示する\u003c/li\u003e\n\u003cli data-sourcepos=\"724:1-724:57\"\u003eコンポーネント：HTMLや処理を部品化する\u003c/li\u003e\n\u003cli data-sourcepos=\"725:1-726:0\"\u003eトランジション：状態変化にアニメーションを付ける\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"727:1-727:192\"\u003e基本的な仕組みを理解した上で、公式ドキュメントなどを確認しながら実際にコードを書いていくと、Vue.jsへの理解を深めることができます。\u003c/p\u003e\n\u003chr data-sourcepos=\"729:1-730:0\"\u003e\n\u003ch2 data-sourcepos=\"731:1-731:15\"\u003e\n\u003cspan id=\"参考資料\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考資料\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"733:1-733:10\"\u003e\n\u003cspan id=\"書籍\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9B%B8%E7%B1%8D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e書籍\u003c/h3\u003e\n\u003cul data-sourcepos=\"735:1-737:0\"\u003e\n\u003cli data-sourcepos=\"735:1-735:71\"\u003e『動かして学ぶ！Vue.js開発入門』森巧尚（翔泳社）\u003c/li\u003e\n\u003cli data-sourcepos=\"736:1-737:0\"\u003e『Vue.js\u0026amp;Nuxt.js超入門』掌田津耶乃（秀和システム）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"738:1-738:16\"\u003e\n\u003cspan id=\"webサイト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#web%E3%82%B5%E3%82%A4%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eWebサイト\u003c/h3\u003e\n\u003cul data-sourcepos=\"740:1-743:130\"\u003e\n\u003cli data-sourcepos=\"740:1-740:57\"\u003e\u003ca href=\"https://jp.vuejs.org/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.js公式ドキュメント\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"741:1-741:67\"\u003e\u003ca href=\"https://jp.vuejs.org/v2/guide/installation.html#CDN\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.js CDN\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"742:1-742:70\"\u003e\u003ca href=\"https://www.tohoho-web.com/ex/vuejs.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eとほほのVue.js入門\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"743:1-743:130\"\u003e\u003ca href=\"https://smallit.co.jp/blog/829/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.jsでトランジションとアニメーションを使って画面に動きをつける\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"\nVue.jsは、JavaScriptコードをより容易に記述できるようにするために設計されたJavaScriptフレームワークの一つです。\n\nこれは手軽に小規模アプリケーションを作ることを目指して作られました。\n\n本記事では、このことについて説明します。\n\nなお、ここではHTML、CSS、JavaScriptの基礎的な内容は既知として進めます。\n\n## 目次\n\n1. [Vue.jsを使う準備](#vuejsを使う準備)\n2. [Vue.jsの書き方の基礎](#vuejsの書き方の基礎)\n3. [ディレクティブについて](#ディレクティブについて)\n4. [コンポーネントについて](#コンポーネントについて)\n5. [トランジションについて](#トランジションについて)\n6. [参考資料](#参考資料)\n\n\n\n## Vue.jsを使う準備\n\nVue.jsを導入する方法は以下の2つがあります。\n\n1. ファイルを直接ダウンロードする\n2. ネットワークで公開されているファイルを読み込む\n\n方法としては②が手軽なので、そちらの方法を用います。\n\nまず、こちらにアクセスします。\n\n[Vue.js CDN](https://jp.vuejs.org/v2/guide/installation.html#CDN)\n![1665914262347-xrBXwOMaJy.png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489516/708a5118-d029-467e-b668-2bb73868b9d9.png)\n\n\n\nここで紹介されている`script`タグをHTML要素内の`head`要素内にコピーすることで、Vue.jsを利用できます。\n\n本記事では、以下のタグを利用します。\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js\"\u003e\u003c/script\u003e\n```\n\nこれで準備が完了です。\n\n\n\n## Vue.jsの書き方の基礎\n\nVue.jsは基本的にこのようにして書きます。\n\nHTMLの役割となる部分は以下のように書きます。\n\n```html\n\u003cdiv id=\"ID名\"\u003e\n  {{ プロパティ名 }}\n\u003c/div\u003e\n```\n\nここで`{{ }}`を**マスタッシュ構文**といいます。\n\nまた、このような書式でHTMLの役割となる部分を書く方法を**テンプレート構文**といいます。\n\nJavaScriptは以下のように書き、これがVue.jsの本体となります。\n\n```javascript\nnew Vue({\n  // Vueインスタンスの中身\n})\n```\n\nVueインスタンスの中身には、例として以下のようなオプションがあります。\n\n* `el`オプション：どのHTML要素とつなげるのかを指定する\n* `data`オプション：どんなデータがあるのかを指定する\n* `methods`オプション：どんな処理を行うのかを指定する\n\nこの他にもオプションがあるので、必要に応じて調べる必要があります。\n\n例えば、`el`オプションと`data`オプションを使う場合は、このように書きます。\n\n```javascript\nnew Vue({\n  el: \"#app\",\n  data: {\n    プロパティ名: 値\n  }\n})\n```\n\nこれは、Vue.jsで`el`にHTMLにあるIDを指定し、そのIDにある`{{ プロパティ名 }}`にデータを渡すことを意味します。\n\n### 例文1\n\n```html\n\u003cdiv id=\"app\"\u003e\n  {{ myText }}\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    myText: \"Hello Symbol!\"\n  }\n})\n\u003c/script\u003e\n```\n\nこの場合、`myText`に設定されている`Hello Symbol!`が画面に表示されます。\n\nまた、HTMLの要素からVue.jsに動作を指定することもできます。\n\nその場合は、HTML要素に`v-〇〇`と記述します。\n\n〇〇には、実行してほしい動作の種類を指定します。\n\nこれを**ディレクティブ**といいます。\n\n```html\n\u003cタグ名 v-〇〇=\"プロパティ名\"\u003e\u003c/タグ名\u003e\n```\n\n### 例文2\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cp v-text=\"myText\"\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    myText: \"Hello Symbol!\"\n  }\n})\n\u003c/script\u003e\n```\n\n`v-text`とは、プロパティで指定した値を出力することができるディレクティブです。\n\nこれは、ID名が`app`である`div`要素内に、\n\n```html\n\u003cp v-text=\"myText\"\u003e\u003c/p\u003e\n```\n\nと記述することで、Vueインスタンスにある`data`内のプロパティ`myText`を指定し、その値である`Hello Symbol!`を表示できます。\n\n基本的には、この仕組みを理解することがVue.jsを学ぶ上での重要なポイントになります。\n\nこれからは、この書き方の応用について考えていきます。\n\n\n\n## ディレクティブについて\n\nここでは、ディレクティブの種類について紹介します。\n\n### ① v-bind\n\n`v-bind`とは、HTMLの要素の属性をデータで指定するディレクティブです。\n\n```html\n\u003cタグ名 v-bind:属性=\"プロパティ名\"\u003e\u003c/タグ名\u003e\n```\n\nプロパティは画像やURL、クラスなどにも利用できます。\n\n### 例文\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cimg v-bind:src=\"filename\"\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    filename: \"symbol.png\"\n  }\n})\n\u003c/script\u003e\n```\n\nこの例では、`filename`に設定されている値が`img`要素の`src`属性に指定されます。\n\nそのため、`filename`が`symbol.png`であれば、\n\n```html\n\u003cimg src=\"symbol.png\"\u003e\n```\n\nと同じような状態になります。\n\n\n\n### ② v-model\n\n`v-model`は、入力フォームとデータをつなげるディレクティブです。\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cinput v-model=\"message\"\u003e\n  \u003cdiv\u003eMessage: {{ message }}\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    message: \"Hello! Symbol\"\n  }\n})\n\u003c/script\u003e\n```\n\nこれは、入力タグに入力したデータがVueインスタンス内の`data`オプションにある`message`に入り、そのデータが`{{ message }}`に反映されることを意味します。\n\n結果として、以下のようになります。\n\n```text\nMessage: Hello! Symbol\n```\n\n初期状態では`Hello! Symbol`が表示されています。\n\nしかし、入力フォームに文字を入力すると、その内容が`Message`にもリアルタイムで反映されます。\n\nつまり、\n\n```text\n入力フォーム\n    ↓\n  message\n    ↓\n{{ message }}\n```\n\nという形でデータが連動しています。\n\n\n\n### ③ v-on\n\n`v-on`は、イベントと`methods`オプションをつなげるディレクティブです。\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cbutton v-on:click=\"hello\"\u003eHello\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  methods: {\n    hello: function() {\n      alert(\"Hello!\");\n    }\n  }\n})\n\u003c/script\u003e\n```\n\nこれは、`Hello`と書かれたボタンをクリックすると、`hello`メソッドが実行され、`Hello!`と書かれたアラートが表示されるというものです。\n\n\n\n### ④ v-if\n\n`v-if`は、条件によって表示を制御するディレクティブです。\n\n条件によって表示要素を切り替える場合は、`v-if`と`v-else`を使い分けます。\n\n条件を満たすときだけ表示するならば、以下のように書きます。\n\n```html\n\u003cタグ名 v-if=\"条件\"\u003e\n  条件がtrueならば表示\n\u003c/タグ名\u003e\n```\n\n### 例文\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003clabel\u003e\n    \u003cinput type=\"checkbox\" v-model=\"myVisible\"\u003e\n    表示する\n  \u003c/label\u003e\n\n  \u003cp v-if=\"myVisible\"\u003e\n    チェックボックスはON\n  \u003c/p\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    myVisible: false\n  }\n})\n\u003c/script\u003e\n```\n\nこれは、`input`要素のチェックボックスに、\n\n```html\nv-model=\"myVisible\"\n```\n\nと指定することで、Vueインスタンス内の`data`オプションにある`myVisible`にチェックボックスの状態が入るようにしています。\n\nそして、`p`要素に、\n\n```html\nv-if=\"myVisible\"\n```\n\nと書くことで、チェックボックスがONのときだけ表示されるようになります。\n\n\n### v-ifとv-else\n\n条件分岐を行う場合は、以下のように`v-if`と`v-else`を組み合わせます。\n\n```html\n\u003cタグ名 v-if=\"条件\"\u003e\n  条件がtrueならば表示\n\u003c/タグ名\u003e\n\n\u003cタグ名 v-else\u003e\n  そうでないならば表示\n\u003c/タグ名\u003e\n```\n\n### 例文\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003clabel\u003e\n    \u003cinput type=\"checkbox\" v-model=\"myVisible\"\u003e\n    表示する\n  \u003c/label\u003e\n\n  \u003cp v-if=\"myVisible\"\u003e\n    チェックボックスはON\n  \u003c/p\u003e\n\n  \u003cp v-else\u003e\n    チェックボックスはOFF\n  \u003c/p\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    myVisible: false\n  }\n})\n\u003c/script\u003e\n```\n\nチェックボックスにチェックを入れると、\n\n```text\nチェックボックスはON\n```\n\nと表示されます。\n\nチェックを外すと、\n\n```text\nチェックボックスはOFF\n```\n\nと表示されます。\n\n### ⑤ v-for\n\n`v-for`は、HTML要素を繰り返し表示するときに使うディレクティブです。\n\n繰り返し使うものには、配列やオブジェクトなどがあります。\n\n基本的な書式は以下のようになります。\n\n```html\n\u003cタグ名 v-for=\"変数 in 繰り返し使うもの\"\u003e\n  繰り返し表示する部分\n\u003c/タグ名\u003e\n```\n\n### 例文\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cul\u003e\n    \u003cli v-for=\"color in colorList\"\u003e\n      {{ color }}\n    \u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nnew Vue({\n  el: \"#app\",\n  data: {\n    colorList: [\"Red\", \"Green\", \"Blue\"]\n  }\n})\n\u003c/script\u003e\n```\n\nこの例では、`colorList`に入っているデータを1つずつ取り出して表示しています。\n\n表示結果は以下のようになります。\n\n```text\nRed\nGreen\nBlue\n```\n\nこのように、配列などのデータを利用してHTML要素を繰り返し生成できます。\n\n\n以上の5つは、代表的なディレクティブとその使い方です。\n\n| ディレクティブ   | 用途               |\n| --------- | ---------------- |\n| `v-bind`  | HTML属性とデータを結び付ける |\n| `v-model` | 入力フォームとデータを結び付ける |\n| `v-on`    | イベントと処理を結び付ける    |\n| `v-if`    | 条件によって表示を切り替える   |\n| `v-for`   | データを繰り返し表示する     |\n\n使い方は他にもありますし、ディレクティブにもさまざまな種類があります。\n\n開発をする際は、必要に応じて公式ドキュメントなどを確認するとよいでしょう。\n\n---\n\n## コンポーネントについて\n\nコンポーネントとは、**同じような処理やHTMLを部品としてまとめる仕組み**です。\n\nVue.jsでは、コンポーネントを作ることで、同じような処理や表示を何度も再利用できます。\n\nコンポーネントの登録方法としては、ローカルに登録する方法とグローバルに登録する方法があります。\n\nそのうち、ここではローカルに登録する方法を紹介します。\n\nその書式は以下のようになっています。\n\n```javascript\nconst コンポーネントのオブジェクト名 = {\n  template: \"HTML部分\"\n}\n\nnew Vue({\n  el: \"#app\",\n  components: {\n    \"コンポーネントのタグ名\": コンポーネントのオブジェクト名\n  }\n})\n```\n\nこのコンポーネントをHTMLに呼び出すときは、以下のタグを使います。\n\n```html\n\u003cコンポーネントのタグ名\u003e\u003c/コンポーネントのタグ名\u003e\n```\n\nここで、コンポーネントのタグ名は好きな名前を指定できます。\n\n### 例文\n\nJavaScriptは以下のようにして書きます。\n\n```javascript\nconst testComponent = {\n  template: \"\u003cp\u003eHello Symbol!\u003c/p\u003e\"\n}\n\nnew Vue({\n  el: \"#app\",\n  components: {\n    \"my-component\": testComponent\n  }\n})\n```\n\nこれをHTMLに呼び出します。\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003cmy-component\u003e\u003c/my-component\u003e\n  \u003cmy-component\u003e\u003c/my-component\u003e\n\u003c/div\u003e\n```\n\nすると、\n\n```text\nHello Symbol!\nHello Symbol!\n```\n\nと2行に渡って表示されます。\n\nこれは、コンポーネントとして定義した`testComponent`内で、\n\n```html\n\u003cp\u003eHello Symbol!\u003c/p\u003e\n```\n\nという要素を定義しているためです。\n\nその`testComponent`をVueインスタンス内の`components`オプションに、\n\n```javascript\n\"my-component\": testComponent\n```\n\nとして登録しています。\n\nそして、HTMLの、\n\n```html\n\u003cmy-component\u003e\u003c/my-component\u003e\n```\n\nで呼び出すことで、コンポーネントに定義した内容が表示されます。\n\nこのように、コンポーネントを利用することで、同じ部品を何度も再利用できます。\n\n\n## トランジションについて\n\nトランジションとは、CSSの状態変化を扱う機能です。\n\nこの機能を使うことで、要素の表示・非表示などにアニメーションを付けることができます。\n\n表示箇所の切り替えにトランジションを適用させたい場合は、`\u003ctransition\u003e`と`\u003c/transition\u003e`で囲む以下のような形で記述します。\n\n```html\n\u003ctransition name=\"任意の名前\"\u003e\n  表示内容\n\u003c/transition\u003e\n```\n\nここで`name`属性を定義しない場合、デフォルトでは`v`という名前が使用されます。\n\n### 例文\n\n```html\n\u003ctransition name=\"opacity\"\u003e\n  \u003cp style=\"margin-top: 30px\" v-if=\"display\"\u003e\n    表示箇所\n  \u003c/p\u003e\n\u003c/transition\u003e\n```\n\n次にCSSではクラスを指定することで、「どのようなタイミングで、どのように変化するのか」を指定します。\n\n例えば、以下のようなクラスがあります。\n\n| クラス                  | 役割            |\n| -------------------- | ------------- |\n| `.name-enter`        | 表示されるときの最初の状態 |\n| `.name-enter-active` | 表示されている間の状態   |\n| `.name-enter-to`     | 表示されるときの最後の状態 |\n| `.name-leave`        | 消えるときの最初の状態   |\n| `.name-leave-active` | 消えている間の状態     |\n| `.name-leave-to`     | 消えるときの最後の状態   |\n\n※`name`には`transition`タグで指定した`name`属性の値が入ります。\n\n例えば、\n\n```html\n\u003ctransition name=\"opacity\"\u003e\n```\n\nとした場合は、\n\n```css\n.opacity-enter\n.opacity-enter-active\n.opacity-enter-to\n.opacity-leave\n.opacity-leave-active\n.opacity-leave-to\n```\n\nというクラスが使用されます。\n\n`name`属性を指定しない場合は、`v`が使用されます。\n\nその場合は、\n\n```css\n.v-enter\n.v-enter-active\n.v-enter-to\n.v-leave\n.v-leave-active\n.v-leave-to\n```\n\nとなります。\n\n### 例文\n\n#### HTML側\n\n```html\n\u003cdiv id=\"app\"\u003e\n  \u003clabel\u003e\n    \u003cinput type=\"checkbox\" v-model=\"isOK\"\u003e\n    表示する\n  \u003c/label\u003e\n\n  \u003ctransition\u003e\n    \u003cp v-if=\"isOK\"\u003e\n      表示、非表示のアニメーション\n    \u003c/p\u003e\n  \u003c/transition\u003e\n\u003c/div\u003e\n```\n\n#### Vueインスタンス側\n\n```javascript\nnew Vue({\n  el: \"#app\",\n  data: {\n    isOK: false\n  }\n})\n```\n\n#### CSS側\n\n```css\n.v-enter {\n  opacity: 0;\n}\n\n.v-enter-active {\n  transition: 0.5s;\n}\n```\n\nまず、HTMLの、\n\n```html\n\u003cinput type=\"checkbox\" v-model=\"isOK\"\u003e\n```\n\nの情報がVueインスタンス内の、\n\n```javascript\nisOK: false\n```\n\nに反映されます。\n\n次に、`isOK`の情報がHTMLの、\n\n```html\n\u003cp v-if=\"isOK\"\u003e\n```\n\nに渡されます。\n\n`isOK`が`true`になると、`p`要素が表示されます。\n\n最後に、`transition`タグによって表示・非表示の状態変化にCSSのトランジションが適用されます。\n\nこのように、情報は、\n\n```text\nチェックボックス\n      ↓\n    isOK\n      ↓\n    v-if\n      ↓\n transition\n      ↓\nCSSによるアニメーション\n```\n\nという流れで処理されます。\n\nVue.jsでは、このようにデータの変化とHTMLの状態変化を結び付けることができます。\n\n---\n\n## まとめ\n\n今回は、Vue.jsの基本的な使い方について紹介しました。\n\nVue.jsでは、JavaScriptでデータや処理を定義し、それをHTML側と結び付けることで、動的なWebページを作ることができます。\n\n特に重要なのは、以下のような仕組みです。\n\n* `new Vue()`：Vueインスタンスを作成する\n* `el`：Vue.jsとHTMLを結び付ける\n* `data`：Vue.jsで扱うデータを定義する\n* `{{ }}`：データをHTMLに表示する\n* `v-bind`：HTML属性とデータを結び付ける\n* `v-model`：入力フォームとデータを結び付ける\n* `v-on`：イベントと処理を結び付ける\n* `v-if`：条件によって表示を切り替える\n* `v-for`：データを繰り返し表示する\n* コンポーネント：HTMLや処理を部品化する\n* トランジション：状態変化にアニメーションを付ける\n\n基本的な仕組みを理解した上で、公式ドキュメントなどを確認しながら実際にコードを書いていくと、Vue.jsへの理解を深めることができます。\n\n---\n\n## 参考資料\n\n### 書籍\n\n* 『動かして学ぶ！Vue.js開発入門』森巧尚（翔泳社）\n* 『Vue.js\u0026Nuxt.js超入門』掌田津耶乃（秀和システム）\n\n### Webサイト\n\n* [Vue.js公式ドキュメント](https://jp.vuejs.org/)\n* [Vue.js CDN](https://jp.vuejs.org/v2/guide/installation.html#CDN)\n* [とほほのVue.js入門](https://www.tohoho-web.com/ex/vuejs.html)\n* [Vue.jsでトランジションとアニメーションを使って画面に動きをつける](https://smallit.co.jp/blog/829/)\n","coediting":false,"comments_count":0,"created_at":"2026-09-21T02:41:44+09:00","group":null,"id":"7beb47974302d11b79cd","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]}],"title":"Vue.jsの基礎について","updated_at":"2026-09-21T02:41:44+09:00","url":"https://qiita.com/YutopianF/items/7beb47974302d11b79cd","user":{"description":"","facebook_id":"","followees_count":25,"followers_count":10,"github_login_name":"YUTOpian","id":"YutopianF","items_count":14,"linkedin_id":"","location":"","name":"YUTO","organization":"","permanent_id":489516,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/489516/cc2459b40c36ad0b1f8e81a608c821d6d2572c61/x_large.png?1660980311","team_only":false,"twitter_screen_name":"YutopianF","website_url":"https://lit.link/yutopian"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:12\"\u003e\n\u003cspan id=\"最初に\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E5%88%9D%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e最初に\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:211\"\u003e以下の記事でHono + Vueの組み合わせを紹介しました。以下の記事では本題から逸れる為、詳しい設計については説明しなかったのでこの記事を執筆しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:46\"\u003e\u003ciframe id=\"qiita-embed-content__3fa8412139b80ae137eb057e2f91ef45\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__3fa8412139b80ae137eb057e2f91ef45\" data-content=\"https%3A%2F%2Fzenn.dev%2Frobot%2Farticles%2Fe2f4ed2d60d4d3\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:91\"\u003eHonoを採用する場合フロントエンドは以下が主な選択肢となります。\u003c/p\u003e\n\u003cul data-sourcepos=\"9:1-12:0\"\u003e\n\u003cli data-sourcepos=\"9:1-9:7\"\u003eReact\u003c/li\u003e\n\u003cli data-sourcepos=\"10:1-10:7\"\u003eHonoX\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-12:0\"\u003e他フレームワークとの併用\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"13:1-14:122\"\u003eしかしそれ以外に最近登場した方法としてInertia.jsを使う方法があります。\u003cbr\u003e\nInertia.jsによってReact/Vue/Svelteを従来より手軽にフロントエンドで使用することが可能です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"16:1-16:12\"\u003e\n\u003cspan id=\"実装例\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A3%85%E4%BE%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実装例\u003c/h2\u003e\n\u003cp data-sourcepos=\"18:1-18:36\"\u003e以下が実装例になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"20:1-20:44\"\u003e\u003ciframe id=\"qiita-embed-content__82842debaacb07c827dc41c64c7e4f07\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__82842debaacb07c827dc41c64c7e4f07\" data-content=\"https%3A%2F%2Fgithub.com%2Fro80t%2Fservice-status-page\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"22:1-22:132\"\u003e上記のリポジトリは\u003ca href=\"https://github.com/yusukebe/hono-inertia-example\" rel=\"nofollow noopener\" target=\"_blank\"\u003eHono作者のexample\u003c/a\u003eを参考にしたものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:86\"\u003eHono + Inertia.js + Vueの場合は以下のような順番で処理が走ります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"26:1-26:34\"\u003e\n\u003cspan id=\"1-vueページの自動検出\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-vue%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E8%87%AA%E5%8B%95%E6%A4%9C%E5%87%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. Vueページの自動検出\u003c/h3\u003e\n\u003cp data-sourcepos=\"28:1-28:92\"\u003e以下が app/pages/*.vue をスキャンし、\u003ccode\u003eapp/pages.gen.ts\u003c/code\u003eを自動生成します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"30:1-34:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003einertiaPages\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e@hono/inertia/vite\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003einertiaPages\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003eextensions\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"36:1-36:93\"\u003e自動生成された\u003ccode\u003eapp/pages.gen.ts\u003c/code\u003eは以下のようなコードになっています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"38:1-55:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// AUTO-GENERATED by @hono/inertia/vite. Do not edit.\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eapp\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e./server\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003ePageName\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eError404\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHome\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kr\"\u003edeclare\u003c/span\u003e \u003cspan class=\"kr\"\u003emodule\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e@hono/inertia\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eInertiaPages\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eError404\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunknown\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eHome\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunknown\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eAppRegistry\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003etypeof\u003c/span\u003e \u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ePageProps\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e@hono/inertia\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"57:1-57:114\"\u003eこれによって型チェックやエディターの補完機能を利用することが可能になります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"59:1-59:46\"\u003e\n\u003cspan id=\"2-フロントエンド側のバンドル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E5%81%B4%E3%81%AE%E3%83%90%E3%83%B3%E3%83%89%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. フロントエンド側のバンドル\u003c/h3\u003e\n\u003cp data-sourcepos=\"60:1-60:293\"\u003e\u003ccode\u003e@vitejs/plugin-vue\u003c/code\u003eが\u003ccode\u003eapp/client.ts\u003c/code\u003eをエントリに、.vueをSFC(単一ファイルコンポーネント)にコンパイルするように設定しています。 (\u003ca href=\"https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/client.ts\" rel=\"nofollow noopener\" target=\"_blank\"\u003e該当コード\u003c/a\u003e)\u003c/p\u003e\n\u003cp data-sourcepos=\"62:1-62:44\"\u003e結果はdist/clientに出力されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"64:1-64:33\"\u003e\n\u003cspan id=\"3-workerssrのバンドル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-workerssr%E3%81%AE%E3%83%90%E3%83%B3%E3%83%89%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. Worker(SSR)のバンドル\u003c/h3\u003e\n\u003cp data-sourcepos=\"66:1-66:289\"\u003e\u003ccode\u003e@cloudflare/vite-plugin\u003c/code\u003eが\u003ccode\u003ewrangler.jsonc\u003c/code\u003eの\u003ccode\u003emain: \"./app/server.ts\"\u003c/code\u003eを見て、Cloudflare Workers 環境向けにサーバーコードをビルドします。 (\u003ca href=\"https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/server.ts\" rel=\"nofollow noopener\" target=\"_blank\"\u003e該当コード\u003c/a\u003e)\u003c/p\u003e\n\u003cp data-sourcepos=\"68:1-68:458\"\u003e以下の部分で呼び出されている\u003ca href=\"https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/root-view.tsx\" rel=\"nofollow noopener\" target=\"_blank\"\u003e\u003ccode\u003eapp/root-view.tsx\u003c/code\u003e\u003c/a\u003eの\u003ccode\u003erootView\u003c/code\u003eが各リクエストで\u003ca href=\"https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/ssr.ts\" rel=\"nofollow noopener\" target=\"_blank\"\u003e\u003ccode\u003eapp/ssr.ts\u003c/code\u003e\u003c/a\u003eの\u003ccode\u003erenderPage\u003c/code\u003eを呼び、\u003ccode\u003e@vue/server-renderer\u003c/code\u003eの\u003ccode\u003erenderToString\u003c/code\u003eでVueコンポーネントをSSRレンダリングします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"70:1-74:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eapp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003eHono\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003eBindings\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eCloudflareBindings\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003euse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003einertia\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003erootView\u003c/span\u003e \u003cspan class=\"p\"\u003e}));\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"75:1-75:132\"\u003eこれによってHonoのc.renderを使って行ったVueのレンダリングが実際のウェブサイトに適応されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"77:1-77:155\"\u003e\u003ccode\u003evite-ssr-components\u003c/code\u003eの\u003ccode\u003e\u0026lt;ViteClient /\u0026gt;\u003c/code\u003e \u003ccode\u003e\u0026lt;Script\u0026gt;\u003c/code\u003e \u003ccode\u003e\u0026lt;Link\u0026gt;\u003c/code\u003eはdev時はHMR用タグ、build時は実際の生成済みアセットパスになります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"79:1-79:39\"\u003e\n\u003cspan id=\"4-environmentsssr-の特殊処理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-environmentsssr-%E3%81%AE%E7%89%B9%E6%AE%8A%E5%87%A6%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. environments.ssr の特殊処理\u003c/h3\u003e\n\u003cp data-sourcepos=\"81:1-82:117\"\u003e\u003ccode\u003evite-ssr-components\u003c/code\u003eが内部的にもう1つのssr environmentを生成してしまうため、\u003ccode\u003evite.config.ts\u003c/code\u003eでダミーの\u003ccode\u003eentry (./app/server.ts) \u003c/code\u003eと\u003ccode\u003eemptyOutDir: false\u003c/code\u003eを明示的に設定しています。\u003cbr\u003e\nこれがないと1～3までの処理によって作られたdist配下の生成物を空にしてしまいます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"84:1-84:25\"\u003e\n\u003cspan id=\"5-deploy-おまけ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-deploy-%E3%81%8A%E3%81%BE%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. Deploy (おまけ)\u003c/h3\u003e\n\u003cp data-sourcepos=\"86:1-86:143\"\u003e1～4の処理によってdist配下にworker本体と静的アセットが揃っているので後は\u003ccode\u003ewrangler deploy\u003c/code\u003eをするだけです。\u003c/p\u003e\n\u003cp data-sourcepos=\"88:1-88:176\"\u003e\u003cstrong\u003e注意点: Deploy時にデータベース等に個別にマイグレーションが必要な場合は\u003ccode\u003ewrangler deploy\u003c/code\u003eだけでは駄目なので気を付けてください\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"90:1-90:12\"\u003e\n\u003cspan id=\"最後に\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E5%BE%8C%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e最後に\u003c/h2\u003e\n\u003cp data-sourcepos=\"92:1-92:176\"\u003e\u003ca href=\"#%E5%AE%9F%E8%A3%85%E4%BE%8B\"\u003e実装例\u003c/a\u003eでも示したようにHono + Inertia.jsの組み合わせでReact/Vue/Svelteをフロントエンドで手軽に使用可能です。\u003c/p\u003e\n\u003cp data-sourcepos=\"94:1-94:134\"\u003eCloudFlare Workers + Honoでウェブサービスを作る際は是非Inertia.jsの採用も考えていただけると幸いです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"96:1-96:9\"\u003e\n\u003cspan id=\"宣伝\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%A3%E4%BC%9D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e宣伝\u003c/h2\u003e\n\u003cp data-sourcepos=\"98:1-98:67\"\u003eTwitterとGitHubも良かったらフォローお願いします。\u003c/p\u003e\n\u003cp data-sourcepos=\"100:1-100:29\"\u003e\u003ciframe id=\"qiita-embed-content__651491577ce24de43c6db29c93dc4d12\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__651491577ce24de43c6db29c93dc4d12\" data-content=\"https%3A%2F%2Ftwitter.com%2Fro80t_dev\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"102:1-102:24\"\u003e\u003ciframe id=\"qiita-embed-content__24c9cbfe5e083be771f874271080a0bb\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__24c9cbfe5e083be771f874271080a0bb\" data-content=\"https%3A%2F%2Fgithub.com%2Fro80t\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n","body":"## 最初に\n\n以下の記事でHono + Vueの組み合わせを紹介しました。以下の記事では本題から逸れる為、詳しい設計については説明しなかったのでこの記事を執筆しました。\n\nhttps://zenn.dev/robot/articles/e2f4ed2d60d4d3\n\nHonoを採用する場合フロントエンドは以下が主な選択肢となります。\n\n- React\n- HonoX\n- 他フレームワークとの併用\n\nしかしそれ以外に最近登場した方法としてInertia.jsを使う方法があります。\nInertia.jsによってReact/Vue/Svelteを従来より手軽にフロントエンドで使用することが可能です。\n\n## 実装例\n\n以下が実装例になります。\n\nhttps://github.com/ro80t/service-status-page\n\n上記のリポジトリは[Hono作者のexample](https://github.com/yusukebe/hono-inertia-example)を参考にしたものです。\n\nHono + Inertia.js + Vueの場合は以下のような順番で処理が走ります。\n\n### 1. Vueページの自動検出\n\n以下が app/pages/*.vue をスキャンし、`app/pages.gen.ts`を自動生成します。\n\n```ts\nimport { inertiaPages } from \"@hono/inertia/vite\";\n\ninertiaPages({ extensions: [\"vue\"] })\n```\n\n自動生成された`app/pages.gen.ts`は以下のようなコードになっています。\n\n```ts\n// AUTO-GENERATED by @hono/inertia/vite. Do not edit.\nimport type app from \"./server\";\n\nexport type PageName = \"Error404\" | \"Home\";\n\ndeclare module \"@hono/inertia\" {\n  interface InertiaPages {\n    Error404: unknown;\n    Home: unknown;\n  }\n  interface AppRegistry {\n    app: typeof app;\n  }\n}\n\nexport type { PageProps } from \"@hono/inertia\";\n```\n\nこれによって型チェックやエディターの補完機能を利用することが可能になります。\n\n### 2. フロントエンド側のバンドル\n`@vitejs/plugin-vue`が`app/client.ts`をエントリに、.vueをSFC(単一ファイルコンポーネント)にコンパイルするように設定しています。 ([該当コード](https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/client.ts))\n\n結果はdist/clientに出力されます。\n\n### 3. Worker(SSR)のバンドル\n\n`@cloudflare/vite-plugin`が`wrangler.jsonc`の`main: \"./app/server.ts\"`を見て、Cloudflare Workers 環境向けにサーバーコードをビルドします。 ([該当コード](https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/server.ts))\n\n以下の部分で呼び出されている[`app/root-view.tsx`](https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/root-view.tsx)の`rootView`が各リクエストで[`app/ssr.ts`](https://github.com/ro80t/service-status-page/blob/6b5ed9db9c426ee550b94c595621b64019ab75d9/app/ssr.ts)の`renderPage`を呼び、`@vue/server-renderer`の`renderToString`でVueコンポーネントをSSRレンダリングします。\n\n```ts\nconst app = new Hono\u003c{ Bindings: CloudflareBindings }\u003e();\n\napp.use(inertia({ rootView }));\n```\nこれによってHonoのc.renderを使って行ったVueのレンダリングが実際のウェブサイトに適応されます。\n\n`vite-ssr-components`の`\u003cViteClient /\u003e` `\u003cScript\u003e` `\u003cLink\u003e`はdev時はHMR用タグ、build時は実際の生成済みアセットパスになります。\n\n### 4. environments.ssr の特殊処理\n\n`vite-ssr-components`が内部的にもう1つのssr environmentを生成してしまうため、`vite.config.ts`でダミーの`entry (./app/server.ts) `と`emptyOutDir: false`を明示的に設定しています。\nこれがないと1～3までの処理によって作られたdist配下の生成物を空にしてしまいます。\n\n### 5. Deploy (おまけ)\n\n1～4の処理によってdist配下にworker本体と静的アセットが揃っているので後は`wrangler deploy`をするだけです。\n\n**注意点: Deploy時にデータベース等に個別にマイグレーションが必要な場合は`wrangler deploy`だけでは駄目なので気を付けてください**\n\n## 最後に\n\n[実装例](#%E5%AE%9F%E8%A3%85%E4%BE%8B)でも示したようにHono + Inertia.jsの組み合わせでReact/Vue/Svelteをフロントエンドで手軽に使用可能です。\n\nCloudFlare Workers + Honoでウェブサービスを作る際は是非Inertia.jsの採用も考えていただけると幸いです。\n\n## 宣伝\n\nTwitterとGitHubも良かったらフォローお願いします。\n\nhttps://twitter.com/ro80t_dev\n\nhttps://github.com/ro80t\n","coediting":false,"comments_count":0,"created_at":"2026-09-20T22:09:36+09:00","group":null,"id":"3afe3034396e3acb8cae","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"TypeScript","versions":[]},{"name":"Hono","versions":[]},{"name":"CloudflareWorkers","versions":[]},{"name":"Inertia.js","versions":[]},{"name":"Vue.js","versions":[]}],"title":"Hono + Inertia.jsによってReact/Vue/Svelteを採用できるようになった","updated_at":"2026-09-20T22:09:36+09:00","url":"https://qiita.com/ro80t/items/3afe3034396e3acb8cae","user":{"description":"Qiitaは趣味程度にボチボチ書いてます。記事に多少ミスがあるかもしれませんがお手柔らかにお願いします。メインはNode.jsとTypeScriptです。Rust Python GO JAVAなど他にもいくつかできますが自信はないのであまり記事は書きません。","facebook_id":"","followees_count":2,"followers_count":3,"github_login_name":"ro80t","id":"ro80t","items_count":10,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":2676619,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2676619/profile-images/1789909811","team_only":false,"twitter_screen_name":"AlwaysHarapan","website_url":"https://x.com/AlwaysHarapan"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:168\"\u003e\u003ccode\u003ev-model\u003c/code\u003eをVue.jsの基礎から理解します。便利な\u003ccode\u003ev-model\u003c/code\u003eですが、曖昧な理解になりがちだったため、今回の記事を書いています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"5:1-5:28\"\u003e\n\u003cspan id=\"この記事の対象者\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E5%AF%BE%E8%B1%A1%E8%80%85\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事の対象者\u003c/h3\u003e\n\u003cul data-sourcepos=\"7:1-9:0\"\u003e\n\u003cli data-sourcepos=\"7:1-7:68\"\u003eVue.jsに入門してチュートリアルを一通り進めた人\u003c/li\u003e\n\u003cli data-sourcepos=\"8:1-9:0\"\u003e\n\u003ccode\u003ev-model\u003c/code\u003eが具体的に何をしているのか理解したい人\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"10:1-10:21\"\u003e\n\u003cspan id=\"v-modelとは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#v-model%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003ev-model\u003c/code\u003eとは？\u003c/h2\u003e\n\u003cp data-sourcepos=\"12:1-12:162\"\u003e\u003ccode\u003ev-model\u003c/code\u003eとは、双方向バインディングを実現する構文です。例えば、\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003eを使ってフォームを設置する場面を考えます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"15:1-29:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eApp.vue\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e''\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"name\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\n    \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003e入力された名前: \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"31:1-32:130\"\u003e画面(入力前)\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F0b21c729-4a2c-4bba-a243-07f8682be0ca.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=16e73ef67b3155818ed3fbd728db6c6b\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F0b21c729-4a2c-4bba-a243-07f8682be0ca.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=16e73ef67b3155818ed3fbd728db6c6b\" alt=\"v-model-image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F0b21c729-4a2c-4bba-a243-07f8682be0ca.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=8dfa9efc8f198603c04f87165252d786 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/0b21c729-4a2c-4bba-a243-07f8682be0ca.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-36:129\"\u003e画面(入力後)\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F82c5afbf-fa85-4c27-a5c4-c642b9d1722f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f0a1a5082e8bf67fea65647bf9d35b10\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F82c5afbf-fa85-4c27-a5c4-c642b9d1722f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f0a1a5082e8bf67fea65647bf9d35b10\" alt=\"v-model-edit.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F82c5afbf-fa85-4c27-a5c4-c642b9d1722f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=86ea50539d41a5248b408ce3c1acf8fd 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/82c5afbf-fa85-4c27-a5c4-c642b9d1722f.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:162\"\u003eフォームに文字を入力すると、すぐに「入力された名前：」の右に内容が表示されます。双方向バインディングとして、\u003c/p\u003e\n\u003cul data-sourcepos=\"41:1-43:0\"\u003e\n\u003cli data-sourcepos=\"41:1-41:75\"\u003eフォームの値を変更するとJavaScript側の値\u003ccode\u003ename\u003c/code\u003eが変わる\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-43:0\"\u003eJavaScript側の値\u003ccode\u003ename\u003c/code\u003eが変わると、画面が変わる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"44:1-44:24\"\u003eが起きています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"46:1-46:48\"\u003e\n\u003cspan id=\"もしv-modelを使わずに書くと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%82%E3%81%97v-model%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%9A%E3%81%AB%E6%9B%B8%E3%81%8F%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eもし、\u003ccode\u003ev-model\u003c/code\u003eを使わずに書くと？\u003c/h2\u003e\n\u003cp data-sourcepos=\"48:1-48:171\"\u003e\u003ccode\u003ev-model\u003c/code\u003eは双方向バインディングを簡潔に書くための構文であるため、\u003ccode\u003ev-model\u003c/code\u003eを使わずに双方向バインディングを実現できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"50:1-62:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eApp.vue\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003e:value=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"name\"\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003einput=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"name = $event.target.value\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003e入力された名前: \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"64:1-64:78\"\u003e\u003ccode\u003ev-model\u003c/code\u003eが内部で行っている処理のイメージをつかめます。\u003c/p\u003e\n\u003cul data-sourcepos=\"66:1-68:0\"\u003e\n\u003cli data-sourcepos=\"66:1-66:82\"\u003e\n\u003ccode\u003e@input=\"name = $event.target.value\"\u003c/code\u003eでJavaScriptの値を更新しています\u003c/li\u003e\n\u003cli data-sourcepos=\"67:1-68:0\"\u003e\n\u003ccode\u003e:value=\"name\"\u003c/code\u003eでJavaScriptの値を画面に表示しています\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"69:1-69:78\"\u003e\n\u003cspan id=\"子コンポーネントとの双方向バインディングにも使える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A8%E3%81%AE%E5%8F%8C%E6%96%B9%E5%90%91%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AB%E3%82%82%E4%BD%BF%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e子コンポーネントとの双方向バインディングにも使える\u003c/h2\u003e\n\u003cp data-sourcepos=\"71:1-71:105\"\u003e\u003ccode\u003ev-model\u003c/code\u003eは子コンポーネントとの双方向バインディングに使うことができます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"73:1-84:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eApp.vue (親コンポーネント)\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003eChild\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e@/components/Child.vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecount\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;Child\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"count\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"86:1-100:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eChild.vue (子コンポーネント)\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineModel\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003emodel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003emodel\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"update\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eカウントを増やす\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"102:1-103:130\"\u003e画面\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F6da8c34b-2ae9-4d99-bbe9-16aa0576d4d7.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=02e4b04a107bd347ebc9ee8fc836d9ce\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F6da8c34b-2ae9-4d99-bbe9-16aa0576d4d7.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=02e4b04a107bd347ebc9ee8fc836d9ce\" alt=\"v-model-count.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4227297%2F6da8c34b-2ae9-4d99-bbe9-16aa0576d4d7.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=ab06a591d0f3ed43cfd1f21104d58f45 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/6da8c34b-2ae9-4d99-bbe9-16aa0576d4d7.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"105:1-106:175\"\u003e子コンポーネントでは、\u003ccode\u003edefineModel()\u003c/code\u003eを使っています。\u003cbr\u003e\n\u003ccode\u003edefineModel()\u003c/code\u003eを使うことで、\u003ccode\u003ev-model\u003c/code\u003eで必要となる\u003ccode\u003emodelValue\u003c/code\u003eの受け取りと\u003ccode\u003eupdate:modelValue\u003c/code\u003eイベントの通知を簡潔に書くことができます。\u003c/p\u003e\n\u003cp data-sourcepos=\"108:1-108:96\"\u003e\u003ccode\u003ev-model\u003c/code\u003eと\u003ccode\u003edefineModel()\u003c/code\u003eを使わずに書くと、次のようなコードになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"110:1-121:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eApp.vue (親コンポーネント)\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003eChild\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e@/components/Child.vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecount\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;Child\u003c/span\u003e \u003cspan class=\"na\"\u003e:modelValue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"count\"\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eupdate:modelValue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"count = $event\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"123:1-138:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eChild.vue (子コンポーネント)\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eprops\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003emodelValue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eemit\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineEmits\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eupdate:modelValue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003eemit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eupdate:modelValue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eprops\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emodelValue\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003eprops\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emodelValue\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"update\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eカウントを増やす\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"140:1-141:51\"\u003e個人的に、\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003eなどのフォーム要素に対する双方向バインディングはすぐにイメージできましたが、子コンポーネントとの双方向バインディングは少しイメージしにくいと感じていました。\u003cbr\u003e\nしかし、親子コンポーネント間でも、\u003c/p\u003e\n\u003cul data-sourcepos=\"143:1-145:0\"\u003e\n\u003cli data-sourcepos=\"143:1-143:81\"\u003e親コンポーネントはJavaScriptの値を子コンポーネントに渡す\u003c/li\u003e\n\u003cli data-sourcepos=\"144:1-145:0\"\u003e子コンポーネントで値が変更されたら、変更後の値を親コンポーネントに通知し、親コンポーネント側の値を更新する\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"146:1-146:259\"\u003eという処理を行っています。これは、フォーム要素に対する「値を渡す」「変更後の値を受け取ってJavaScript側の値を更新する」という双方向バインディングと、同じ構造だと理解できました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"148:1-148:15\"\u003e\n\u003cspan id=\"参考文献\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考文献\u003c/h2\u003e\n\u003cul data-sourcepos=\"149:1-154:93\"\u003e\n\u003cli data-sourcepos=\"149:1-149:126\"\u003e\u003ca href=\"https://ja.vuejs.org/guide/essentials/forms.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eフォーム入力バインディング | Vue.js公式ドキュメント\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"150:1-150:116\"\u003e\u003ca href=\"https://ja.vuejs.org/guide/components/v-model\" rel=\"nofollow noopener\" target=\"_blank\"\u003eコンポーネントの v-model | Vue.js公式ドキュメント\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"151:1-151:100\"\u003e\u003ca href=\"https://qiita.com/simezi9/items/c27d69f17d2d08722b3a\" id=\"reference-70f6ab6887d32cf1aa19\"\u003eVue.jsのv-modelを正しく使う | Qiita\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"152:1-152:146\"\u003e\u003ca href=\"https://qiita.com/whopper1962/items/2335618146ec52aa4c81\" id=\"reference-3d73111fa78a4476f29c\"\u003e【Vue3】バージョン3.4から安定版となった「defineModel」が超便利\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"153:1-153:124\"\u003e\u003ca href=\"https://zenn.dev/comm_vue_nuxt/articles/7ad7338080482f\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.jsの v-model 正しく活用できていますか？ | Zenn\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"154:1-154:93\"\u003e\u003ca href=\"https://zenn.dev/find/articles/11b872f9915896\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVueのdefineModelが便利だった | Zenn\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## はじめに\n\n`v-model`をVue.jsの基礎から理解します。便利な`v-model`ですが、曖昧な理解になりがちだったため、今回の記事を書いています。\n\n### この記事の対象者\n\n- Vue.jsに入門してチュートリアルを一通り進めた人\n- `v-model`が具体的に何をしているのか理解したい人\n\n## `v-model`とは？\n\n`v-model`とは、双方向バインディングを実現する構文です。例えば、`\u003cinput\u003e`を使ってフォームを設置する場面を考えます。\n\n\n```vue:App.vue\n\u003cscript setup\u003e\nimport { ref } from 'vue'\n\nconst name = ref('')\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003cinput v-model=\"name\" /\u003e\n\n    \u003cp\u003e入力された名前: {{ name }}\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n画面(入力前)\n![v-model-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/0b21c729-4a2c-4bba-a243-07f8682be0ca.png)\n\n\n画面(入力後)\n![v-model-edit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/82c5afbf-fa85-4c27-a5c4-c642b9d1722f.png)\n\n\nフォームに文字を入力すると、すぐに「入力された名前：」の右に内容が表示されます。双方向バインディングとして、\n\n- フォームの値を変更するとJavaScript側の値`name`が変わる\n- JavaScript側の値`name`が変わると、画面が変わる\n\nが起きています。\n\n## もし、`v-model`を使わずに書くと？\n\n`v-model`は双方向バインディングを簡潔に書くための構文であるため、`v-model`を使わずに双方向バインディングを実現できます。\n\n```vue:App.vue\n\u003cscript setup\u003e\nimport { ref } from \"vue\";\n\nconst name = ref(\"\");\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cinput :value=\"name\" @input=\"name = $event.target.value\" /\u003e\n\n  \u003cp\u003e入力された名前: {{ name }}\u003c/p\u003e\n\u003c/template\u003e\n```\n\n`v-model`が内部で行っている処理のイメージをつかめます。\n\n- `@input=\"name = $event.target.value\"`でJavaScriptの値を更新しています\n- `:value=\"name\"`でJavaScriptの値を画面に表示しています\n\n## 子コンポーネントとの双方向バインディングにも使える\n\n`v-model`は子コンポーネントとの双方向バインディングに使うことができます。\n\n```vue:App.vue (親コンポーネント)\n\u003cscript setup\u003e\nimport { ref } from \"vue\";\nimport Child from \"@/components/Child.vue\";\n\nconst count = ref(0);\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cChild v-model=\"count\" /\u003e\n\u003c/template\u003e\n```\n\n```vue:Child.vue (子コンポーネント)\n\u003cscript setup\u003e\nconst model = defineModel();\n\nfunction update() {\n  model.value++;\n}\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cp\u003e{{ model }}\u003c/p\u003e\n\n  \u003cbutton @click=\"update\"\u003eカウントを増やす\u003c/button\u003e\n\u003c/template\u003e\n```\n\n画面\n![v-model-count.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4227297/6da8c34b-2ae9-4d99-bbe9-16aa0576d4d7.png)\n\n子コンポーネントでは、`defineModel()`を使っています。\n`defineModel()`を使うことで、`v-model`で必要となる`modelValue`の受け取りと`update:modelValue`イベントの通知を簡潔に書くことができます。\n\n`v-model`と`defineModel()`を使わずに書くと、次のようなコードになります。\n\n```vue:App.vue (親コンポーネント)\n\u003cscript setup\u003e\nimport { ref } from \"vue\";\nimport Child from \"@/components/Child.vue\";\n\nconst count = ref(0);\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cChild :modelValue=\"count\" @update:modelValue=\"count = $event\" /\u003e\n\u003c/template\u003e\n```\n\n```vue:Child.vue (子コンポーネント)\n\u003cscript setup\u003e\nconst props = defineProps([\"modelValue\"]);\nconst emit = defineEmits([\"update:modelValue\"]);\n\nfunction update() {\n  emit(\"update:modelValue\", props.modelValue + 1);\n}\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cp\u003e{{ props.modelValue }}\u003c/p\u003e\n\n  \u003cbutton @click=\"update\"\u003eカウントを増やす\u003c/button\u003e\n\u003c/template\u003e\n```\n\n個人的に、`\u003cinput\u003e`などのフォーム要素に対する双方向バインディングはすぐにイメージできましたが、子コンポーネントとの双方向バインディングは少しイメージしにくいと感じていました。\nしかし、親子コンポーネント間でも、\n\n- 親コンポーネントはJavaScriptの値を子コンポーネントに渡す\n- 子コンポーネントで値が変更されたら、変更後の値を親コンポーネントに通知し、親コンポーネント側の値を更新する\n\nという処理を行っています。これは、フォーム要素に対する「値を渡す」「変更後の値を受け取ってJavaScript側の値を更新する」という双方向バインディングと、同じ構造だと理解できました。\n\n## 参考文献\n- [フォーム入力バインディング | Vue.js公式ドキュメント](https://ja.vuejs.org/guide/essentials/forms.html)\n- [コンポーネントの v-model | Vue.js公式ドキュメント](https://ja.vuejs.org/guide/components/v-model)\n- [Vue.jsのv-modelを正しく使う | Qiita](https://qiita.com/simezi9/items/c27d69f17d2d08722b3a)\n- [【Vue3】バージョン3.4から安定版となった「defineModel」が超便利](https://qiita.com/whopper1962/items/2335618146ec52aa4c81)\n- [Vue.jsの v-model 正しく活用できていますか？ | Zenn](https://zenn.dev/comm_vue_nuxt/articles/7ad7338080482f)\n- [VueのdefineModelが便利だった | Zenn](https://zenn.dev/find/articles/11b872f9915896)\n","coediting":false,"comments_count":0,"created_at":"2026-09-20T21:56:33+09:00","group":null,"id":"f2674d6cdb9a90dac7ec","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]}],"title":"Vue.jsのv-modelを基礎から理解する","updated_at":"2026-09-20T21:56:33+09:00","url":"https://qiita.com/fj71tree/items/f2674d6cdb9a90dac7ec","user":{"description":"","facebook_id":"","followees_count":1,"followers_count":0,"github_login_name":"fj71tree","id":"fj71tree","items_count":4,"linkedin_id":"","location":"","name":"Kaito Fujiwara","organization":"","permanent_id":4227297,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4227297/586fee66100041a3a6e29b91985a7633230a9122/x_large.png?1780499965","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:14\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h1\u003e\n\u003cp data-sourcepos=\"3:1-3:110\"\u003eVue.js Devtools の Pinia タブでは、ストアの状態を JSON としてエクスポートできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:51\"\u003e本記事では、その手順を紹介します。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"7:1-7:8\"\u003e\n\u003cspan id=\"手順\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%89%8B%E9%A0%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e手順\u003c/h1\u003e\n\u003col data-sourcepos=\"9:1-13:0\"\u003e\n\u003cli data-sourcepos=\"9:1-9:45\"\u003eブラウザの開発者ツールを開く\u003c/li\u003e\n\u003cli data-sourcepos=\"10:1-10:26\"\u003e\n\u003cstrong\u003eVue\u003c/strong\u003e タブを開く\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-11:34\"\u003e\n\u003cstrong\u003ePinia\u003c/strong\u003e タブを選択する\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-13:0\"\u003eエクスポートしたいストアを選択し、画面右上の保存アイコンをクリックする\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"14:1-14:121\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F105859%2F84190750-aa92-49b0-ad71-0ef95bf54827.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1ffb978536ea11c9870fb68daeeaf7be\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F105859%2F84190750-aa92-49b0-ad71-0ef95bf54827.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1ffb978536ea11c9870fb68daeeaf7be\" alt=\"image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F105859%2F84190750-aa92-49b0-ad71-0ef95bf54827.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=904877beeccae2329d42371938422b71 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105859/84190750-aa92-49b0-ad71-0ef95bf54827.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:93\"\u003eすると、ストアの状態が JSON ファイルとしてダウンロードされます。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"18:1-18:14\"\u003e\n\u003cspan id=\"動作環境\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e動作環境\u003c/h1\u003e\n\u003cul data-sourcepos=\"20:1-20:23\"\u003e\n\u003cli data-sourcepos=\"20:1-20:23\"\u003eVue.js Devtools 7.7.7\u003c/li\u003e\n\u003c/ul\u003e\n","body":"# はじめに\n\nVue.js Devtools の Pinia タブでは、ストアの状態を JSON としてエクスポートできます。\n\n本記事では、その手順を紹介します。\n\n# 手順\n\n1. ブラウザの開発者ツールを開く\n2. **Vue** タブを開く\n3. **Pinia** タブを選択する\n4. エクスポートしたいストアを選択し、画面右上の保存アイコンをクリックする\n\n![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105859/84190750-aa92-49b0-ad71-0ef95bf54827.png)\n\nすると、ストアの状態が JSON ファイルとしてダウンロードされます。\n\n# 動作環境\n\n- Vue.js Devtools 7.7.7\n","coediting":false,"comments_count":0,"created_at":"2026-09-20T21:31:37+09:00","group":null,"id":"af1c9edb698ba797267b","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"Pinia","versions":[]}],"title":"Vue.js DevtoolsでPiniaをエクスポートする方法","updated_at":"2026-09-20T21:31:37+09:00","url":"https://qiita.com/neko_the_shadow/items/af1c9edb698ba797267b","user":{"description":"IT業界の片隅でひっそり生きるシステムエンジニアです(´・ω・｀)","facebook_id":"","followees_count":0,"followers_count":45,"github_login_name":"nekoTheShadow","id":"neko_the_shadow","items_count":488,"linkedin_id":"hajime-nakamura-a78b44198/","location":"大阪府大阪市","name":"","organization":"","permanent_id":105859,"profile_image_url":"https://qiita-image-store.s3.amazonaws.com/0/105859/profile-images/1473709753","team_only":false,"twitter_screen_name":"neko_the_shadow","website_url":"https://github.com/nekoTheShadow"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:389\"\u003e複数タブで同じWebアプリを操作すると、あるタブの検索状態が別タブへ混入するケースがあります。この種の不具合は、Piniaの反応性や画面遷移の問題に見えます。しかし原因は、タブ固有であるべき検索・遷移状態を、同一サイト内で共有されるCookieに保存していたことかもしれません。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:294\"\u003eこの記事では、検索画面から詳細画面へ渡す一時状態を例に、Cookie、Pinia、\u003ccode\u003esessionStorage\u003c/code\u003eの責務を整理します。状態を「どこに置けるか」ではなく、「誰と共有し、いつまで残すか」で選べるようになることが目的です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"7:1-7:9\"\u003e\n\u003cspan id=\"結論\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B5%90%E8%AB%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e結論\u003c/h2\u003e\n\u003cp data-sourcepos=\"9:1-9:455\"\u003eCookieが悪いのではありません。問題は、Cookieの共有スコープと検索状態のスコープが一致していないことです。HTTPリクエストに自動送信したい認証セッションなどはCookieに置き、タブごとに分離したい画面状態は、要件に応じてPiniaまたは\u003ccode\u003esessionStorage\u003c/code\u003eに置きます。\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMDN Web Docs: Using HTTP cookies\u003c/a\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"11:1-18:144\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"11:1-11:43\"\u003e\n\u003cth data-sourcepos=\"11:2-11:18\"\u003e状態の要件\u003c/th\u003e\n\u003cth data-sourcepos=\"11:20-11:33\"\u003e第一候補\u003c/th\u003e\n\u003cth data-sourcepos=\"11:35-11:42\"\u003e理由\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"13:1-13:124\"\u003e\n\u003ctd data-sourcepos=\"13:2-13:48\"\u003eコンポーネントの表示中だけ使う\u003c/td\u003e\n\u003ctd data-sourcepos=\"13:50-13:69\"\u003e\n\u003ccode\u003eref\u003c/code\u003e / \u003ccode\u003ereactive\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"13:71-13:123\"\u003eコンポーネントの寿命と一致します。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"14:1-14:129\"\u003e\n\u003ctd data-sourcepos=\"14:2-14:39\"\u003eSPA遷移中だけ画面間で使う\u003c/td\u003e\n\u003ctd data-sourcepos=\"14:41-14:47\"\u003ePinia\u003c/td\u003e\n\u003ctd data-sourcepos=\"14:49-14:128\"\u003e現在動いているアプリケーションの状態を共有できます。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"15:1-15:215\"\u003e\n\u003ctd data-sourcepos=\"15:2-15:63\"\u003eタブごとに分離し、リロード後も復元したい\u003c/td\u003e\n\u003ctd data-sourcepos=\"15:65-15:90\"\u003e\n\u003ccode\u003esessionStorage\u003c/code\u003e + Pinia\u003c/td\u003e\n\u003ctd data-sourcepos=\"15:92-15:214\"\u003e\n\u003ccode\u003esessionStorage\u003c/code\u003eはオリジンとトップレベルのブラウジングコンテキストごとに分離されます。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"16:1-16:144\"\u003e\n\u003ctd data-sourcepos=\"16:2-16:54\"\u003e複数タブ・再起動後も共有したい設定\u003c/td\u003e\n\u003ctd data-sourcepos=\"16:56-16:71\"\u003e\u003ccode\u003elocalStorage\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"16:73-16:143\"\u003eオリジン単位で永続し、タブ固有にはなりません。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"17:1-17:131\"\u003e\n\u003ctd data-sourcepos=\"17:2-17:52\"\u003eHTTPリクエストに自動送信したい状態\u003c/td\u003e\n\u003ctd data-sourcepos=\"17:54-17:61\"\u003eCookie\u003c/td\u003e\n\u003ctd data-sourcepos=\"17:63-17:130\"\u003eブラウザが対象リクエストにCookieを送信します。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"18:1-18:144\"\u003e\n\u003ctd data-sourcepos=\"18:2-18:48\"\u003eブラウザへ実値を保存したくない\u003c/td\u003e\n\u003ctd data-sourcepos=\"18:50-18:92\"\u003eサーバー側ストア + 不透明なID\u003c/td\u003e\n\u003ctd data-sourcepos=\"18:94-18:143\"\u003e実値をサーバー側で管理できます。\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"20:1-20:72\"\u003e\n\u003cspan id=\"最小再現共有cookieからタブ固有の状態を復元する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E5%B0%8F%E5%86%8D%E7%8F%BE%E5%85%B1%E6%9C%89cookie%E3%81%8B%E3%82%89%E3%82%BF%E3%83%96%E5%9B%BA%E6%9C%89%E3%81%AE%E7%8A%B6%E6%85%8B%E3%82%92%E5%BE%A9%E5%85%83%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e最小再現：共有Cookieからタブ固有の状態を復元する\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-22:117\"\u003e検索成功時に、次のような画面遷移用の状態を固定名のCookieへ保存しているとします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"24:1-40:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003erecordId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esearchInput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esearch\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eother\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ependingSearchState\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003euseCookie\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003epending_search_state\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003ependingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003erecordId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-a\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esearchInput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eA社\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esearch\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"42:1-42:107\"\u003eタブAとタブBが同じサイトを開くと、操作の順番によって次の状態になります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"44:1-53:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eタブAでデータAを検索\n  pending_search_state = record-a\n\nタブBでデータBを検索\n  pending_search_state = record-b\n\nタブAで詳細画面へ遷移し、CookieからPiniaを復元\n  PiniaのrecordId = record-b\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"55:1-55:96\"\u003e状態の流れを図にすると、共有領域が汚染源であることが分かります。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__d2d2b12c059183068a4d91b14065ab02\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__d2d2b12c059183068a4d91b14065ab02\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart LR\\n A[タブA: データAを検索] --\u0026gt; C[共有Cookie: pending_search_state]\\n B[タブB: データBを検索] --\u0026gt; C\\n C --\u0026gt; R[タブAの復元処理]\\n R --\u0026gt; P[タブAのPiniaがデータBへ変わる]\",\"key\":\"0fc7fdde58fd54e04ec0550d558fd3fe\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"65:1-65:285\"\u003eこの時点では、タブAとタブBのPinia自体は別々に存在していても不思議ではありません。問題は、画面遷移や再初期化のタイミングで、両タブに共通のCookieを読み直し、タブごとのPiniaへコピーしていることです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"67:1-67:45\"\u003e\n\u003cspan id=\"cookieはタブの保存領域ではない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#cookie%E3%81%AF%E3%82%BF%E3%83%96%E3%81%AE%E4%BF%9D%E5%AD%98%E9%A0%98%E5%9F%9F%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eCookieはタブの保存領域ではない\u003c/h2\u003e\n\u003cp data-sourcepos=\"69:1-69:447\"\u003eCookieは、サーバーが\u003ccode\u003eSet-Cookie\u003c/code\u003eレスポンスヘッダーでブラウザへ渡し、その後の対象リクエストにブラウザが返送する仕組みです。\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMDN Web Docs: Set-Cookie\u003c/a\u003e Cookieの送信範囲は主に\u003ccode\u003eDomain\u003c/code\u003e、\u003ccode\u003ePath\u003c/code\u003e、\u003ccode\u003eSecure\u003c/code\u003e、\u003ccode\u003eSameSite\u003c/code\u003eなどで制御されますが、タブA用とタブB用を分ける属性はありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"71:1-71:90\"\u003eたとえば、次のCookieは\u003ccode\u003e/search\u003c/code\u003e配下へのリクエストで送信されます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"http\" data-sourcepos=\"73:1-75:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"err\"\u003eSet-Cookie: pending_search_state=...; Path=/search\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"77:1-77:328\"\u003eしかし\u003ccode\u003ePath=/search\u003c/code\u003eは、同じサイトの別タブを分離しません。タブAとタブBがともに\u003ccode\u003e/search\u003c/code\u003eを開いていれば、同じ条件に一致するCookieを利用します。\u003ccode\u003ePath\u003c/code\u003eはCookieをどのURLパスへ送るかを制御するものであり、タブ分離のための境界ではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"79:1-79:350\"\u003e\u003ccode\u003eSameSite\u003c/code\u003e、\u003ccode\u003eSecure\u003c/code\u003e、\u003ccode\u003eHttpOnly\u003c/code\u003eはいずれも重要な属性です。しかし、同一サイトを開いた複数タブの画面状態を分ける機能ではありません。認証セッションのようにHTTPリクエストで共有すべき状態にはCookieを使い、タブごとの検索文脈には使わないことが重要です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"81:1-81:71\"\u003e\n\u003cspan id=\"piniaは原因ではなく状態のコピー先になりやすい\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#pinia%E3%81%AF%E5%8E%9F%E5%9B%A0%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E7%8A%B6%E6%85%8B%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC%E5%85%88%E3%81%AB%E3%81%AA%E3%82%8A%E3%82%84%E3%81%99%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003ePiniaは原因ではなく、状態のコピー先になりやすい\u003c/h2\u003e\n\u003cp data-sourcepos=\"83:1-83:378\"\u003ePiniaはアプリケーションの状態を\u003ccode\u003estate()\u003c/code\u003eで定義し、ストアインスタンスを通じて参照・更新する仕組みです。\u003ca href=\"https://pinia.vuejs.org/core-concepts/state.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePinia: State\u003c/a\u003e 通常、別タブは別のJavaScript実行環境で動くため、タブAとタブBが同じPiniaインスタンスを直接共有するわけではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"85:1-85:95\"\u003eそのため、次のような構造なら、Piniaを疑う前に復元元を確認します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"87:1-93:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e共有Cookie\n  ↓ 画面初期化・遷移時に読み直す\nタブごとのPinia\n  ↓\n画面\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"95:1-95:367\"\u003ePiniaは通常のSPA遷移では状態を保てますが、ページをリロードするとアプリケーションが初期化されます。永続化が必要なら、別の保存先から明示的に復元する必要があります。Piniaの永続化プラグインを使う場合も、本質的には別のストレージへ書き出しているだけです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"97:1-97:52\"\u003e\n\u003cspan id=\"sessionstorageはタブ単位の復元に向く\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#sessionstorage%E3%81%AF%E3%82%BF%E3%83%96%E5%8D%98%E4%BD%8D%E3%81%AE%E5%BE%A9%E5%85%83%E3%81%AB%E5%90%91%E3%81%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003esessionStorage\u003c/code\u003eはタブ単位の復元に向く\u003c/h2\u003e\n\u003cp data-sourcepos=\"99:1-99:399\"\u003e\u003ccode\u003esessionStorage\u003c/code\u003eは、オリジンに加えてトップレベルのブラウジングコンテキスト、実質的にはタブごとに分離されます。ページのリロードや復元をまたいで残り、タブまたはウィンドウを閉じると破棄されます。\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMDN Web Docs: Window: sessionStorage property\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"101:1-101:165\"\u003eこの性質は、「タブAではデータA、タブBではデータBを扱い、各タブでF5後も検索文脈を戻したい」という要件と合います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"103:1-109:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eタブA\n  sessionStorage.pending_search_state = record-a\n\nタブB\n  sessionStorage.pending_search_state = record-b\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"111:1-111:91\"\u003e\n\u003cspan id=\"実装例piniaを通常の参照先にしsessionstorageを復元用に限定する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A3%85%E4%BE%8Bpinia%E3%82%92%E9%80%9A%E5%B8%B8%E3%81%AE%E5%8F%82%E7%85%A7%E5%85%88%E3%81%AB%E3%81%97sessionstorage%E3%82%92%E5%BE%A9%E5%85%83%E7%94%A8%E3%81%AB%E9%99%90%E5%AE%9A%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実装例：Piniaを通常の参照先にし、sessionStorageを復元用に限定する\u003c/h2\u003e\n\u003cp data-sourcepos=\"113:1-113:272\"\u003e保存先を増やすと、どちらが正なのかが曖昧になりがちです。通常の画面表示ではPiniaだけを読むようにし、\u003ccode\u003esessionStorage\u003c/code\u003eはリロード直後にPiniaを復元するためだけに使うと、責務を分けやすくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"115:1-115:177\"\u003eまず、保存する値を最小限に絞ります。詳細データ全体ではなく、必要なら短命な識別子と画面制御に必要な値だけを持たせます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"117:1-123:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003erecordId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esearchInput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esearch\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eother\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"125:1-125:172\"\u003e次に、\u003ccode\u003esessionStorage\u003c/code\u003eを扱う関数を一箇所へ閉じ込めます。Nuxtではブラウザでのみ実行するため、\u003ccode\u003eimport.meta.client\u003c/code\u003eでガードします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"127:1-155:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eKEY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003epending_search_state\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003esavePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emeta\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\n  \u003cspan class=\"nx\"\u003esessionStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eKEY\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003erestorePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emeta\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\n\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eraw\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esessionStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eKEY\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eraw\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003etry\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eraw\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003esessionStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eremoveItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eKEY\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eclearPendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emeta\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\n  \u003cspan class=\"nx\"\u003esessionStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eremoveItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eKEY\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"157:1-157:71\"\u003ePiniaには、画面が使う現在の状態だけを持たせます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"159:1-170:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003euseSearchStore\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esearch\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e({\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epending\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}),\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eactions\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nf\"\u003esetPending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"na\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epending\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003estate\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"172:1-172:107\"\u003e検索成功時には、Piniaを先に更新し、同じ値を復元用ストレージへ保存します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"174:1-181:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003estore\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseSearchStore\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eonSearchSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003estore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetPending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003esavePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"183:1-183:192\"\u003e詳細画面で必要なら、クライアント側の初期化時だけ復元します。すでにPiniaに値がある場合は、\u003ccode\u003esessionStorage\u003c/code\u003eで上書きしないことが重要です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"185:1-194:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nf\"\u003eonMounted\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epending\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003erestored\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003erestorePendingSearchState\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erestored\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003estore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetPending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erestored\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"196:1-196:237\"\u003e状態が一回限りの遷移用で、詳細画面を閉じた後に不要なら、利用後に\u003ccode\u003eclearPendingSearchState()\u003c/code\u003eを呼びます。無期限に残さず、業務上の寿命に合わせて消す方針を決めてください。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"198:1-198:78\"\u003e\n\u003cspan id=\"サーバーがcookieを書いている場合は責務を移し替える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8Ccookie%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AF%E8%B2%AC%E5%8B%99%E3%82%92%E7%A7%BB%E3%81%97%E6%9B%BF%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eサーバーがCookieを書いている場合は、責務を移し替える\u003c/h2\u003e\n\u003cp data-sourcepos=\"200:1-200:313\"\u003eBFFやサーバーサイドの処理は\u003ccode\u003esessionStorage\u003c/code\u003eを直接操作できません。サーバーがCookieへ一時状態を書いていたなら、必要最小限の状態をレスポンス本文として返し、ブラウザ側でPiniaと\u003ccode\u003esessionStorage\u003c/code\u003eへ保存するように受け渡しを変えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"202:1-202:228\"\u003eURLに識別子を置けず、ブラウザに実値も保存したくない場合は、サーバー側に短いTTL付きで状態を保存し、ブラウザには推測困難な\u003ccode\u003estateId\u003c/code\u003eだけを渡す方法を検討します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"204:1-210:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eBrowser: stateId\n  ↓\nBFF: ログイン中の利用者とstateIdを検証\n  ↓\nServer-side store: 検索条件やレコード識別子をTTL付きで保持\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"212:1-212:179\"\u003eこの方式では、\u003ccode\u003estateId\u003c/code\u003eと利用者・権限を必ず対応付けます。単にIDを知っているだけで他人の状態を読める設計にしてはいけません。\u003c/p\u003e\n\u003cp data-sourcepos=\"214:1-214:155\"\u003e\u003ccode\u003esessionStorage\u003c/code\u003eはJavaScriptから読めるため、認証トークンや詳細データ全体を複製せず、保存する値を最小限にします。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"216:1-216:36\"\u003e\n\u003cspan id=\"修正後に確認するテスト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BF%AE%E6%AD%A3%E5%BE%8C%E3%81%AB%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E3%83%86%E3%82%B9%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e修正後に確認するテスト\u003c/h2\u003e\n\u003cp data-sourcepos=\"218:1-218:246\"\u003eこの不具合は、単一画面の単体テストだけでは見落としやすい問題です。同一ブラウザコンテキスト内で複数タブを使うE2Eテストを追加し、共有状態が混入しないことを確認します。\u003c/p\u003e\n\u003ctable data-sourcepos=\"220:1-226:118\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"220:1-220:34\"\u003e\n\u003cth data-sourcepos=\"220:2-220:9\"\u003e観点\u003c/th\u003e\n\u003cth data-sourcepos=\"220:11-220:18\"\u003e操作\u003c/th\u003e\n\u003cth data-sourcepos=\"220:20-220:33\"\u003e期待結果\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"222:1-222:167\"\u003e\n\u003ctd data-sourcepos=\"222:2-222:15\"\u003eタブ分離\u003c/td\u003e\n\u003ctd data-sourcepos=\"222:17-222:76\"\u003eタブAでデータA、タブBでデータBを検索する\u003c/td\u003e\n\u003ctd data-sourcepos=\"222:78-222:166\"\u003eそれぞれの詳細画面に対応する対象データだけが表示されます。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"223:1-223:129\"\u003e\n\u003ctd data-sourcepos=\"223:2-223:19\"\u003eタブAの遷移\u003c/td\u003e\n\u003ctd data-sourcepos=\"223:21-223:87\"\u003eタブBの検索後に、タブAを詳細画面へ遷移させる\u003c/td\u003e\n\u003ctd data-sourcepos=\"223:89-223:128\"\u003eタブAはデータAのままです。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"224:1-224:139\"\u003e\n\u003ctd data-sourcepos=\"224:2-224:15\"\u003eリロード\u003c/td\u003e\n\u003ctd data-sourcepos=\"224:17-224:48\"\u003e各タブでリロードする\u003c/td\u003e\n\u003ctd data-sourcepos=\"224:50-224:138\"\u003e要件が復元を求めるなら、各タブで元の検索状態を復元します。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"225:1-225:109\"\u003e\n\u003ctd data-sourcepos=\"225:2-225:9\"\u003e破棄\u003c/td\u003e\n\u003ctd data-sourcepos=\"225:11-225:54\"\u003eタブを閉じて新しいタブを開く\u003c/td\u003e\n\u003ctd data-sourcepos=\"225:56-225:108\"\u003eタブ単位の一時状態を復元しません。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"226:1-226:118\"\u003e\n\u003ctd data-sourcepos=\"226:2-226:15\"\u003e移行確認\u003c/td\u003e\n\u003ctd data-sourcepos=\"226:17-226:57\"\u003e検索後にCookie一覧を確認する\u003c/td\u003e\n\u003ctd data-sourcepos=\"226:59-226:117\"\u003e廃止対象の画面状態Cookieが作られません。\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"228:1-228:231\"\u003ePlaywrightでは、同じ\u003ccode\u003eBrowserContext\u003c/code\u003eから2つの\u003ccode\u003ePage\u003c/code\u003eを作ると、Cookieを共有する複数タブの状況を再現できます。セレクタとURLは実際のアプリケーションに合わせて置き換えます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"230:1-252:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eexpect\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etest\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@playwright/test\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e別タブの検索状態を混ぜない\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003easync \u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003ebrowser\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003econtext\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003ebrowser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003enewContext\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003enewPage\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etabB\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003enewPage\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egoto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e/search\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByLabel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e検索条件\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003efill\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-a\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ebutton\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e検索\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e}).\u003c/span\u003e\u003cspan class=\"nf\"\u003eclick\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabB\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egoto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e/search\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabB\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByLabel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e検索条件\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003efill\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-b\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabB\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ebutton\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e検索\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e}).\u003c/span\u003e\u003cspan class=\"nf\"\u003eclick\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elink\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e詳細\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e}).\u003c/span\u003e\u003cspan class=\"nf\"\u003eclick\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003eexpect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByTestId\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)).\u003c/span\u003e\u003cspan class=\"nf\"\u003etoHaveText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-a\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ereload\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003eexpect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etabA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByTestId\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)).\u003c/span\u003e\u003cspan class=\"nf\"\u003etoHaveText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecord-a\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"254:1-254:21\"\u003e\n\u003cspan id=\"再現サンプル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%86%8D%E7%8F%BE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e再現サンプル\u003c/h2\u003e\n\u003cp data-sourcepos=\"256:1-256:475\"\u003eこの記事で扱ったCookie共有による状態混入と、メモリおよび\u003ccode\u003esessionStorage\u003c/code\u003eによるタブ分離は、\u003ca href=\"https://github.com/tonbiattack/cookie-tab-state-reproduction\" rel=\"nofollow noopener\" target=\"_blank\"\u003ecookie-tab-state-reproduction\u003c/a\u003e で実際に確認できます。ブラウザ画面では「不具合を再現」と「修正後を検証」を切り替えられ、PlaywrightのE2Eテストでは同一\u003ccode\u003eBrowserContext\u003c/code\u003eの二つのタブを使って両方の条件を検証しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"258:1-263:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit clone https://github.com/tonbiattack/cookie-tab-state-reproduction.git\n\u003cspan class=\"nb\"\u003ecd \u003c/span\u003ecookie-tab-state-reproduction\npnpm \u003cspan class=\"nb\"\u003einstall\n\u003c/span\u003epnpm \u003cspan class=\"nb\"\u003etest\u003c/span\u003e:e2e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"265:1-265:287\"\u003e不具合条件では、タブAで\u003ccode\u003erecord-a\u003c/code\u003eを検索した後、タブBで\u003ccode\u003erecord-b\u003c/code\u003eを検索すると、タブAの詳細に\u003ccode\u003erecord-b\u003c/code\u003eが表示されます。修正後の条件では、タブAには\u003ccode\u003erecord-a\u003c/code\u003eが残り、リロード後にもタブごとの状態が復元されます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"267:1-267:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"269:1-269:358\"\u003e複数タブで画面状態が混ざる不具合は、状態の値ではなく、状態の共有範囲が過剰であることから起こります。CookieはHTTP通信に必要な状態と相性がよく、Piniaは実行中のSPAの状態と相性がよく、\u003ccode\u003esessionStorage\u003c/code\u003eはタブごとに分離された短命な復元状態と相性があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"271:1-271:51\"\u003e実装前には、次の順序で判断します。\u003c/p\u003e\n\u003col data-sourcepos=\"273:1-277:0\"\u003e\n\u003cli data-sourcepos=\"273:1-273:162\"\u003eこの状態を誰と共有したいかを決めます。コンポーネント、現在のタブ、全タブ、サーバーのどれかを明確にします。\u003c/li\u003e\n\u003cli data-sourcepos=\"274:1-274:159\"\u003eいつまで残す必要があるかを決めます。SPA遷移中だけか、リロード後も必要か、タブを閉じるまでかを確認します。\u003c/li\u003e\n\u003cli data-sourcepos=\"275:1-275:105\"\u003e保存する情報を最小限にし、ブラウザ側に実値を置く必要性を評価します。\u003c/li\u003e\n\u003cli data-sourcepos=\"276:1-277:0\"\u003e複数タブを使うE2Eテストで、共有範囲が要件どおりであることを確認します。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"278:1-278:231\"\u003e状態管理では、保存先の便利さよりも、状態の寿命とスコープを合わせることが優先です。この基準で選べば、Cookie、Pinia、\u003ccode\u003esessionStorage\u003c/code\u003eを競合させずに使い分けられます。\u003c/p\u003e\n","body":"## はじめに\n\n複数タブで同じWebアプリを操作すると、あるタブの検索状態が別タブへ混入するケースがあります。この種の不具合は、Piniaの反応性や画面遷移の問題に見えます。しかし原因は、タブ固有であるべき検索・遷移状態を、同一サイト内で共有されるCookieに保存していたことかもしれません。\n\nこの記事では、検索画面から詳細画面へ渡す一時状態を例に、Cookie、Pinia、`sessionStorage`の責務を整理します。状態を「どこに置けるか」ではなく、「誰と共有し、いつまで残すか」で選べるようになることが目的です。\n\n## 結論\n\nCookieが悪いのではありません。問題は、Cookieの共有スコープと検索状態のスコープが一致していないことです。HTTPリクエストに自動送信したい認証セッションなどはCookieに置き、タブごとに分離したい画面状態は、要件に応じてPiniaまたは`sessionStorage`に置きます。[MDN Web Docs: Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies)\n\n| 状態の要件 | 第一候補 | 理由 |\n|---|---|---|\n| コンポーネントの表示中だけ使う | `ref` / `reactive` | コンポーネントの寿命と一致します。 |\n| SPA遷移中だけ画面間で使う | Pinia | 現在動いているアプリケーションの状態を共有できます。 |\n| タブごとに分離し、リロード後も復元したい | `sessionStorage` + Pinia | `sessionStorage`はオリジンとトップレベルのブラウジングコンテキストごとに分離されます。 |\n| 複数タブ・再起動後も共有したい設定 | `localStorage` | オリジン単位で永続し、タブ固有にはなりません。 |\n| HTTPリクエストに自動送信したい状態 | Cookie | ブラウザが対象リクエストにCookieを送信します。 |\n| ブラウザへ実値を保存したくない | サーバー側ストア + 不透明なID | 実値をサーバー側で管理できます。 |\n\n## 最小再現：共有Cookieからタブ固有の状態を復元する\n\n検索成功時に、次のような画面遷移用の状態を固定名のCookieへ保存しているとします。\n\n```ts\ntype PendingSearchState = {\n  recordId: string\n  searchInput: string\n  source: 'search' | 'other'\n}\n\nconst pendingSearchState = useCookie\u003cPendingSearchState\u003e(\n  'pending_search_state'\n)\n\npendingSearchState.value = {\n  recordId: 'record-a',\n  searchInput: 'A社',\n  source: 'search',\n}\n```\n\nタブAとタブBが同じサイトを開くと、操作の順番によって次の状態になります。\n\n```text\nタブAでデータAを検索\n  pending_search_state = record-a\n\nタブBでデータBを検索\n  pending_search_state = record-b\n\nタブAで詳細画面へ遷移し、CookieからPiniaを復元\n  PiniaのrecordId = record-b\n```\n\n状態の流れを図にすると、共有領域が汚染源であることが分かります。\n\n```mermaid\nflowchart LR\n  A[タブA: データAを検索] --\u003e C[共有Cookie: pending_search_state]\n  B[タブB: データBを検索] --\u003e C\n  C --\u003e R[タブAの復元処理]\n  R --\u003e P[タブAのPiniaがデータBへ変わる]\n```\n\nこの時点では、タブAとタブBのPinia自体は別々に存在していても不思議ではありません。問題は、画面遷移や再初期化のタイミングで、両タブに共通のCookieを読み直し、タブごとのPiniaへコピーしていることです。\n\n## Cookieはタブの保存領域ではない\n\nCookieは、サーバーが`Set-Cookie`レスポンスヘッダーでブラウザへ渡し、その後の対象リクエストにブラウザが返送する仕組みです。[MDN Web Docs: Set-Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) Cookieの送信範囲は主に`Domain`、`Path`、`Secure`、`SameSite`などで制御されますが、タブA用とタブB用を分ける属性はありません。\n\nたとえば、次のCookieは`/search`配下へのリクエストで送信されます。\n\n```http\nSet-Cookie: pending_search_state=...; Path=/search\n```\n\nしかし`Path=/search`は、同じサイトの別タブを分離しません。タブAとタブBがともに`/search`を開いていれば、同じ条件に一致するCookieを利用します。`Path`はCookieをどのURLパスへ送るかを制御するものであり、タブ分離のための境界ではありません。\n\n`SameSite`、`Secure`、`HttpOnly`はいずれも重要な属性です。しかし、同一サイトを開いた複数タブの画面状態を分ける機能ではありません。認証セッションのようにHTTPリクエストで共有すべき状態にはCookieを使い、タブごとの検索文脈には使わないことが重要です。\n\n## Piniaは原因ではなく、状態のコピー先になりやすい\n\nPiniaはアプリケーションの状態を`state()`で定義し、ストアインスタンスを通じて参照・更新する仕組みです。[Pinia: State](https://pinia.vuejs.org/core-concepts/state.html) 通常、別タブは別のJavaScript実行環境で動くため、タブAとタブBが同じPiniaインスタンスを直接共有するわけではありません。\n\nそのため、次のような構造なら、Piniaを疑う前に復元元を確認します。\n\n```text\n共有Cookie\n  ↓ 画面初期化・遷移時に読み直す\nタブごとのPinia\n  ↓\n画面\n```\n\nPiniaは通常のSPA遷移では状態を保てますが、ページをリロードするとアプリケーションが初期化されます。永続化が必要なら、別の保存先から明示的に復元する必要があります。Piniaの永続化プラグインを使う場合も、本質的には別のストレージへ書き出しているだけです。\n\n## `sessionStorage`はタブ単位の復元に向く\n\n`sessionStorage`は、オリジンに加えてトップレベルのブラウジングコンテキスト、実質的にはタブごとに分離されます。ページのリロードや復元をまたいで残り、タブまたはウィンドウを閉じると破棄されます。[MDN Web Docs: Window: sessionStorage property](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)\n\nこの性質は、「タブAではデータA、タブBではデータBを扱い、各タブでF5後も検索文脈を戻したい」という要件と合います。\n\n```text\nタブA\n  sessionStorage.pending_search_state = record-a\n\nタブB\n  sessionStorage.pending_search_state = record-b\n```\n\n## 実装例：Piniaを通常の参照先にし、sessionStorageを復元用に限定する\n\n保存先を増やすと、どちらが正なのかが曖昧になりがちです。通常の画面表示ではPiniaだけを読むようにし、`sessionStorage`はリロード直後にPiniaを復元するためだけに使うと、責務を分けやすくなります。\n\nまず、保存する値を最小限に絞ります。詳細データ全体ではなく、必要なら短命な識別子と画面制御に必要な値だけを持たせます。\n\n```ts\nexport type PendingSearchState = {\n  recordId: string\n  searchInput: string\n  source: 'search' | 'other'\n}\n```\n\n次に、`sessionStorage`を扱う関数を一箇所へ閉じ込めます。Nuxtではブラウザでのみ実行するため、`import.meta.client`でガードします。\n\n```ts\nconst KEY = 'pending_search_state'\n\nexport function savePendingSearchState(state: PendingSearchState) {\n  if (!import.meta.client) return\n\n  sessionStorage.setItem(KEY, JSON.stringify(state))\n}\n\nexport function restorePendingSearchState(): PendingSearchState | null {\n  if (!import.meta.client) return null\n\n  const raw = sessionStorage.getItem(KEY)\n  if (!raw) return null\n\n  try {\n    return JSON.parse(raw) as PendingSearchState\n  } catch {\n    sessionStorage.removeItem(KEY)\n    return null\n  }\n}\n\nexport function clearPendingSearchState() {\n  if (!import.meta.client) return\n\n  sessionStorage.removeItem(KEY)\n}\n```\n\nPiniaには、画面が使う現在の状態だけを持たせます。\n\n```ts\nexport const useSearchStore = defineStore('search', {\n  state: () =\u003e ({\n    pending: null as PendingSearchState | null,\n  }),\n  actions: {\n    setPending(state: PendingSearchState) {\n      this.pending = state\n    },\n  },\n})\n```\n\n検索成功時には、Piniaを先に更新し、同じ値を復元用ストレージへ保存します。\n\n```ts\nconst store = useSearchStore()\n\nfunction onSearchSucceeded(state: PendingSearchState) {\n  store.setPending(state)\n  savePendingSearchState(state)\n}\n```\n\n詳細画面で必要なら、クライアント側の初期化時だけ復元します。すでにPiniaに値がある場合は、`sessionStorage`で上書きしないことが重要です。\n\n```ts\nonMounted(() =\u003e {\n  if (store.pending) return\n\n  const restored = restorePendingSearchState()\n  if (restored) {\n    store.setPending(restored)\n  }\n})\n```\n\n状態が一回限りの遷移用で、詳細画面を閉じた後に不要なら、利用後に`clearPendingSearchState()`を呼びます。無期限に残さず、業務上の寿命に合わせて消す方針を決めてください。\n\n## サーバーがCookieを書いている場合は、責務を移し替える\n\nBFFやサーバーサイドの処理は`sessionStorage`を直接操作できません。サーバーがCookieへ一時状態を書いていたなら、必要最小限の状態をレスポンス本文として返し、ブラウザ側でPiniaと`sessionStorage`へ保存するように受け渡しを変えます。\n\nURLに識別子を置けず、ブラウザに実値も保存したくない場合は、サーバー側に短いTTL付きで状態を保存し、ブラウザには推測困難な`stateId`だけを渡す方法を検討します。\n\n```text\nBrowser: stateId\n  ↓\nBFF: ログイン中の利用者とstateIdを検証\n  ↓\nServer-side store: 検索条件やレコード識別子をTTL付きで保持\n```\n\nこの方式では、`stateId`と利用者・権限を必ず対応付けます。単にIDを知っているだけで他人の状態を読める設計にしてはいけません。\n\n`sessionStorage`はJavaScriptから読めるため、認証トークンや詳細データ全体を複製せず、保存する値を最小限にします。\n\n## 修正後に確認するテスト\n\nこの不具合は、単一画面の単体テストだけでは見落としやすい問題です。同一ブラウザコンテキスト内で複数タブを使うE2Eテストを追加し、共有状態が混入しないことを確認します。\n\n| 観点 | 操作 | 期待結果 |\n|---|---|---|\n| タブ分離 | タブAでデータA、タブBでデータBを検索する | それぞれの詳細画面に対応する対象データだけが表示されます。 |\n| タブAの遷移 | タブBの検索後に、タブAを詳細画面へ遷移させる | タブAはデータAのままです。 |\n| リロード | 各タブでリロードする | 要件が復元を求めるなら、各タブで元の検索状態を復元します。 |\n| 破棄 | タブを閉じて新しいタブを開く | タブ単位の一時状態を復元しません。 |\n| 移行確認 | 検索後にCookie一覧を確認する | 廃止対象の画面状態Cookieが作られません。 |\n\nPlaywrightでは、同じ`BrowserContext`から2つの`Page`を作ると、Cookieを共有する複数タブの状況を再現できます。セレクタとURLは実際のアプリケーションに合わせて置き換えます。\n\n```ts\nimport { expect, test } from '@playwright/test'\n\ntest('別タブの検索状態を混ぜない', async ({ browser }) =\u003e {\n  const context = await browser.newContext()\n  const tabA = await context.newPage()\n  const tabB = await context.newPage()\n\n  await tabA.goto('/search')\n  await tabA.getByLabel('検索条件').fill('record-a')\n  await tabA.getByRole('button', { name: '検索' }).click()\n\n  await tabB.goto('/search')\n  await tabB.getByLabel('検索条件').fill('record-b')\n  await tabB.getByRole('button', { name: '検索' }).click()\n\n  await tabA.getByRole('link', { name: '詳細' }).click()\n  await expect(tabA.getByTestId('record-id')).toHaveText('record-a')\n\n  await tabA.reload()\n  await expect(tabA.getByTestId('record-id')).toHaveText('record-a')\n})\n```\n\n## 再現サンプル\n\nこの記事で扱ったCookie共有による状態混入と、メモリおよび`sessionStorage`によるタブ分離は、[cookie-tab-state-reproduction](https://github.com/tonbiattack/cookie-tab-state-reproduction) で実際に確認できます。ブラウザ画面では「不具合を再現」と「修正後を検証」を切り替えられ、PlaywrightのE2Eテストでは同一`BrowserContext`の二つのタブを使って両方の条件を検証しています。\n\n```bash\ngit clone https://github.com/tonbiattack/cookie-tab-state-reproduction.git\ncd cookie-tab-state-reproduction\npnpm install\npnpm test:e2e\n```\n\n不具合条件では、タブAで`record-a`を検索した後、タブBで`record-b`を検索すると、タブAの詳細に`record-b`が表示されます。修正後の条件では、タブAには`record-a`が残り、リロード後にもタブごとの状態が復元されます。\n\n## まとめ\n\n複数タブで画面状態が混ざる不具合は、状態の値ではなく、状態の共有範囲が過剰であることから起こります。CookieはHTTP通信に必要な状態と相性がよく、Piniaは実行中のSPAの状態と相性がよく、`sessionStorage`はタブごとに分離された短命な復元状態と相性があります。\n\n実装前には、次の順序で判断します。\n\n1. この状態を誰と共有したいかを決めます。コンポーネント、現在のタブ、全タブ、サーバーのどれかを明確にします。\n2. いつまで残す必要があるかを決めます。SPA遷移中だけか、リロード後も必要か、タブを閉じるまでかを確認します。\n3. 保存する情報を最小限にし、ブラウザ側に実値を置く必要性を評価します。\n4. 複数タブを使うE2Eテストで、共有範囲が要件どおりであることを確認します。\n\n状態管理では、保存先の便利さよりも、状態の寿命とスコープを合わせることが優先です。この基準で選べば、Cookie、Pinia、`sessionStorage`を競合させずに使い分けられます。\n","coediting":false,"comments_count":0,"created_at":"2026-09-20T19:08:12+09:00","group":null,"id":"fc3f2de25a6ec58c6542","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"Nuxt","versions":[]},{"name":"Pinia","versions":[]},{"name":"cookie","versions":[]},{"name":"sessionStorage","versions":[]}],"title":"Cookieに検索状態を保存したら別タブの状態が混ざった：PiniaとsessionStorageの責務を整理する","updated_at":"2026-09-20T21:37:52+09:00","url":"https://qiita.com/tonbi_attack/items/fc3f2de25a6ec58c6542","user":{"description":"","facebook_id":"","followees_count":3,"followers_count":76,"github_login_name":"tonbiattack","id":"tonbi_attack","items_count":205,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":273375,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273375/profile-images/1706618486","team_only":false,"twitter_screen_name":"tonnbiattack","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cblockquote data-sourcepos=\"2:1-2:91\"\u003e\n\u003cp data-sourcepos=\"2:3-2:91\"\u003e\u003cstrong\u003eLecture 08: Deep Dive into DRF Views — Request-Response Lifecycle \u0026amp; Class Hierarchy\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"4:1-4:195\"\u003e\u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003e← DRF × Vue.js 実践講座：まとめページに戻る\u003c/a\u003e | \u003ca href=\"/items/04b2c3524ec379961d82\"\u003e← 第七講：シリアライザのバリデーション完全攻略\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"6:1-6:67\"\u003e\n\u003cspan id=\"はじめにdrfにおけるviewの役割と責務境界\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%ABdrf%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bview%E3%81%AE%E5%BD%B9%E5%89%B2%E3%81%A8%E8%B2%AC%E5%8B%99%E5%A2%83%E7%95%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに：DRFにおける「View」の役割と責務境界\u003c/h2\u003e\n\u003cp data-sourcepos=\"8:1-8:258\"\u003eこれまでの講座では、データベースの構造を定義する \u003cstrong\u003e「Model（第四講・第五講）」\u003c/strong\u003e と、データの入出力変換やバリデーションを担う \u003cstrong\u003e「Serializer（第六講・第七講）」\u003c/strong\u003e を学んできました。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-10:88\"\u003eでは、今回学ぶ \u003cstrong\u003e「View（ビュー）」\u003c/strong\u003e とは一体何者でしょうか？\u003c/p\u003e\n\u003cp data-sourcepos=\"12:1-12:191\"\u003e一言で言えば、View は \u003cstrong\u003e「Model と Serializer を束ねてリクエストからレスポンスまでを取り仕切る『総合窓口・現場監督（司令塔）』」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"14:1-19:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e【DRFの3大要素と役割分担】\n・Model      : データベースの設計図・倉庫（データの保持）\n・Serializer : 翻訳係・検品所（Model/Pythonオブジェクト ⇄ Pythonの辞書等の表現への変換・バリデーション）\n・View       : 総合窓口・現場監督（リクエスト受付 ➔ 認証 ➔ 処理の実行 ➔ レスポンス返却）\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"21:1-21:28\"\u003e\n\u003cspan id=\"view-が担う4大責任\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#view-%E3%81%8C%E6%8B%85%E3%81%864%E5%A4%A7%E8%B2%AC%E4%BB%BB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eView が担う4大責任\u003c/h3\u003e\n\u003col data-sourcepos=\"22:1-30:0\"\u003e\n\u003cli data-sourcepos=\"22:1-23:156\"\u003e\n\u003cstrong\u003eリクエストの受付と振り分け\u003c/strong\u003e:\u003cbr\u003e\nクライアントから届いた HTTP メソッド（GET / POST / PUT / PATCH / DELETE）を解釈し、適切な処理へルーティングします。\u003c/li\u003e\n\u003cli data-sourcepos=\"24:1-25:210\"\u003e\n\u003cstrong\u003e認証・権限（セキュリティ）のチェック\u003c/strong\u003e:\u003cbr\u003e\n「ログインしているユーザーか？（認証）」「このデータを閲覧・編集する権限があるか？（認可）」を判定し、不正アクセスを遮断（401 / 403）します。\u003c/li\u003e\n\u003cli data-sourcepos=\"26:1-27:129\"\u003e\n\u003cstrong\u003eModel と Serializer の連携・統括\u003c/strong\u003e:\u003cbr\u003e\nシリアライザに入力データを検証させ、モデルを通じてデータベースへ保存・取得させます。\u003c/li\u003e\n\u003cli data-sourcepos=\"28:1-30:0\"\u003e\n\u003cstrong\u003eHTTP レスポンスの返却\u003c/strong\u003e:\u003cbr\u003e\n処理結果を適切なステータスコード（200 OK、201 Created、400 Bad Request など）とともにクライアントへ返します。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr data-sourcepos=\"31:1-32:0\"\u003e\n\u003ch3 data-sourcepos=\"33:1-33:63\"\u003e\n\u003cspan id=\"超重要view-と-serializer-の明確な責務境界\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%B6%85%E9%87%8D%E8%A6%81view-%E3%81%A8-serializer-%E3%81%AE%E6%98%8E%E7%A2%BA%E3%81%AA%E8%B2%AC%E5%8B%99%E5%A2%83%E7%95%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【超重要】View と Serializer の明確な責務境界\u003c/h3\u003e\n\u003cp data-sourcepos=\"35:1-35:253\"\u003eコードを書き始めると、「この処理は View に書くべきか？ それとも Serializer に書くべきか？」で誰もが一度は迷います。混乱を防ぐために、両者の境界線を明確に整理しておきましょう。\u003c/p\u003e\n\u003ctable data-sourcepos=\"37:1-45:230\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"37:1-37:116\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"37:2-37:24\"\u003e責務・判断項目\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"37:26-37:62\"\u003eView がやること（司令塔）\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"37:64-37:115\"\u003eSerializer がやること（翻訳・検品係）\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"39:1-39:225\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"39:2-39:25\"\u003e\u003cstrong\u003eデータの取得\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"39:27-39:149\"\u003e\n\u003cstrong\u003eどのレコードをDBから引いてくるか\u003c/strong\u003e\u003cbr\u003e（絞り込み、検索、ソート、ページネーション）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"39:151-39:224\"\u003e関与しない（渡されたインスタンスを処理するだけ）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"40:1-40:139\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"40:2-40:25\"\u003e\u003cstrong\u003eアクセス制御\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"40:27-40:120\"\u003e\n\u003cstrong\u003e誰がアクセスできるか\u003c/strong\u003e\u003cbr\u003e（ログイン認証、管理者権限チェック）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"40:122-40:138\"\u003e関与しない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"41:1-41:175\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"41:2-41:31\"\u003e\u003cstrong\u003eシリアライザ選択\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"41:33-41:156\"\u003e\n\u003cstrong\u003eどのアクションでどのシリアライザを使うか\u003c/strong\u003e\u003cbr\u003e（一覧用・詳細用・登録用の出し分け）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"41:158-41:174\"\u003e関与しない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"42:1-42:203\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"42:2-42:25\"\u003e\u003cstrong\u003eデータの検証\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"42:27-42:98\"\u003e検証をシリアライザに命令する（\u003ccode\u003eis_valid()\u003c/code\u003e を呼ぶ）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"42:100-42:202\"\u003e\n\u003cstrong\u003e入力値の妥当性を検証する\u003c/strong\u003e\u003cbr\u003e（型チェック、文字数、業務ルール検証）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"43:1-43:244\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"43:2-43:22\"\u003e\u003cstrong\u003eデータ変換\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"43:24-43:104\"\u003e変換後のデータを受け取ってレスポンス（\u003ccode\u003eResponse\u003c/code\u003e）にする\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"43:106-43:243\"\u003e\n\u003cstrong\u003eModel/Pythonオブジェクト ⇄ Pythonの辞書等の表現への変換\u003c/strong\u003e\u003cbr\u003e（日付フォーマット、数値の整形など）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"44:1-44:166\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"44:2-44:28\"\u003e\u003cstrong\u003eレスポンス項目\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"44:30-44:46\"\u003e関与しない\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"44:48-44:165\"\u003e\n\u003cstrong\u003eどのフィールドをクライアントに見せるか\u003c/strong\u003e\u003cbr\u003e（\u003ccode\u003efields\u003c/code\u003e、\u003ccode\u003eSerializerMethodField\u003c/code\u003e、\u003ccode\u003esource\u003c/code\u003e）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"45:1-45:230\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"45:2-45:25\"\u003e\u003cstrong\u003e保存時の補完\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"45:27-45:150\"\u003e\n\u003cstrong\u003eリクエスト情報から不足データを補う\u003c/strong\u003e\u003cbr\u003e（ログインユーザー \u003ccode\u003ecreated_by=request.user\u003c/code\u003e など）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"45:152-45:229\"\u003e渡されたデータを元にモデルインスタンスを \u003ccode\u003esave()\u003c/code\u003e する\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv data-sourcepos=\"47:1-53:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"48:1-49:103\"\u003e\u003cstrong\u003e💡 Serializer と Renderer の役割分担\u003c/strong\u003e\u003cbr\u003e\n厳密には、Serializer 自身が JSON 文字列を生成しているわけではありません。\u003c/p\u003e\n\u003col data-sourcepos=\"50:1-52:69\"\u003e\n\u003cli data-sourcepos=\"50:1-50:140\"\u003e\n\u003cstrong\u003eSerializer\u003c/strong\u003e: Model / Python オブジェクト ⇄ \u003cstrong\u003ePython のプリミティブ型（dict, list, str, int 等）\u003c/strong\u003e の相互変換\u003c/li\u003e\n\u003cli data-sourcepos=\"51:1-52:69\"\u003e\n\u003cstrong\u003eRenderer\u003c/strong\u003e: Python の辞書表現 ➔ \u003cstrong\u003eJSON 文字列\u003c/strong\u003e（View が \u003ccode\u003eResponse()\u003c/code\u003e を返す際に内部で実行）\u003cbr\u003e\nという 2 段階のパイプラインで処理されています。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cblockquote data-sourcepos=\"55:1-57:117\"\u003e\n\u003cp data-sourcepos=\"55:3-55:43\"\u003e💡 \u003cstrong\u003e迷ったときの判断基準\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"56:3-57:117\"\u003e\n\u003cli data-sourcepos=\"56:3-56:113\"\u003e「\u003cstrong\u003eHTTP リクエスト情報（誰が・何を求めているか）\u003c/strong\u003e」に依存する処理 ➔ \u003cstrong\u003eView\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"57:3-57:117\"\u003e「\u003cstrong\u003eデータそのものの構造・フォーマット・整合性\u003c/strong\u003e」に依存する処理 ➔ \u003cstrong\u003eSerializer\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003ch2 data-sourcepos=\"60:1-60:74\"\u003e\n\u003cspan id=\"1-リクエストからレスポンスまでの5大crud処理フロー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%8B%E3%82%89%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%81%BE%E3%81%A7%E3%81%AE5%E5%A4%A7crud%E5%87%A6%E7%90%86%E3%83%95%E3%83%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. リクエストからレスポンスまでの5大CRUD処理フロー\u003c/h2\u003e\n\u003cp data-sourcepos=\"62:1-62:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F26b261c3-89ab-4f92-9d7d-34ffcf965233.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=3fa6ce9e0f263f57f917cc42e92768b8\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F26b261c3-89ab-4f92-9d7d-34ffcf965233.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=3fa6ce9e0f263f57f917cc42e92768b8\" alt=\"263d85f3-0826-4b4a-a60a-bfa950d76f11.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F26b261c3-89ab-4f92-9d7d-34ffcf965233.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=1e4689248c5715ebcfd4a317f3fe7312 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/26b261c3-89ab-4f92-9d7d-34ffcf965233.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"64:1-64:218\"\u003eクライアントから HTTP リクエストが届いてからレスポンスが返るまで、サーバー内部で「Model（DB）」と「Serializer」がどの順序で関与しているかを表した図です。\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-66:82\"\u003e画像を見る上で押さえておくべきポイントは以下の2点です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"68:1-68:70\"\u003e\n\u003cspan id=\"-serializer-の使い分け入力用出力用なし\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-serializer-%E3%81%AE%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E5%85%A5%E5%8A%9B%E7%94%A8%E5%87%BA%E5%8A%9B%E7%94%A8%E3%81%AA%E3%81%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e① Serializer の使い分け（入力用・出力用・なし）\u003c/h3\u003e\n\u003cul data-sourcepos=\"69:1-75:0\"\u003e\n\u003cli data-sourcepos=\"69:1-70:234\"\u003e\n\u003cstrong\u003e登録・更新（POST / PUT / PATCH）\u003c/strong\u003e:\u003cbr\u003e\nクライアントから届いた生データを検証・保存するため、\u003cstrong\u003e入力用 Serializer（\u003ccode\u003eis_valid()\u003c/code\u003e）\u003c/strong\u003e を通します。バリデーションに失敗した場合は即座に \u003cstrong\u003e400 Bad Request\u003c/strong\u003e を返します。\u003c/li\u003e\n\u003cli data-sourcepos=\"71:1-72:249\"\u003e\n\u003cstrong\u003e取得（GET 詳細 / 一覧）\u003c/strong\u003e:\u003cbr\u003e\nDB から取り出した Model オブジェクトを JSON に変換するため、\u003cstrong\u003e出力用 Serializer\u003c/strong\u003e を通します。データの検証は不要なため、\u003ccode\u003eis_valid()\u003c/code\u003e は実行しません（一覧取得時は \u003ccode\u003emany=True\u003c/code\u003e を指定）。\u003c/li\u003e\n\u003cli data-sourcepos=\"73:1-75:0\"\u003e\n\u003cstrong\u003e削除（DELETE）\u003c/strong\u003e:\u003cbr\u003e\n対象レコードを検索して削除するだけなので、入力ボディが存在せず、\u003cstrong\u003e原則として Serializer は介しません\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"76:1-76:53\"\u003e\n\u003cspan id=\"-404-と-400-の発生タイミングの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-404-%E3%81%A8-400-%E3%81%AE%E7%99%BA%E7%94%9F%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e② 404 と 400 の発生タイミングの違い\u003c/h3\u003e\n\u003cul data-sourcepos=\"77:1-83:0\"\u003e\n\u003cli data-sourcepos=\"77:1-78:151\"\u003e\n\u003cstrong\u003e404 Not Found\u003c/strong\u003e:\u003cbr\u003e\npk（ID）を伴う処理（更新・削除・詳細取得）において、\u003cstrong\u003eDB に対象レコードが存在しない場合\u003c/strong\u003eに発生します。\u003c/li\u003e\n\u003cli data-sourcepos=\"79:1-83:0\"\u003e\n\u003cstrong\u003e400 Bad Request\u003c/strong\u003e:\u003cbr\u003e\nレコード取得後の \u003cstrong\u003eSerializer のバリデーションに引っかかった場合\u003c/strong\u003eに発生します。\u003cbr\u003e\n「まず pk でモデルを取得し、見つかった後に入力検証を行う」という順序が共通しています。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"84:1-84:48\"\u003e\n\u003cspan id=\"2-drf-view-のクラス階層と継承関係\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-drf-view-%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E9%9A%8E%E5%B1%A4%E3%81%A8%E7%B6%99%E6%89%BF%E9%96%A2%E4%BF%82\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. DRF View のクラス階層と継承関係\u003c/h2\u003e\n\u003cp data-sourcepos=\"86:1-86:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F625c5fea-626c-4d27-b97e-797b5bb3ee88.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=88065fffa7274be4f8a9af92c259137c\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F625c5fea-626c-4d27-b97e-797b5bb3ee88.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=88065fffa7274be4f8a9af92c259137c\" alt=\"1f407066-90dc-48e5-a9c2-093d6b0e90de.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F625c5fea-626c-4d27-b97e-797b5bb3ee88.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=cb98ca84e428dfab4b7a0424dbada418 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/625c5fea-626c-4d27-b97e-797b5bb3ee88.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"88:1-89:164\"\u003eDjango 標準の View から DRF の ViewSet に至るまでのクラス継承ツリーです。\u003cbr\u003e\n下層に進むにつれて抽象化が進み、\u003cstrong\u003e「自分で書くべき定型コード（ボイラープレート）」が減る\u003c/strong\u003e 構造になっています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"91:1-91:40\"\u003e\n\u003cspan id=\"クラスの進化と機能の追加\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E9%80%B2%E5%8C%96%E3%81%A8%E6%A9%9F%E8%83%BD%E3%81%AE%E8%BF%BD%E5%8A%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eクラスの進化と機能の追加\u003c/h3\u003e\n\u003col data-sourcepos=\"92:1-98:0\"\u003e\n\u003cli data-sourcepos=\"92:1-93:157\"\u003e\n\u003cstrong\u003e\u003ccode\u003eDjango View\u003c/code\u003e（基本）\u003c/strong\u003e:\u003cbr\u003e\nHTTP メソッド（GET / POST など）に応じたメソッドへ処理を振り分ける（ディスパッチする）だけの最小限のクラス。\u003c/li\u003e\n\u003cli data-sourcepos=\"94:1-95:218\"\u003e\n\u003cstrong\u003e\u003ccode\u003eDRF APIView\u003c/code\u003e（REST API 基盤）\u003c/strong\u003e:\u003cbr\u003e\n\u003ccode\u003eDjango View\u003c/code\u003e を継承。JSON の自動パース（\u003ccode\u003erequest.data\u003c/code\u003e）、JSON レスポンス変換（\u003ccode\u003eResponse\u003c/code\u003e）、認証・権限（Permissions）、統一されたエラーハンドリングが加わります。\u003c/li\u003e\n\u003cli data-sourcepos=\"96:1-98:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eDRF GenericAPIView\u003c/code\u003e（Model / Serializer 連携）\u003c/strong\u003e:\u003cbr\u003e\n\u003ccode\u003eAPIView\u003c/code\u003e を継承。\u003ccode\u003equeryset\u003c/code\u003e と \u003ccode\u003eserializer_class\u003c/code\u003e を属性として持たせることで、DB からのレコード取得（\u003ccode\u003eget_object()\u003c/code\u003e, \u003ccode\u003eget_queryset()\u003c/code\u003e）やシリアライザ生成（\u003ccode\u003eget_serializer()\u003c/code\u003e）の共通ロジックを提供します。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 data-sourcepos=\"99:1-99:52\"\u003e\n\u003cspan id=\"下流の2大分岐汎用-api-view-vs-viewset\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%B8%8B%E6%B5%81%E3%81%AE2%E5%A4%A7%E5%88%86%E5%B2%90%E6%B1%8E%E7%94%A8-api-view-vs-viewset\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e下流の2大分岐：汎用 API View vs ViewSet\u003c/h3\u003e\n\u003cp data-sourcepos=\"101:1-101:105\"\u003e\u003ccode\u003eGenericAPIView\u003c/code\u003e をベースとして、用途に合わせて2つのアプローチに分かれます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"103:1-108:206\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"103:1-103:68\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"103:2-103:9\"\u003e分類\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"103:11-103:42\"\u003e汎用 API View（特化型）\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"103:44-103:67\"\u003eViewSet（統合型）\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"105:1-105:131\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"105:2-105:19\"\u003e\u003cstrong\u003e設計思想\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"105:21-105:74\"\u003e\u003cstrong\u003e特定の処理・エンドポイントに特化\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"105:76-105:130\"\u003e\u003cstrong\u003eリソースの CRUD 全体を1クラスに集約\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"106:1-106:264\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"106:2-106:22\"\u003e\u003cstrong\u003e主なクラス\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"106:24-106:177\"\u003e\n\u003ccode\u003eCreateAPIView\u003c/code\u003e（作成）\u003cbr\u003e\u003ccode\u003eListAPIView\u003c/code\u003e（一覧）\u003cbr\u003e\u003ccode\u003eRetrieveAPIView\u003c/code\u003e（詳細）\u003cbr\u003e\u003ccode\u003eUpdateAPIView\u003c/code\u003e（更新）\u003cbr\u003e\u003ccode\u003eDestroyAPIView\u003c/code\u003e（削除）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"106:179-106:263\"\u003e\n\u003ccode\u003eModelViewSet\u003c/code\u003e（CRUD全般）\u003cbr\u003e\u003ccode\u003eReadOnlyModelViewSet\u003c/code\u003e（一覧・詳細のみ）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"107:1-107:137\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"107:2-107:20\"\u003e\u003cstrong\u003eURL の設定\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"107:22-107:77\"\u003e\n\u003ccode\u003epath()\u003c/code\u003e でエンドポイントごとに手動定義\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"107:79-107:136\"\u003e\n\u003ccode\u003eRouter\u003c/code\u003e（DefaultRouter）で複数 URL を自動生成\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"108:1-108:206\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"108:2-108:19\"\u003e\u003cstrong\u003e使い分け\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"108:21-108:125\"\u003e「会員登録専用」「公開一覧のみ」など、\u003cstrong\u003e機能を限定して公開したい場合\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"108:127-108:205\"\u003e\u003cstrong\u003e標準的な CRUD API を一式まとめて素早く構築したい場合\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"110:1-110:64\"\u003e\n\u003cspan id=\"-汎用-api-viewconcrete-viewの対応操作一覧\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E6%B1%8E%E7%94%A8-api-viewconcrete-view%E3%81%AE%E5%AF%BE%E5%BF%9C%E6%93%8D%E4%BD%9C%E4%B8%80%E8%A6%A7\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 汎用 API View（Concrete View）の対応操作一覧\u003c/h4\u003e\n\u003ctable data-sourcepos=\"112:1-122:74\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"112:1-112:103\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"112:2-112:12\"\u003eクラス\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:14-112:25\"\u003e一覧 GET\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:27-112:38\"\u003e詳細 GET\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:40-112:52\"\u003e登録 POST\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:54-112:65\"\u003e更新 PUT\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:67-112:86\"\u003e一部更新 PATCH\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"112:88-112:102\"\u003e削除 DELETE\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"114:1-114:57\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"114:2-114:20\"\u003e\u003cstrong\u003e\u003ccode\u003eListAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:22-114:26\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:28-114:32\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:34-114:38\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:40-114:44\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:46-114:50\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"114:52-114:56\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"115:1-115:61\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"115:2-115:24\"\u003e\u003cstrong\u003e\u003ccode\u003eRetrieveAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:26-115:30\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:32-115:36\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:38-115:42\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:44-115:48\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:50-115:54\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"115:56-115:60\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"116:1-116:63\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"116:2-116:26\"\u003e\u003cstrong\u003e\u003ccode\u003eListCreateAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:28-116:32\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:34-116:38\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:40-116:44\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:46-116:50\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:52-116:56\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"116:58-116:62\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"117:1-117:59\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"117:2-117:22\"\u003e\u003cstrong\u003e\u003ccode\u003eCreateAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:24-117:28\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:30-117:34\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:36-117:40\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:42-117:46\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:48-117:52\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"117:54-117:58\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"118:1-118:59\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"118:2-118:22\"\u003e\u003cstrong\u003e\u003ccode\u003eUpdateAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:24-118:28\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:30-118:34\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:36-118:40\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:42-118:46\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:48-118:52\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"118:54-118:58\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"119:1-119:60\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"119:2-119:23\"\u003e\u003cstrong\u003e\u003ccode\u003eDestroyAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:25-119:29\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:31-119:35\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:37-119:41\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:43-119:47\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:49-119:53\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"119:55-119:59\"\u003e○\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"120:1-120:67\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"120:2-120:30\"\u003e\u003cstrong\u003e\u003ccode\u003eRetrieveUpdateAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:32-120:36\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:38-120:42\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:44-120:48\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:50-120:54\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:56-120:60\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"120:62-120:66\"\u003e—\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"121:1-121:68\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"121:2-121:31\"\u003e\u003cstrong\u003e\u003ccode\u003eRetrieveDestroyAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:33-121:37\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:39-121:43\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:45-121:49\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:51-121:55\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:57-121:61\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"121:63-121:67\"\u003e○\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"122:1-122:74\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"122:2-122:37\"\u003e\u003cstrong\u003e\u003ccode\u003eRetrieveUpdateDestroyAPIView\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:39-122:43\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:45-122:49\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:51-122:55\"\u003e—\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:57-122:61\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:63-122:67\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"122:69-122:73\"\u003e○\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"125:1-125:93\"\u003e\n\u003cspan id=\"3-基本的なコード比較と使い分けapiview--genericapiview--modelviewset\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E3%82%B3%E3%83%BC%E3%83%89%E6%AF%94%E8%BC%83%E3%81%A8%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91apiview--genericapiview--modelviewset\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 基本的なコード比較と使い分け（APIView / GenericAPIView / ModelViewSet）\u003c/h2\u003e\n\u003cp data-sourcepos=\"127:1-127:90\"\u003e同じ「メッセージ一覧・作成」を扱う場合のコード量の違いです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"129:1-129:62\"\u003e\n\u003cspan id=\"-apiviewすべて手動で記述する基本形\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-apiview%E3%81%99%E3%81%B9%E3%81%A6%E6%89%8B%E5%8B%95%E3%81%A7%E8%A8%98%E8%BF%B0%E3%81%99%E3%82%8B%E5%9F%BA%E6%9C%AC%E5%BD%A2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e① \u003ccode\u003eAPIView\u003c/code\u003e（すべて手動で記述する基本形）\u003c/h3\u003e\n\u003cp data-sourcepos=\"130:1-130:134\"\u003eDB取得、シリアライザへの受け渡し、バリデーション、レスポンス返却をすべて明示的に書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"132:1-151:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.response\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.views\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eAPIView\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageSerializer\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageListCreateAPIView\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAPIView\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emessages\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessages\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emany\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_200_OK\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003epost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eraise_exception\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_201_CREATED\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"153:1-153:72\"\u003e\n\u003cspan id=\"-genericapiview汎用ビューで定型コードを削減\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-genericapiview%E6%B1%8E%E7%94%A8%E3%83%93%E3%83%A5%E3%83%BC%E3%81%A7%E5%AE%9A%E5%9E%8B%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%89%8A%E6%B8%9B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e② \u003ccode\u003eGenericAPIView\u003c/code\u003e（汎用ビューで定型コードを削減）\u003c/h3\u003e\n\u003cp data-sourcepos=\"154:1-154:153\"\u003e\u003ccode\u003equeryset\u003c/code\u003e と \u003ccode\u003eserializer_class\u003c/code\u003e を指定するだけで、一覧取得（\u003ccode\u003eGET\u003c/code\u003e）と新規作成（\u003ccode\u003ePOST\u003c/code\u003e）の定型処理を自動化できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"156:1-165:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003egenerics\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageSerializer\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageListCreateView\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003egenerics\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eListCreateAPIView\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageSerializer\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"167:1-167:60\"\u003e\n\u003cspan id=\"-modelviewsetcrud全般を1クラスに統合\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-modelviewsetcrud%E5%85%A8%E8%88%AC%E3%82%921%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E7%B5%B1%E5%90%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e③ \u003ccode\u003eModelViewSet\u003c/code\u003e（CRUD全般を1クラスに統合）\u003c/h3\u003e\n\u003cp data-sourcepos=\"168:1-168:127\"\u003e一覧・詳細取得・新規作成・更新・削除のすべてを、わずか2行の設定だけで一括提供します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"170:1-179:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eviewsets\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageSerializer\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eviewsets\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageSerializer\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"181:1-181:51\"\u003e\n\u003cspan id=\"-ルーティングurlspyの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0urlspy%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ ルーティング（\u003ccode\u003eurls.py\u003c/code\u003e）の違い\u003c/h4\u003e\n\u003cul data-sourcepos=\"182:1-186:0\"\u003e\n\u003cli data-sourcepos=\"182:1-183:102\"\u003e\n\u003cstrong\u003e\u003ccode\u003eAPIView\u003c/code\u003e / \u003ccode\u003eGenericAPIView\u003c/code\u003e\u003c/strong\u003e:\u003cbr\u003e\n\u003ccode\u003epath('messages/', MessageListCreateView.as_view())\u003c/code\u003e のように URL ごとに手動登録する。\u003c/li\u003e\n\u003cli data-sourcepos=\"184:1-186:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eModelViewSet\u003c/code\u003e\u003c/strong\u003e:\u003cbr\u003e\n\u003ccode\u003erouter = DefaultRouter()\u003c/code\u003e ➔ \u003ccode\u003erouter.register('messages', MessageViewSet)\u003c/code\u003e で複数 URL が自動生成される。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"187:1-187:41\"\u003e\n\u003cspan id=\"3大アプローチの観点別比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3%E5%A4%A7%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81%E3%81%AE%E8%A6%B3%E7%82%B9%E5%88%A5%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3大アプローチの観点別比較\u003c/h3\u003e\n\u003ctable data-sourcepos=\"189:1-197:47\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"189:1-189:51\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"189:2-189:9\"\u003e観点\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"189:11-189:19\"\u003eAPIView\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"189:21-189:35\"\u003e汎用APIView\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"189:37-189:50\"\u003eModelViewSet\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"191:1-191:35\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"191:2-191:16\"\u003e\u003cstrong\u003e簡単さ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"191:18-191:22\"\u003e△\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"191:24-191:28\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"191:30-191:34\"\u003e◎\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"192:1-192:56\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"192:2-192:19\"\u003e\u003cstrong\u003eコード量\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"192:21-192:28\"\u003e多い\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"192:30-192:43\"\u003e中くらい\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"192:45-192:55\"\u003e少ない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"193:1-193:35\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"193:2-193:16\"\u003e\u003cstrong\u003e自由度\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"193:18-193:22\"\u003e◎\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"193:24-193:28\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"193:30-193:34\"\u003e△\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"194:1-194:39\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"194:2-194:20\"\u003e\u003cstrong\u003eCRUD自動化\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"194:22-194:26\"\u003e△\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"194:28-194:32\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"194:34-194:38\"\u003e◎\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"195:1-195:51\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"195:2-195:34\"\u003e\u003cstrong\u003eルーティング自動化\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"195:36-195:39\"\u003e×\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"195:41-195:44\"\u003e×\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"195:46-195:50\"\u003e◎\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"196:1-196:41\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"196:2-196:22\"\u003e\u003cstrong\u003e初心者向け\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"196:24-196:28\"\u003e△\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"196:30-196:34\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"196:36-196:40\"\u003e◎\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"197:1-197:47\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"197:2-197:28\"\u003e\u003cstrong\u003e複雑な処理向け\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"197:30-197:34\"\u003e◎\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"197:36-197:40\"\u003e○\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"197:42-197:46\"\u003e△\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"200:1-200:52\"\u003e\n\u003cspan id=\"4-router-と-viewset-の-crud-メソッド対応\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-router-%E3%81%A8-viewset-%E3%81%AE-crud-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AF%BE%E5%BF%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. Router と ViewSet の CRUD メソッド対応\u003c/h2\u003e\n\u003cp data-sourcepos=\"202:1-202:342\"\u003eModelViewSet を使うと「わずか数行で CRUD が完成する」と言われますが、\u003cstrong\u003e「URL から届いたリクエストが、ViewSet のどのメソッドに届き、どうやって Serializer を呼んでいるのか」\u003c/strong\u003e という実行時の繋がりを理解していないと、カスタマイズで行き詰まります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"204:1-204:50\"\u003e\n\u003cspan id=\"url-と-viewset-メソッドの対応マップ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#url-%E3%81%A8-viewset-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%AF%BE%E5%BF%9C%E3%83%9E%E3%83%83%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eURL と ViewSet メソッドの対応マップ\u003c/h3\u003e\n\u003cp data-sourcepos=\"206:1-206:204\"\u003eRouter に \u003ccode\u003erouter.register('items', ItemViewSet)\u003c/code\u003e と登録したとき、自動生成されるエンドポイントと ViewSet のメソッドは次のように 1 対 1 でマッピングされます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"208:1-215:85\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"208:1-208:86\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"208:2-208:19\"\u003eHTTPメソッド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"208:21-208:38\"\u003eURL パターン\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"208:40-208:70\"\u003eViewSet の実行メソッド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"208:72-208:85\"\u003e処理内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"210:1-210:77\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"210:2-210:12\"\u003e\u003cstrong\u003e\u003ccode\u003eGET\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"210:14-210:24\"\u003e\u003ccode\u003e/items/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"210:26-210:46\"\u003e\u003cstrong\u003e\u003ccode\u003elist(request)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"210:48-210:76\"\u003e一覧取得（複数件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"211:1-211:75\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"211:2-211:13\"\u003e\u003cstrong\u003e\u003ccode\u003ePOST\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"211:15-211:25\"\u003e\u003ccode\u003e/items/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"211:27-211:49\"\u003e\u003cstrong\u003e\u003ccode\u003ecreate(request)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"211:51-211:74\"\u003e新規作成（1件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"212:1-212:89\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"212:2-212:12\"\u003e\u003cstrong\u003e\u003ccode\u003eGET\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"212:14-212:29\"\u003e\u003ccode\u003e/items/{pk}/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"212:31-212:63\"\u003e\u003cstrong\u003e\u003ccode\u003eretrieve(request, pk=...)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"212:65-212:88\"\u003e詳細取得（1件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"213:1-213:90\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"213:2-213:12\"\u003e\u003cstrong\u003e\u003ccode\u003ePUT\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"213:14-213:29\"\u003e\u003ccode\u003e/items/{pk}/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"213:31-213:61\"\u003e\u003cstrong\u003e\u003ccode\u003eupdate(request, pk=...)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"213:63-213:89\"\u003e全項目更新（1件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"214:1-214:103\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"214:2-214:14\"\u003e\u003cstrong\u003e\u003ccode\u003ePATCH\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"214:16-214:31\"\u003e\u003ccode\u003e/items/{pk}/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"214:33-214:71\"\u003e\u003cstrong\u003e\u003ccode\u003epartial_update(request, pk=...)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"214:73-214:102\"\u003e一部項目更新（1件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"215:1-215:85\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"215:2-215:15\"\u003e\u003cstrong\u003e\u003ccode\u003eDELETE\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"215:17-215:32\"\u003e\u003ccode\u003e/items/{pk}/\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"215:34-215:65\"\u003e\u003cstrong\u003e\u003ccode\u003edestroy(request, pk=...)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"215:67-215:84\"\u003e削除（1件）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"217:1-217:67\"\u003e\n\u003cspan id=\"実行時の処理の流れ内部で起きていること\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81%E3%82%8C%E5%86%85%E9%83%A8%E3%81%A7%E8%B5%B7%E3%81%8D%E3%81%A6%E3%81%84%E3%82%8B%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実行時の処理の流れ（内部で起きていること）\u003c/h3\u003e\n\u003cp data-sourcepos=\"219:1-219:129\"\u003eクライアントから \u003ccode\u003eGET /items/\u003c/code\u003e が届いた場合、裏側では以下のように処理がリレーされています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"221:1-235:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e1. Client Request: GET /items/\n        ↓\n2. urls.py (Router)\n   「/items/ への GET だから、ItemViewSet の list() メソッドを呼び出そう」\n        ↓\n3. ItemViewSet.list()\n   「まず対象のデータを DB から取得しよう」 ➔ self.get_queryset()\n        ↓\n4. Serializer による変換\n   「取得したクエリセットを出力用 JSON に変換しよう」 ➔ self.get_serializer(queryset, many=True)\n        ↓\n5. Response 返却\n   Response(serializer.data, status=200) をクライアントに返す\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"237:1-237:262\"\u003eこのように、Router はリクエストを解釈して \u003cstrong\u003e\u003ccode\u003elist\u003c/code\u003e や \u003ccode\u003ecreate\u003c/code\u003e といったアクションメソッド\u003c/strong\u003e に橋渡しし、各メソッドが内部で \u003cstrong\u003e\u003ccode\u003eget_queryset()\u003c/code\u003e\u003c/strong\u003e や \u003cstrong\u003e\u003ccode\u003eget_serializer()\u003c/code\u003e\u003c/strong\u003e を呼び出す構造になっています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"240:1-240:47\"\u003e\n\u003cspan id=\"5-queryset-と-get_queryset-の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-queryset-%E3%81%A8-get_queryset-%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. \u003ccode\u003equeryset\u003c/code\u003e と \u003ccode\u003eget_queryset()\u003c/code\u003e の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"242:1-242:203\"\u003eViewSet のコードを見ていると、ある場所では \u003ccode\u003equeryset = ...\u003c/code\u003e というクラス変数が使われ、別の場所では \u003ccode\u003edef get_queryset(self):\u003c/code\u003e というメソッドが登場します。\u003c/p\u003e\n\u003cp data-sourcepos=\"244:1-244:132\"\u003e「なぜ急にプロパティから関数に変わるのか？」と疑問に思う初心者が非常に多いポイントです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"246:1-250:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e【2つの決定的な違い】\n・queryset       : クラス属性として基本となる QuerySet を定義\n・get_queryset() : リクエストごとに取得対象を動的に決定できる\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"252:1-252:60\"\u003e\n\u003cspan id=\"1-queryset-属性基本となる固定の定義\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-queryset-%E5%B1%9E%E6%80%A7%E5%9F%BA%E6%9C%AC%E3%81%A8%E3%81%AA%E3%82%8B%E5%9B%BA%E5%AE%9A%E3%81%AE%E5%AE%9A%E7%BE%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003equeryset\u003c/code\u003e 属性（基本となる固定の定義）\u003c/h3\u003e\n\u003cp data-sourcepos=\"253:1-253:156\"\u003e「ベースとして全件を対象にする」「誰がアクセスしても同じデータを返す」というシンプルなケースで使います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"255:1-260:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 基本となる固定の QuerySet\n\u003c/span\u003e    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItemSerializer\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"262:1-265:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"263:1-264:268\"\u003e\u003cstrong\u003e💡 QuerySet は遅延評価（Lazy Evaluation）\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003equeryset = Item.objects.all()\u003c/code\u003e と定義しても、その瞬間に SQL が実行されるわけではありません。実際にデータへアクセス（反復処理やシリアライズなど）されたタイミングで初めてクエリが発行されます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"267:1-267:72\"\u003e\n\u003cspan id=\"2-get_queryset-メソッド動的リクエスト依存\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-get_queryset-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%8B%95%E7%9A%84%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E4%BE%9D%E5%AD%98\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eget_queryset()\u003c/code\u003e メソッド（動的・リクエスト依存）\u003c/h3\u003e\n\u003cp data-sourcepos=\"268:1-268:353\"\u003e「ログインユーザー自身のデータだけに絞り込みたい」「URL のクエリパラメータ（\u003ccode\u003e?category=news\u003c/code\u003e）に応じて抽出したい」など、\u003cstrong\u003eリクエスト（\u003ccode\u003eself.request\u003c/code\u003e）の内容に応じて動的に条件を変えたい場合\u003c/strong\u003e は、\u003ccode\u003eget_queryset()\u003c/code\u003e メソッドを上書き（オーバーライド）します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"270:1-278:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItemSerializer\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_queryset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# ログインユーザーの所属会社に紐づくアイテムだけに動的フィルタリング！\n\u003c/span\u003e        \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecompany\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecompany\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"280:1-288:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"281:1-282:329\"\u003e\u003cstrong\u003e💡 queryset の省略と Router の basename\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003equeryset\u003c/code\u003e 属性を省略して \u003ccode\u003eget_queryset()\u003c/code\u003e だけを定義することも可能です。ただしその場合、Router がモデル名を自動推論できず、URL パターンの名前（\u003ccode\u003ebasename\u003c/code\u003e）生成に失敗することがあります。その際は以下のように \u003ccode\u003ebasename\u003c/code\u003e を明示的に指定します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"284:1-287:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# queryset を省略する場合は basename を指定する\n\u003c/span\u003e\u003cspan class=\"n\"\u003erouter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eregister\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitems\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebasename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"291:1-291:74\"\u003e\n\u003cspan id=\"6-viewset-でよく上書きするメソッドとライフサイクル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-viewset-%E3%81%A7%E3%82%88%E3%81%8F%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%99%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. ViewSet でよく上書きするメソッドとライフサイクル\u003c/h2\u003e\n\u003cp data-sourcepos=\"293:1-293:310\"\u003eModelViewSet は便利な反面、「保存時にログインユーザーを自動で紐付けたい」「一覧画面と詳細画面でシリアライザを切り替えたい」といった実務要件に直面したとき、どのメソッドを書き換えるべきか知っておく必要があります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"295:1-295:47\"\u003e\n\u003cspan id=\"create-と-perform_create-の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#create-%E3%81%A8-perform_create-%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003ecreate()\u003c/code\u003e と \u003ccode\u003eperform_create()\u003c/code\u003e の違い\u003c/h3\u003e\n\u003cp data-sourcepos=\"297:1-297:185\"\u003e一番の疑問になりやすいのが「\u003ccode\u003ecreate()\u003c/code\u003e と \u003ccode\u003eperform_create()\u003c/code\u003e は何が違うのか？」です。DRF の内部コードを覗くと、その関係は一目瞭然です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"299:1-319:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 【DRF内部の CreateModelMixin.create の実装イメージ】\n\u003c/span\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ecreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ekwargs\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 1. リクエストデータからシリアライザを生成\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget_serializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 2. バリデーション実行（エラーなら 400 を投げる）\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eraise_exception\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 3. ★ここで perform_create を呼び出す！\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eperform_create\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 4. 成功レスポンス（201 Created）を返却\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eheaders\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget_success_headers\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_201_CREATED\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eheaders\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eperform_create\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# デフォルトは単に save() を呼ぶだけ\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"321:1-321:69\"\u003e処理の流れを構造化すると以下のようになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"323:1-332:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecreate()\n│\n├─ Serializerを作る (get_serializer)\n├─ is_valid()\n├─ perform_create()\n│      └─ serializer.save()\n│\n└─ Responseを作る (Response)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"334:1-334:12\"\u003eつまり、\u003c/p\u003e\n\u003cul data-sourcepos=\"335:1-337:0\"\u003e\n\u003cli data-sourcepos=\"335:1-335:123\"\u003e\n\u003cstrong\u003e\u003ccode\u003ecreate()\u003c/code\u003e\u003c/strong\u003e: バリデーションからレスポンス作成までの「処理全体」を取り仕切るメソッド\u003c/li\u003e\n\u003cli data-sourcepos=\"336:1-337:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eperform_create()\u003c/code\u003e\u003c/strong\u003e: バリデーション成功後、\u003cstrong\u003eモデルを保存するタイミングの処理をカスタマイズするためのフックメソッド\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"338:1-338:177\"\u003eこのような保存時の追加情報だけを補いたい場合は、\u003ccode\u003ecreate()\u003c/code\u003e 全体ではなく \u003cstrong\u003e\u003ccode\u003eperform_create()\u003c/code\u003e をオーバーライドするのが基本\u003c/strong\u003e です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"340:1-343:184\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"340:1-340:82\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"340:2-340:24\"\u003e変更したいもの\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"340:26-340:54\"\u003e上書きするメソッド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"340:56-340:81\"\u003e主なユースケース\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"342:1-342:175\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"342:2-342:24\"\u003e\u003cstrong\u003ePOST 処理全体\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"342:26-342:41\"\u003e\u003cstrong\u003e\u003ccode\u003ecreate()\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"342:43-342:174\"\u003e・レスポンス形式やステータスコードを変えたい\u003cbr\u003e・保存前後の処理フローそのものを変えたい\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"343:1-343:184\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"343:2-343:31\"\u003e\u003cstrong\u003e保存時の処理だけ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"343:33-343:56\"\u003e\u003cstrong\u003e\u003ccode\u003eperform_create()\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"343:58-343:183\"\u003e・\u003ccode\u003ecreated_by\u003c/code\u003e や \u003ccode\u003ecompany\u003c/code\u003e を自動設定したい\u003cbr\u003e・保存時にリクエスト由来の追加情報を渡したい\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"345:1-346:0\"\u003e\n\u003ch3 data-sourcepos=\"347:1-347:58\"\u003e\n\u003cspan id=\"実務でよく上書きする代表的なメソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E5%8B%99%E3%81%A7%E3%82%88%E3%81%8F%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%99%E3%82%8B%E4%BB%A3%E8%A1%A8%E7%9A%84%E3%81%AA%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実務でよく上書きする代表的なメソッド\u003c/h3\u003e\n\u003ctable data-sourcepos=\"349:1-355:195\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"349:1-349:67\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"349:2-349:30\"\u003e上書きするメソッド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"349:32-349:66\"\u003e主な役割とユースケース\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"351:1-351:180\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"351:2-351:27\"\u003e\u003cstrong\u003e\u003ccode\u003eget_queryset(self)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"351:29-351:179\"\u003e\n\u003cstrong\u003e取得レコードの動的絞り込み\u003c/strong\u003e\u003cbr\u003eログインユーザーの所属テナント、公開中フラグによるフィルタリングなど\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"352:1-352:221\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"352:2-352:35\"\u003e\u003cstrong\u003e\u003ccode\u003eget_serializer_class(self)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"352:37-352:220\"\u003e\n\u003cstrong\u003eアクションに応じたシリアライザの切り替え\u003c/strong\u003e\u003cbr\u003e一覧表示用（軽量シリアライザ）と詳細・更新用（全項目シリアライザ）の出し分け\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"353:1-353:179\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"353:2-353:41\"\u003e\u003cstrong\u003e\u003ccode\u003eperform_create(self, serializer)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"353:43-353:178\"\u003e\n\u003cstrong\u003e新規保存時の追加情報注入\u003c/strong\u003e\u003cbr\u003e\u003ccode\u003eserializer.save(created_by=self.request.user)\u003c/code\u003e のようにリクエスト情報を付与\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"354:1-354:176\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"354:2-354:41\"\u003e\u003cstrong\u003e\u003ccode\u003eperform_update(self, serializer)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"354:43-354:175\"\u003e\n\u003cstrong\u003e更新保存時の追加情報注入・監査ログ\u003c/strong\u003e\u003cbr\u003e\u003ccode\u003eserializer.save(updated_by=self.request.user)\u003c/code\u003e や更新ログの出力\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"355:1-355:195\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"355:2-355:67\"\u003e\n\u003cstrong\u003e\u003ccode\u003edestroy(self, request, ...)\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003eまたは \u003ccode\u003eperform_destroy\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"355:69-355:194\"\u003e\n\u003cstrong\u003e論理削除へのカスタマイズ\u003c/strong\u003e\u003cbr\u003eDB から物理削除せず、\u003ccode\u003einstance.is_deleted = True\u003c/code\u003e にして保存する\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"357:1-357:63\"\u003e\n\u003cspan id=\"-実務でよくあるオーバーライドの実装例\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E5%AE%9F%E5%8B%99%E3%81%A7%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%A9%E3%82%A4%E3%83%89%E3%81%AE%E5%AE%9F%E8%A3%85%E4%BE%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 実務でよくあるオーバーライドの実装例\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"359:1-381:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArticleViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eArticle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 1. アクションごとにシリアライザを切り替える\n\u003c/span\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_serializer_class\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eaction\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003elist\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eArticleListSerializer\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 一覧は軽量版\n\u003c/span\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eArticleDetailSerializer\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 詳細・作成・更新は完全版\n\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 2. ログインユーザーに応じて取得データを絞り込む\n\u003c/span\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_queryset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eArticle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eauthor\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 3. 新規作成時に author フィールドへログインユーザーを注入\n\u003c/span\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eperform_create\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eauthor\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 4. 削除を物理削除ではなく論理削除にする\n\u003c/span\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eperform_destroy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_deleted\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\n        \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"384:1-384:55\"\u003e\n\u003cspan id=\"7-デコレータの活用と-viewset-の-action\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%AE%E6%B4%BB%E7%94%A8%E3%81%A8-viewset-%E3%81%AE-action\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. デコレータの活用と ViewSet の \u003ccode\u003e@action\u003c/code\u003e\n\u003c/h2\u003e\n\u003cp data-sourcepos=\"386:1-386:159\"\u003ePython / Django / DRF では、クラスや関数の直前に \u003ccode\u003e@\u003c/code\u003e を付ける「デコレータ」を使って、追加機能を宣言的に付与します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"388:1-388:38\"\u003e\n\u003cspan id=\"代表的な3つのデコレータ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BB%A3%E8%A1%A8%E7%9A%84%E3%81%AA3%E3%81%A4%E3%81%AE%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e代表的な3つのデコレータ\u003c/h3\u003e\n\u003col data-sourcepos=\"389:1-395:0\"\u003e\n\u003cli data-sourcepos=\"389:1-390:107\"\u003e\n\u003cstrong\u003e\u003ccode\u003e@action\u003c/code\u003e（DRF ViewSet）\u003c/strong\u003e:\u003cbr\u003e\nViewSet の中に標準 CRUD 以外の\u003cstrong\u003e追加 API（独自エンドポイント）\u003c/strong\u003e を作成する。\u003c/li\u003e\n\u003cli data-sourcepos=\"391:1-392:156\"\u003e\n\u003cstrong\u003e\u003ccode\u003e@api_view\u003c/code\u003e ＋ \u003ccode\u003e@permission_classes\u003c/code\u003e（関数ベース View）\u003c/strong\u003e:\u003cbr\u003e\n通常の Python 関数を REST API 化し、アクセス権限（\u003ccode\u003eAllowAny\u003c/code\u003e など）や CSRF Cookie 付与（\u003ccode\u003e@ensure_csrf_cookie\u003c/code\u003e）を設定する。\u003c/li\u003e\n\u003cli data-sourcepos=\"393:1-395:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003e@admin.register\u003c/code\u003e（Django Admin）\u003c/strong\u003e:\u003cbr\u003e\nモデルを Django 管理画面に紐付け、表示項目（\u003ccode\u003elist_display\u003c/code\u003e）や検索・絞り込みを一括設定する。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr data-sourcepos=\"396:1-397:0\"\u003e\n\u003ch3 data-sourcepos=\"398:1-398:52\"\u003e\n\u003cspan id=\"viewset-に独自処理を追加する-action\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#viewset-%E3%81%AB%E7%8B%AC%E8%87%AA%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B-action\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eViewSet に独自処理を追加する \u003ccode\u003e@action\u003c/code\u003e\n\u003c/h3\u003e\n\u003cp data-sourcepos=\"400:1-400:351\"\u003eModelViewSet は標準で CRUD に対応する \u003cstrong\u003e6 つのアクション（\u003ccode\u003elist\u003c/code\u003e, \u003ccode\u003eretrieve\u003c/code\u003e, \u003ccode\u003ecreate\u003c/code\u003e, \u003ccode\u003eupdate\u003c/code\u003e, \u003ccode\u003epartial_update\u003c/code\u003e, \u003ccode\u003edestroy\u003c/code\u003e）\u003c/strong\u003e を提供しますが、実務では「有効なデータだけの一覧」「特定レコードへのお気に入り登録」など、\u003cstrong\u003e標準 CRUD に収まらない処理\u003c/strong\u003e が必ず発生します。\u003c/p\u003e\n\u003cp data-sourcepos=\"402:1-402:62\"\u003eそれを解決するのが \u003ccode\u003e@action\u003c/code\u003e デコレータです。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"404:1-404:26\"\u003e\n\u003cspan id=\"1-特徴と仕組み\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E7%89%B9%E5%BE%B4%E3%81%A8%E4%BB%95%E7%B5%84%E3%81%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 特徴と仕組み\u003c/h4\u003e\n\u003cul data-sourcepos=\"405:1-408:0\"\u003e\n\u003cli data-sourcepos=\"405:1-405:136\"\u003e\n\u003cstrong\u003eRouter との連携\u003c/strong\u003e: ViewSet のメソッドに \u003ccode\u003e@action\u003c/code\u003e を付けるだけで、Router が URL を自動生成してくれる。\u003c/li\u003e\n\u003cli data-sourcepos=\"406:1-406:117\"\u003e\n\u003cstrong\u003eHTTP メソッドの指定\u003c/strong\u003e: \u003ccode\u003emethods=['get']\u003c/code\u003e や \u003ccode\u003emethods=['post']\u003c/code\u003e で受付メソッドを制限できる。\u003c/li\u003e\n\u003cli data-sourcepos=\"407:1-408:0\"\u003e\n\u003cstrong\u003eViewSet 専用\u003c/strong\u003e: 単体の関数や APIView では使えず、ViewSet のメソッドとして定義する。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 data-sourcepos=\"409:1-409:71\"\u003e\n\u003cspan id=\"2-detailtrue-と-detailfalse-の使い分け最重要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-detailtrue-%E3%81%A8-detailfalse-%E3%81%AE%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E6%9C%80%E9%87%8D%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003edetail=True\u003c/code\u003e と \u003ccode\u003edetail=False\u003c/code\u003e の使い分け（最重要）\u003c/h4\u003e\n\u003ctable data-sourcepos=\"411:1-414:298\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"411:1-411:59\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"411:2-411:9\"\u003e設定\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"411:11-411:18\"\u003e対象\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"411:20-411:31\"\u003eURL の形\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"411:33-411:58\"\u003e主な用途・具体例\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"413:1-413:273\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"413:2-413:21\"\u003e\u003cstrong\u003e\u003ccode\u003edetail=False\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"413:23-413:87\"\u003e\n\u003cstrong\u003e全体・一覧レベル\u003c/strong\u003e\u003cbr\u003e（特定の1件ではない）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"413:89-413:150\"\u003e\n\u003ccode\u003eGET /staff/active/\u003c/code\u003e\u003cbr\u003e（URL に ID が\u003cstrong\u003e入らない\u003c/strong\u003e）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"413:152-413:272\"\u003e・有効なデータのみの一覧取得\u003cbr\u003e・全体の集計・統計データの取得\u003cbr\u003e・一括データ処理\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"414:1-414:298\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"414:2-414:20\"\u003e\u003cstrong\u003e\u003ccode\u003edetail=True\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"414:22-414:83\"\u003e\n\u003cstrong\u003e個別レベル\u003c/strong\u003e\u003cbr\u003e（特定の1件に対する処理）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"414:85-414:148\"\u003e\n\u003ccode\u003ePOST /staff/{id}/favorite/\u003c/code\u003e\u003cbr\u003e（URL に ID が\u003cstrong\u003e入る\u003c/strong\u003e）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"414:150-414:297\"\u003e・特定レコードへのお気に入り登録\u003cbr\u003e・特定ユーザーのパスワード変更\u003cbr\u003e・単一アイテムのステータス更新\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv data-sourcepos=\"416:1-419:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"417:1-418:217\"\u003e\u003cstrong\u003e💡 \u003ccode\u003edetail=True\u003c/code\u003e の便利なポイント\u003c/strong\u003e\u003cbr\u003e\nメソッド内で \u003ccode\u003eself.get_object()\u003c/code\u003e を呼び出すだけで、URL に含まれる \u003ccode\u003e{id}\u003c/code\u003e の Model インスタンスを自動取得できます（見つからない場合は自動で 404 Not Found を返却）。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"421:1-440:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eStaffViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eStaff\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eStaffSerializer\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 全体に対する独自エンドポイント: GET /staff/active/\n\u003c/span\u003e    \u003cspan class=\"nd\"\u003e@action\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edetail\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emethods\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eget\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eactive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eactive_staff\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget_queryset\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eis_active\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget_serializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eactive_staff\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emany\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# 特定の1件に対する独自エンドポイント: POST /staff/{pk}/favorite/\n\u003c/span\u003e    \u003cspan class=\"nd\"\u003e@action\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edetail\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emethods\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003epost\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003efavorite\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epk\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003estaff\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget_object\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# URLのpkから対象インスタンスを自動取得\n\u003c/span\u003e        \u003cspan class=\"n\"\u003estaff\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_favorite\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\n        \u003cspan class=\"n\"\u003estaff\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estatus\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003efavorited\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"443:1-443:96\"\u003e\n\u003cspan id=\"8-実務での重要パターンリレーション先の取得と既存テーブル連携\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-%E5%AE%9F%E5%8B%99%E3%81%A7%E3%81%AE%E9%87%8D%E8%A6%81%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%85%88%E3%81%AE%E5%8F%96%E5%BE%97%E3%81%A8%E6%97%A2%E5%AD%98%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E9%80%A3%E6%90%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. 実務での重要パターン：リレーション先の取得と既存テーブル連携\u003c/h2\u003e\n\u003cp data-sourcepos=\"445:1-445:263\"\u003e実務の API 開発では、単一テーブルだけでなく「外部キー（ForeignKey）先のデータをどう効率よくレスポンスに含めるか」や「既存のデータベース（レガシーDB）とどう連携するか」が頻出します。\u003c/p\u003e\n\u003cp data-sourcepos=\"447:1-447:99\"\u003e製品（\u003ccode\u003eProduct\u003c/code\u003e）と、それに紐づく品目（\u003ccode\u003eItem\u003c/code\u003e）を例に見ていきましょう。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"449:1-449:39\"\u003e\n\u003cspan id=\"1-modelspyモデル定義\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-modelspy%E3%83%A2%E3%83%87%E3%83%AB%E5%AE%9A%E7%BE%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003emodels.py\u003c/code\u003e（モデル定義）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"451:1-479:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003edjango.db\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModel\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"nb\"\u003eid\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eAutoField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprimary_key\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eproduct_code\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eunique\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003edb_table\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emanaged\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModel\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eproduct\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eForeignKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eto_field\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct_code\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 主キー(id)ではなく、業務コード(product_code)を参照\n\u003c/span\u003e        \u003cspan class=\"n\"\u003edb_column\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct_code\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eon_delete\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDO_NOTHING\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"n\"\u003erelated_name\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_list\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eitem_code\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edb_column\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_code\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eitem_name\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emodels\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edb_column\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003edb_table\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emanaged\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"481:1-481:53\"\u003e\n\u003cspan id=\"2-serializerspyシリアライザ定義\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-serializerspy%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E5%AE%9A%E7%BE%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eserializers.py\u003c/code\u003e（シリアライザ定義）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"483:1-503:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 親モデル(Product)の名前を取り出してフラットに含める\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eproduct_name\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"n\"\u003esource\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct.name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eread_only\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_code\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct_name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"505:1-505:37\"\u003e\n\u003cspan id=\"3-viewspyviewset-定義\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-viewspyviewset-%E5%AE%9A%E7%BE%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. \u003ccode\u003eviews.py\u003c/code\u003e（ViewSet 定義）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"507:1-517:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.viewsets\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eItemSerializer\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 親モデルを JOIN して一括取得\n\u003c/span\u003e    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eselect_related\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItemSerializer\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"519:1-520:0\"\u003e\n\u003ch3 data-sourcepos=\"521:1-521:44\"\u003e\n\u003cspan id=\"実務で頻出する4つの重要設定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E5%8B%99%E3%81%A7%E9%A0%BB%E5%87%BA%E3%81%99%E3%82%8B4%E3%81%A4%E3%81%AE%E9%87%8D%E8%A6%81%E8%A8%AD%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実務で頻出する4つの重要設定\u003c/h3\u003e\n\u003cp data-sourcepos=\"523:1-523:179\"\u003e上記のコードで登場した 4 つの設定は、実務（特に既存データベース連携や API レスポンス設計）で必須となる重要テクニックです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"525:1-530:319\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"525:1-525:55\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"525:2-525:15\"\u003e設定項目\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"525:17-525:36\"\u003e指定する場所\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"525:38-525:54\"\u003e役割と用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"527:1-527:366\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"527:2-527:24\"\u003e\u003cstrong\u003e\u003ccode\u003emanaged = False\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"527:26-527:52\"\u003eモデルの \u003ccode\u003eclass Meta\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"527:54-527:365\"\u003e\n\u003cstrong\u003eDjango migrations にテーブル構造を管理させない\u003c/strong\u003e\u003cbr\u003eマイグレーションによるテーブル作成・変更・削除の対象から外す設定。なお、Django ORM からの検索（\u003ccode\u003eall()\u003c/code\u003e, \u003ccode\u003efilter()\u003c/code\u003e）や登録・更新・削除（\u003ccode\u003esave()\u003c/code\u003e, \u003ccode\u003edelete()\u003c/code\u003e）は通常通り実行可能。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"528:1-528:271\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"528:2-528:18\"\u003e\u003cstrong\u003e\u003ccode\u003edb_column\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"528:20-528:54\"\u003eモデルのフィールド引数\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"528:56-528:270\"\u003e\n\u003cstrong\u003e実際の DB カラム名を明示的にマッピングする\u003c/strong\u003e\u003cbr\u003ePython 側の変数名（\u003ccode\u003eproduct\u003c/code\u003e や \u003ccode\u003eitem_code\u003c/code\u003e）と、実際の DB カラム名（\u003ccode\u003eproduct_code\u003c/code\u003e 等）が異なる場合に紐付ける。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"529:1-529:379\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"529:2-529:17\"\u003e\u003cstrong\u003e\u003ccode\u003eto_field\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"529:19-529:42\"\u003e\n\u003ccode\u003eForeignKey\u003c/code\u003e の引数\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"529:44-529:378\"\u003e\n\u003cstrong\u003e主キーではなく、業務コード（UNIQUE列）を外部キーとして参照する\u003c/strong\u003e\u003cbr\u003eForeignKey はデフォルトで参照先モデルの主キー（\u003ccode\u003eid\u003c/code\u003e）を見るため、主キーではなく一意制約（\u003ccode\u003eunique=True\u003c/code\u003e）のついた業務コード（\u003ccode\u003eproduct_code\u003c/code\u003e 等）で結合したい場合に指定する。\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"530:1-530:319\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"530:2-530:15\"\u003e\u003cstrong\u003e\u003ccode\u003esource\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"530:17-530:53\"\u003eSerializer フィールドの引数\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"530:55-530:318\"\u003e\n\u003cstrong\u003eシリアライズするデータの取得元をカスタマイズする\u003c/strong\u003e\u003cbr\u003eドット記法（\u003ccode\u003esource=\"product.name\"\u003c/code\u003e）でリレーション先（親モデル）の項目を取り出してフラットにレスポンスへ含めたい場合に指定する。\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"532:1-532:87\"\u003e\n\u003cspan id=\"-補足特定のカラムだけを辞書で高速取得する-values\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E8%A3%9C%E8%B6%B3%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%A0%E3%81%91%E3%82%92%E8%BE%9E%E6%9B%B8%E3%81%A7%E9%AB%98%E9%80%9F%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B-values\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 【補足】特定のカラムだけを辞書で高速取得する \u003ccode\u003e.values()\u003c/code\u003e\n\u003c/h4\u003e\n\u003cp data-sourcepos=\"534:1-534:197\"\u003eModel インスタンス全体を生成するのではなく、\u003cstrong\u003e必要なカラムだけを Python の辞書（\u003ccode\u003edict\u003c/code\u003e）として軽量に取得したい\u003c/strong\u003e 場合は \u003ccode\u003e.values()\u003c/code\u003e を使います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"536:1-545:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 必要なカラムだけを dict のリストとして取得（リレーション先は __ で指定）\n\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003evalues\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_code\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eitem_name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eproduct__name\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 取得されるデータ形式のイメージ:\n# [\n#     {\"item_code\": \"A01\", \"item_name\": \"品目A\", \"product__name\": \"製品X\"},\n#     {\"item_code\": \"A02\", \"item_name\": \"品目B\", \"product__name\": \"製品Y\"},\n# ]\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"547:1-553:0\"\u003e\n\u003cli data-sourcepos=\"547:1-549:170\"\u003e\n\u003cstrong\u003eメリット\u003c/strong\u003e:\n\u003cul data-sourcepos=\"548:3-549:170\"\u003e\n\u003cli data-sourcepos=\"548:3-548:210\"\u003eModel インスタンス生成のオーバーヘッドを省き、指定したカラムのみを \u003ccode\u003eSELECT\u003c/code\u003e するため、\u003cstrong\u003e大量データでは高速化・メモリ削減につながることがある\u003c/strong\u003e。\u003c/li\u003e\n\u003cli data-sourcepos=\"549:3-549:170\"\u003e\n\u003ccode\u003eproduct__name\u003c/code\u003e（アンダースコア2つ）のように書くことで、リレーション先（親テーブル）も自動 JOIN して一緒に取得できる。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"550:1-553:0\"\u003e\n\u003cstrong\u003e注意点\u003c/strong\u003e:\n\u003cul data-sourcepos=\"551:3-553:0\"\u003e\n\u003cli data-sourcepos=\"551:3-551:266\"\u003e返り値が Model インスタンスではなく単なる辞書（\u003ccode\u003edict\u003c/code\u003e）になるため、モデルメソッドやモデルプロパティ、通常の属性アクセス（\u003ccode\u003einstance.name\u003c/code\u003e）を前提にした処理はそのまま使えない場合がある。\u003c/li\u003e\n\u003cli data-sourcepos=\"552:3-553:0\"\u003e※シリアライザを使う場合でも、辞書のキーアクセス（\u003ccode\u003eobj[\"item_name\"]\u003c/code\u003e）を前提にコードを書けば \u003ccode\u003eSerializerMethodField\u003c/code\u003e 等を利用することは可能です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"554:1-555:0\"\u003e\n\u003ch3 data-sourcepos=\"556:1-556:94\"\u003e\n\u003cspan id=\"最重要serializer-での関連先参照と-view-の-select_related-の因果関係\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E9%87%8D%E8%A6%81serializer-%E3%81%A7%E3%81%AE%E9%96%A2%E9%80%A3%E5%85%88%E5%8F%82%E7%85%A7%E3%81%A8-view-%E3%81%AE-select_related-%E3%81%AE%E5%9B%A0%E6%9E%9C%E9%96%A2%E4%BF%82\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【最重要】Serializer での関連先参照と View の select_related の因果関係\u003c/h3\u003e\n\u003cp data-sourcepos=\"558:1-558:181\"\u003eここで絶対に押さえておきたいのが、\u003cstrong\u003e「なぜ View 側で \u003ccode\u003eselect_related(\"product\")\u003c/code\u003e を書かなければならないのか？」\u003c/strong\u003e という因果関係です。\u003c/p\u003e\n\u003cp data-sourcepos=\"560:1-560:324\"\u003e原因は \u003ccode\u003esource\u003c/code\u003e そのものではなく、\u003cstrong\u003e「Serializer が関連先（ここでは \u003ccode\u003eproduct\u003c/code\u003e）を参照する際、各 Item について関連先オブジェクトが未取得であること」\u003c/strong\u003e にあります（ネストした \u003ccode\u003eProductSerializer\u003c/code\u003e を埋め込む場合でも全く同じ現象が起きます）。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"562:1-576:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e【裏で起きている致命的な問題の流れ】\n1. Serializer が関連先（product）を参照する\n   （例: source=\"product.name\" や ネストした ProductSerializer）\n        ↓\n2. 各 Item について親 Product が DB から未取得の状態\n        ↓\n3. 1件シリアライズして product を参照するたびに、追加の SQL が発行される\n        ↓\n4. アイテムが 100 件あれば、ベースの取得 1 回 ＋ 関連先の取得 100 回 ＝ 合計 101 回の SQL が走る（N+1 問題）\n        ↓\n【解決策】\nだからこそ、View 側で事前に Item.objects.select_related(\"product\").all() と書き、\nSQL の JOIN を使ってあらかじめ親データまでまとめて引っ張っておく必要がある！\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"578:1-578:179\"\u003eこの「N+1 問題」の仕組みと、Django が誇るもう1つの解決策 \u003ccode\u003eprefetch_related\u003c/code\u003e について、次の章で図解を使って深掘りしていきましょう。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"581:1-581:86\"\u003e\n\u003cspan id=\"9-図解でわかる-n1-問題と-select_related--prefetch_related-の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9-%E5%9B%B3%E8%A7%A3%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B-n1-%E5%95%8F%E9%A1%8C%E3%81%A8-select_related--prefetch_related-%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. 図解でわかる N+1 問題と \u003ccode\u003eselect_related\u003c/code\u003e / \u003ccode\u003eprefetch_related\u003c/code\u003e の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"583:1-583:173\"\u003e実務でリレーションを扱う際に避けて通れない「N+1 問題」の正体と、Django が用意している 2 つの解決アプローチの仕組みです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"585:1-585:83\"\u003e\n\u003cspan id=\"1-select_relatedauthorforeignkey多対1を-join-で一括取得\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-select_relatedauthorforeignkey%E5%A4%9A%E5%AF%BE1%E3%82%92-join-%E3%81%A7%E4%B8%80%E6%8B%AC%E5%8F%96%E5%BE%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003eselect_related(\"author\")\u003c/code\u003e：ForeignKey（多対1）を JOIN で一括取得\u003c/h3\u003e\n\u003cp data-sourcepos=\"587:1-587:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fd61f1d3c-ee56-4b96-badb-a107665e4acd.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=df74e0c9dd8aa4873e1f4faf9d0566a0\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fd61f1d3c-ee56-4b96-badb-a107665e4acd.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=df74e0c9dd8aa4873e1f4faf9d0566a0\" alt=\"a892c76d-4697-469e-ba4c-26b69bec49ce.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fd61f1d3c-ee56-4b96-badb-a107665e4acd.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=1c30a4699662001760c8be6ec055e1bc 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/d61f1d3c-ee56-4b96-badb-a107665e4acd.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"589:1-589:124\"\u003e「本（100冊）の一覧と、それぞれの著者名（Author）を表示したい」ケース（ForeignKey）です。\u003c/p\u003e\n\u003cul data-sourcepos=\"591:1-600:0\"\u003e\n\u003cli data-sourcepos=\"591:1-594:109\"\u003e\n\u003cstrong\u003e❌ \u003ccode\u003eselect_related(\"author\")\u003c/code\u003e なし（N+1 問題）\u003c/strong\u003e:\n\u003col data-sourcepos=\"592:3-593:113\"\u003e\n\u003cli data-sourcepos=\"592:3-592:58\"\u003eBook テーブルから 100 冊取得（\u003ccode\u003eSQL 1回\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"593:3-593:113\"\u003eそれぞれの本の著者を取得するため、Author テーブルを個別に検索（\u003ccode\u003eSQL 100回\u003c/code\u003e）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul data-sourcepos=\"594:3-594:109\"\u003e\n\u003cli data-sourcepos=\"594:3-594:109\"\u003e➔ \u003cstrong\u003e合計 101 回\u003c/strong\u003e の SQL が発行され、本の数だけ追加 SQL が発生して遅くなる。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"595:1-600:0\"\u003e\n\u003cstrong\u003e⭕ \u003ccode\u003eselect_related(\"author\")\u003c/code\u003e あり（効率的！）\u003c/strong\u003e:\n\u003cul data-sourcepos=\"596:3-600:0\"\u003e\n\u003cli data-sourcepos=\"596:3-596:61\"\u003eクエリ：\u003ccode\u003eBook.objects.select_related(\"author\").all()\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"597:3-597:112\"\u003e仕組み：データベース側で \u003cstrong\u003eSQL の JOIN を使って関連先をまとめて取得\u003c/strong\u003e する。\u003c/li\u003e\n\u003cli data-sourcepos=\"598:3-598:123\"\u003e➔ \u003cstrong\u003e合計 1 回の SQL で完了！\u003c/strong\u003e（後から \u003ccode\u003ebook.author\u003c/code\u003e を参照しても追加 SQL は発生しない）\u003c/li\u003e\n\u003cli data-sourcepos=\"599:3-600:0\"\u003e\u003cem\u003e※図では INNER JOIN と表記していますが、外部キーが nullable（\u003ccode\u003enull=True\u003c/code\u003e）な場合などは LEFT OUTER JOIN になることがあります。\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"601:1-602:0\"\u003e\n\u003ch3 data-sourcepos=\"603:1-603:104\"\u003e\n\u003cspan id=\"2-prefetch_relatedtagsmanytomany多対多を-2-回の-sql--python-側で紐付け\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-prefetch_relatedtagsmanytomany%E5%A4%9A%E5%AF%BE%E5%A4%9A%E3%82%92-2-%E5%9B%9E%E3%81%AE-sql--python-%E5%81%B4%E3%81%A7%E7%B4%90%E4%BB%98%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eprefetch_related(\"tags\")\u003c/code\u003e：ManyToMany（多対多）を 2 回の SQL ＋ Python 側で紐付け\u003c/h3\u003e\n\u003cp data-sourcepos=\"605:1-605:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fbad86436-22ba-4b5c-bd3d-78cbe20c3200.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9ac3a9f7bce5c10aedd2e75807ca3981\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fbad86436-22ba-4b5c-bd3d-78cbe20c3200.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9ac3a9f7bce5c10aedd2e75807ca3981\" alt=\"9e7ad70f-0487-473b-b29a-0bfe54a3ec95.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fbad86436-22ba-4b5c-bd3d-78cbe20c3200.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=159a99915ee99e59c52fa886ae6eedea 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/bad86436-22ba-4b5c-bd3d-78cbe20c3200.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"607:1-607:173\"\u003e「本（100冊）と、それぞれに付いている複数のタグ（Tag）を表示したい」ケース（中間テーブル \u003ccode\u003eBook_Tag\u003c/code\u003e を介す ManyToMany）です。\u003c/p\u003e\n\u003cul data-sourcepos=\"609:1-620:0\"\u003e\n\u003cli data-sourcepos=\"609:1-612:56\"\u003e\n\u003cstrong\u003e❌ \u003ccode\u003eprefetch_related(\"tags\")\u003c/code\u003e なし（N+1 問題）\u003c/strong\u003e:\n\u003col data-sourcepos=\"610:3-611:118\"\u003e\n\u003cli data-sourcepos=\"610:3-610:50\"\u003eまず Book を 100 冊取得（\u003ccode\u003eSQL 1回\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"611:3-611:118\"\u003eその後、各本のタグを取得するために中間テーブル経由で個別に検索（\u003ccode\u003eSQL 100回\u003c/code\u003e）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul data-sourcepos=\"612:3-612:56\"\u003e\n\u003cli data-sourcepos=\"612:3-612:56\"\u003e➔ \u003cstrong\u003e合計 101 回\u003c/strong\u003e の SQL が発行される。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"613:1-620:0\"\u003e\n\u003cstrong\u003e⭕ \u003ccode\u003eprefetch_related(\"tags\")\u003c/code\u003e あり（効率的！）\u003c/strong\u003e:\n\u003cul data-sourcepos=\"614:3-620:0\"\u003e\n\u003cli data-sourcepos=\"614:3-614:61\"\u003eクエリ：\u003ccode\u003eBook.objects.prefetch_related(\"tags\").all()\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"615:3-618:86\"\u003e仕組み：\u003cstrong\u003e合計 2 回の SQL\u003c/strong\u003e で完了し、\u003cstrong\u003eDjango が Python 側で紐付ける\u003c/strong\u003e。\n\u003col data-sourcepos=\"616:5-618:86\"\u003e\n\u003cli data-sourcepos=\"616:5-616:62\"\u003e1回目の SQL：Book を 100 冊取得（\u003ccode\u003eSQL 1回\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"617:5-617:132\"\u003e2回目の SQL：\u003ccode\u003eWHERE book_tag.book_id IN (1, 2, ..., 100)\u003c/code\u003e で 100 冊に関連するタグを一括取得（\u003ccode\u003eSQL 1回\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"618:5-618:86\"\u003eDjango がメモリ（Python 側）で各 Book と Tag を自動マッピング\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"619:3-620:0\"\u003e➔ \u003cstrong\u003e合計 2 回の SQL で完了！\u003c/strong\u003e（後から \u003ccode\u003ebook.tags.all()\u003c/code\u003e を参照しても基本的に追加 SQL なし）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"621:1-622:0\"\u003e\n\u003ch3 data-sourcepos=\"623:1-623:31\"\u003e\n\u003cspan id=\"3-使い分けのまとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 使い分けのまとめ\u003c/h3\u003e\n\u003ctable data-sourcepos=\"625:1-628:254\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"625:1-625:91\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"625:2-625:15\"\u003eメソッド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"625:17-625:36\"\u003e結合の仕組み\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"625:38-625:57\"\u003e発行クエリ数\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"625:59-625:90\"\u003e主な対象リレーション\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"627:1-627:148\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"627:2-627:23\"\u003e\u003cstrong\u003e\u003ccode\u003eselect_related\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"627:25-627:64\"\u003eDB 側で \u003cstrong\u003eSQL の \u003ccode\u003eJOIN\u003c/code\u003e\u003c/strong\u003e を実行\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"627:66-627:75\"\u003e\u003cstrong\u003e1回\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"627:77-627:147\"\u003e\n\u003cstrong\u003e1対1 / 多対1（ForeignKey）\u003c/strong\u003e\u003cbr\u003e（例: \u003ccode\u003eBook\u003c/code\u003e ➔ \u003ccode\u003eAuthor\u003c/code\u003e）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"628:1-628:254\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"628:2-628:25\"\u003e\u003cstrong\u003e\u003ccode\u003eprefetch_related\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"628:27-628:88\"\u003e\n\u003cstrong\u003e\u003ccode\u003eIN\u003c/code\u003e 句\u003c/strong\u003e で一括取得し、\u003cstrong\u003ePython 側で紐付け\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"628:90-628:141\"\u003e\n\u003cstrong\u003e2回\u003c/strong\u003e\u003cbr\u003e（リレーションごとに+1回）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"628:143-628:253\"\u003e\n\u003cstrong\u003e多対多（ManyToMany） / 1対多の逆参照\u003c/strong\u003e\u003cbr\u003e（例: \u003ccode\u003eBook\u003c/code\u003e ➔ \u003ccode\u003eTag\u003c/code\u003e、中間テーブル経由）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"631:1-631:114\"\u003e\n\u003cspan id=\"10-view-実装での落とし穴リレーション先が-none-の場合のガード500エラー防止\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10-view-%E5%AE%9F%E8%A3%85%E3%81%A7%E3%81%AE%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%85%88%E3%81%8C-none-%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AE%E3%82%AC%E3%83%BC%E3%83%89500%E3%82%A8%E3%83%A9%E3%83%BC%E9%98%B2%E6%AD%A2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. View 実装での落とし穴：リレーション先が None の場合のガード（500エラー防止）\u003c/h2\u003e\n\u003cp data-sourcepos=\"633:1-633:162\"\u003eここまでは「いかに正しく・効率よくデータを取得するか（パフォーマンス最適化）」という攻めの話をしてきました。\u003c/p\u003e\n\u003cp data-sourcepos=\"635:1-635:237\"\u003e次に、実務で View を書くときに極めて頻発する \u003cstrong\u003e「リレーション先が存在しない場合の例外（例外安全性・500エラー防止）」\u003c/strong\u003e という守りの重要ポイントを見ていきましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"637:1-637:303\"\u003eView の中では、「ログインユーザーの所属情報で絞り込む」「特定のリレーション先を自動でセットする」といった目的で、\u003cstrong\u003e外部キー（ForeignKey）や 1対1（OneToOneField）のリレーション先を参照するコード\u003c/strong\u003e を頻繁に書きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"639:1-639:228\"\u003eここでも、\u003cstrong\u003e「リレーション先が None（未設定）のケースを考慮していないと、AttributeError や DoesNotExist が飛んで 500 Internal Server Error になる」\u003c/strong\u003e という罠が潜んでいます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"641:1-641:75\"\u003e\n\u003cspan id=\"1-get_queryset-での絞り込み所属テナント参照\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-get_queryset-%E3%81%A7%E3%81%AE%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%81%BF%E6%89%80%E5%B1%9E%E3%83%86%E3%83%8A%E3%83%B3%E3%83%88%E5%8F%82%E7%85%A7\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003eget_queryset()\u003c/code\u003e での絞り込み（所属・テナント参照）\u003c/h3\u003e\n\u003cp data-sourcepos=\"642:1-642:119\"\u003eログインユーザー（\u003ccode\u003erequest.user\u003c/code\u003e）に紐づく会社や部署のデータだけに絞り込む実装です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"644:1-659:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eItemSerializer\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_queryset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# ❌ NG 例: 管理者や外部アカウントなど、company が None のユーザーで即座に 500 エラー！\n\u003c/span\u003e        \u003cspan class=\"c1\"\u003e# return Item.objects.filter(company_id=user.company.id)\n\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# ⭕ 安全な書き方: リレーション先が存在するか必ずガードする\n\u003c/span\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_authenticated\u003c/span\u003e \u003cspan class=\"ow\"\u003eor\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"nf\"\u003egetattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecompany\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003enone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 未所属なら空のクエリセットを返す\n\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eItem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecompany\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecompany\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"661:1-662:0\"\u003e\n\u003ch3 data-sourcepos=\"663:1-663:65\"\u003e\n\u003cspan id=\"2-perform_create-でのリレーション自動セット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-perform_create-%E3%81%A7%E3%81%AE%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E8%87%AA%E5%8B%95%E3%82%BB%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eperform_create()\u003c/code\u003e でのリレーション自動セット\u003c/h3\u003e\n\u003cp data-sourcepos=\"664:1-664:126\"\u003e登録時に、ログインユーザーの所属情報（部署など）を自動で紐付けて保存するケースです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"666:1-678:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eItemViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"bp\"\u003e...\u003c/span\u003e\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eperform_create\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# ❌ NG 例: user.department が None だと AttributeError でクラッシュ\n\u003c/span\u003e        \u003cspan class=\"c1\"\u003e# serializer.save(created_by=user, department_code=user.department.code)\n\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# ⭕ 安全な書き方: 安全に取得するか None を許容する\n\u003c/span\u003e        \u003cspan class=\"n\"\u003edepartment\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003egetattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edepartment\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecreated_by\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edepartment\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003edepartment\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"680:1-681:0\"\u003e\n\u003ch3 data-sourcepos=\"682:1-682:85\"\u003e\n\u003cspan id=\"3-onetoonefield-の逆参照none-ではなく例外が飛ぶ特殊な罠\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-onetoonefield-%E3%81%AE%E9%80%86%E5%8F%82%E7%85%A7none-%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E4%BE%8B%E5%A4%96%E3%81%8C%E9%A3%9B%E3%81%B6%E7%89%B9%E6%AE%8A%E3%81%AA%E7%BD%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. \u003ccode\u003eOneToOneField\u003c/code\u003e の逆参照（None ではなく例外が飛ぶ特殊な罠）\u003c/h3\u003e\n\u003cp data-sourcepos=\"683:1-683:164\"\u003e1対1（\u003ccode\u003eOneToOneField\u003c/code\u003e）のリレーション先が存在しない場合、\u003cstrong\u003e\u003ccode\u003eNone\u003c/code\u003e ではなく \u003ccode\u003eDoesNotExist\u003c/code\u003e 例外が発生してクラッシュします\u003c/strong\u003e。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"685:1-695:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# User モデルと 1対1 の Profile モデルがある場合\n\u003c/span\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_user_profile\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ❌ Profile レコードが存在しない場合、User.profile.RelatedObjectDoesNotExist が発生！\n\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# return user.profile.theme\n\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ⭕ hasattr で安全にチェックする\n\u003c/span\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nf\"\u003ehasattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eprofile\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eprofile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etheme\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edefault\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"697:1-700:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"698:1-699:331\"\u003e\u003cstrong\u003e💡 実務の鉄則：View でも「リレーション先は常に None の可能性がある」と疑う\u003c/strong\u003e\u003cbr\u003e\n認証ユーザーであっても、システム管理者（スーパーユーザー）や移行直後のアカウントなどは「会社や部署が NULL」であるケースが多々あります。ガード節（\u003ccode\u003egetattr\u003c/code\u003e や \u003ccode\u003ehasattr\u003c/code\u003e）を習慣づけておくことで、本番障害を未然に防ぐことができます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"703:1-703:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"705:1-705:200\"\u003e本講では、DRF の司令塔である「View」の構造と、実務で必須となるライフサイクル・パフォーマンス・例外処理のテクニックを体系的に学びました。\u003c/p\u003e\n\u003cul data-sourcepos=\"707:1-728:0\"\u003e\n\u003cli data-sourcepos=\"707:1-709:102\"\u003e\n\u003cstrong\u003eView の役割と明確な責務境界\u003c/strong\u003e:\n\u003cul data-sourcepos=\"708:3-709:102\"\u003e\n\u003cli data-sourcepos=\"708:3-708:99\"\u003eView は「どのデータを取得するか・誰がアクセスできるか（司令塔）」\u003c/li\u003e\n\u003cli data-sourcepos=\"709:3-709:102\"\u003eSerializer は「入力値検証・データ変換・表示項目定義（検品・翻訳係）」\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"710:1-712:84\"\u003e\n\u003cstrong\u003e3大アプローチと使い分け\u003c/strong\u003e:\n\u003cul data-sourcepos=\"711:3-712:84\"\u003e\n\u003cli data-sourcepos=\"711:3-711:82\"\u003e自由度最優先なら \u003ccode\u003eAPIView\u003c/code\u003e、特定機能特化なら \u003ccode\u003eGenericAPIView\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"712:3-712:84\"\u003e標準的な CRUD を高速構築するなら \u003ccode\u003eModelViewSet\u003c/code\u003e ＋ \u003ccode\u003eDefaultRouter\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"713:1-714:114\"\u003e\n\u003cstrong\u003eRouter と ViewSet の 6 大アクション\u003c/strong\u003e:\n\u003cul data-sourcepos=\"714:3-714:114\"\u003e\n\u003cli data-sourcepos=\"714:3-714:114\"\u003e\n\u003ccode\u003elist\u003c/code\u003e, \u003ccode\u003eretrieve\u003c/code\u003e, \u003ccode\u003ecreate\u003c/code\u003e, \u003ccode\u003eupdate\u003c/code\u003e, \u003ccode\u003epartial_update\u003c/code\u003e, \u003ccode\u003edestroy\u003c/code\u003e の実行時マッピングを把握する\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"715:1-717:121\"\u003e\n\u003cstrong\u003e\u003ccode\u003equeryset\u003c/code\u003e と \u003ccode\u003eget_queryset()\u003c/code\u003e の違い\u003c/strong\u003e:\n\u003cul data-sourcepos=\"716:3-717:121\"\u003e\n\u003cli data-sourcepos=\"716:3-716:69\"\u003e基本の固定クエリセットは \u003ccode\u003equeryset\u003c/code\u003e（遅延評価）\u003c/li\u003e\n\u003cli data-sourcepos=\"717:3-717:121\"\u003eリクエストやログインユーザーに依存する動的クエリは \u003ccode\u003eget_queryset()\u003c/code\u003e をオーバーライド\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"718:1-720:82\"\u003e\n\u003cstrong\u003eViewSet のライフサイクルとオーバーライド\u003c/strong\u003e:\n\u003cul data-sourcepos=\"719:3-720:82\"\u003e\n\u003cli data-sourcepos=\"719:3-719:114\"\u003ePOST 処理全体を変えるなら \u003ccode\u003ecreate()\u003c/code\u003e、保存時の追加情報注入だけなら \u003ccode\u003eperform_create()\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"720:3-720:82\"\u003e削除処理の論理削除化には \u003ccode\u003edestroy()\u003c/code\u003e または \u003ccode\u003eperform_destroy()\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"721:1-722:99\"\u003e\n\u003cstrong\u003e\u003ccode\u003e@action\u003c/code\u003e による独自エンドポイント\u003c/strong\u003e:\n\u003cul data-sourcepos=\"722:3-722:99\"\u003e\n\u003cli data-sourcepos=\"722:3-722:99\"\u003e全体（一覧）レベルなら \u003ccode\u003edetail=False\u003c/code\u003e、特定レコード対象なら \u003ccode\u003edetail=True\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"723:1-725:192\"\u003e\n\u003cstrong\u003eリレーション設計と N+1 問題の完全撃破\u003c/strong\u003e:\n\u003cul data-sourcepos=\"724:3-725:192\"\u003e\n\u003cli data-sourcepos=\"724:3-724:87\"\u003e既存テーブル連携には \u003ccode\u003emanaged=False\u003c/code\u003e、\u003ccode\u003edb_column\u003c/code\u003e、\u003ccode\u003eto_field\u003c/code\u003e を駆使\u003c/li\u003e\n\u003cli data-sourcepos=\"725:3-725:192\"\u003eSerializer が関連先を参照する場合、View 側で \u003ccode\u003eselect_related\u003c/code\u003e（ForeignKey / 1対1）や \u003ccode\u003eprefetch_related\u003c/code\u003e（ManyToMany / 逆参照）を指定してクエリ爆発を防ぐ\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"726:1-728:0\"\u003e\n\u003cstrong\u003eView でのリレーション安全性\u003c/strong\u003e:\n\u003cul data-sourcepos=\"727:3-728:0\"\u003e\n\u003cli data-sourcepos=\"727:3-728:0\"\u003e外部キーや OneToOneField を参照する際は \u003ccode\u003egetattr\u003c/code\u003e や \u003ccode\u003ehasattr\u003c/code\u003e で None / DoesNotExist をガードし、500 エラーを未然に防止する\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"729:1-730:185\"\u003eこれで、Model・Serializer・View の DRF 3大コア要素がすべて揃い、美しく堅牢な REST API を自由自在に設計・構築できるようになりました！\u003cbr\u003e\n次回は、API を不正アクセスから守り実務開発で不可欠となる \u003cstrong\u003e「認証・権限（Authentication \u0026amp; Permissions）」\u003c/strong\u003e の世界へ進んでいきましょう！\u003c/p\u003e\n\u003chr data-sourcepos=\"732:1-733:0\"\u003e\n\u003cp data-sourcepos=\"734:1-734:166\"\u003e\u003cstrong\u003e\u003ca href=\"/items/04b2c3524ec379961d82\"\u003e← 第七講：シリアライザのバリデーション完全攻略\u003c/a\u003e | \u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003eまとめページに戻る\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n","body":"\n\u003e **Lecture 08: Deep Dive into DRF Views — Request-Response Lifecycle \u0026 Class Hierarchy**\n\n[← DRF × Vue.js 実践講座：まとめページに戻る](/items/4250ea04fb61f5bc4f0d) | [← 第七講：シリアライザのバリデーション完全攻略](/items/04b2c3524ec379961d82)\n\n## はじめに：DRFにおける「View」の役割と責務境界\n\nこれまでの講座では、データベースの構造を定義する **「Model（第四講・第五講）」** と、データの入出力変換やバリデーションを担う **「Serializer（第六講・第七講）」** を学んできました。\n\nでは、今回学ぶ **「View（ビュー）」** とは一体何者でしょうか？\n\n一言で言えば、View は **「Model と Serializer を束ねてリクエストからレスポンスまでを取り仕切る『総合窓口・現場監督（司令塔）』」** です。\n\n```text\n【DRFの3大要素と役割分担】\n・Model      : データベースの設計図・倉庫（データの保持）\n・Serializer : 翻訳係・検品所（Model/Pythonオブジェクト ⇄ Pythonの辞書等の表現への変換・バリデーション）\n・View       : 総合窓口・現場監督（リクエスト受付 ➔ 認証 ➔ 処理の実行 ➔ レスポンス返却）\n```\n\n### View が担う4大責任\n1. **リクエストの受付と振り分け**:  \n   クライアントから届いた HTTP メソッド（GET / POST / PUT / PATCH / DELETE）を解釈し、適切な処理へルーティングします。\n2. **認証・権限（セキュリティ）のチェック**:  \n   「ログインしているユーザーか？（認証）」「このデータを閲覧・編集する権限があるか？（認可）」を判定し、不正アクセスを遮断（401 / 403）します。\n3. **Model と Serializer の連携・統括**:  \n   シリアライザに入力データを検証させ、モデルを通じてデータベースへ保存・取得させます。\n4. **HTTP レスポンスの返却**:  \n   処理結果を適切なステータスコード（200 OK、201 Created、400 Bad Request など）とともにクライアントへ返します。\n\n---\n\n### 【超重要】View と Serializer の明確な責務境界\n\nコードを書き始めると、「この処理は View に書くべきか？ それとも Serializer に書くべきか？」で誰もが一度は迷います。混乱を防ぐために、両者の境界線を明確に整理しておきましょう。\n\n| 責務・判断項目 | View がやること（司令塔） | Serializer がやること（翻訳・検品係） |\n| :--- | :--- | :--- |\n| **データの取得** | **どのレコードをDBから引いてくるか**\u003cbr\u003e（絞り込み、検索、ソート、ページネーション） | 関与しない（渡されたインスタンスを処理するだけ） |\n| **アクセス制御** | **誰がアクセスできるか**\u003cbr\u003e（ログイン認証、管理者権限チェック） | 関与しない |\n| **シリアライザ選択** | **どのアクションでどのシリアライザを使うか**\u003cbr\u003e（一覧用・詳細用・登録用の出し分け） | 関与しない |\n| **データの検証** | 検証をシリアライザに命令する（`is_valid()` を呼ぶ） | **入力値の妥当性を検証する**\u003cbr\u003e（型チェック、文字数、業務ルール検証） |\n| **データ変換** | 変換後のデータを受け取ってレスポンス（`Response`）にする | **Model/Pythonオブジェクト ⇄ Pythonの辞書等の表現への変換**\u003cbr\u003e（日付フォーマット、数値の整形など） |\n| **レスポンス項目** | 関与しない | **どのフィールドをクライアントに見せるか**\u003cbr\u003e（`fields`、`SerializerMethodField`、`source`） |\n| **保存時の補完** | **リクエスト情報から不足データを補う**\u003cbr\u003e（ログインユーザー `created_by=request.user` など） | 渡されたデータを元にモデルインスタンスを `save()` する |\n\n:::note info\n**💡 Serializer と Renderer の役割分担**  \n厳密には、Serializer 自身が JSON 文字列を生成しているわけではありません。  \n1. **Serializer**: Model / Python オブジェクト ⇄ **Python のプリミティブ型（dict, list, str, int 等）** の相互変換  \n2. **Renderer**: Python の辞書表現 ➔ **JSON 文字列**（View が `Response()` を返す際に内部で実行）  \nという 2 段階のパイプラインで処理されています。\n:::\n\n\u003e 💡 **迷ったときの判断基準**  \n\u003e * 「**HTTP リクエスト情報（誰が・何を求めているか）**」に依存する処理 ➔ **View**\n\u003e * 「**データそのものの構造・フォーマット・整合性**」に依存する処理 ➔ **Serializer**\n\n\n## 1. リクエストからレスポンスまでの5大CRUD処理フロー\n\n![263d85f3-0826-4b4a-a60a-bfa950d76f11.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/26b261c3-89ab-4f92-9d7d-34ffcf965233.avif)\n\nクライアントから HTTP リクエストが届いてからレスポンスが返るまで、サーバー内部で「Model（DB）」と「Serializer」がどの順序で関与しているかを表した図です。\n\n画像を見る上で押さえておくべきポイントは以下の2点です。\n\n### ① Serializer の使い分け（入力用・出力用・なし）\n* **登録・更新（POST / PUT / PATCH）**:  \n  クライアントから届いた生データを検証・保存するため、**入力用 Serializer（`is_valid()`）** を通します。バリデーションに失敗した場合は即座に **400 Bad Request** を返します。\n* **取得（GET 詳細 / 一覧）**:  \n  DB から取り出した Model オブジェクトを JSON に変換するため、**出力用 Serializer** を通します。データの検証は不要なため、`is_valid()` は実行しません（一覧取得時は `many=True` を指定）。\n* **削除（DELETE）**:  \n  対象レコードを検索して削除するだけなので、入力ボディが存在せず、**原則として Serializer は介しません**。\n\n### ② 404 と 400 の発生タイミングの違い\n* **404 Not Found**:  \n  pk（ID）を伴う処理（更新・削除・詳細取得）において、**DB に対象レコードが存在しない場合**に発生します。\n* **400 Bad Request**:  \n  レコード取得後の **Serializer のバリデーションに引っかかった場合**に発生します。  \n  「まず pk でモデルを取得し、見つかった後に入力検証を行う」という順序が共通しています。\n\n\n## 2. DRF View のクラス階層と継承関係\n\n![1f407066-90dc-48e5-a9c2-093d6b0e90de.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/625c5fea-626c-4d27-b97e-797b5bb3ee88.avif)\n\nDjango 標準の View から DRF の ViewSet に至るまでのクラス継承ツリーです。  \n下層に進むにつれて抽象化が進み、**「自分で書くべき定型コード（ボイラープレート）」が減る** 構造になっています。\n\n### クラスの進化と機能の追加\n1. **`Django View`（基本）**:  \n   HTTP メソッド（GET / POST など）に応じたメソッドへ処理を振り分ける（ディスパッチする）だけの最小限のクラス。\n2. **`DRF APIView`（REST API 基盤）**:  \n   `Django View` を継承。JSON の自動パース（`request.data`）、JSON レスポンス変換（`Response`）、認証・権限（Permissions）、統一されたエラーハンドリングが加わります。\n3. **`DRF GenericAPIView`（Model / Serializer 連携）**:  \n   `APIView` を継承。`queryset` と `serializer_class` を属性として持たせることで、DB からのレコード取得（`get_object()`, `get_queryset()`）やシリアライザ生成（`get_serializer()`）の共通ロジックを提供します。\n\n### 下流の2大分岐：汎用 API View vs ViewSet\n\n`GenericAPIView` をベースとして、用途に合わせて2つのアプローチに分かれます。\n\n| 分類 | 汎用 API View（特化型） | ViewSet（統合型） |\n| :--- | :--- | :--- |\n| **設計思想** | **特定の処理・エンドポイントに特化** | **リソースの CRUD 全体を1クラスに集約** |\n| **主なクラス** | `CreateAPIView`（作成）\u003cbr\u003e`ListAPIView`（一覧）\u003cbr\u003e`RetrieveAPIView`（詳細）\u003cbr\u003e`UpdateAPIView`（更新）\u003cbr\u003e`DestroyAPIView`（削除） | `ModelViewSet`（CRUD全般）\u003cbr\u003e`ReadOnlyModelViewSet`（一覧・詳細のみ） |\n| **URL の設定** | `path()` でエンドポイントごとに手動定義 | `Router`（DefaultRouter）で複数 URL を自動生成 |\n| **使い分け** | 「会員登録専用」「公開一覧のみ」など、**機能を限定して公開したい場合** | **標準的な CRUD API を一式まとめて素早く構築したい場合** |\n\n#### ▼ 汎用 API View（Concrete View）の対応操作一覧\n\n| クラス | 一覧 GET | 詳細 GET | 登録 POST | 更新 PUT | 一部更新 PATCH | 削除 DELETE |\n| :--- | :---: | :---: | :---: | :---: | :---: | :---: |\n| **`ListAPIView`** | ○ | — | — | — | — | — |\n| **`RetrieveAPIView`** | — | ○ | — | — | — | — |\n| **`ListCreateAPIView`** | ○ | — | ○ | — | — | — |\n| **`CreateAPIView`** | — | — | ○ | — | — | — |\n| **`UpdateAPIView`** | — | — | — | ○ | ○ | — |\n| **`DestroyAPIView`** | — | — | — | — | — | ○ |\n| **`RetrieveUpdateAPIView`** | — | ○ | — | ○ | ○ | — |\n| **`RetrieveDestroyAPIView`** | — | ○ | — | — | — | ○ |\n| **`RetrieveUpdateDestroyAPIView`** | — | ○ | — | ○ | ○ | ○ |\n\n\n## 3. 基本的なコード比較と使い分け（APIView / GenericAPIView / ModelViewSet）\n\n同じ「メッセージ一覧・作成」を扱う場合のコード量の違いです。\n\n### ① `APIView`（すべて手動で記述する基本形）\nDB取得、シリアライザへの受け渡し、バリデーション、レスポンス返却をすべて明示的に書きます。\n\n```python\nfrom rest_framework import status\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\nfrom .models import Message\nfrom .serializers import MessageSerializer\n\n\nclass MessageListCreateAPIView(APIView):\n    def get(self, request):\n        messages = Message.objects.all()\n        serializer = MessageSerializer(messages, many=True)\n        return Response(serializer.data, status=status.HTTP_200_OK)\n\n    def post(self, request):\n        serializer = MessageSerializer(data=request.data)\n        serializer.is_valid(raise_exception=True)\n        serializer.save()\n        return Response(serializer.data, status=status.HTTP_201_CREATED)\n```\n\n### ② `GenericAPIView`（汎用ビューで定型コードを削減）\n`queryset` と `serializer_class` を指定するだけで、一覧取得（`GET`）と新規作成（`POST`）の定型処理を自動化できます。\n\n```python\nfrom rest_framework import generics\nfrom .models import Message\nfrom .serializers import MessageSerializer\n\n\nclass MessageListCreateView(generics.ListCreateAPIView):\n    queryset = Message.objects.all()\n    serializer_class = MessageSerializer\n```\n\n### ③ `ModelViewSet`（CRUD全般を1クラスに統合）\n一覧・詳細取得・新規作成・更新・削除のすべてを、わずか2行の設定だけで一括提供します。\n\n```python\nfrom rest_framework import viewsets\nfrom .models import Message\nfrom .serializers import MessageSerializer\n\n\nclass MessageViewSet(viewsets.ModelViewSet):\n    queryset = Message.objects.all()\n    serializer_class = MessageSerializer\n```\n\n#### ▼ ルーティング（`urls.py`）の違い\n* **`APIView` / `GenericAPIView`**:  \n  `path('messages/', MessageListCreateView.as_view())` のように URL ごとに手動登録する。\n* **`ModelViewSet`**:  \n  `router = DefaultRouter()` ➔ `router.register('messages', MessageViewSet)` で複数 URL が自動生成される。\n\n### 3大アプローチの観点別比較\n\n| 観点 | APIView | 汎用APIView | ModelViewSet |\n| :--- | :---: | :---: | :---: |\n| **簡単さ** | △ | ○ | ◎ |\n| **コード量** | 多い | 中くらい | 少ない |\n| **自由度** | ◎ | ○ | △ |\n| **CRUD自動化** | △ | ○ | ◎ |\n| **ルーティング自動化** | × | × | ◎ |\n| **初心者向け** | △ | ○ | ◎ |\n| **複雑な処理向け** | ◎ | ○ | △ |\n\n\n## 4. Router と ViewSet の CRUD メソッド対応\n\nModelViewSet を使うと「わずか数行で CRUD が完成する」と言われますが、**「URL から届いたリクエストが、ViewSet のどのメソッドに届き、どうやって Serializer を呼んでいるのか」** という実行時の繋がりを理解していないと、カスタマイズで行き詰まります。\n\n### URL と ViewSet メソッドの対応マップ\n\nRouter に `router.register('items', ItemViewSet)` と登録したとき、自動生成されるエンドポイントと ViewSet のメソッドは次のように 1 対 1 でマッピングされます。\n\n| HTTPメソッド | URL パターン | ViewSet の実行メソッド | 処理内容 |\n| :--- | :--- | :--- | :--- |\n| **`GET`** | `/items/` | **`list(request)`** | 一覧取得（複数件） |\n| **`POST`** | `/items/` | **`create(request)`** | 新規作成（1件） |\n| **`GET`** | `/items/{pk}/` | **`retrieve(request, pk=...)`** | 詳細取得（1件） |\n| **`PUT`** | `/items/{pk}/` | **`update(request, pk=...)`** | 全項目更新（1件） |\n| **`PATCH`** | `/items/{pk}/` | **`partial_update(request, pk=...)`** | 一部項目更新（1件） |\n| **`DELETE`** | `/items/{pk}/` | **`destroy(request, pk=...)`** | 削除（1件） |\n\n### 実行時の処理の流れ（内部で起きていること）\n\nクライアントから `GET /items/` が届いた場合、裏側では以下のように処理がリレーされています。\n\n```text\n1. Client Request: GET /items/\n        ↓\n2. urls.py (Router)\n   「/items/ への GET だから、ItemViewSet の list() メソッドを呼び出そう」\n        ↓\n3. ItemViewSet.list()\n   「まず対象のデータを DB から取得しよう」 ➔ self.get_queryset()\n        ↓\n4. Serializer による変換\n   「取得したクエリセットを出力用 JSON に変換しよう」 ➔ self.get_serializer(queryset, many=True)\n        ↓\n5. Response 返却\n   Response(serializer.data, status=200) をクライアントに返す\n```\n\nこのように、Router はリクエストを解釈して **`list` や `create` といったアクションメソッド** に橋渡しし、各メソッドが内部で **`get_queryset()`** や **`get_serializer()`** を呼び出す構造になっています。\n\n\n## 5. `queryset` と `get_queryset()` の違い\n\nViewSet のコードを見ていると、ある場所では `queryset = ...` というクラス変数が使われ、別の場所では `def get_queryset(self):` というメソッドが登場します。\n\n「なぜ急にプロパティから関数に変わるのか？」と疑問に思う初心者が非常に多いポイントです。\n\n```text\n【2つの決定的な違い】\n・queryset       : クラス属性として基本となる QuerySet を定義\n・get_queryset() : リクエストごとに取得対象を動的に決定できる\n```\n\n### 1. `queryset` 属性（基本となる固定の定義）\n「ベースとして全件を対象にする」「誰がアクセスしても同じデータを返す」というシンプルなケースで使います。\n\n```python\nclass ItemViewSet(ModelViewSet):\n    # 基本となる固定の QuerySet\n    queryset = Item.objects.all()\n    serializer_class = ItemSerializer\n```\n\n:::note info\n**💡 QuerySet は遅延評価（Lazy Evaluation）**  \n`queryset = Item.objects.all()` と定義しても、その瞬間に SQL が実行されるわけではありません。実際にデータへアクセス（反復処理やシリアライズなど）されたタイミングで初めてクエリが発行されます。\n:::\n\n### 2. `get_queryset()` メソッド（動的・リクエスト依存）\n「ログインユーザー自身のデータだけに絞り込みたい」「URL のクエリパラメータ（`?category=news`）に応じて抽出したい」など、**リクエスト（`self.request`）の内容に応じて動的に条件を変えたい場合** は、`get_queryset()` メソッドを上書き（オーバーライド）します。\n\n```python\nclass ItemViewSet(ModelViewSet):\n    serializer_class = ItemSerializer\n\n    def get_queryset(self):\n        # ログインユーザーの所属会社に紐づくアイテムだけに動的フィルタリング！\n        user = self.request.user\n        return Item.objects.filter(company=user.company)\n```\n\n:::note info\n**💡 queryset の省略と Router の basename**  \n`queryset` 属性を省略して `get_queryset()` だけを定義することも可能です。ただしその場合、Router がモデル名を自動推論できず、URL パターンの名前（`basename`）生成に失敗することがあります。その際は以下のように `basename` を明示的に指定します。\n\n```python\n# queryset を省略する場合は basename を指定する\nrouter.register(\"items\", ItemViewSet, basename=\"item\")\n```\n:::\n\n\n## 6. ViewSet でよく上書きするメソッドとライフサイクル\n\nModelViewSet は便利な反面、「保存時にログインユーザーを自動で紐付けたい」「一覧画面と詳細画面でシリアライザを切り替えたい」といった実務要件に直面したとき、どのメソッドを書き換えるべきか知っておく必要があります。\n\n### `create()` と `perform_create()` の違い\n\n一番の疑問になりやすいのが「`create()` と `perform_create()` は何が違うのか？」です。DRF の内部コードを覗くと、その関係は一目瞭然です。\n\n```python\n# 【DRF内部の CreateModelMixin.create の実装イメージ】\ndef create(self, request, *args, **kwargs):\n    # 1. リクエストデータからシリアライザを生成\n    serializer = self.get_serializer(data=request.data)\n\n    # 2. バリデーション実行（エラーなら 400 を投げる）\n    serializer.is_valid(raise_exception=True)\n\n    # 3. ★ここで perform_create を呼び出す！\n    self.perform_create(serializer)\n\n    # 4. 成功レスポンス（201 Created）を返却\n    headers = self.get_success_headers(serializer.data)\n    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)\n\n\ndef perform_create(self, serializer):\n    # デフォルトは単に save() を呼ぶだけ\n    serializer.save()\n```\n\n処理の流れを構造化すると以下のようになります。\n\n```text\ncreate()\n│\n├─ Serializerを作る (get_serializer)\n├─ is_valid()\n├─ perform_create()\n│      └─ serializer.save()\n│\n└─ Responseを作る (Response)\n```\n\nつまり、\n* **`create()`**: バリデーションからレスポンス作成までの「処理全体」を取り仕切るメソッド\n* **`perform_create()`**: バリデーション成功後、**モデルを保存するタイミングの処理をカスタマイズするためのフックメソッド**\n\nこのような保存時の追加情報だけを補いたい場合は、`create()` 全体ではなく **`perform_create()` をオーバーライドするのが基本** です。\n\n| 変更したいもの | 上書きするメソッド | 主なユースケース |\n| :--- | :--- | :--- |\n| **POST 処理全体** | **`create()`** | ・レスポンス形式やステータスコードを変えたい\u003cbr\u003e・保存前後の処理フローそのものを変えたい |\n| **保存時の処理だけ** | **`perform_create()`** | ・`created_by` や `company` を自動設定したい\u003cbr\u003e・保存時にリクエスト由来の追加情報を渡したい |\n\n---\n\n### 実務でよく上書きする代表的なメソッド\n\n| 上書きするメソッド | 主な役割とユースケース |\n| :--- | :--- |\n| **`get_queryset(self)`** | **取得レコードの動的絞り込み**\u003cbr\u003eログインユーザーの所属テナント、公開中フラグによるフィルタリングなど |\n| **`get_serializer_class(self)`** | **アクションに応じたシリアライザの切り替え**\u003cbr\u003e一覧表示用（軽量シリアライザ）と詳細・更新用（全項目シリアライザ）の出し分け |\n| **`perform_create(self, serializer)`** | **新規保存時の追加情報注入**\u003cbr\u003e`serializer.save(created_by=self.request.user)` のようにリクエスト情報を付与 |\n| **`perform_update(self, serializer)`** | **更新保存時の追加情報注入・監査ログ**\u003cbr\u003e`serializer.save(updated_by=self.request.user)` や更新ログの出力 |\n| **`destroy(self, request, ...)`**\u003cbr\u003eまたは `perform_destroy` | **論理削除へのカスタマイズ**\u003cbr\u003eDB から物理削除せず、`instance.is_deleted = True` にして保存する |\n\n#### ▼ 実務でよくあるオーバーライドの実装例\n\n```python\nclass ArticleViewSet(ModelViewSet):\n    queryset = Article.objects.all()\n\n    # 1. アクションごとにシリアライザを切り替える\n    def get_serializer_class(self):\n        if self.action == \"list\":\n            return ArticleListSerializer  # 一覧は軽量版\n        return ArticleDetailSerializer  # 詳細・作成・更新は完全版\n\n    # 2. ログインユーザーに応じて取得データを絞り込む\n    def get_queryset(self):\n        return Article.objects.filter(author=self.request.user)\n\n    # 3. 新規作成時に author フィールドへログインユーザーを注入\n    def perform_create(self, serializer):\n        serializer.save(author=self.request.user)\n\n    # 4. 削除を物理削除ではなく論理削除にする\n    def perform_destroy(self, instance):\n        instance.is_deleted = True\n        instance.save()\n```\n\n\n## 7. デコレータの活用と ViewSet の `@action`\n\nPython / Django / DRF では、クラスや関数の直前に `@` を付ける「デコレータ」を使って、追加機能を宣言的に付与します。\n\n### 代表的な3つのデコレータ\n1. **`@action`（DRF ViewSet）**:  \n   ViewSet の中に標準 CRUD 以外の**追加 API（独自エンドポイント）** を作成する。\n2. **`@api_view` ＋ `@permission_classes`（関数ベース View）**:  \n   通常の Python 関数を REST API 化し、アクセス権限（`AllowAny` など）や CSRF Cookie 付与（`@ensure_csrf_cookie`）を設定する。\n3. **`@admin.register`（Django Admin）**:  \n   モデルを Django 管理画面に紐付け、表示項目（`list_display`）や検索・絞り込みを一括設定する。\n\n---\n\n### ViewSet に独自処理を追加する `@action`\n\nModelViewSet は標準で CRUD に対応する **6 つのアクション（`list`, `retrieve`, `create`, `update`, `partial_update`, `destroy`）** を提供しますが、実務では「有効なデータだけの一覧」「特定レコードへのお気に入り登録」など、**標準 CRUD に収まらない処理** が必ず発生します。\n\nそれを解決するのが `@action` デコレータです。\n\n#### 1. 特徴と仕組み\n* **Router との連携**: ViewSet のメソッドに `@action` を付けるだけで、Router が URL を自動生成してくれる。\n* **HTTP メソッドの指定**: `methods=['get']` や `methods=['post']` で受付メソッドを制限できる。\n* **ViewSet 専用**: 単体の関数や APIView では使えず、ViewSet のメソッドとして定義する。\n\n#### 2. `detail=True` と `detail=False` の使い分け（最重要）\n\n| 設定 | 対象 | URL の形 | 主な用途・具体例 |\n| :--- | :--- | :--- | :--- |\n| **`detail=False`** | **全体・一覧レベル**\u003cbr\u003e（特定の1件ではない） | `GET /staff/active/`\u003cbr\u003e（URL に ID が**入らない**） | ・有効なデータのみの一覧取得\u003cbr\u003e・全体の集計・統計データの取得\u003cbr\u003e・一括データ処理 |\n| **`detail=True`** | **個別レベル**\u003cbr\u003e（特定の1件に対する処理） | `POST /staff/{id}/favorite/`\u003cbr\u003e（URL に ID が**入る**） | ・特定レコードへのお気に入り登録\u003cbr\u003e・特定ユーザーのパスワード変更\u003cbr\u003e・単一アイテムのステータス更新 |\n\n:::note info\n**💡 `detail=True` の便利なポイント**  \nメソッド内で `self.get_object()` を呼び出すだけで、URL に含まれる `{id}` の Model インスタンスを自動取得できます（見つからない場合は自動で 404 Not Found を返却）。\n:::\n\n```python\nclass StaffViewSet(ModelViewSet):\n    queryset = Staff.objects.all()\n    serializer_class = StaffSerializer\n\n    # 全体に対する独自エンドポイント: GET /staff/active/\n    @action(detail=False, methods=[\"get\"])\n    def active(self, request):\n        active_staff = self.get_queryset().filter(is_active=True)\n        serializer = self.get_serializer(active_staff, many=True)\n        return Response(serializer.data)\n\n    # 特定の1件に対する独自エンドポイント: POST /staff/{pk}/favorite/\n    @action(detail=True, methods=[\"post\"])\n    def favorite(self, request, pk=None):\n        staff = self.get_object()  # URLのpkから対象インスタンスを自動取得\n        staff.is_favorite = True\n        staff.save()\n        return Response({\"status\": \"favorited\"})\n```\n\n\n## 8. 実務での重要パターン：リレーション先の取得と既存テーブル連携\n\n実務の API 開発では、単一テーブルだけでなく「外部キー（ForeignKey）先のデータをどう効率よくレスポンスに含めるか」や「既存のデータベース（レガシーDB）とどう連携するか」が頻出します。\n\n製品（`Product`）と、それに紐づく品目（`Item`）を例に見ていきましょう。\n\n### 1. `models.py`（モデル定義）\n\n```python\nfrom django.db import models\n\n\nclass Product(models.Model):\n    id = models.AutoField(primary_key=True)\n    product_code = models.CharField(max_length=20, unique=True)\n    name = models.CharField(max_length=100)\n\n    class Meta:\n        db_table = \"product\"\n        managed = False\n\n\nclass Item(models.Model):\n    product = models.ForeignKey(\n        Product,\n        to_field=\"product_code\",  # 主キー(id)ではなく、業務コード(product_code)を参照\n        db_column=\"product_code\",\n        on_delete=models.DO_NOTHING,\n        related_name=\"item_list\",\n    )\n    item_code = models.CharField(max_length=20, db_column=\"item_code\")\n    item_name = models.CharField(max_length=100, db_column=\"item_name\")\n\n    class Meta:\n        db_table = \"item\"\n        managed = False\n```\n\n### 2. `serializers.py`（シリアライザ定義）\n\n```python\nfrom rest_framework import serializers\nfrom .models import Item\n\n\nclass ItemSerializer(serializers.ModelSerializer):\n    # 親モデル(Product)の名前を取り出してフラットに含める\n    product_name = serializers.CharField(\n        source=\"product.name\",\n        read_only=True,\n    )\n\n    class Meta:\n        model = Item\n        fields = [\n            \"product\",\n            \"item_code\",\n            \"item_name\",\n            \"product_name\",\n        ]\n```\n\n### 3. `views.py`（ViewSet 定義）\n\n```python\nfrom rest_framework.viewsets import ModelViewSet\nfrom .models import Item\nfrom .serializers import ItemSerializer\n\n\nclass ItemViewSet(ModelViewSet):\n    # 親モデルを JOIN して一括取得\n    queryset = Item.objects.select_related(\"product\").all()\n    serializer_class = ItemSerializer\n```\n\n---\n\n### 実務で頻出する4つの重要設定\n\n上記のコードで登場した 4 つの設定は、実務（特に既存データベース連携や API レスポンス設計）で必須となる重要テクニックです。\n\n| 設定項目 | 指定する場所 | 役割と用途 |\n| :--- | :--- | :--- |\n| **`managed = False`** | モデルの `class Meta` | **Django migrations にテーブル構造を管理させない**\u003cbr\u003eマイグレーションによるテーブル作成・変更・削除の対象から外す設定。なお、Django ORM からの検索（`all()`, `filter()`）や登録・更新・削除（`save()`, `delete()`）は通常通り実行可能。 |\n| **`db_column`** | モデルのフィールド引数 | **実際の DB カラム名を明示的にマッピングする**\u003cbr\u003ePython 側の変数名（`product` や `item_code`）と、実際の DB カラム名（`product_code` 等）が異なる場合に紐付ける。 |\n| **`to_field`** | `ForeignKey` の引数 | **主キーではなく、業務コード（UNIQUE列）を外部キーとして参照する**\u003cbr\u003eForeignKey はデフォルトで参照先モデルの主キー（`id`）を見るため、主キーではなく一意制約（`unique=True`）のついた業務コード（`product_code` 等）で結合したい場合に指定する。 |\n| **`source`** | Serializer フィールドの引数 | **シリアライズするデータの取得元をカスタマイズする**\u003cbr\u003eドット記法（`source=\"product.name\"`）でリレーション先（親モデル）の項目を取り出してフラットにレスポンスへ含めたい場合に指定する。 |\n\n#### ▼ 【補足】特定のカラムだけを辞書で高速取得する `.values()`\n\nModel インスタンス全体を生成するのではなく、**必要なカラムだけを Python の辞書（`dict`）として軽量に取得したい** 場合は `.values()` を使います。\n\n```python\n# 必要なカラムだけを dict のリストとして取得（リレーション先は __ で指定）\nitems = Item.objects.values(\"item_code\", \"item_name\", \"product__name\")\n\n# 取得されるデータ形式のイメージ:\n# [\n#     {\"item_code\": \"A01\", \"item_name\": \"品目A\", \"product__name\": \"製品X\"},\n#     {\"item_code\": \"A02\", \"item_name\": \"品目B\", \"product__name\": \"製品Y\"},\n# ]\n```\n\n* **メリット**:\n  * Model インスタンス生成のオーバーヘッドを省き、指定したカラムのみを `SELECT` するため、**大量データでは高速化・メモリ削減につながることがある**。\n  * `product__name`（アンダースコア2つ）のように書くことで、リレーション先（親テーブル）も自動 JOIN して一緒に取得できる。\n* **注意点**:\n  * 返り値が Model インスタンスではなく単なる辞書（`dict`）になるため、モデルメソッドやモデルプロパティ、通常の属性アクセス（`instance.name`）を前提にした処理はそのまま使えない場合がある。\n  * ※シリアライザを使う場合でも、辞書のキーアクセス（`obj[\"item_name\"]`）を前提にコードを書けば `SerializerMethodField` 等を利用することは可能です。\n\n---\n\n### 【最重要】Serializer での関連先参照と View の select_related の因果関係\n\nここで絶対に押さえておきたいのが、**「なぜ View 側で `select_related(\"product\")` を書かなければならないのか？」** という因果関係です。\n\n原因は `source` そのものではなく、**「Serializer が関連先（ここでは `product`）を参照する際、各 Item について関連先オブジェクトが未取得であること」** にあります（ネストした `ProductSerializer` を埋め込む場合でも全く同じ現象が起きます）。\n\n```text\n【裏で起きている致命的な問題の流れ】\n1. Serializer が関連先（product）を参照する\n   （例: source=\"product.name\" や ネストした ProductSerializer）\n        ↓\n2. 各 Item について親 Product が DB から未取得の状態\n        ↓\n3. 1件シリアライズして product を参照するたびに、追加の SQL が発行される\n        ↓\n4. アイテムが 100 件あれば、ベースの取得 1 回 ＋ 関連先の取得 100 回 ＝ 合計 101 回の SQL が走る（N+1 問題）\n        ↓\n【解決策】\nだからこそ、View 側で事前に Item.objects.select_related(\"product\").all() と書き、\nSQL の JOIN を使ってあらかじめ親データまでまとめて引っ張っておく必要がある！\n```\n\nこの「N+1 問題」の仕組みと、Django が誇るもう1つの解決策 `prefetch_related` について、次の章で図解を使って深掘りしていきましょう。\n\n\n## 9. 図解でわかる N+1 問題と `select_related` / `prefetch_related` の違い\n\n実務でリレーションを扱う際に避けて通れない「N+1 問題」の正体と、Django が用意している 2 つの解決アプローチの仕組みです。\n\n### 1. `select_related(\"author\")`：ForeignKey（多対1）を JOIN で一括取得\n\n![a892c76d-4697-469e-ba4c-26b69bec49ce.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/d61f1d3c-ee56-4b96-badb-a107665e4acd.avif)\n\n「本（100冊）の一覧と、それぞれの著者名（Author）を表示したい」ケース（ForeignKey）です。\n\n* **❌ `select_related(\"author\")` なし（N+1 問題）**:  \n  1. Book テーブルから 100 冊取得（`SQL 1回`）\n  2. それぞれの本の著者を取得するため、Author テーブルを個別に検索（`SQL 100回`）  \n  * ➔ **合計 101 回** の SQL が発行され、本の数だけ追加 SQL が発生して遅くなる。\n* **⭕ `select_related(\"author\")` あり（効率的！）**:  \n  * クエリ：`Book.objects.select_related(\"author\").all()`\n  * 仕組み：データベース側で **SQL の JOIN を使って関連先をまとめて取得** する。  \n  * ➔ **合計 1 回の SQL で完了！**（後から `book.author` を参照しても追加 SQL は発生しない）  \n  * *※図では INNER JOIN と表記していますが、外部キーが nullable（`null=True`）な場合などは LEFT OUTER JOIN になることがあります。*\n\n---\n\n### 2. `prefetch_related(\"tags\")`：ManyToMany（多対多）を 2 回の SQL ＋ Python 側で紐付け\n\n![9e7ad70f-0487-473b-b29a-0bfe54a3ec95.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/bad86436-22ba-4b5c-bd3d-78cbe20c3200.avif)\n\n「本（100冊）と、それぞれに付いている複数のタグ（Tag）を表示したい」ケース（中間テーブル `Book_Tag` を介す ManyToMany）です。\n\n* **❌ `prefetch_related(\"tags\")` なし（N+1 問題）**:  \n  1. まず Book を 100 冊取得（`SQL 1回`）\n  2. その後、各本のタグを取得するために中間テーブル経由で個別に検索（`SQL 100回`）  \n  * ➔ **合計 101 回** の SQL が発行される。\n* **⭕ `prefetch_related(\"tags\")` あり（効率的！）**:  \n  * クエリ：`Book.objects.prefetch_related(\"tags\").all()`\n  * 仕組み：**合計 2 回の SQL** で完了し、**Django が Python 側で紐付ける**。  \n    1. 1回目の SQL：Book を 100 冊取得（`SQL 1回`）\n    2. 2回目の SQL：`WHERE book_tag.book_id IN (1, 2, ..., 100)` で 100 冊に関連するタグを一括取得（`SQL 1回`）\n    3. Django がメモリ（Python 側）で各 Book と Tag を自動マッピング\n  * ➔ **合計 2 回の SQL で完了！**（後から `book.tags.all()` を参照しても基本的に追加 SQL なし）\n\n---\n\n### 3. 使い分けのまとめ\n\n| メソッド | 結合の仕組み | 発行クエリ数 | 主な対象リレーション |\n| :--- | :--- | :---: | :--- |\n| **`select_related`** | DB 側で **SQL の `JOIN`** を実行 | **1回** | **1対1 / 多対1（ForeignKey）**\u003cbr\u003e（例: `Book` ➔ `Author`） |\n| **`prefetch_related`** | **`IN` 句** で一括取得し、**Python 側で紐付け** | **2回**\u003cbr\u003e（リレーションごとに+1回） | **多対多（ManyToMany） / 1対多の逆参照**\u003cbr\u003e（例: `Book` ➔ `Tag`、中間テーブル経由） |\n\n\n## 10. View 実装での落とし穴：リレーション先が None の場合のガード（500エラー防止）\n\nここまでは「いかに正しく・効率よくデータを取得するか（パフォーマンス最適化）」という攻めの話をしてきました。\n\n次に、実務で View を書くときに極めて頻発する **「リレーション先が存在しない場合の例外（例外安全性・500エラー防止）」** という守りの重要ポイントを見ていきましょう。\n\nView の中では、「ログインユーザーの所属情報で絞り込む」「特定のリレーション先を自動でセットする」といった目的で、**外部キー（ForeignKey）や 1対1（OneToOneField）のリレーション先を参照するコード** を頻繁に書きます。\n\nここでも、**「リレーション先が None（未設定）のケースを考慮していないと、AttributeError や DoesNotExist が飛んで 500 Internal Server Error になる」** という罠が潜んでいます。\n\n### 1. `get_queryset()` での絞り込み（所属・テナント参照）\nログインユーザー（`request.user`）に紐づく会社や部署のデータだけに絞り込む実装です。\n\n```python\nclass ItemViewSet(ModelViewSet):\n    serializer_class = ItemSerializer\n\n    def get_queryset(self):\n        user = self.request.user\n\n        # ❌ NG 例: 管理者や外部アカウントなど、company が None のユーザーで即座に 500 エラー！\n        # return Item.objects.filter(company_id=user.company.id)\n\n        # ⭕ 安全な書き方: リレーション先が存在するか必ずガードする\n        if not user.is_authenticated or not getattr(user, \"company\", None):\n            return Item.objects.none()  # 未所属なら空のクエリセットを返す\n\n        return Item.objects.filter(company=user.company)\n```\n\n---\n\n### 2. `perform_create()` でのリレーション自動セット\n登録時に、ログインユーザーの所属情報（部署など）を自動で紐付けて保存するケースです。\n\n```python\nclass ItemViewSet(ModelViewSet):\n    ...\n    def perform_create(self, serializer):\n        user = self.request.user\n\n        # ❌ NG 例: user.department が None だと AttributeError でクラッシュ\n        # serializer.save(created_by=user, department_code=user.department.code)\n\n        # ⭕ 安全な書き方: 安全に取得するか None を許容する\n        department = getattr(user, \"department\", None)\n        serializer.save(created_by=user, department=department)\n```\n\n---\n\n### 3. `OneToOneField` の逆参照（None ではなく例外が飛ぶ特殊な罠）\n1対1（`OneToOneField`）のリレーション先が存在しない場合、**`None` ではなく `DoesNotExist` 例外が発生してクラッシュします**。\n\n```python\n# User モデルと 1対1 の Profile モデルがある場合\ndef get_user_profile(user):\n    # ❌ Profile レコードが存在しない場合、User.profile.RelatedObjectDoesNotExist が発生！\n    # return user.profile.theme\n\n    # ⭕ hasattr で安全にチェックする\n    if hasattr(user, \"profile\"):\n        return user.profile.theme\n    return \"default\"\n```\n\n:::note info\n**💡 実務の鉄則：View でも「リレーション先は常に None の可能性がある」と疑う**  \n認証ユーザーであっても、システム管理者（スーパーユーザー）や移行直後のアカウントなどは「会社や部署が NULL」であるケースが多々あります。ガード節（`getattr` や `hasattr`）を習慣づけておくことで、本番障害を未然に防ぐことができます。\n:::\n\n\n## まとめ\n\n本講では、DRF の司令塔である「View」の構造と、実務で必須となるライフサイクル・パフォーマンス・例外処理のテクニックを体系的に学びました。\n\n* **View の役割と明確な責務境界**:\n  * View は「どのデータを取得するか・誰がアクセスできるか（司令塔）」\n  * Serializer は「入力値検証・データ変換・表示項目定義（検品・翻訳係）」\n* **3大アプローチと使い分け**:\n  * 自由度最優先なら `APIView`、特定機能特化なら `GenericAPIView`\n  * 標準的な CRUD を高速構築するなら `ModelViewSet` ＋ `DefaultRouter`\n* **Router と ViewSet の 6 大アクション**:\n  * `list`, `retrieve`, `create`, `update`, `partial_update`, `destroy` の実行時マッピングを把握する\n* **`queryset` と `get_queryset()` の違い**:\n  * 基本の固定クエリセットは `queryset`（遅延評価）\n  * リクエストやログインユーザーに依存する動的クエリは `get_queryset()` をオーバーライド\n* **ViewSet のライフサイクルとオーバーライド**:\n  * POST 処理全体を変えるなら `create()`、保存時の追加情報注入だけなら `perform_create()`\n  * 削除処理の論理削除化には `destroy()` または `perform_destroy()`\n* **`@action` による独自エンドポイント**:\n  * 全体（一覧）レベルなら `detail=False`、特定レコード対象なら `detail=True`\n* **リレーション設計と N+1 問題の完全撃破**:\n  * 既存テーブル連携には `managed=False`、`db_column`、`to_field` を駆使\n  * Serializer が関連先を参照する場合、View 側で `select_related`（ForeignKey / 1対1）や `prefetch_related`（ManyToMany / 逆参照）を指定してクエリ爆発を防ぐ\n* **View でのリレーション安全性**:\n  * 外部キーや OneToOneField を参照する際は `getattr` や `hasattr` で None / DoesNotExist をガードし、500 エラーを未然に防止する\n\nこれで、Model・Serializer・View の DRF 3大コア要素がすべて揃い、美しく堅牢な REST API を自由自在に設計・構築できるようになりました！  \n次回は、API を不正アクセスから守り実務開発で不可欠となる **「認証・権限（Authentication \u0026 Permissions）」** の世界へ進んでいきましょう！\n\n---\n\n**[← 第七講：シリアライザのバリデーション完全攻略](/items/04b2c3524ec379961d82) | [まとめページに戻る](/items/4250ea04fb61f5bc4f0d)**\n\n","coediting":false,"comments_count":0,"created_at":"2026-09-20T00:26:01+09:00","group":null,"id":"5260e8be137026ca99d5","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Django","versions":[]},{"name":"DRF","versions":[]},{"name":"Python","versions":[]},{"name":"rest","versions":[]},{"name":"Vue.js","versions":[]}],"title":"DRF × Vue.js 実践講座【第八講】DRFのView徹底解剖・リクエスト処理フローとクラス階層","updated_at":"2026-09-20T01:24:55+09:00","url":"https://qiita.com/mashikokoutaro1019/items/5260e8be137026ca99d5","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":4,"github_login_name":"kotaromashiko","id":"mashikokoutaro1019","items_count":34,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4223670,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocLhGA1m69Wj3mwBbWNN_K3bGO3DrJ7lGj36By-be2U_y9c9Mw=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-3:57\"\u003e今回は\u003cbr\u003e\nLaravel x Vue3 x Inertia環境にて\u003cbr\u003e\n論理削除を実装する方法をまとめました。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"5:1-5:32\"\u003e\n\u003cspan id=\"そもそも論理削除とは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eそもそも論理削除とは\u003c/h1\u003e\n\u003cp data-sourcepos=\"6:1-7:62\"\u003e別名SoftDelete（ソフトデリート）\u003cbr\u003e\n逆に完全にDBから消し去るのがハードデリート\u003c/p\u003e\n\u003ch3 data-sourcepos=\"9:1-9:49\"\u003e\n\u003cspan id=\"論理削除のメリットデメリット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4%E3%81%AE%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e論理削除のメリット、デメリット\u003c/h3\u003e\n\u003ch4 data-sourcepos=\"10:1-10:17\"\u003e\n\u003cspan id=\"メリット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eメリット\u003c/h4\u003e\n\u003cul data-sourcepos=\"11:1-17:0\"\u003e\n\u003cli data-sourcepos=\"11:1-12:85\"\u003eデータの復元が簡単\u003cbr\u003e\n誤って消去しても、Laravelの標準機能で一瞬で元の状態に戻せる\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-14:105\"\u003eデータが残る安心感\u003cbr\u003e\nデータが物理的に消えないため、削除履歴の追跡や分析にそのまま活用できる\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-17:0\"\u003eLaravelが自動で除外\u003cbr\u003e\n設定するだけで、通常の検索クエリから削除済みのデータを自動的に隠してくれる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"18:1-18:99\"\u003eデータ紛失のリスクを抑えつつ、開発の手間もかからない点が最大の強み\u003c/p\u003e\n\u003ch4 data-sourcepos=\"20:1-20:20\"\u003e\n\u003cspan id=\"デメリット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eデメリット\u003c/h4\u003e\n\u003cul data-sourcepos=\"21:1-32:0\"\u003e\n\u003cli data-sourcepos=\"21:1-23:132\"\u003eデータベースの容量が肥大化する\u003cbr\u003e\nデータが物理的に消去されず蓄積され続けるため、ストレージ容量を圧迫\u003cbr\u003e\n長期的には、不要なデータを完全に消去する定期的なクリーニング（物理削除）の仕組みが必要。\u003c/li\u003e\n\u003cli data-sourcepos=\"24:1-27:69\"\u003eクエリのパフォーマンスが低下する\u003cbr\u003e\nデータ量が増えることに加え、Laravelが自動的に WHERE deleted_at IS NULL\u003cbr\u003e\nという条件をすべての検索クエリに付与します。適切なインデックス（複合インデックスなど）\u003cbr\u003e\nを設定しないと、検索速度が低下する可能性あり。\u003c/li\u003e\n\u003cli data-sourcepos=\"28:1-32:0\"\u003e一意制約（UNIQUE）との相性が悪い\u003cbr\u003e\n例えば「メールアドレス」にUNIQUE制約をかけている場合、論理削除されたユーザーが\u003cbr\u003e\n使うアドレスと同じアドレスで新しいユーザーが登録できなくなります\u003cbr\u003e\n（削除されたデータがまだデータベース上に存在するため）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"33:1-33:23\"\u003e\n\u003cspan id=\"論理削除の実装\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4%E3%81%AE%E5%AE%9F%E8%A3%85\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e論理削除の実装\u003c/h1\u003e\n\u003cp data-sourcepos=\"35:1-37:81\"\u003e手順はこんな感じ。\u003cbr\u003e\n１：論理削除用のカラムを用意する\u003cbr\u003e\nマイグレーションファイルの用意と記述、マイグレーション\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:30\"\u003e２：ルーティング記述\u003c/p\u003e\n\u003cp data-sourcepos=\"41:1-43:30\"\u003e３：コントローラーに記述\u003cbr\u003e\n「本当に削除しますか？」といったメッセージも\u003cbr\u003e\n表示できるようにする\u003c/p\u003e\n\u003cp data-sourcepos=\"45:1-45:27\"\u003e４：モデルにも記述\u003c/p\u003e\n\u003cp data-sourcepos=\"47:1-47:24\"\u003e５：ビューも記述\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-49:24\"\u003e６：動作チェック\u003c/p\u003e\n\u003ch3 data-sourcepos=\"51:1-51:52\"\u003e\n\u003cspan id=\"１論理削除用のカラムを用意する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%91%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4%E7%94%A8%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e１：論理削除用のカラムを用意する\u003c/h3\u003e\n\u003cul data-sourcepos=\"52:1-53:0\"\u003e\n\u003cli data-sourcepos=\"52:1-53:0\"\u003eマイグレーションファイルの用意と記述、マイグレーション\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"54:1-57:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e// create_deelete_tableと言う名前のマイグレーションファイル作成\nphp artisan make:migration create_Delete_table\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"58:1-58:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F3a365c13-527d-4dc7-8691-8ce4f411eac0.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=460f5b8b94f4a824fe82b935d925535c\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F3a365c13-527d-4dc7-8691-8ce4f411eac0.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=460f5b8b94f4a824fe82b935d925535c\" alt=\"スクリーンショット 2026-09-19 16.31.30.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F3a365c13-527d-4dc7-8691-8ce4f411eac0.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=765dc383759b5de53f91ee48d24dbc1d 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/3a365c13-527d-4dc7-8691-8ce4f411eac0.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-61:43\"\u003eその後マイグレーションファイルを編集\u003cbr\u003e\ndatabase\\migrationsの中にあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-63:59\"\u003eこの記述をpublic function up(): voidの中に入れる\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"64:1-68:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e        \u003cspan class=\"nc\"\u003eSchema\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003etable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'products'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eBlueprint\u003c/span\u003e \u003cspan class=\"nv\"\u003e$table\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n            \u003cspan class=\"nv\"\u003e$table\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003esoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"69:1-69:30\"\u003e下記のようにします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"70:1-98:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Migrations\\Migration\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Schema\\Blueprint\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Support\\Facades\\Schema\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\n\u003cspan class=\"c1\"\u003e//public function up(): voidの中を編集\u003c/span\u003e\n\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"kd\"\u003eextends\u003c/span\u003e \u003cspan class=\"nc\"\u003eMigration\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"cd\"\u003e/**\n     * Run the migrations.\n     */\u003c/span\u003e\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eup\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"nc\"\u003eSchema\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003etable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'products'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eBlueprint\u003c/span\u003e \u003cspan class=\"nv\"\u003e$table\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n            \u003cspan class=\"nv\"\u003e$table\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003esoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"cd\"\u003e/**\n     * Reverse the migrations.\n     */\u003c/span\u003e\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003edown\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"nc\"\u003eSchema\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003edropIfExists\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'_delete'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"99:1-99:48\"\u003eその後マイグレートして適用させる\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"100:1-102:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003esail artisan migrate \n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"103:1-103:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F0ac75660-b925-4a1e-808d-5f2d097b90ce.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1481b5630f9f1173deae652143c1819c\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F0ac75660-b925-4a1e-808d-5f2d097b90ce.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1481b5630f9f1173deae652143c1819c\" alt=\"スクリーンショット 2026-09-19 20.06.07.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F0ac75660-b925-4a1e-808d-5f2d097b90ce.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=fc1ec26dca669dd75996d18c7ee51c25 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/0ac75660-b925-4a1e-808d-5f2d097b90ce.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch3 data-sourcepos=\"105:1-105:28\"\u003e\n\u003cspan id=\"２ルートに記述\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%92%E3%83%AB%E3%83%BC%E3%83%88%E3%81%AB%E8%A8%98%E8%BF%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e２：ルートに記述\u003c/h3\u003e\n\u003cp data-sourcepos=\"106:1-107:14\"\u003e通常のdelete処理と同じように書いていきます。\u003cbr\u003e\nroutes\\web.php\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"108:1-110:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nc\"\u003eRoute\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'/products/{product}'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nc\"\u003eProductController\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eclass\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'destroy'\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'users.destroy'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"111:1-111:40\"\u003e\n\u003cspan id=\"３コントローラーに記述\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%93%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E3%81%AB%E8%A8%98%E8%BF%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e３：コントローラーに記述\u003c/h3\u003e\n\u003cp data-sourcepos=\"112:1-112:57\"\u003eついでに「本当に削除しますか？」も記述\u003c/p\u003e\n\u003cp data-sourcepos=\"114:1-114:38\"\u003eHttp\\Controllers\\ProductController.php\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"115:1-124:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// deleteのところだけ抜粋\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003edestroy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eProduct\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// ソフトデリート実行（deleted_atに現在時刻が入る）\u003c/span\u003e\n    \n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003eredirect\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003eroute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'products.index'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n                         \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'success'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'商品を削除しました'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"126:1-126:28\"\u003e\n\u003cspan id=\"４モデルに記述\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%94%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E8%A8%98%E8%BF%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e４：モデルに記述\u003c/h3\u003e\n\u003cp data-sourcepos=\"127:1-129:47\"\u003eモデルは二行ほど追加\u003cbr\u003e\nnamespaceの下に\u003cbr\u003e\n\u003ccode\u003euse Illuminate\\Database\\Eloquent\\SoftDeletes;\u003c/code\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"131:1-132:18\"\u003eclass Product extends Modelの下に\u003cbr\u003e\n\u003ccode\u003euse SoftDeletes;\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"134:1-153:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"cp\"\u003e\u0026lt;?php\u003c/span\u003e\n\n\u003cspan class=\"kn\"\u003enamespace\u003c/span\u003e \u003cspan class=\"nn\"\u003eApp\\Models\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Eloquent\\Model\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eIlluminate\\Database\\Eloquent\\SoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\n\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProduct\u003c/span\u003e \u003cspan class=\"kd\"\u003eextends\u003c/span\u003e \u003cspan class=\"nc\"\u003eModel\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eSoftDeletes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eprotected\u003c/span\u003e \u003cspan class=\"nv\"\u003e$fillable\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'name'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e//（省略）\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"155:1-155:28\"\u003e\n\u003cspan id=\"５ビューに記述\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%95%E3%83%93%E3%83%A5%E3%83%BC%E3%81%AB%E8%A8%98%E8%BF%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e５：ビューに記述\u003c/h3\u003e\n\u003cp data-sourcepos=\"156:1-156:37\"\u003eresources\\js\\pages\\Products\\Index.vue\u003c/p\u003e\n\u003cp data-sourcepos=\"158:1-159:46\"\u003escriptタグ内に削除処理とconfirmを追加\u003cbr\u003e\nrouterも使えるように import内に記述\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"160:1-181:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"n\"\u003esetup\u003c/span\u003e \u003cspan class=\"n\"\u003elang\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"n\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nc\"\u003eHead\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nc\"\u003eLink\u003c/span\u003e \u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erouter\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003efrom\u003c/span\u003e \u003cspan class=\"s1\"\u003e'@inertiajs/vue3'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eProps\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eusers\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eArray\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// propsに型を適用する\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003eprops\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 削除処理とconfirmによる確認\u003c/span\u003e\n\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"no\"\u003edeleteProduct\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproductId\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003econfirm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'本当に削除しますか？'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003erouter\u003c/span\u003e\u003cspan class=\"mf\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`/products/${productId}`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\n\n\u003cspan class=\"o\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"n\"\u003escript\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"183:1-183:44\"\u003etemplate内のボタンにも処理を追加\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"185:1-193:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ebutton\u003c/span\u003e\n    \u003cspan class=\"o\"\u003e@\u003c/span\u003e\u003cspan class=\"n\"\u003eclick\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"deleteProduct(product.id)\"\u003c/span\u003e\n    \u003cspan class=\"n\"\u003etype\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"button\"\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eclass\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"text-red-600 hover:bg-red-100 px-3 py-1 border \n    border-red-500rounded\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"n\"\u003e削除\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"n\"\u003ebutton\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"195:1-195:28\"\u003e\n\u003cspan id=\"６動作チェック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%BC%96%E5%8B%95%E4%BD%9C%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e６：動作チェック\u003c/h3\u003e\n\u003cp data-sourcepos=\"197:1-200:164\"\u003e削除ボタンクリックで\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9664a738-3e4a-40b9-8bc4-dde5b5c49774.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=606c329092df9b37838dd6483daa7732\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9664a738-3e4a-40b9-8bc4-dde5b5c49774.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=606c329092df9b37838dd6483daa7732\" alt=\"スクリーンショット 2026-09-19 17.01.47.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9664a738-3e4a-40b9-8bc4-dde5b5c49774.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=ad856ca890511f4fa7a5ccdaa86ac8b9 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/9664a738-3e4a-40b9-8bc4-dde5b5c49774.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\nscriptで設定したconfirm処理が来て　OKを押すと\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F04ea5675-1231-4cb6-9459-0a171220c4e6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=cc0b8c9aaace25867d16a40ec9c1fb88\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F04ea5675-1231-4cb6-9459-0a171220c4e6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=cc0b8c9aaace25867d16a40ec9c1fb88\" alt=\"スクリーンショット 2026-09-19 17.01.18.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F04ea5675-1231-4cb6-9459-0a171220c4e6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=cbe235a8226298dcdb3f7d9110822860 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/04ea5675-1231-4cb6-9459-0a171220c4e6.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"202:1-204:30\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9af23f58-6b67-43fe-8ceb-84b38784b3dd.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=aea78e950dc788627aeaf114f9cd3cdd\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9af23f58-6b67-43fe-8ceb-84b38784b3dd.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=aea78e950dc788627aeaf114f9cd3cdd\" alt=\"スクリーンショット 2026-09-19 17.03.56.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F9af23f58-6b67-43fe-8ceb-84b38784b3dd.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=8f33745e0452b629afe1d0476950a43d 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/9af23f58-6b67-43fe-8ceb-84b38784b3dd.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Fd7817704-c49e-4fc7-944f-031b46f1960b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0b89d9ef43c5ae23adebaef64a33ad61\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Fd7817704-c49e-4fc7-944f-031b46f1960b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0b89d9ef43c5ae23adebaef64a33ad61\" alt=\"スクリーンショット 2026-09-19 17.04.28.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Fd7817704-c49e-4fc7-944f-031b46f1960b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=20ce3d0d738162637b2e60b5c4b80ff4 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/d7817704-c49e-4fc7-944f-031b46f1960b.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cbr\u003e\nしっかり消えました。\u003c/p\u003e\n\u003cp data-sourcepos=\"206:1-207:164\"\u003e念の為、論理削除できているか確認\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Ff49be776-cbf8-4e45-b05f-db894751334c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=73283366f1a418279bd3f4282ec610c4\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Ff49be776-cbf8-4e45-b05f-db894751334c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=73283366f1a418279bd3f4282ec610c4\" alt=\"スクリーンショット 2026-09-19 17.05.43.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2Ff49be776-cbf8-4e45-b05f-db894751334c.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=ced01325869b9673bcbf17fa5b87d51a 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/f49be776-cbf8-4e45-b05f-db894751334c.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"209:1-212:164\"\u003edeleted_atに日付が入ることによって非表示にするようなイメージです。\u003cbr\u003e\nしっかり動作してますね。\u003cbr\u003e\n試しに　NULLに戻したらどうなるか見てみます。\u003cbr\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F05f39d82-2fb0-400f-894c-e007b02ff67a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0d489f3b2a22b9948a9d364d20a7b812\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F05f39d82-2fb0-400f-894c-e007b02ff67a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0d489f3b2a22b9948a9d364d20a7b812\" alt=\"スクリーンショット 2026-09-19 17.09.47.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F05f39d82-2fb0-400f-894c-e007b02ff67a.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=9f908db0bce294b8f166b9fd7de80eb6 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/05f39d82-2fb0-400f-894c-e007b02ff67a.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"214:1-214:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F508b95c4-932e-4a5c-8c31-0522f42ec649.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f918954393dc8528869bb644b6f8a986\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F508b95c4-932e-4a5c-8c31-0522f42ec649.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f918954393dc8528869bb644b6f8a986\" alt=\"スクリーンショット 2026-09-19 17.09.28.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4155281%2F508b95c4-932e-4a5c-8c31-0522f42ec649.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=7bcfe530b3cdd0805c0ee262f5879aa7 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/508b95c4-932e-4a5c-8c31-0522f42ec649.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"216:1-216:30\"\u003eしっかり戻りました。\u003c/p\u003e\n","body":"今回は\nLaravel x Vue3 x Inertia環境にて\n論理削除を実装する方法をまとめました。\n\n# そもそも論理削除とは\n別名SoftDelete（ソフトデリート）\n逆に完全にDBから消し去るのがハードデリート\n\n### 論理削除のメリット、デメリット\n#### メリット\n* データの復元が簡単\n誤って消去しても、Laravelの標準機能で一瞬で元の状態に戻せる\n* データが残る安心感\nデータが物理的に消えないため、削除履歴の追跡や分析にそのまま活用できる\n* Laravelが自動で除外\n設定するだけで、通常の検索クエリから削除済みのデータを自動的に隠してくれる\n\nデータ紛失のリスクを抑えつつ、開発の手間もかからない点が最大の強み\n\n#### デメリット\n* データベースの容量が肥大化する\nデータが物理的に消去されず蓄積され続けるため、ストレージ容量を圧迫\n長期的には、不要なデータを完全に消去する定期的なクリーニング（物理削除）の仕組みが必要。\n* クエリのパフォーマンスが低下する\nデータ量が増えることに加え、Laravelが自動的に WHERE deleted_at IS NULL \nという条件をすべての検索クエリに付与します。適切なインデックス（複合インデックスなど）\nを設定しないと、検索速度が低下する可能性あり。\n* 一意制約（UNIQUE）との相性が悪い\n例えば「メールアドレス」にUNIQUE制約をかけている場合、論理削除されたユーザーが\n使うアドレスと同じアドレスで新しいユーザーが登録できなくなります\n（削除されたデータがまだデータベース上に存在するため）\n\n# 論理削除の実装\n\n手順はこんな感じ。\n１：論理削除用のカラムを用意する\nマイグレーションファイルの用意と記述、マイグレーション\n\n２：ルーティング記述\n\n３：コントローラーに記述\n「本当に削除しますか？」といったメッセージも\n表示できるようにする\n\n４：モデルにも記述\n\n５：ビューも記述\n\n６：動作チェック\n\n### １：論理削除用のカラムを用意する\n- マイグレーションファイルの用意と記述、マイグレーション\n\n~~~bash\n// create_deelete_tableと言う名前のマイグレーションファイル作成\nphp artisan make:migration create_Delete_table\n~~~\n![スクリーンショット 2026-09-19 16.31.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/3a365c13-527d-4dc7-8691-8ce4f411eac0.png)\n\nその後マイグレーションファイルを編集\ndatabase\\migrationsの中にあります。\n\nこの記述をpublic function up(): voidの中に入れる\n~~~php\n        Schema::table('products', function (Blueprint $table) {\n            $table-\u003esoftDeletes();\n        });\n~~~\n下記のようにします。\n~~~php\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\n\n//public function up(): voidの中を編集\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('products', function (Blueprint $table) {\n            $table-\u003esoftDeletes();\n        });\n\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('_delete');\n    }\n};\n~~~\nその後マイグレートして適用させる\n~~~\nsail artisan migrate \n~~~\n![スクリーンショット 2026-09-19 20.06.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/0ac75660-b925-4a1e-808d-5f2d097b90ce.png)\n\n### ２：ルートに記述\n通常のdelete処理と同じように書いていきます。\nroutes\\web.php\n~~~php\nRoute::delete('/products/{product}', [ProductController::class, 'destroy'])-\u003ename('users.destroy');\n~~~\n### ３：コントローラーに記述\nついでに「本当に削除しますか？」も記述\n\nHttp\\Controllers\\ProductController.php\n~~~php\n// deleteのところだけ抜粋\n\n    public function destroy(Product $product)\n    {\n        $product-\u003edelete(); // ソフトデリート実行（deleted_atに現在時刻が入る）\n    \n        return redirect()-\u003eroute('products.index')\n                         -\u003ewith('success', '商品を削除しました');\n~~~\n\n### ４：モデルに記述\nモデルは二行ほど追加\nnamespaceの下に\n`use Illuminate\\Database\\Eloquent\\SoftDeletes;`\n\nclass Product extends Modelの下に\n`use SoftDeletes;`\n\n~~~php\n\u003c?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n\nclass Product extends Model\n{\n    use SoftDeletes;\n\n    protected $fillable = [\n        'name',\n        //（省略）\n    ];\n\n}\n~~~\n\n### ５：ビューに記述\nresources\\js\\pages\\Products\\Index.vue\n\nscriptタグ内に削除処理とconfirmを追加\nrouterも使えるように import内に記述\n~~~php\n\u003cscript setup lang=\"ts\"\u003e\nimport { Head, Link , router } from '@inertiajs/vue3';\n\ninterface Props {\n    users: Array\u003c{\n        name: string;\n    }\u003e;\n}\n// propsに型を適用する\nconst props = defineProps\u003cProps\u003e();\n\n// 削除処理とconfirmによる確認\nconst deleteProduct = (productId: number) =\u003e {\n    if (confirm('本当に削除しますか？')) {\n        router.delete(`/products/${productId}`);\n    }\n};\n\n\n\u003c/script\u003e\n~~~\n\ntemplate内のボタンにも処理を追加\n\n~~~php\n\u003cbutton\n    @click=\"deleteProduct(product.id)\"\n    type=\"button\"\n    class=\"text-red-600 hover:bg-red-100 px-3 py-1 border \n    border-red-500rounded\"\u003e\n    削除\n\u003c/button\u003e\n~~~\n\n### ６：動作チェック\n\n削除ボタンクリックで\n![スクリーンショット 2026-09-19 17.01.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/9664a738-3e4a-40b9-8bc4-dde5b5c49774.png)\nscriptで設定したconfirm処理が来て　OKを押すと\n![スクリーンショット 2026-09-19 17.01.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/04ea5675-1231-4cb6-9459-0a171220c4e6.png)\n\n![スクリーンショット 2026-09-19 17.03.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/9af23f58-6b67-43fe-8ceb-84b38784b3dd.png)\n![スクリーンショット 2026-09-19 17.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/d7817704-c49e-4fc7-944f-031b46f1960b.png)\nしっかり消えました。\n\n念の為、論理削除できているか確認\n![スクリーンショット 2026-09-19 17.05.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/f49be776-cbf8-4e45-b05f-db894751334c.png)\n\ndeleted_atに日付が入ることによって非表示にするようなイメージです。\nしっかり動作してますね。\n試しに　NULLに戻したらどうなるか見てみます。\n![スクリーンショット 2026-09-19 17.09.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/05f39d82-2fb0-400f-894c-e007b02ff67a.png)\n\n![スクリーンショット 2026-09-19 17.09.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/508b95c4-932e-4a5c-8c31-0522f42ec649.png)\n\nしっかり戻りました。\n","coediting":false,"comments_count":0,"created_at":"2026-09-19T17:42:04+09:00","group":null,"id":"3f28221ce51af89f5df9","likes_count":3,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Laravel","versions":[]},{"name":"Vue.js","versions":[]},{"name":"inertia","versions":[]}],"title":"論理削除の実装　Laravel x Vue x Inertia","updated_at":"2026-09-24T14:28:14+09:00","url":"https://qiita.com/metappi/items/3f28221ce51af89f5df9","user":{"description":"プログラミングスクールRUNTEQ卒業後、\r\n受託企業に就職しました。\r\nPHP Laravel Vueをメインに学習中。\r\n楽しみながら学んでいきたいと考えております。\r\n夢は所さんになることです。\r\nまだまだエンジニアのタマゴなので間違い、ご意見等ございましたら\r\nぜひ頂戴したいと思っております。よろしくお願いいたします。","facebook_id":"","followees_count":21,"followers_count":32,"github_login_name":"Zundabyon","id":"metappi","items_count":79,"linkedin_id":"","location":"Japan","name":"(めたっぴ72期A) 小笠原","organization":"","permanent_id":4155281,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/profile-images/1789107820","team_only":false,"twitter_screen_name":"zundabyon","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cblockquote data-sourcepos=\"2:1-2:93\"\u003e\n\u003cp data-sourcepos=\"2:3-2:93\"\u003e\u003cstrong\u003eLecture 06: Deep Dive into Serializers — Architecture, The 3 Types \u0026amp; Update Lifecycle\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"4:1-4:225\"\u003e\u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003e← DRF × Vue.js 実践講座：まとめページに戻る\u003c/a\u003e | \u003ca href=\"/items/c835d3ff5c824439f248\"\u003e← 第五講：マイグレーション完全攻略・Django管理画面とリレーション設計\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"6:1-6:80\"\u003e\n\u003cspan id=\"はじめになぜ-drf-ではシリアライザが主役なのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB%E3%81%AA%E3%81%9C-drf-%E3%81%A7%E3%81%AF%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%8C%E4%B8%BB%E5%BD%B9%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに：なぜ DRF では「シリアライザ」が主役なのか？\u003c/h2\u003e\n\u003cp data-sourcepos=\"8:1-8:250\"\u003e第五講では、マイグレーションの仕組みから Django Admin（管理画面）によるデータ登録、そしてテーブル間のリレーション設計までをマスターしました。データベースの準備は万全です！\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-12:146\"\u003eしかし、ここで一つの疑問が湧きます。\u003cbr\u003e\n\u003cstrong\u003e「データベースにある Python のモデルオブジェクトは、どうやってフロントエンド（Vue.js）に届くのだろう？」\u003c/strong\u003e\u003cbr\u003e\n\u003cstrong\u003e「逆に、Vue.js から送られてくる JSON データは、どうやって安全にデータベースへ保存されるのだろう？」\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-14:196\"\u003eその疑問をすべて一身に引き受け、解決してくれるのが、今回学ぶ \u003cstrong\u003eDRF（Django REST Framework）最大の中核部品「シリアライザ（Serializer）」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:93\"\u003e本講では、シリアライザの仕組みを図解とともに整理していきます！\u003c/p\u003e\n\u003ch2 data-sourcepos=\"20:1-20:67\"\u003e\n\u003cspan id=\"1-そもそもシリアライザserializerとは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6serializer%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. そもそも「シリアライザ（Serializer）」とは？\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-22:189\"\u003eシリアライザ（Serializer）を一言で表すと、\u003cstrong\u003e「フロントエンド（Vue.js）とバックエンド（Django/DB）をつなぐ“通訳者（翻訳機）”」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F76325194-6943-40cf-8fd9-0d4ceccb3874.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=27d91a2310c6e3c468e0776fa1b0c09a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F76325194-6943-40cf-8fd9-0d4ceccb3874.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=27d91a2310c6e3c468e0776fa1b0c09a\" alt=\"397e1a26-e26a-4744-8d4f-6220eb159cc5.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F76325194-6943-40cf-8fd9-0d4ceccb3874.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=e3a23360dc796ba24727afeb70b31cf7 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/76325194-6943-40cf-8fd9-0d4ceccb3874.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"26:1-27:150\"\u003eDjango REST Framework の内部では、下図のように \u003cstrong\u003eView が Serializer を使って Model とやり取り\u003c/strong\u003e を行っています。\u003cbr\u003e\nシリアライザは、リクエスト側（入力）とレスポンス側（出力）でそれぞれ重要な変換と検証を担っています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"29:1-29:74\"\u003e\n\u003cspan id=\"-1-入力リクエスト側デシリアライズと検証\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-1-%E5%85%A5%E5%8A%9B%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E5%81%B4%E3%83%87%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA%E3%81%A8%E6%A4%9C%E8%A8%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 1. 入力（リクエスト側）：デシリアライズと検証\u003c/h3\u003e\n\u003cul data-sourcepos=\"30:1-33:0\"\u003e\n\u003cli data-sourcepos=\"30:1-30:161\"\u003e\n\u003cstrong\u003e処理の流れ\u003c/strong\u003e: \u003ccode\u003eClient（リクエスト）\u003c/code\u003e ➔ \u003ccode\u003eURL / Router\u003c/code\u003e ➔ \u003ccode\u003eView / ViewSet\u003c/code\u003e ➔ \u003cstrong\u003e\u003ccode\u003eSerializer（入力の検証）\u003c/code\u003e\u003c/strong\u003e ➔ \u003ccode\u003eModel\u003c/code\u003e ➔ \u003ccode\u003eDatabase\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"31:1-33:0\"\u003e\n\u003cstrong\u003eシリアライザの役割（ここが最重要！）\u003c/strong\u003e:\u003cbr\u003e\nクライアントから送られてきた生の JSON データを Python 辞書に復元し、\u003cstrong\u003e\u003ccode\u003eis_valid()\u003c/code\u003e\u003c/strong\u003e で型や文字数・必須項目などのルールに適合しているかを厳格に検証します。合格したクリーンなデータ（\u003cstrong\u003e\u003ccode\u003evalidated_data\u003c/code\u003e\u003c/strong\u003e）のみがモデルへ渡され、データベースへ安全に保存されます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"34:1-34:71\"\u003e\n\u003cspan id=\"-2-出力レスポンス側シリアライズと変換\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-2-%E5%87%BA%E5%8A%9B%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E5%81%B4%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA%E3%81%A8%E5%A4%89%E6%8F%9B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 2. 出力（レスポンス側）：シリアライズと変換\u003c/h3\u003e\n\u003cul data-sourcepos=\"35:1-39:0\"\u003e\n\u003cli data-sourcepos=\"35:1-35:169\"\u003e\n\u003cstrong\u003e処理の流れ\u003c/strong\u003e: \u003ccode\u003eDatabase\u003c/code\u003e ➔ \u003ccode\u003eModel\u003c/code\u003e ➔ \u003cstrong\u003e\u003ccode\u003eSerializer（出力の変換）\u003c/code\u003e\u003c/strong\u003e ➔ \u003ccode\u003eResponse（レスポンス）\u003c/code\u003e ➔ \u003ccode\u003eClient（クライアントへ返す）\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"36:1-39:0\"\u003e\n\u003cstrong\u003eシリアライザの役割\u003c/strong\u003e:\u003cbr\u003e\nデータベースから取り出した Python のモデルオブジェクトを、フロントエンド（Vue.js）が解釈できる JSON 形式へと変換します（内部的には \u003cstrong\u003e\u003ccode\u003eto_representation()\u003c/code\u003e\u003c/strong\u003e メソッドが動作）。変換されたデータがレスポンスとしてクライアントに返されます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"40:1-40:37\"\u003e\n\u003cspan id=\"2-シリアライザの3大分類\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%AE3%E5%A4%A7%E5%88%86%E9%A1%9E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. シリアライザの3大分類\u003c/h2\u003e\n\u003cp data-sourcepos=\"42:1-42:128\"\u003eDRF のシリアライザには、用途に応じて使い分ける \u003cstrong\u003e3つの代表的な種類\u003c/strong\u003e が用意されています。\u003c/p\u003e\n\n\u003cp data-sourcepos=\"45:1-45:153\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F95c65714-5635-4601-8fbb-7ac68a46d989.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2d5086a135f0295022e3f79eb4328db7\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F95c65714-5635-4601-8fbb-7ac68a46d989.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2d5086a135f0295022e3f79eb4328db7\" alt=\"12616ced-419b-4fdd-bb7e-f64185445127.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F95c65714-5635-4601-8fbb-7ac68a46d989.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=7f51d6bb1c5912110233c549439e0e3f 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/95c65714-5635-4601-8fbb-7ac68a46d989.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"47:1-51:159\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"47:1-47:64\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"47:2-47:9\"\u003e種類\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"47:11-47:18\"\u003e特徴\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"47:20-47:42\"\u003e主な使いどころ\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"47:44-47:63\"\u003e実務での比率\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"49:1-49:133\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"49:2-49:21\"\u003e\u003cstrong\u003e① Serializer\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"49:23-49:58\"\u003eフィールドを\u003cstrong\u003e自由定義\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"49:60-49:117\"\u003e非DBデータ、外部API連携、認証・集計など\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"49:119-49:132\"\u003e約 10〜15%\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:149\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"50:2-50:26\"\u003e\u003cstrong\u003e② ModelSerializer\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"50:28-50:56\"\u003e\u003cstrong\u003eModelから自動生成\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"50:58-50:117\"\u003e通常のCRUD操作（一覧・詳細・登録・更新）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"50:119-50:148\"\u003e\u003cstrong\u003e約 80〜90%（主役）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:159\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"51:2-51:25\"\u003e\u003cstrong\u003e③ ListSerializer\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"51:27-51:62\"\u003e\u003cstrong\u003e複数件をまとめて処理\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"51:64-51:128\"\u003e一覧取得（\u003ccode\u003emany=True\u003c/code\u003e 指定時に裏側で自動動作）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"51:130-51:158\"\u003e内部利用 / 特殊用途\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"53:1-53:43\"\u003e\n\u003cspan id=\"--serializer自由定義型\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#--serializer%E8%87%AA%E7%94%B1%E5%AE%9A%E7%BE%A9%E5%9E%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e■ ① Serializer（自由定義型）\u003c/h3\u003e\n\u003cp data-sourcepos=\"54:1-55:152\"\u003eモデルに縛られず、必要なフィールド（\u003ccode\u003eCharField\u003c/code\u003e, \u003ccode\u003eIntegerField\u003c/code\u003e など）をゼロから手動で組み立てる汎用クラスです。\u003cbr\u003e\nDBに保存しないログイン認証（ユーザー名・パスワードの受け取り）や、外部APIのデータ中継などで利用します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"57:1-65:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eContactFormSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDBモデルと紐付かない、自由定義のシリアライザ\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eemail\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eEmailField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"67:1-67:71\"\u003e\n\u003cspan id=\"--modelserializermodel自動生成型実務の主役\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#--modelserializermodel%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90%E5%9E%8B%E5%AE%9F%E5%8B%99%E3%81%AE%E4%B8%BB%E5%BD%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e■ ② ModelSerializer（Model自動生成型・実務の主役）\u003c/h3\u003e\n\u003cp data-sourcepos=\"68:1-69:238\"\u003e実務で書くシリアライザのほとんどはこれです。\u003cbr\u003e\n\u003ccode\u003eclass Meta:\u003c/code\u003e にモデル名を指定するだけで、モデルのフィールド定義や制約（必須チェック・最大文字数など）を引き継ぎ、\u003ccode\u003ecreate\u003c/code\u003e / \u003ccode\u003eupdate\u003c/code\u003e の保存処理まで自動生成してくれます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"71:1-81:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eModelから自動生成されるシリアライザ（実務の標準）\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003eid\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003esender\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003erecipient\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003econtent\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eread_only_fields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003eid\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"83:1-83:76\"\u003e\n\u003cspan id=\"-meta-クラスの設定一覧主に-modelserializer-で使用\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-meta-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E8%A8%AD%E5%AE%9A%E4%B8%80%E8%A6%A7%E4%B8%BB%E3%81%AB-modelserializer-%E3%81%A7%E4%BD%BF%E7%94%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ Meta クラスの設定一覧（主に ModelSerializer で使用）\u003c/h4\u003e\n\u003cp data-sourcepos=\"85:1-85:155\"\u003e\u003ccode\u003eModelSerializer\u003c/code\u003e では、内部の \u003ccode\u003eclass Meta\u003c/code\u003e を使って、対象となるモデルやAPIで扱うフィールドなどの設定を定義します。\u003c/p\u003e\n\u003cp data-sourcepos=\"87:1-87:172\"\u003e\u003ccode\u003eclass Meta\u003c/code\u003e 自体は通常の \u003ccode\u003eSerializer\u003c/code\u003e にも記述できますが、\u003ccode\u003emodel\u003c/code\u003e や \u003ccode\u003efields\u003c/code\u003e など、以下の設定は主に \u003ccode\u003eModelSerializer\u003c/code\u003e で利用されます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"89:1-97:83\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"89:1-89:61\"\u003e\n\u003cth data-sourcepos=\"89:2-89:25\"\u003e設定\u003c/th\u003e\n\u003cth data-sourcepos=\"89:27-89:60\"\u003e意味\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"91:1-91:65\"\u003e\n\u003ctd data-sourcepos=\"91:2-91:21\"\u003e\u003ccode\u003emodel\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"91:23-91:64\"\u003e対象の Model を指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"92:1-92:75\"\u003e\n\u003ctd data-sourcepos=\"92:2-92:21\"\u003e\u003ccode\u003efields\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"92:23-92:74\"\u003eAPIで扱うフィールドを指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"93:1-93:81\"\u003e\n\u003ctd data-sourcepos=\"93:2-93:21\"\u003e\u003ccode\u003eexclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"93:23-93:80\"\u003eAPIから除外するフィールドを指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"94:1-94:81\"\u003e\n\u003ctd data-sourcepos=\"94:2-94:21\"\u003e\u003ccode\u003eread_only_fields\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"94:23-94:80\"\u003e読み取り専用フィールドを指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"95:1-95:81\"\u003e\n\u003ctd data-sourcepos=\"95:2-95:21\"\u003e\u003ccode\u003eextra_kwargs\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"95:23-95:80\"\u003e各フィールドに追加設定を指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"96:1-96:87\"\u003e\n\u003ctd data-sourcepos=\"96:2-96:21\"\u003e\u003ccode\u003edepth\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"96:23-96:86\"\u003eForeignKey などの関連をどこまで展開するか指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"97:1-97:83\"\u003e\n\u003ctd data-sourcepos=\"97:2-97:21\"\u003e\u003ccode\u003evalidators\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"97:23-97:82\"\u003eSerializer 全体に適用するバリデータを指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv data-sourcepos=\"99:1-102:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"100:1-101:366\"\u003e\u003cstrong\u003e💡 \u003ccode\u003eextra_kwargs\u003c/code\u003e と \u003ccode\u003evalidators\u003c/code\u003e の実践的な使い方\u003c/strong\u003e\u003cbr\u003e\nフィールド個別の正規表現チェック（\u003ccode\u003eRegexValidator\u003c/code\u003e）や、複数フィールドの組み合わせ一意性チェック（\u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e）などの設定ベースのバリデーションは、次回の \u003cstrong\u003e\u003ca href=\"/items/04b2c3524ec379961d82\"\u003e【第七講】シリアライザのバリデーション完全攻略\u003c/a\u003e\u003c/strong\u003e で詳しく解説します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch4 data-sourcepos=\"104:1-104:100\"\u003e\n\u003cspan id=\"-モデルとシリアライザの-field-クラス対応表自動マッピング規則\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A8%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%AE-field-%E3%82%AF%E3%83%A9%E3%82%B9%E5%AF%BE%E5%BF%9C%E8%A1%A8%E8%87%AA%E5%8B%95%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E8%A6%8F%E5%89%87\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ モデルとシリアライザの Field クラス対応表（自動マッピング規則）\u003c/h4\u003e\n\u003cp data-sourcepos=\"105:1-106:159\"\u003e\u003ccode\u003eModelSerializer\u003c/code\u003e がモデル定義をもとにフィールドを自動生成する際、Django モデルの Field クラスは以下のように DRF のシリアライザ Field クラスへと自動マッピングされます。\u003cbr\u003e\n（※右側のシリアライザ Field クラス自体は、通常の \u003ccode\u003eSerializer\u003c/code\u003e でフィールドを手動定義する際にもそのまま使います）\u003c/p\u003e\n\u003ctable data-sourcepos=\"108:1-118:68\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"108:1-108:72\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"108:2-108:31\"\u003eモデルの Field クラス\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"108:33-108:71\"\u003eシリアライザの Field クラス\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"110:1-110:74\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"110:2-110:34\"\u003e\u003ccode\u003edjango.db.models.BooleanField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"110:36-110:73\"\u003e\u003ccode\u003erest_framework.fields.BooleanField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"111:1-111:68\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"111:2-111:31\"\u003e\u003ccode\u003edjango.db.models.CharField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"111:33-111:67\"\u003e\u003ccode\u003erest_framework.fields.CharField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"112:1-112:68\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"112:2-112:31\"\u003e\u003ccode\u003edjango.db.models.TextField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"112:33-112:67\"\u003e\u003ccode\u003erest_framework.fields.CharField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"113:1-113:70\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"113:2-113:32\"\u003e\u003ccode\u003edjango.db.models.EmailField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"113:34-113:69\"\u003e\u003ccode\u003erest_framework.fields.EmailField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"114:1-114:74\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"114:2-114:34\"\u003e\u003ccode\u003edjango.db.models.IntegerField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"114:36-114:73\"\u003e\u003ccode\u003erest_framework.fields.IntegerField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"115:1-115:70\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"115:2-115:32\"\u003e\u003ccode\u003edjango.db.models.FloatField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"115:34-115:69\"\u003e\u003ccode\u003erest_framework.fields.FloatField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"116:1-116:68\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"116:2-116:31\"\u003e\u003ccode\u003edjango.db.models.DateField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"116:33-116:67\"\u003e\u003ccode\u003erest_framework.fields.DateField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"117:1-117:76\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"117:2-117:35\"\u003e\u003ccode\u003edjango.db.models.DateTimeField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"117:37-117:75\"\u003e\u003ccode\u003erest_framework.fields.DateTimeField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"118:1-118:68\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"118:2-118:31\"\u003e\u003ccode\u003edjango.db.models.FileField\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"118:33-118:67\"\u003e\u003ccode\u003erest_framework.fields.FileField\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"120:1-120:81\"\u003e\n\u003cspan id=\"--listserializer複数件まとめて処理manytrue-の正体\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#--listserializer%E8%A4%87%E6%95%B0%E4%BB%B6%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E5%87%A6%E7%90%86manytrue-%E3%81%AE%E6%AD%A3%E4%BD%93\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e■ ③ ListSerializer（複数件まとめて処理・many=True の正体）\u003c/h3\u003e\n\u003cp data-sourcepos=\"121:1-122:201\"\u003e複数のレコードを一括で処理するためのシリアライザです。\u003cbr\u003e\n普段自作することはほぼなく、シリアライザに \u003cstrong\u003e\u003ccode\u003emany=True\u003c/code\u003e\u003c/strong\u003e を渡した際に、DRF が内部で自動的に呼び出して複数件をリスト形式にまとめてくれます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"124:1-128:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 複数件（クエリセット）を渡すときは many=True を指定\n\u003c/span\u003e\u003cspan class=\"n\"\u003emessages\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessages\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emany\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"130:1-130:79\"\u003e\n\u003cspan id=\"共通シリアライザの主要フィールドオプション一覧\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%85%B1%E9%80%9A%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%AE%E4%B8%BB%E8%A6%81%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E4%B8%80%E8%A6%A7\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【共通】シリアライザの主要フィールドオプション一覧\u003c/h3\u003e\n\u003cp data-sourcepos=\"131:1-132:84\"\u003eフィールドを手動定義する際（\u003ccode\u003eSerializer\u003c/code\u003e）や、フィールドのカスタマイズ・\u003ccode\u003eextra_kwargs\u003c/code\u003e で追加設定を行う際（\u003ccode\u003eModelSerializer\u003c/code\u003e）に指定できる代表的なオプションです。\u003cbr\u003e\n\u003cstrong\u003e\u003ccode\u003eSerializer\u003c/code\u003e と \u003ccode\u003eModelSerializer\u003c/code\u003e の両方で共通して利用できます。\u003c/strong\u003e\u003c/p\u003e\n\u003ctable data-sourcepos=\"134:1-148:101\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"134:1-134:43\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"134:2-134:33\"\u003eフィールドオプション\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"134:35-134:42\"\u003e説明\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"136:1-136:202\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"136:2-136:15\"\u003e\u003ccode\u003ewrite_only\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"136:17-136:201\"\u003e登録・更新・一部更新時の 入力用フィールドには含めるが、出力用フィールドには含めたくない場合に True を指定(デフォルト値は False)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"137:1-137:305\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"137:2-137:14\"\u003e\u003ccode\u003eread_only\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"137:16-137:304\"\u003e出力用フィールドには含めるが、登録・更新・一部更新時の入力用のフィールドには含めたくない場合に True を指定。read_only=True を指定したフィールドは登録・更新・一部更新の対象外となる(デフォルト値は False)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"138:1-138:303\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"138:2-138:13\"\u003e\u003ccode\u003erequired\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"138:15-138:302\"\u003e入力データにフィールドが指定されなかった場合にバリデーション NG にするかどうか。なお、default オプションが設定されている場合や read_only=True の場合は required=False となる(デフォルト値は True:フィールド必須)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"139:1-139:168\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"139:2-139:12\"\u003e\u003ccode\u003edefault\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"139:14-139:167\"\u003e登録・更新時にフィールドが指定されなかったときに使われるデフォルト値(ただし一部更新のときは使われない)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"140:1-140:239\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"140:2-140:15\"\u003e\u003ccode\u003eallow_null\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"140:17-140:238\"\u003e入力値に null を許可するかどうか。False の場合、登録・更新・一部更新時に入力値が null になっているとバリデーション NG となる(デフォルト値は False:許可しない)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"141:1-141:99\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"141:2-141:16\"\u003e\u003ccode\u003eallow_blank\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"141:18-141:98\"\u003e入力値に空文字を許可するかどうか。CharField などで利用可\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"142:1-142:255\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"142:2-142:11\"\u003e\u003ccode\u003esource\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"142:13-142:254\"\u003e値を出力する際の参照先をデフォルトから変更したい場合に使う。関連モデルの属性をドット区切りで指定したりすることも可能。参照先が見つからない場合は default の値を使う\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"143:1-143:89\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"143:2-143:15\"\u003e\u003ccode\u003emax_length\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"143:17-143:88\"\u003eCharField などのバリデーションで利用される最大桁数\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"144:1-144:89\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"144:2-144:15\"\u003e\u003ccode\u003emin_length\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"144:17-144:88\"\u003eCharField などのバリデーションで利用される最小桁数\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"145:1-145:97\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"145:2-145:14\"\u003e\u003ccode\u003emax_value\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"145:16-145:96\"\u003eIntegerField や FloatField のバリデーションで利用される最大値\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"146:1-146:97\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"146:2-146:14\"\u003e\u003ccode\u003emin_value\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"146:16-146:96\"\u003eIntegerField や FloatField のバリデーションで利用される最小値\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"147:1-147:97\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"147:2-147:15\"\u003e\u003ccode\u003evalidators\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"147:17-147:96\"\u003e文字種チェックなどのバリデーション。list や tuple で指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"148:1-148:101\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"148:2-148:19\"\u003e\u003ccode\u003eerror_messages\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"148:21-148:100\"\u003eバリデーション NG の場合のエラーメッセージ。dict で指定\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"150:1-150:105\"\u003e\n\u003cspan id=\"実践モデルに存在しない動的な計算値を追加するserializermethodfield\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%B7%B5%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84%E5%8B%95%E7%9A%84%E3%81%AA%E8%A8%88%E7%AE%97%E5%80%A4%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8Bserializermethodfield\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【実践】モデルに存在しない動的な計算値を追加する（\u003ccode\u003eSerializerMethodField\u003c/code\u003e）\u003c/h3\u003e\n\u003cp data-sourcepos=\"152:1-153:138\"\u003e実務では、「モデルのテーブルには保存していないが、フロントエンドに計算結果や加工した値を返したい」というケースが頻繁にあります。\u003cbr\u003e\n（例：文字数のカウント、経過時間のフォーマット、複数項目の結合、権限に応じた判定フラグなど）\u003c/p\u003e\n\u003cp data-sourcepos=\"155:1-155:65\"\u003eこれを実現するのが \u003cstrong\u003e\u003ccode\u003eSerializerMethodField\u003c/code\u003e\u003c/strong\u003e です。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"157:1-157:99\"\u003e\n\u003cspan id=\"-実装例メッセージに文字数や表示用要約を動的追加する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E5%AE%9F%E8%A3%85%E4%BE%8B%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AB%E6%96%87%E5%AD%97%E6%95%B0%E3%82%84%E8%A1%A8%E7%A4%BA%E7%94%A8%E8%A6%81%E7%B4%84%E3%82%92%E5%8B%95%E7%9A%84%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 実装例：メッセージに「文字数」や「表示用要約」を動的追加する\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"159:1-190:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ① SerializerMethodField を定義（自動的に読み取り専用 read_only=True になる）\n\u003c/span\u003e    \u003cspan class=\"n\"\u003econtent_length\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eSerializerMethodField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eformatted_summary\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eSerializerMethodField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eid\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esender\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003erecipient\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003econtent\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003econtent_length\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e     \u003cspan class=\"c1\"\u003e# ② fields に追加\n\u003c/span\u003e            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eformatted_summary\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# ② fields に追加\n\u003c/span\u003e        \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eread_only_fields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eid\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ③ 「get_\u0026lt;フィールド名\u0026gt;(self, obj)」の命名規則でメソッドを定義\n\u003c/span\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_content_length\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eメッセージ本文の文字数を計算して返す\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econtent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_formatted_summary\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"nb\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eobj（Messageインスタンス）を使ってカスタム文字列を生成\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e[\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esender\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s\"\u003e] \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econtent\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"si\"\u003e:\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s\"\u003e...\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"192:1-192:39\"\u003e\n\u003cspan id=\"-ポイントと命名ルール\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%A8%E5%91%BD%E5%90%8D%E3%83%AB%E3%83%BC%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ ポイントと命名ルール\u003c/h4\u003e\n\u003col data-sourcepos=\"193:1-199:0\"\u003e\n\u003cli data-sourcepos=\"193:1-194:130\"\u003e\n\u003cstrong\u003eメソッド名の命名規則\u003c/strong\u003e:\u003cbr\u003e\nデフォルトでは \u003cstrong\u003e\u003ccode\u003eget_\u0026lt;フィールド名\u0026gt;(self, obj)\u003c/code\u003e\u003c/strong\u003e という名前のメソッドが自動的に呼ばれます。\u003c/li\u003e\n\u003cli data-sourcepos=\"195:1-196:181\"\u003e\n\u003cstrong\u003e引数 \u003ccode\u003eobj\u003c/code\u003e\u003c/strong\u003e:\u003cbr\u003e\nシリアライズ対象の \u003cstrong\u003eモデルインスタンス\u003c/strong\u003e が渡されます（\u003ccode\u003eobj.sender\u003c/code\u003e や \u003ccode\u003eobj.content\u003c/code\u003e などモデルの属性に自由にアクセスできます）。\u003c/li\u003e\n\u003cli data-sourcepos=\"197:1-199:0\"\u003e\n\u003cstrong\u003e読み取り専用（Read-Only）\u003c/strong\u003e:\u003cbr\u003e\n\u003ccode\u003eSerializerMethodField\u003c/code\u003e は \u003cstrong\u003e出力（GET）専用\u003c/strong\u003e です。クライアントからの新規登録や更新（POST / PUT / PATCH）では自動的に無視されます。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 data-sourcepos=\"200:1-200:102\"\u003e\n\u003cspan id=\"️-実務での重要注意点nonenull対策を怠ると-500-エラーになる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%B8%8F-%E5%AE%9F%E5%8B%99%E3%81%A7%E3%81%AE%E9%87%8D%E8%A6%81%E6%B3%A8%E6%84%8F%E7%82%B9nonenull%E5%AF%BE%E7%AD%96%E3%82%92%E6%80%A0%E3%82%8B%E3%81%A8-500-%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AB%E3%81%AA%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e⚠️ 実務での重要注意点：\u003ccode\u003eNone\u003c/code\u003e（Null）対策を怠ると 500 エラーになる！\u003c/h4\u003e\n\u003cp data-sourcepos=\"202:1-202:249\"\u003e\u003ccode\u003eSerializerMethodField\u003c/code\u003e の実装で最も初心者が踏みやすい罠が、\u003cstrong\u003e「値が \u003ccode\u003eNone\u003c/code\u003e のケースを考慮せずにメソッドや属性を呼び出し、本番環境で \u003ccode\u003e500 Internal Server Error\u003c/code\u003e を引き起こす」\u003c/strong\u003e ことです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"204:1-214:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# ❌ NG 例: リレーション先やフィールドが None（Null）だと即クラッシュ！\n\u003c/span\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_recipient_name\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# recipient が NULL（未設定）のレコードが存在すると、\n\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# AttributeError: 'NoneType' object has no attribute 'name' で 500 エラー！\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erecipient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_formatted_created_at\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# created_at が None の場合、同様にクラッシュ\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estrftime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e%Y/%m/%d %H:%M\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"216:1-216:190\"\u003e実務では、以下のように \u003cstrong\u003e事前に \u003ccode\u003eNone\u003c/code\u003e チェック（ガード節や三項演算子）を挟み、安全なデフォルト値や \u003ccode\u003eNone\u003c/code\u003e を返す\u003c/strong\u003e ように徹底します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"218:1-237:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# ⭕ OK 例: 安全なハンドリング\n\u003c/span\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_recipient_name\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"nb\"\u003estr\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eリレーション先が存在しない場合は安全に None や代替文字列を返す\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erecipient\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e未設定\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# または None\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erecipient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_formatted_created_at\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"nb\"\u003estr\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003e日付が未設定の場合は None を返す\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecreated_at\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estrftime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e%Y/%m/%d %H:%M\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_content_length\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003e文字列が None の場合は 0 を返す\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econtent\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econtent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"239:1-242:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"240:1-241:319\"\u003e\u003cstrong\u003e💡 鉄則：SerializerMethodField 内の属性アクセスは常に None を疑う\u003c/strong\u003e\u003cbr\u003e\n「開発環境ではテストデータが入っているから動いていたが、本番環境で NULL カラムを持つレコードが1件でも入った瞬間に一覧 API 全体が 500 で落ちる」というのは実務で極めて頻出するバグです。必ず \u003ccode\u003eNone\u003c/code\u003e の安全対策を行いましょう。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"246:1-246:58\"\u003e\n\u003cspan id=\"3-webapi-のリクエストレスポンス全体像\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-webapi-%E3%81%AE%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E5%85%A8%E4%BD%93%E5%83%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. WebAPI のリクエスト〜レスポンス全体像\u003c/h2\u003e\n\u003cp data-sourcepos=\"248:1-248:124\"\u003eWebAPI の通信全体の中で、シリアライザがどこでどう動いているのかを俯瞰してみましょう。\u003c/p\u003e\n\n\u003cp data-sourcepos=\"251:1-251:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F1450b210-417f-4922-8e51-182ef0e9bcb6.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=049200dd4f556157d9a11579d12e164c\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F1450b210-417f-4922-8e51-182ef0e9bcb6.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=049200dd4f556157d9a11579d12e164c\" alt=\"0934b0c7-6947-4176-8e08-a34276dbb60a.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2F1450b210-417f-4922-8e51-182ef0e9bcb6.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=eeb49e0f3eb03723106869f57fab9609 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/1450b210-417f-4922-8e51-182ef0e9bcb6.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003col data-sourcepos=\"253:1-259:0\"\u003e\n\u003cli data-sourcepos=\"253:1-253:84\"\u003e\n\u003cstrong\u003eParser\u003c/strong\u003e: JSON文字列を解析し、Python辞書（\u003ccode\u003erequest.data\u003c/code\u003e）へ変換\u003c/li\u003e\n\u003cli data-sourcepos=\"254:1-254:98\"\u003e\n\u003cstrong\u003eSerializerに渡す\u003c/strong\u003e: 生データをシリアライザへセット（\u003ccode\u003edata=request.data\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"255:1-255:78\"\u003e\n\u003cstrong\u003eバリデーション\u003c/strong\u003e: \u003ccode\u003eserializer.is_valid()\u003c/code\u003e で型や制約を検証\u003c/li\u003e\n\u003cli data-sourcepos=\"256:1-256:71\"\u003e\n\u003cstrong\u003e保存\u003c/strong\u003e: \u003ccode\u003eserializer.save()\u003c/code\u003e でデータベースへ書き込み\u003c/li\u003e\n\u003cli data-sourcepos=\"257:1-257:98\"\u003e\n\u003cstrong\u003e出力用データへ\u003c/strong\u003e: 保存後のモデルをPython辞書（\u003ccode\u003eserializer.data\u003c/code\u003e）へ変換\u003c/li\u003e\n\u003cli data-sourcepos=\"258:1-259:0\"\u003e\n\u003cstrong\u003eRenderer\u003c/strong\u003e: Python辞書をJSON文字列にしてクライアントへ返送\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 data-sourcepos=\"260:1-260:50\"\u003e\n\u003cspan id=\"-コード例入力側の処理フロー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B%E5%85%A5%E5%8A%9B%E5%81%B4%E3%81%AE%E5%87%A6%E7%90%86%E3%83%95%E3%83%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ コード例：入力側の処理フロー\u003c/h3\u003e\n\u003cp data-sourcepos=\"261:1-261:158\"\u003e上記の「1. Parser ➔ 2. Serializer ➔ 3. バリデーション」という入力側の流れをコードで追うと、以下のようになります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"263:1-278:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003eio\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eBytesIO\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.parsers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eJSONParser\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 1. Parser: JSON文字列をパースしてPython辞書に変換\n\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eJSONParser\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nc\"\u003eBytesIO\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejson_string\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e()))\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 2. Serializerに渡す: 生データをシリアライザへセット\n\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# 3. バリデーション: 入力値を検証\n\u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n    \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 検証成功: クリーンなデータ\n\u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e          \u003cspan class=\"c1\"\u003e# 検証失敗: エラー内容\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"280:1-280:297\"\u003e※実務の View（APIView や ViewSet）では、Parser の呼び出しは裏側で自動化されており \u003ccode\u003erequest.data\u003c/code\u003e で受け取れますが、DRF の内部動作を追うとこのように非常に素直なパイプラインでデータが処理されていることが分かります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"282:1-282:96\"\u003e\n\u003cspan id=\"-実務で差がつくデータの流れparserを押さえるべき理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E5%AE%9F%E5%8B%99%E3%81%A7%E5%B7%AE%E3%81%8C%E3%81%A4%E3%81%8F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%B5%81%E3%82%8Cparser%E3%82%92%E6%8A%BC%E3%81%95%E3%81%88%E3%82%8B%E3%81%B9%E3%81%8D%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e💡 実務で差がつく！「データの流れ（Parser）」を押さえるべき理由\u003c/h3\u003e\n\u003cp data-sourcepos=\"283:1-283:201\"\u003e「普段 View を書くときは \u003ccode\u003erequest.data\u003c/code\u003e と書くだけで自動で動くのに、なぜ Parser やデータの流れなんて知る必要があるの？」と思ったかもしれません。\u003c/p\u003e\n\u003cp data-sourcepos=\"285:1-285:210\"\u003eしかし、この \u003cstrong\u003eデータの流れを理解していないと絶対に自力で解決できない実務の代表例\u003c/strong\u003e が、まさに \u003cstrong\u003e「画像のアップロード（ファイル送信）」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"287:1-287:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe466feeb-85ac-44ff-9e9d-2cb6d733793e.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=35a5cd5b961ab25d53a5eba3a8237e7a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe466feeb-85ac-44ff-9e9d-2cb6d733793e.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=35a5cd5b961ab25d53a5eba3a8237e7a\" alt=\"80550749-0d03-4e17-9f3b-36b07e8cfb83.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe466feeb-85ac-44ff-9e9d-2cb6d733793e.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=c862f265d1e8f668d68ce2e653d13dc1 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e466feeb-85ac-44ff-9e9d-2cb6d733793e.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch4 data-sourcepos=\"289:1-289:81\"\u003e\n\u003cspan id=\"実例ファイル送信multipartform-dataで必須になる知識\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E4%BE%8B%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E9%80%81%E4%BF%A1multipartform-data%E3%81%A7%E5%BF%85%E9%A0%88%E3%81%AB%E3%81%AA%E3%82%8B%E7%9F%A5%E8%AD%98\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実例：ファイル送信（multipart/form-data）で必須になる知識\u003c/h4\u003e\n\u003cp data-sourcepos=\"290:1-290:237\"\u003eフロントエンドから「タイトル（文字）」と一緒に「画像ファイル（バイナリ）」をまとめて送信する場合、リクエストの形式は JSON ではなく \u003cstrong\u003e\u003ccode\u003emultipart/form-data\u003c/code\u003e\u003c/strong\u003e になります。\u003c/p\u003e\n\u003col data-sourcepos=\"292:1-298:0\"\u003e\n\u003cli data-sourcepos=\"292:1-292:155\"\u003e\n\u003cstrong\u003eクライアント\u003c/strong\u003e: \u003ccode\u003emultipart/form-data\u003c/code\u003e 形式で、文字データ（\u003ccode\u003etitle\u003c/code\u003e, \u003ccode\u003eprompt\u003c/code\u003e）とファイル（\u003ccode\u003ephoto.jpg\u003c/code\u003e）をまとめて送信。\u003c/li\u003e\n\u003cli data-sourcepos=\"293:1-294:98\"\u003e\n\u003cstrong\u003eMultiPartParser\u003c/strong\u003e: リクエストを分解し、文字データ（\u003ccode\u003erequest.data\u003c/code\u003e）とファイルデータ（\u003ccode\u003erequest.FILES\u003c/code\u003e）を取り出す。\u003cbr\u003e\n※DRF では \u003ccode\u003erequest.data\u003c/code\u003e 経由でファイルを直接扱えるよう統合されます。\u003c/li\u003e\n\u003cli data-sourcepos=\"295:1-295:142\"\u003e\n\u003cstrong\u003eSerializer\u003c/strong\u003e: 取り出された入力値（文字＋ファイル）の型・必須・ファイルサイズ・拡張子などを検証。\u003c/li\u003e\n\u003cli data-sourcepos=\"296:1-296:138\"\u003e\n\u003cstrong\u003e保存処理\u003c/strong\u003e: 実ファイルをストレージへ保存し、ファイルパスやメタデータをデータベースへ保存。\u003c/li\u003e\n\u003cli data-sourcepos=\"297:1-298:0\"\u003e\n\u003cstrong\u003eレスポンス\u003c/strong\u003e: 保存結果をクライアントへ返却。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"299:1-299:224\"\u003eこのとき、もしデフォルトの \u003ccode\u003eJSONParser\u003c/code\u003e しか知らなければ、\u003cstrong\u003e「なぜ画像を送ると 415 Unsupported Media Type やバリデーションエラーになるのか？」\u003c/strong\u003e が全く分かりません。\u003c/p\u003e\n\u003cp data-sourcepos=\"301:1-301:163\"\u003eリクエストを正しくパースして Serializer に届けるために、View 側で \u003cstrong\u003e\u003ccode\u003eMultiPartParser\u003c/code\u003e\u003c/strong\u003e を明示的に指定する必要があります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"303:1-315:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.parsers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMultiPartParser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eJSONParser\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.viewsets\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eImage\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eImageSerializer\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eImageViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelViewSet\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eImage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eImageSerializer\u003c/span\u003e\n    \u003cspan class=\"n\"\u003epagination_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eImagePagination\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ★ ここ！データの流れ（Parser）を理解していないと、なぜこれが必要か分からない！\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eparser_classes\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eMultiPartParser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eJSONParser\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"317:1-321:0\"\u003e\n\u003cli data-sourcepos=\"317:1-318:189\"\u003e\n\u003cstrong\u003eデータの流れを知らない人\u003c/strong\u003e:\u003cbr\u003e\n「画像送信でなぜかエラーが出る…ネットに \u003ccode\u003eparser_classes = [MultiPartParser, ...]\u003c/code\u003e と書けとあったからコピペした（動いたけれど仕組みは謎）」\u003c/li\u003e\n\u003cli data-sourcepos=\"319:1-321:0\"\u003e\n\u003cstrong\u003eデータの流れを理解している人\u003c/strong\u003e:\u003cbr\u003e\n「今回は \u003ccode\u003emultipart/form-data\u003c/code\u003e だから、Serializer にデータが届く前に \u003ccode\u003eMultiPartParser\u003c/code\u003e で分解して \u003ccode\u003erequest.data\u003c/code\u003e を生成してあげる必要があるんだな」\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"322:1-322:332\"\u003eこのように、\u003cstrong\u003e「リクエストがどうパースされ、どうシリアライザに渡されるか」というデータのパイプラインを把握していること\u003c/strong\u003e こそが、コピペプログラマから脱却し、実務でトラブルを自力解決できるようになるための必須の土台なのです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"325:1-325:65\"\u003e\n\u003cspan id=\"4-serializer-内部の処理サイクル全体の流れ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-serializer-%E5%86%85%E9%83%A8%E3%81%AE%E5%87%A6%E7%90%86%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E5%85%A8%E4%BD%93%E3%81%AE%E6%B5%81%E3%82%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. Serializer 内部の処理サイクル（全体の流れ）\u003c/h2\u003e\n\u003cp data-sourcepos=\"327:1-327:96\"\u003e続いて、シリアライザ内部でのデータの状態変化にズームインします。\u003c/p\u003e\n\n\u003cp data-sourcepos=\"330:1-330:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fa3b97d2a-27f9-4f47-9066-63bfcc69164f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=4d26a4fe7f06868c1929785b9293ee71\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fa3b97d2a-27f9-4f47-9066-63bfcc69164f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=4d26a4fe7f06868c1929785b9293ee71\" alt=\"74fdce93-f73d-463f-bdbf-b6aa391ea0f3.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fa3b97d2a-27f9-4f47-9066-63bfcc69164f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5ef54e9167cbf22621890a1f9908502b 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/a3b97d2a-27f9-4f47-9066-63bfcc69164f.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003col data-sourcepos=\"332:1-339:0\"\u003e\n\u003cli data-sourcepos=\"332:1-332:90\"\u003e\n\u003cstrong\u003e入力\u003c/strong\u003e: 生データを渡す（\u003ccode\u003eserializer = TaskSerializer(data=request.data)\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"333:1-333:63\"\u003e\n\u003cstrong\u003eバリデーション\u003c/strong\u003e: \u003ccode\u003eserializer.is_valid()\u003c/code\u003e を実行\u003c/li\u003e\n\u003cli data-sourcepos=\"334:1-334:92\"\u003e\n\u003cstrong\u003e検証済みデータ\u003c/strong\u003e: 合格した値だけが \u003ccode\u003eserializer.validated_data\u003c/code\u003e に入る\u003c/li\u003e\n\u003cli data-sourcepos=\"335:1-335:94\"\u003e\n\u003cstrong\u003e保存\u003c/strong\u003e: \u003ccode\u003eserializer.save()\u003c/code\u003e でモデルを作成・更新（\u003ccode\u003ecreate()\u003c/code\u003e / \u003ccode\u003eupdate()\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"336:1-336:97\"\u003e\n\u003cstrong\u003eモデル保持\u003c/strong\u003e: \u003ccode\u003eserializer.instance\u003c/code\u003e に保存後のモデルオブジェクトを保持\u003c/li\u003e\n\u003cli data-sourcepos=\"337:1-339:0\"\u003e\n\u003cstrong\u003e出力生成\u003c/strong\u003e: \u003ccode\u003eserializer.data\u003c/code\u003e で出力用データを生成し、JSONレスポンスとして返却\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"340:1-340:63\"\u003e\n\u003cspan id=\"5-入力処理新規登録時のデシリアライズ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E5%85%A5%E5%8A%9B%E5%87%A6%E7%90%86%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2%E6%99%82%E3%81%AE%E3%83%87%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 入力処理（新規登録時のデシリアライズ）\u003c/h2\u003e\n\u003cp data-sourcepos=\"342:1-342:127\"\u003e新規作成（POST）の場面で、生データを受け取ってモデルへ保存するまでの「入力処理」です。\u003c/p\u003e\n\n\u003cp data-sourcepos=\"345:1-345:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe57aa6fe-8f90-44ed-a70d-ba3860e5931f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=571e88cd03586df5d04bad0ac32b1aeb\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe57aa6fe-8f90-44ed-a70d-ba3860e5931f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=571e88cd03586df5d04bad0ac32b1aeb\" alt=\"7399a2dc-b786-4882-8bf1-24361e2a3949.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe57aa6fe-8f90-44ed-a70d-ba3860e5931f.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5560931f68c3731e7e8d09a8337bcf3e 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e57aa6fe-8f90-44ed-a70d-ba3860e5931f.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"347:1-353:0\"\u003e\n\u003cli data-sourcepos=\"347:1-347:89\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer = UserSerializer(data=request.data)\u003c/code\u003e\u003c/strong\u003e: 生の入力データをセット\u003c/li\u003e\n\u003cli data-sourcepos=\"348:1-348:85\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.is_valid()\u003c/code\u003e\u003c/strong\u003e: ルールに沿って型・必須・文字数を検証\u003c/li\u003e\n\u003cli data-sourcepos=\"349:1-349:80\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.validated_data\u003c/code\u003e\u003c/strong\u003e: 検証を通過したクリーンなデータ\u003c/li\u003e\n\u003cli data-sourcepos=\"350:1-350:75\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.save()\u003c/code\u003e\u003c/strong\u003e: データベースへ新規レコードを作成\u003c/li\u003e\n\u003cli data-sourcepos=\"351:1-353:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.instance\u003c/code\u003e\u003c/strong\u003e: 保存が完了したモデルインスタンスを保持\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"354:1-354:58\"\u003e\n\u003cspan id=\"6-更新処理instance--data-による-update\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E6%9B%B4%E6%96%B0%E5%87%A6%E7%90%86instance--data-%E3%81%AB%E3%82%88%E3%82%8B-update\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. 更新処理（instance ＋ data による update）\u003c/h2\u003e\n\u003cp data-sourcepos=\"356:1-356:98\"\u003e既存レコードを更新（PUT / PATCH）する場面でのシリアライザの動きです。\u003c/p\u003e\n\n\u003cp data-sourcepos=\"359:1-359:158\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fae1422d8-1247-4fcf-8944-05ec02aed399.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c3fd5acc423e5e02f85543eff03d34a3\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fae1422d8-1247-4fcf-8944-05ec02aed399.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c3fd5acc423e5e02f85543eff03d34a3\" alt=\"ChatGPT-Image-2026年9月18日-19_50_04.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fae1422d8-1247-4fcf-8944-05ec02aed399.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5d86e2594aa05e33b4deb7db95286216 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/ae1422d8-1247-4fcf-8944-05ec02aed399.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"361:1-366:0\"\u003e\n\u003cli data-sourcepos=\"361:1-362:115\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer = UserSerializer(instance=user, data=request.data)\u003c/code\u003e\u003c/strong\u003e:\u003cbr\u003e\n新規作成との違いは \u003cstrong\u003e第1引数に更新対象の既存モデル（\u003ccode\u003einstance\u003c/code\u003e）を渡す点\u003c/strong\u003e です。\u003c/li\u003e\n\u003cli data-sourcepos=\"363:1-363:52\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.is_valid()\u003c/code\u003e\u003c/strong\u003e: 変更内容を検証\u003c/li\u003e\n\u003cli data-sourcepos=\"364:1-364:125\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.save()\u003c/code\u003e\u003c/strong\u003e: 既存の \u003ccode\u003einstance\u003c/code\u003e に対して自動的に \u003ccode\u003eupdate()\u003c/code\u003e が走り、差分がDBに反映される\u003c/li\u003e\n\u003cli data-sourcepos=\"365:1-366:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eserializer.instance\u003c/code\u003e\u003c/strong\u003e: 更新後の最新モデルオブジェクトが手に入る\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"367:1-367:86\"\u003e\n\u003cspan id=\"-コード例update-の動作検証更新前後の変化を可視化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8Bupdate-%E3%81%AE%E5%8B%95%E4%BD%9C%E6%A4%9C%E8%A8%BC%E6%9B%B4%E6%96%B0%E5%89%8D%E5%BE%8C%E3%81%AE%E5%A4%89%E5%8C%96%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ コード例：update() の動作検証（更新前後の変化を可視化）\u003c/h3\u003e\n\u003cp data-sourcepos=\"368:1-368:251\"\u003e\u003ccode\u003eserializer.save()\u003c/code\u003e を呼び出した際、裏側で \u003ccode\u003eupdate()\u003c/code\u003e メソッドがどのように既存インスタンスを受け取り、更新を行っているかを実際に \u003ccode\u003eupdate()\u003c/code\u003e をオーバーライドして可視化してみましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"370:1-415:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e__all__\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# 更新前のインスタンスデータを出力\n\u003c/span\u003e        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e--- 更新前 (instance) ---\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eデータ: \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003e__dict__\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# 入力されたデータを出力\n\u003c/span\u003e        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e--- 入力データ (validated_data) ---\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eデータ: \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# 親クラスの update メソッドを呼び出してインスタンスを更新\n\u003c/span\u003e        \u003cspan class=\"n\"\u003einstance\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# 更新後のインスタンスデータを出力\n\u003c/span\u003e        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e--- 更新後 (instance) ---\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eデータ: \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003e__dict__\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\n\n\n\u003cspan class=\"c1\"\u003e# テスト用のコードを追加\n\u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003e__name__\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e__main__\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 既存のインスタンスを取得\n\u003c/span\u003e    \u003cspan class=\"n\"\u003einstance\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efirst\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 最初のMessageインスタンスを取得\n\u003c/span\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eMessageインスタンスが存在しません。データベースにデータを追加してください。\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# 更新用のデータ\n\u003c/span\u003e        \u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esender\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eupdated_sender\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003erecipient\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eupdated_recipient\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003econtent\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eUpdated content\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# シリアライザを使用して更新（partial=True で一部更新）\n\u003c/span\u003e        \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epartial\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n            \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# ここで update メソッドが呼び出される\n\u003c/span\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eバリデーションエラー:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"417:1-417:35\"\u003e\n\u003cspan id=\"ターミナル実行結果\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eターミナル実行結果：\u003c/h4\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"418:1-428:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e(venv) PS C:\\Users\\...\\backend\u0026gt; python a.py\n--- 更新前 (instance) ---\nデータ: {'_state': \u0026lt;django.db.models.base.ModelState object at 0x...\u0026gt;, ...: datetime.datetime(2026, 5, 13, 12, 55, 37, 36593, tzinfo=datetime.timezone.utc)}\n\n--- 入力データ (validated_data) ---\nデータ: {'sender': 'updated_sender', 'recipient': 'updated_recipient', 'content': 'Updated content'}\n\n--- 更新後 (instance) ---\nデータ: {'_state': \u0026lt;django.db.models.base.ModelState object at 0x...\u0026gt;, ...: datetime.datetime(2026, 5, 13, 12, 55, 37, 36593, tzinfo=datetime.timezone.utc)}\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"430:1-430:287\"\u003eこのように、\u003ccode\u003einstance\u003c/code\u003e（更新前データ）に対して入力された \u003ccode\u003evalidated_data\u003c/code\u003e がマージされ、\u003ccode\u003esuper().update()\u003c/code\u003e によってデータベースへ保存されて最新のインスタンスへと書き換わっていることがはっきりと確認できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"432:1-432:100\"\u003e\n\u003cspan id=\"️-実務の落とし穴patch一部更新と相関バリデーションの注意点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%B8%8F-%E5%AE%9F%E5%8B%99%E3%81%AE%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4patch%E4%B8%80%E9%83%A8%E6%9B%B4%E6%96%B0%E3%81%A8%E7%9B%B8%E9%96%A2%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e⚠️ 実務の落とし穴：PATCH（一部更新）と相関バリデーションの注意点\u003c/h3\u003e\n\u003cp data-sourcepos=\"434:1-434:196\"\u003e実務で非常に多くのエンジニアが直面するトラブルが、\u003cstrong\u003e「PATCH（一部更新）時の複数フィールドをまたぐバリデーション（\u003ccode\u003evalidate()\u003c/code\u003e）」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"436:1-439:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"437:1-438:167\"\u003e\u003cstrong\u003e【超重要】PATCH リクエストでは「変更する項目しか送られてこない」！\u003c/strong\u003e\u003cbr\u003e\nそのため、送られてきていない項目を \u003ccode\u003eattrs\u003c/code\u003e から直接取得しようとすると \u003ccode\u003eNone\u003c/code\u003e になってしまい、正しい判定ができません。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch4 data-sourcepos=\"441:1-441:77\"\u003e\n\u003cspan id=\"1-発生する問題attrs-に未送信の項目が含まれない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B%E5%95%8F%E9%A1%8Cattrs-%E3%81%AB%E6%9C%AA%E9%80%81%E4%BF%A1%E3%81%AE%E9%A0%85%E7%9B%AE%E3%81%8C%E5%90%AB%E3%81%BE%E3%82%8C%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 発生する問題（attrs に未送信の項目が含まれない）\u003c/h4\u003e\n\u003cp data-sourcepos=\"442:1-442:139\"\u003e例えば、以下のように「開始日（\u003ccode\u003estart_date\u003c/code\u003e）と終了日（\u003ccode\u003eend_date\u003c/code\u003e）」を持つ既存データがあるとします。\u003c/p\u003e\n\u003ctable data-sourcepos=\"444:1-449:60\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"444:1-444:133\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"444:2-444:9\"\u003e項目\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"444:11-444:32\"\u003eDBの既存データ\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"444:34-444:94\"\u003eクライアントからのPATCH（開始日のみ更新）\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"444:96-444:132\"\u003e\n\u003ccode\u003evalidate(self, attrs)\u003c/code\u003e の \u003ccode\u003eattrs\u003c/code\u003e\n\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"446:1-446:67\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"446:2-446:9\"\u003e\u003ccode\u003ename\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"446:11-446:24\"\u003e山田太郎\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"446:26-446:42\"\u003e（未送信）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"446:44-446:66\"\u003e（含まれない）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"447:1-447:65\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"447:2-447:15\"\u003e\u003ccode\u003estart_date\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"447:17-447:28\"\u003e2024-06-01\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"447:30-447:45\"\u003e\u003cstrong\u003e2024-06-10\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"447:47-447:64\"\u003e\u003cstrong\u003e\u003ccode\u003e2024-06-10\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"448:1-448:81\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"448:2-448:13\"\u003e\u003ccode\u003eend_date\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"448:15-448:26\"\u003e2024-06-30\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"448:28-448:44\"\u003e（未送信）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"448:46-448:80\"\u003e\u003cstrong\u003e❌ 含まれない（None）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"449:1-449:60\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"449:2-449:13\"\u003e\u003ccode\u003ecategory\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"449:15-449:17\"\u003eA\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"449:19-449:35\"\u003e（未送信）\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"449:37-449:59\"\u003e（含まれない）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"451:1-451:151\"\u003eこのとき、「開始日 \u0026lt;= 終了日」をチェックしようとして以下のように書いてしまうと、バグの原因になります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"453:1-462:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estart_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eend_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# ❌ PATCHで未送信だと None になる！\n\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# end_date が None のため、比較処理がスルーされて不正な日付でも通過してしまう！\n\u003c/span\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eValidationError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e開始日は終了日以前を指定してください。\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"464:1-464:99\"\u003e\n\u003cspan id=\"2-解決策既存インスタンスselfinstanceから値を取得して補完する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E8%A7%A3%E6%B1%BA%E7%AD%96%E6%97%A2%E5%AD%98%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9selfinstance%E3%81%8B%E3%82%89%E5%80%A4%E3%82%92%E5%8F%96%E5%BE%97%E3%81%97%E3%81%A6%E8%A3%9C%E5%AE%8C%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 解決策：既存インスタンス（self.instance）から値を取得して補完する\u003c/h4\u003e\n\u003cp data-sourcepos=\"465:1-466:200\"\u003eシリアライザは更新時、\u003cstrong\u003e\u003ccode\u003eself.instance\u003c/code\u003e（更新前の既存インスタンス）を保持\u003c/strong\u003e しています。\u003cbr\u003e\nそのため、「\u003cstrong\u003e今回送られてきた値があればそれ（\u003ccode\u003eattrs\u003c/code\u003e）を使い、なければ既存インスタンス（\u003ccode\u003eself.instance\u003c/code\u003e）から取得する\u003c/strong\u003e」という設計にします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"468:1-492:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eEventSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEvent\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e'\u003c/span\u003e\u003cspan class=\"s\"\u003e__all__\u003c/span\u003e\u003cspan class=\"sh\"\u003e'\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003e_resolve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efield_name\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003e今回の送信値があればそれを使い、なければ既存インスタンスから取得するヘルパー\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003efield_name\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003efield_name\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"nf\"\u003ehasattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efield_name\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003egetattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einstance\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efield_name\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# 今回の送信値 or 既存インスタンスの値を取得\n\u003c/span\u003e        \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003e_resolve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estart_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003e_resolve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eend_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"c1\"\u003e# PATCH で片方しか送られてこなくても正確に日付順序をチェック可能！\n\u003c/span\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eValidationError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e開始日は終了日以前の日付を指定してください。\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"494:1-494:41\"\u003e\n\u003cspan id=\"補完後の完成形イメージ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A3%9C%E5%AE%8C%E5%BE%8C%E3%81%AE%E5%AE%8C%E6%88%90%E5%BD%A2%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e補完後の完成形イメージ：\u003c/h4\u003e\n\u003ctable data-sourcepos=\"495:1-499:69\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"495:1-495:76\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"495:2-495:18\"\u003eフィールド\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"495:20-495:57\"\u003eバリデーション時の参照値\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"495:59-495:75\"\u003e値の取得元\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"497:1-497:72\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"497:2-497:15\"\u003e\u003ccode\u003estart_date\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"497:17-497:30\"\u003e\u003ccode\u003e2024-06-10\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"497:32-497:71\"\u003e\u003cstrong\u003eリクエストの値（\u003ccode\u003eattrs\u003c/code\u003e）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"498:1-498:78\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"498:2-498:13\"\u003e\u003ccode\u003eend_date\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"498:15-498:28\"\u003e\u003ccode\u003e2024-06-30\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"498:30-498:77\"\u003e\u003cstrong\u003e既存データの値（\u003ccode\u003eself.instance\u003c/code\u003e）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"499:1-499:69\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"499:2-499:13\"\u003e\u003ccode\u003ecategory\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"499:15-499:19\"\u003e\u003ccode\u003eA\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"499:21-499:68\"\u003e\u003cstrong\u003e既存データの値（\u003ccode\u003eself.instance\u003c/code\u003e）\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"501:1-501:424\"\u003eこのように、\u003cstrong\u003e「一部更新（PATCH）の相関バリデーションでは、既存インスタンス（\u003ccode\u003eself.instance\u003c/code\u003e）を参照して未送信項目を補う」\u003c/strong\u003e という考え方は、実務開発において必須のテクニックです（※相関バリデーションの詳しい実装パターンや基底クラスでの共通化は、次回のバリデーション特集でじっくり扱います）。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"504:1-504:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"506:1-514:0\"\u003e\n\u003cli data-sourcepos=\"506:1-508:111\"\u003e\n\u003cstrong\u003e3つのシリアライザ\u003c/strong\u003e:\n\u003cul data-sourcepos=\"507:3-508:111\"\u003e\n\u003cli data-sourcepos=\"507:3-507:74\"\u003e基本は \u003cstrong\u003e\u003ccode\u003eModelSerializer\u003c/code\u003e\u003c/strong\u003e（モデル定義から自動生成）\u003c/li\u003e\n\u003cli data-sourcepos=\"508:3-508:111\"\u003e特殊な用途には \u003cstrong\u003e\u003ccode\u003eSerializer\u003c/code\u003e\u003c/strong\u003e（自由定義）、一覧には \u003cstrong\u003e\u003ccode\u003emany=True\u003c/code\u003e\u003c/strong\u003e（ListSerializer）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"509:1-511:82\"\u003e\n\u003cstrong\u003e新規作成と更新の違い\u003c/strong\u003e:\n\u003cul data-sourcepos=\"510:3-511:82\"\u003e\n\u003cli data-sourcepos=\"510:3-510:68\"\u003e新規作成: \u003ccode\u003eSerializer(data=...)\u003c/code\u003e ➔ \u003ccode\u003esave()\u003c/code\u003e で \u003ccode\u003ecreate()\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"511:3-511:82\"\u003e既存更新: \u003ccode\u003eSerializer(instance=..., data=...)\u003c/code\u003e ➔ \u003ccode\u003esave()\u003c/code\u003e で \u003ccode\u003eupdate()\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"512:1-514:0\"\u003e\n\u003cstrong\u003ePATCH（一部更新）の注意点\u003c/strong\u003e:\n\u003cul data-sourcepos=\"513:3-514:0\"\u003e\n\u003cli data-sourcepos=\"513:3-514:0\"\u003e未送信項目は \u003ccode\u003eattrs\u003c/code\u003e に含まれないため、既存インスタンス（\u003ccode\u003eself.instance\u003c/code\u003e）から補完する\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"515:1-516:366\"\u003eシリアライザの全体像と、リクエストからDB保存・レスポンスまでのライフサイクルを押さえておくことで、DRFの内部動作への理解が一気にクリアになります！\u003cbr\u003e\n次回は \u003cstrong\u003e\u003ca href=\"/items/04b2c3524ec379961d82\"\u003e【第七講】シリアライザのバリデーション完全攻略・is_validの仕組みとエラー制御\u003c/a\u003e\u003c/strong\u003e で、単一・複数フィールドのバリデーション定義やエラー制御、そして実務で役立つカスタムバリデーションの実践テクニックを深掘りしていきましょう！\u003c/p\u003e\n\u003chr data-sourcepos=\"518:1-519:0\"\u003e\n\u003cp data-sourcepos=\"520:1-520:246\"\u003e\u003cstrong\u003e\u003ca href=\"/items/c835d3ff5c824439f248\"\u003e← 第五講：マイグレーション完全攻略・Django管理画面とリレーション設計\u003c/a\u003e | \u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003eまとめページに戻る\u003c/a\u003e | \u003ca href=\"/items/04b2c3524ec379961d82\"\u003e第七講へ →\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n","body":"\n\u003e **Lecture 06: Deep Dive into Serializers — Architecture, The 3 Types \u0026 Update Lifecycle**\n\n[← DRF × Vue.js 実践講座：まとめページに戻る](/items/4250ea04fb61f5bc4f0d) | [← 第五講：マイグレーション完全攻略・Django管理画面とリレーション設計](/items/c835d3ff5c824439f248)\n\n## はじめに：なぜ DRF では「シリアライザ」が主役なのか？\n\n第五講では、マイグレーションの仕組みから Django Admin（管理画面）によるデータ登録、そしてテーブル間のリレーション設計までをマスターしました。データベースの準備は万全です！\n\nしかし、ここで一つの疑問が湧きます。  \n**「データベースにある Python のモデルオブジェクトは、どうやってフロントエンド（Vue.js）に届くのだろう？」**  \n**「逆に、Vue.js から送られてくる JSON データは、どうやって安全にデータベースへ保存されるのだろう？」**\n\nその疑問をすべて一身に引き受け、解決してくれるのが、今回学ぶ **DRF（Django REST Framework）最大の中核部品「シリアライザ（Serializer）」** です。\n\n本講では、シリアライザの仕組みを図解とともに整理していきます！\n\n\n\n## 1. そもそも「シリアライザ（Serializer）」とは？\n\nシリアライザ（Serializer）を一言で表すと、**「フロントエンド（Vue.js）とバックエンド（Django/DB）をつなぐ“通訳者（翻訳機）”」** です。\n\n![397e1a26-e26a-4744-8d4f-6220eb159cc5.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/76325194-6943-40cf-8fd9-0d4ceccb3874.avif)\n\nDjango REST Framework の内部では、下図のように **View が Serializer を使って Model とやり取り** を行っています。  \nシリアライザは、リクエスト側（入力）とレスポンス側（出力）でそれぞれ重要な変換と検証を担っています。\n\n### ▼ 1. 入力（リクエスト側）：デシリアライズと検証\n* **処理の流れ**: `Client（リクエスト）` ➔ `URL / Router` ➔ `View / ViewSet` ➔ **`Serializer（入力の検証）`** ➔ `Model` ➔ `Database`\n* **シリアライザの役割（ここが最重要！）**:  \n  クライアントから送られてきた生の JSON データを Python 辞書に復元し、**`is_valid()`** で型や文字数・必須項目などのルールに適合しているかを厳格に検証します。合格したクリーンなデータ（**`validated_data`**）のみがモデルへ渡され、データベースへ安全に保存されます。\n\n### ▼ 2. 出力（レスポンス側）：シリアライズと変換\n* **処理の流れ**: `Database` ➔ `Model` ➔ **`Serializer（出力の変換）`** ➔ `Response（レスポンス）` ➔ `Client（クライアントへ返す）`\n* **シリアライザの役割**:  \n  データベースから取り出した Python のモデルオブジェクトを、フロントエンド（Vue.js）が解釈できる JSON 形式へと変換します（内部的には **`to_representation()`** メソッドが動作）。変換されたデータがレスポンスとしてクライアントに返されます。\n\n\n## 2. シリアライザの3大分類\n\nDRF のシリアライザには、用途に応じて使い分ける **3つの代表的な種類** が用意されています。\n\n\u003c!-- 画像1: 3つのシリアライザ (Serializer / ModelSerializer / ListSerializer) --\u003e\n![12616ced-419b-4fdd-bb7e-f64185445127.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/95c65714-5635-4601-8fbb-7ac68a46d989.png)\n\n| 種類 | 特徴 | 主な使いどころ | 実務での比率 |\n| :--- | :--- | :--- | :--- |\n| **① Serializer** | フィールドを**自由定義** | 非DBデータ、外部API連携、認証・集計など | 約 10〜15% |\n| **② ModelSerializer** | **Modelから自動生成** | 通常のCRUD操作（一覧・詳細・登録・更新） | **約 80〜90%（主役）** |\n| **③ ListSerializer** | **複数件をまとめて処理** | 一覧取得（`many=True` 指定時に裏側で自動動作） | 内部利用 / 特殊用途 |\n\n### ■ ① Serializer（自由定義型）\nモデルに縛られず、必要なフィールド（`CharField`, `IntegerField` など）をゼロから手動で組み立てる汎用クラスです。  \nDBに保存しないログイン認証（ユーザー名・パスワードの受け取り）や、外部APIのデータ中継などで利用します。\n\n```python\nfrom rest_framework import serializers\n\nclass ContactFormSerializer(serializers.Serializer):\n    \"\"\"DBモデルと紐付かない、自由定義のシリアライザ\"\"\"\n    name = serializers.CharField(max_length=50)\n    email = serializers.EmailField()\n    message = serializers.CharField(max_length=1000)\n```\n\n### ■ ② ModelSerializer（Model自動生成型・実務の主役）\n実務で書くシリアライザのほとんどはこれです。  \n`class Meta:` にモデル名を指定するだけで、モデルのフィールド定義や制約（必須チェック・最大文字数など）を引き継ぎ、`create` / `update` の保存処理まで自動生成してくれます。\n\n```python\nfrom rest_framework import serializers\nfrom .models import Message\n\nclass MessageSerializer(serializers.ModelSerializer):\n    \"\"\"Modelから自動生成されるシリアライザ（実務の標準）\"\"\"\n    class Meta:\n        model = Message\n        fields = ['id', 'sender', 'recipient', 'content', 'created_at']\n        read_only_fields = ['id', 'created_at']\n```\n\n#### ▼ Meta クラスの設定一覧（主に ModelSerializer で使用）\n\n`ModelSerializer` では、内部の `class Meta` を使って、対象となるモデルやAPIで扱うフィールドなどの設定を定義します。\n\n`class Meta` 自体は通常の `Serializer` にも記述できますが、`model` や `fields` など、以下の設定は主に `ModelSerializer` で利用されます。\n\n| 設定                 | 意味                           |\n| ------------------ | ---------------------------- |\n| `model`            | 対象の Model を指定                |\n| `fields`           | APIで扱うフィールドを指定               |\n| `exclude`          | APIから除外するフィールドを指定            |\n| `read_only_fields` | 読み取り専用フィールドを指定               |\n| `extra_kwargs`     | 各フィールドに追加設定を指定               |\n| `depth`            | ForeignKey などの関連をどこまで展開するか指定 |\n| `validators`       | Serializer 全体に適用するバリデータを指定   |\n\n:::note info\n**💡 `extra_kwargs` と `validators` の実践的な使い方**  \nフィールド個別の正規表現チェック（`RegexValidator`）や、複数フィールドの組み合わせ一意性チェック（`UniqueTogetherValidator`）などの設定ベースのバリデーションは、次回の **[【第七講】シリアライザのバリデーション完全攻略](/items/04b2c3524ec379961d82)** で詳しく解説します。\n:::\n\n#### ▼ モデルとシリアライザの Field クラス対応表（自動マッピング規則）\n`ModelSerializer` がモデル定義をもとにフィールドを自動生成する際、Django モデルの Field クラスは以下のように DRF のシリアライザ Field クラスへと自動マッピングされます。  \n（※右側のシリアライザ Field クラス自体は、通常の `Serializer` でフィールドを手動定義する際にもそのまま使います）\n\n| モデルの Field クラス | シリアライザの Field クラス |\n| :--- | :--- |\n| `django.db.models.BooleanField` | `rest_framework.fields.BooleanField` |\n| `django.db.models.CharField` | `rest_framework.fields.CharField` |\n| `django.db.models.TextField` | `rest_framework.fields.CharField` |\n| `django.db.models.EmailField` | `rest_framework.fields.EmailField` |\n| `django.db.models.IntegerField` | `rest_framework.fields.IntegerField` |\n| `django.db.models.FloatField` | `rest_framework.fields.FloatField` |\n| `django.db.models.DateField` | `rest_framework.fields.DateField` |\n| `django.db.models.DateTimeField` | `rest_framework.fields.DateTimeField` |\n| `django.db.models.FileField` | `rest_framework.fields.FileField` |\n\n### ■ ③ ListSerializer（複数件まとめて処理・many=True の正体）\n複数のレコードを一括で処理するためのシリアライザです。  \n普段自作することはほぼなく、シリアライザに **`many=True`** を渡した際に、DRF が内部で自動的に呼び出して複数件をリスト形式にまとめてくれます。\n\n```python\n# 複数件（クエリセット）を渡すときは many=True を指定\nmessages = Message.objects.all()\nserializer = MessageSerializer(messages, many=True)\n```\n\n### 【共通】シリアライザの主要フィールドオプション一覧\nフィールドを手動定義する際（`Serializer`）や、フィールドのカスタマイズ・`extra_kwargs` で追加設定を行う際（`ModelSerializer`）に指定できる代表的なオプションです。  \n**`Serializer` と `ModelSerializer` の両方で共通して利用できます。**\n\n| フィールドオプション | 説明 |\n| :--- | :--- |\n| `write_only` | 登録・更新・一部更新時の 入力用フィールドには含めるが、出力用フィールドには含めたくない場合に True を指定(デフォルト値は False) |\n| `read_only` | 出力用フィールドには含めるが、登録・更新・一部更新時の入力用のフィールドには含めたくない場合に True を指定。read_only=True を指定したフィールドは登録・更新・一部更新の対象外となる(デフォルト値は False) |\n| `required` | 入力データにフィールドが指定されなかった場合にバリデーション NG にするかどうか。なお、default オプションが設定されている場合や read_only=True の場合は required=False となる(デフォルト値は True:フィールド必須) |\n| `default` | 登録・更新時にフィールドが指定されなかったときに使われるデフォルト値(ただし一部更新のときは使われない) |\n| `allow_null` | 入力値に null を許可するかどうか。False の場合、登録・更新・一部更新時に入力値が null になっているとバリデーション NG となる(デフォルト値は False:許可しない) |\n| `allow_blank` | 入力値に空文字を許可するかどうか。CharField などで利用可 |\n| `source` | 値を出力する際の参照先をデフォルトから変更したい場合に使う。関連モデルの属性をドット区切りで指定したりすることも可能。参照先が見つからない場合は default の値を使う |\n| `max_length` | CharField などのバリデーションで利用される最大桁数 |\n| `min_length` | CharField などのバリデーションで利用される最小桁数 |\n| `max_value` | IntegerField や FloatField のバリデーションで利用される最大値 |\n| `min_value` | IntegerField や FloatField のバリデーションで利用される最小値 |\n| `validators` | 文字種チェックなどのバリデーション。list や tuple で指定 |\n| `error_messages` | バリデーション NG の場合のエラーメッセージ。dict で指定 |\n\n### 【実践】モデルに存在しない動的な計算値を追加する（`SerializerMethodField`）\n\n実務では、「モデルのテーブルには保存していないが、フロントエンドに計算結果や加工した値を返したい」というケースが頻繁にあります。  \n（例：文字数のカウント、経過時間のフォーマット、複数項目の結合、権限に応じた判定フラグなど）\n\nこれを実現するのが **`SerializerMethodField`** です。\n\n#### ▼ 実装例：メッセージに「文字数」や「表示用要約」を動的追加する\n\n```python\nfrom rest_framework import serializers\nfrom .models import Message\n\n\nclass MessageSerializer(serializers.ModelSerializer):\n    # ① SerializerMethodField を定義（自動的に読み取り専用 read_only=True になる）\n    content_length = serializers.SerializerMethodField()\n    formatted_summary = serializers.SerializerMethodField()\n\n    class Meta:\n        model = Message\n        fields = [\n            \"id\",\n            \"sender\",\n            \"recipient\",\n            \"content\",\n            \"created_at\",\n            \"content_length\",     # ② fields に追加\n            \"formatted_summary\",  # ② fields に追加\n        ]\n        read_only_fields = [\"id\", \"created_at\"]\n\n    # ③ 「get_\u003cフィールド名\u003e(self, obj)」の命名規則でメソッドを定義\n    def get_content_length(self, obj) -\u003e int:\n        \"\"\"メッセージ本文の文字数を計算して返す\"\"\"\n        return len(obj.content)\n\n    def get_formatted_summary(self, obj) -\u003e str:\n        \"\"\"obj（Messageインスタンス）を使ってカスタム文字列を生成\"\"\"\n        return f\"[{obj.sender}] {obj.content[:20]}...\"\n```\n\n#### ▼ ポイントと命名ルール\n1. **メソッド名の命名規則**:  \n   デフォルトでは **`get_\u003cフィールド名\u003e(self, obj)`** という名前のメソッドが自動的に呼ばれます。\n2. **引数 `obj`**:  \n   シリアライズ対象の **モデルインスタンス** が渡されます（`obj.sender` や `obj.content` などモデルの属性に自由にアクセスできます）。\n3. **読み取り専用（Read-Only）**:  \n   `SerializerMethodField` は **出力（GET）専用** です。クライアントからの新規登録や更新（POST / PUT / PATCH）では自動的に無視されます。\n\n#### ⚠️ 実務での重要注意点：`None`（Null）対策を怠ると 500 エラーになる！\n\n`SerializerMethodField` の実装で最も初心者が踏みやすい罠が、**「値が `None` のケースを考慮せずにメソッドや属性を呼び出し、本番環境で `500 Internal Server Error` を引き起こす」** ことです。\n\n```python\n# ❌ NG 例: リレーション先やフィールドが None（Null）だと即クラッシュ！\ndef get_recipient_name(self, obj):\n    # recipient が NULL（未設定）のレコードが存在すると、\n    # AttributeError: 'NoneType' object has no attribute 'name' で 500 エラー！\n    return obj.recipient.name\n\ndef get_formatted_created_at(self, obj):\n    # created_at が None の場合、同様にクラッシュ\n    return obj.created_at.strftime(\"%Y/%m/%d %H:%M\")\n```\n\n実務では、以下のように **事前に `None` チェック（ガード節や三項演算子）を挟み、安全なデフォルト値や `None` を返す** ように徹底します。\n\n```python\n# ⭕ OK 例: 安全なハンドリング\ndef get_recipient_name(self, obj) -\u003e str | None:\n    \"\"\"リレーション先が存在しない場合は安全に None や代替文字列を返す\"\"\"\n    if not obj.recipient:\n        return \"未設定\"  # または None\n    return obj.recipient.name\n\ndef get_formatted_created_at(self, obj) -\u003e str | None:\n    \"\"\"日付が未設定の場合は None を返す\"\"\"\n    if not obj.created_at:\n        return None\n    return obj.created_at.strftime(\"%Y/%m/%d %H:%M\")\n\ndef get_content_length(self, obj) -\u003e int:\n    \"\"\"文字列が None の場合は 0 を返す\"\"\"\n    if not obj.content:\n        return 0\n    return len(obj.content)\n```\n\n:::note alert\n**💡 鉄則：SerializerMethodField 内の属性アクセスは常に None を疑う**  \n「開発環境ではテストデータが入っているから動いていたが、本番環境で NULL カラムを持つレコードが1件でも入った瞬間に一覧 API 全体が 500 で落ちる」というのは実務で極めて頻出するバグです。必ず `None` の安全対策を行いましょう。\n:::\n\n\n\n## 3. WebAPI のリクエスト〜レスポンス全体像\n\nWebAPI の通信全体の中で、シリアライザがどこでどう動いているのかを俯瞰してみましょう。\n\n\u003c!-- 画像2: シリアライザの全体像フロー --\u003e\n![0934b0c7-6947-4176-8e08-a34276dbb60a.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/1450b210-417f-4922-8e51-182ef0e9bcb6.avif)\n\n1. **Parser**: JSON文字列を解析し、Python辞書（`request.data`）へ変換\n2. **Serializerに渡す**: 生データをシリアライザへセット（`data=request.data`）\n3. **バリデーション**: `serializer.is_valid()` で型や制約を検証\n4. **保存**: `serializer.save()` でデータベースへ書き込み\n5. **出力用データへ**: 保存後のモデルをPython辞書（`serializer.data`）へ変換\n6. **Renderer**: Python辞書をJSON文字列にしてクライアントへ返送\n\n### ▼ コード例：入力側の処理フロー\n上記の「1. Parser ➔ 2. Serializer ➔ 3. バリデーション」という入力側の流れをコードで追うと、以下のようになります。\n\n```python\nfrom io import BytesIO\nfrom rest_framework.parsers import JSONParser\n\n# 1. Parser: JSON文字列をパースしてPython辞書に変換\ndata = JSONParser().parse(BytesIO(json_string.encode()))\n\n# 2. Serializerに渡す: 生データをシリアライザへセット\nserializer = MessageSerializer(data=data)\n\n# 3. バリデーション: 入力値を検証\nif serializer.is_valid():\n    print(serializer.validated_data)  # 検証成功: クリーンなデータ\nelse:\n    print(serializer.errors)          # 検証失敗: エラー内容\n```\n\n※実務の View（APIView や ViewSet）では、Parser の呼び出しは裏側で自動化されており `request.data` で受け取れますが、DRF の内部動作を追うとこのように非常に素直なパイプラインでデータが処理されていることが分かります。\n\n### 💡 実務で差がつく！「データの流れ（Parser）」を押さえるべき理由\n「普段 View を書くときは `request.data` と書くだけで自動で動くのに、なぜ Parser やデータの流れなんて知る必要があるの？」と思ったかもしれません。\n\nしかし、この **データの流れを理解していないと絶対に自力で解決できない実務の代表例** が、まさに **「画像のアップロード（ファイル送信）」** です。\n\n![80550749-0d03-4e17-9f3b-36b07e8cfb83.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e466feeb-85ac-44ff-9e9d-2cb6d733793e.avif)\n\n#### 実例：ファイル送信（multipart/form-data）で必須になる知識\nフロントエンドから「タイトル（文字）」と一緒に「画像ファイル（バイナリ）」をまとめて送信する場合、リクエストの形式は JSON ではなく **`multipart/form-data`** になります。\n\n1. **クライアント**: `multipart/form-data` 形式で、文字データ（`title`, `prompt`）とファイル（`photo.jpg`）をまとめて送信。\n2. **MultiPartParser**: リクエストを分解し、文字データ（`request.data`）とファイルデータ（`request.FILES`）を取り出す。  \n   ※DRF では `request.data` 経由でファイルを直接扱えるよう統合されます。\n3. **Serializer**: 取り出された入力値（文字＋ファイル）の型・必須・ファイルサイズ・拡張子などを検証。\n4. **保存処理**: 実ファイルをストレージへ保存し、ファイルパスやメタデータをデータベースへ保存。\n5. **レスポンス**: 保存結果をクライアントへ返却。\n\nこのとき、もしデフォルトの `JSONParser` しか知らなければ、**「なぜ画像を送ると 415 Unsupported Media Type やバリデーションエラーになるのか？」** が全く分かりません。\n\nリクエストを正しくパースして Serializer に届けるために、View 側で **`MultiPartParser`** を明示的に指定する必要があります。\n\n```python\nfrom rest_framework.parsers import MultiPartParser, JSONParser\nfrom rest_framework.viewsets import ModelViewSet\nfrom .models import Image\nfrom .serializers import ImageSerializer\n\nclass ImageViewSet(ModelViewSet):\n    queryset = Image.objects.all()\n    serializer_class = ImageSerializer\n    pagination_class = ImagePagination\n    # ★ ここ！データの流れ（Parser）を理解していないと、なぜこれが必要か分からない！\n    parser_classes = [MultiPartParser, JSONParser]\n```\n\n* **データの流れを知らない人**:  \n  「画像送信でなぜかエラーが出る…ネットに `parser_classes = [MultiPartParser, ...]` と書けとあったからコピペした（動いたけれど仕組みは謎）」\n* **データの流れを理解している人**:  \n  「今回は `multipart/form-data` だから、Serializer にデータが届く前に `MultiPartParser` で分解して `request.data` を生成してあげる必要があるんだな」\n\nこのように、**「リクエストがどうパースされ、どうシリアライザに渡されるか」というデータのパイプラインを把握していること** こそが、コピペプログラマから脱却し、実務でトラブルを自力解決できるようになるための必須の土台なのです。\n\n\n## 4. Serializer 内部の処理サイクル（全体の流れ）\n\n続いて、シリアライザ内部でのデータの状態変化にズームインします。\n\n\u003c!-- 画像3: Serializerの処理の流れ --\u003e\n![74fdce93-f73d-463f-bdbf-b6aa391ea0f3.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/a3b97d2a-27f9-4f47-9066-63bfcc69164f.avif)\n\n1. **入力**: 生データを渡す（`serializer = TaskSerializer(data=request.data)`）\n2. **バリデーション**: `serializer.is_valid()` を実行\n3. **検証済みデータ**: 合格した値だけが `serializer.validated_data` に入る\n4. **保存**: `serializer.save()` でモデルを作成・更新（`create()` / `update()`）\n5. **モデル保持**: `serializer.instance` に保存後のモデルオブジェクトを保持\n6. **出力生成**: `serializer.data` で出力用データを生成し、JSONレスポンスとして返却\n\n\n## 5. 入力処理（新規登録時のデシリアライズ）\n\n新規作成（POST）の場面で、生データを受け取ってモデルへ保存するまでの「入力処理」です。\n\n\u003c!-- 画像4: Serializerの入力処理 --\u003e\n![7399a2dc-b786-4882-8bf1-24361e2a3949.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e57aa6fe-8f90-44ed-a70d-ba3860e5931f.avif)\n\n* **`serializer = UserSerializer(data=request.data)`**: 生の入力データをセット\n* **`serializer.is_valid()`**: ルールに沿って型・必須・文字数を検証\n* **`serializer.validated_data`**: 検証を通過したクリーンなデータ\n* **`serializer.save()`**: データベースへ新規レコードを作成\n* **`serializer.instance`**: 保存が完了したモデルインスタンスを保持\n\n\n## 6. 更新処理（instance ＋ data による update）\n\n既存レコードを更新（PUT / PATCH）する場面でのシリアライザの動きです。\n\n\u003c!-- 画像5: Serializerの更新処理 --\u003e\n![ChatGPT-Image-2026年9月18日-19_50_04.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/ae1422d8-1247-4fcf-8944-05ec02aed399.avif)\n\n* **`serializer = UserSerializer(instance=user, data=request.data)`**:  \n  新規作成との違いは **第1引数に更新対象の既存モデル（`instance`）を渡す点** です。\n* **`serializer.is_valid()`**: 変更内容を検証\n* **`serializer.save()`**: 既存の `instance` に対して自動的に `update()` が走り、差分がDBに反映される\n* **`serializer.instance`**: 更新後の最新モデルオブジェクトが手に入る\n\n### ▼ コード例：update() の動作検証（更新前後の変化を可視化）\n`serializer.save()` を呼び出した際、裏側で `update()` メソッドがどのように既存インスタンスを受け取り、更新を行っているかを実際に `update()` をオーバーライドして可視化してみましょう。\n\n```python\nclass MessageSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Message\n        fields = \"__all__\"\n\n    def update(self, instance, validated_data):\n        # 更新前のインスタンスデータを出力\n        print(f\"--- 更新前 (instance) ---\")\n        print(f\"データ: {instance.__dict__}\")\n\n        # 入力されたデータを出力\n        print(f\"\\n--- 入力データ (validated_data) ---\")\n        print(f\"データ: {validated_data}\")\n\n        # 親クラスの update メソッドを呼び出してインスタンスを更新\n        instance = super().update(instance, validated_data)\n\n        # 更新後のインスタンスデータを出力\n        print(f\"\\n--- 更新後 (instance) ---\")\n        print(f\"データ: {instance.__dict__}\")\n\n        return instance\n\n\n# テスト用のコードを追加\nif __name__ == \"__main__\":\n    # 既存のインスタンスを取得\n    instance = Message.objects.first()  # 最初のMessageインスタンスを取得\n    if not instance:\n        print(\"Messageインスタンスが存在しません。データベースにデータを追加してください。\")\n    else:\n        # 更新用のデータ\n        validated_data = {\n            \"sender\": \"updated_sender\",\n            \"recipient\": \"updated_recipient\",\n            \"content\": \"Updated content\"\n        }\n\n        # シリアライザを使用して更新（partial=True で一部更新）\n        serializer = MessageSerializer(instance, data=validated_data, partial=True)\n        if serializer.is_valid():\n            serializer.save()  # ここで update メソッドが呼び出される\n        else:\n            print(\"バリデーションエラー:\", serializer.errors)\n```\n\n#### ターミナル実行結果：\n```text\n(venv) PS C:\\Users\\...\\backend\u003e python a.py\n--- 更新前 (instance) ---\nデータ: {'_state': \u003cdjango.db.models.base.ModelState object at 0x...\u003e, ...: datetime.datetime(2026, 5, 13, 12, 55, 37, 36593, tzinfo=datetime.timezone.utc)}\n\n--- 入力データ (validated_data) ---\nデータ: {'sender': 'updated_sender', 'recipient': 'updated_recipient', 'content': 'Updated content'}\n\n--- 更新後 (instance) ---\nデータ: {'_state': \u003cdjango.db.models.base.ModelState object at 0x...\u003e, ...: datetime.datetime(2026, 5, 13, 12, 55, 37, 36593, tzinfo=datetime.timezone.utc)}\n```\n\nこのように、`instance`（更新前データ）に対して入力された `validated_data` がマージされ、`super().update()` によってデータベースへ保存されて最新のインスタンスへと書き換わっていることがはっきりと確認できます。\n\n### ⚠️ 実務の落とし穴：PATCH（一部更新）と相関バリデーションの注意点\n\n実務で非常に多くのエンジニアが直面するトラブルが、**「PATCH（一部更新）時の複数フィールドをまたぐバリデーション（`validate()`）」** です。\n\n:::note warn\n**【超重要】PATCH リクエストでは「変更する項目しか送られてこない」！**  \nそのため、送られてきていない項目を `attrs` から直接取得しようとすると `None` になってしまい、正しい判定ができません。\n:::\n\n#### 1. 発生する問題（attrs に未送信の項目が含まれない）\n例えば、以下のように「開始日（`start_date`）と終了日（`end_date`）」を持つ既存データがあるとします。\n\n| 項目 | DBの既存データ | クライアントからのPATCH（開始日のみ更新） | `validate(self, attrs)` の `attrs` |\n| :--- | :--- | :--- | :--- |\n| `name` | 山田太郎 | （未送信） | （含まれない） |\n| `start_date` | 2024-06-01 | **2024-06-10** | **`2024-06-10`** |\n| `end_date` | 2024-06-30 | （未送信） | **❌ 含まれない（None）** |\n| `category` | A | （未送信） | （含まれない） |\n\nこのとき、「開始日 \u003c= 終了日」をチェックしようとして以下のように書いてしまうと、バグの原因になります。\n\n```python\ndef validate(self, attrs):\n    start_date = attrs.get(\"start_date\")\n    end_date = attrs.get(\"end_date\")  # ❌ PATCHで未送信だと None になる！\n\n    # end_date が None のため、比較処理がスルーされて不正な日付でも通過してしまう！\n    if start_date and end_date and start_date \u003e end_date:\n        raise serializers.ValidationError(\"開始日は終了日以前を指定してください。\")\n    return attrs\n```\n\n#### 2. 解決策：既存インスタンス（self.instance）から値を取得して補完する\nシリアライザは更新時、**`self.instance`（更新前の既存インスタンス）を保持** しています。  \nそのため、「**今回送られてきた値があればそれ（`attrs`）を使い、なければ既存インスタンス（`self.instance`）から取得する**」という設計にします。\n\n```python\nclass EventSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Event\n        fields = '__all__'\n\n    def _resolve(self, field_name, attrs):\n        \"\"\"今回の送信値があればそれを使い、なければ既存インスタンスから取得するヘルパー\"\"\"\n        if field_name in attrs:\n            return attrs[field_name]\n        if self.instance and hasattr(self.instance, field_name):\n            return getattr(self.instance, field_name)\n        return None\n\n    def validate(self, attrs):\n        # 今回の送信値 or 既存インスタンスの値を取得\n        start_date = self._resolve(\"start_date\", attrs)\n        end_date = self._resolve(\"end_date\", attrs)\n\n        # PATCH で片方しか送られてこなくても正確に日付順序をチェック可能！\n        if start_date and end_date and start_date \u003e end_date:\n            raise serializers.ValidationError(\"開始日は終了日以前の日付を指定してください。\")\n\n        return attrs\n```\n\n#### 補完後の完成形イメージ：\n| フィールド | バリデーション時の参照値 | 値の取得元 |\n| :--- | :--- | :--- |\n| `start_date` | `2024-06-10` | **リクエストの値（`attrs`）** |\n| `end_date` | `2024-06-30` | **既存データの値（`self.instance`）** |\n| `category` | `A` | **既存データの値（`self.instance`）** |\n\nこのように、**「一部更新（PATCH）の相関バリデーションでは、既存インスタンス（`self.instance`）を参照して未送信項目を補う」** という考え方は、実務開発において必須のテクニックです（※相関バリデーションの詳しい実装パターンや基底クラスでの共通化は、次回のバリデーション特集でじっくり扱います）。\n\n\n## まとめ\n\n* **3つのシリアライザ**:\n  * 基本は **`ModelSerializer`**（モデル定義から自動生成）\n  * 特殊な用途には **`Serializer`**（自由定義）、一覧には **`many=True`**（ListSerializer）\n* **新規作成と更新の違い**:\n  * 新規作成: `Serializer(data=...)` ➔ `save()` で `create()`\n  * 既存更新: `Serializer(instance=..., data=...)` ➔ `save()` で `update()`\n* **PATCH（一部更新）の注意点**:\n  * 未送信項目は `attrs` に含まれないため、既存インスタンス（`self.instance`）から補完する\n\nシリアライザの全体像と、リクエストからDB保存・レスポンスまでのライフサイクルを押さえておくことで、DRFの内部動作への理解が一気にクリアになります！  \n次回は **[【第七講】シリアライザのバリデーション完全攻略・is_validの仕組みとエラー制御](/items/04b2c3524ec379961d82)** で、単一・複数フィールドのバリデーション定義やエラー制御、そして実務で役立つカスタムバリデーションの実践テクニックを深掘りしていきましょう！\n\n---\n\n**[← 第五講：マイグレーション完全攻略・Django管理画面とリレーション設計](/items/c835d3ff5c824439f248) | [まとめページに戻る](/items/4250ea04fb61f5bc4f0d) | [第七講へ →](/items/04b2c3524ec379961d82)**\n","coediting":false,"comments_count":0,"created_at":"2026-09-19T12:38:15+09:00","group":null,"id":"914c08a85e63388664e8","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Django","versions":[]},{"name":"DRF","versions":[]},{"name":"Python","versions":[]},{"name":"rest","versions":[]},{"name":"Vue.js","versions":[]}],"title":"DRF × Vue.js 実践講座【第六講】DRFの中核「シリアライザ」徹底解剖・3大分類と更新処理","updated_at":"2026-09-20T00:26:01+09:00","url":"https://qiita.com/mashikokoutaro1019/items/914c08a85e63388664e8","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":4,"github_login_name":"kotaromashiko","id":"mashikokoutaro1019","items_count":34,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4223670,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocLhGA1m69Wj3mwBbWNN_K3bGO3DrJ7lGj36By-be2U_y9c9Mw=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cblockquote data-sourcepos=\"2:1-2:88\"\u003e\n\u003cp data-sourcepos=\"2:3-2:88\"\u003e\u003cstrong\u003eLecture 07: Mastering Serializer Validation — is_valid Pipeline \u0026amp; Error Handling\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"4:1-4:217\"\u003e\u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003e← DRF × Vue.js 実践講座：まとめページに戻る\u003c/a\u003e | \u003ca href=\"/items/914c08a85e63388664e8\"\u003e← 第六講：DRFの中核「シリアライザ」徹底解剖・3大分類と更新処理\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"6:1-6:87\"\u003e\n\u003cspan id=\"はじめになぜシリアライザのバリデーションが重要なのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB%E3%81%AA%E3%81%9C%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8C%E9%87%8D%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに：なぜシリアライザのバリデーションが重要なのか？\u003c/h2\u003e\n\u003cp data-sourcepos=\"8:1-8:264\"\u003e第六講では、DRF の最重要コンポーネントであるシリアライザの全体像と3大分類（Serializer / ModelSerializer / ListSerializer）、そして通信全体のデータの流れ（Parser 〜 Serializer 〜 Renderer）を学びました。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-11:346\"\u003eWebAPI を運用する上で、最も恐ろしいのは \u003cstrong\u003e「不正なデータや悪意あるデータがデータベースに入り込んでしまうこと」\u003c/strong\u003e です。\u003cbr\u003e\nフロントエンド（Vue.js）側でもフォームの入力チェックは行いますが、API はブラウザ以外のツール（Postman や curl、悪意のあるスクリプト）からも直接呼び出されるため、\u003cstrong\u003eバックエンドでの徹底的なバリデーション（入力値検証）が最後の砦\u003c/strong\u003e となります。\u003c/p\u003e\n\u003cp data-sourcepos=\"13:1-13:271\"\u003e本講では、DRF のバリデーションの基本サイクルから、裏側で動く検証パイプライン、単一・複数フィールドの独自バリデーション、そして実務で頻出するエラー制御テクニックまでを完全攻略します！\u003c/p\u003e\n\u003ch3 data-sourcepos=\"15:1-15:109\"\u003e\n\u003cspan id=\"-そもそもシリアライザを使わないとどうなる手動検証-vs-serializer集約\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84%E3%81%A8%E3%81%A9%E3%81%86%E3%81%AA%E3%82%8B%E6%89%8B%E5%8B%95%E6%A4%9C%E8%A8%BC-vs-serializer%E9%9B%86%E7%B4%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ そもそもシリアライザを使わないとどうなる？（手動検証 vs Serializer集約）\u003c/h3\u003e\n\u003cp data-sourcepos=\"17:1-17:225\"\u003e「そもそも、View の中で \u003ccode\u003erequest.data\u003c/code\u003e を取り出して \u003ccode\u003eif\u003c/code\u003e 文でチェックすれば、わざわざシリアライザなんて作らなくても動くのでは？」と思ったことはないでしょうか？\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-20:184\"\u003eたしかに Python コードとしては動作しますが、実務では \u003cstrong\u003e「非推奨に近い書き方」\u003c/strong\u003e とされています。\u003cbr\u003e\n具体例として、「複数のキャスト番号（配列）を受け取る API」を例に、シリアライザを使わない場合と使う場合を比較してみましょう。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"22:1-22:74\"\u003e\n\u003cspan id=\"1-非推奨に近い書き方シリアライザを使わない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E9%9D%9E%E6%8E%A8%E5%A5%A8%E3%81%AB%E8%BF%91%E3%81%84%E6%9B%B8%E3%81%8D%E6%96%B9%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 非推奨に近い書き方（シリアライザを使わない）\u003c/h4\u003e\n\u003cp data-sourcepos=\"23:1-23:96\"\u003eView の中で \u003ccode\u003erequest.data\u003c/code\u003e を直接触り、手動でチェックしている状態です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"25:1-56:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# casting/views.py\n\u003c/span\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.decorators\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eapi_view\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.response\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\n\n\u003cspan class=\"nd\"\u003e@api_view\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ePOST\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ecasting_view\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ① request.data を直接取り出す\n\u003c/span\u003e    \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ② 手動でバリデーション（泥沼の if 文地獄...）\n\u003c/span\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"bp\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eerror\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_numbers が必須です\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e400\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"nf\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003elist\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eerror\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_numbers は配列で指定してください\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e400\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eerror\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_numbers は1件以上指定してください\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e400\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003enum\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"nf\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enum\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eerror\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e各要素は文字列で指定してください\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e400\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ③ 実処理\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eresults\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\n    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003enum\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_number\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enum\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estatus\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eprocessed\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ④ レスポンスを返す（DRFがJSONに変換して返す）\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edata\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"58:1-61:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e【処理の流れ（イメージ）】\nリクエスト (JSON) ➔ view（手動バリデーション：必須・型・空配列・要素型チェック ＋ 実処理） ➔ レスポンス (JSON)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"63:1-69:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"64:1-64:53\"\u003e\u003cstrong\u003e❌ デメリット（非推奨に近い理由）\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"65:1-68:121\"\u003e\n\u003cli data-sourcepos=\"65:1-65:134\"\u003e\n\u003cstrong\u003eView の責務が肥大化\u003c/strong\u003e: View が入力チェックまで担当してしまい、ファットビュー（Fat View）になる\u003c/li\u003e\n\u003cli data-sourcepos=\"66:1-66:128\"\u003e\n\u003cstrong\u003eメンテナンスが大変\u003c/strong\u003e: チェック項目が増えるほど View が長くなり、可読性が著しく低下する\u003c/li\u003e\n\u003cli data-sourcepos=\"67:1-67:133\"\u003e\n\u003cstrong\u003e設計の不統一\u003c/strong\u003e: 他の API とバリデーションの書き方やエラー形式が統一されず、バラバラになる\u003c/li\u003e\n\u003cli data-sourcepos=\"68:1-68:121\"\u003e\n\u003cstrong\u003eテストが書きにくい\u003c/strong\u003e: View を経由しないとバリデーション部分を個別にテストできない\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"71:1-72:0\"\u003e\n\u003ch4 data-sourcepos=\"73:1-73:65\"\u003e\n\u003cspan id=\"2-推奨される書き方シリアライザを使う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E6%8E%A8%E5%A5%A8%E3%81%95%E3%82%8C%E3%82%8B%E6%9B%B8%E3%81%8D%E6%96%B9%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%82%92%E4%BD%BF%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 推奨される書き方（シリアライザを使う）\u003c/h4\u003e\n\u003cp data-sourcepos=\"74:1-74:156\"\u003e\u003cstrong\u003e「入力バリデーションはシリアライザに集約し、View は本来の処理に集中する」\u003c/strong\u003e のが DRF の美しい設計思想です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"76:1-86:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# casting/serializers.py\n\u003c/span\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCastingRequestSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# 必須・配列・空不可・各要素は文字列をたった数行で宣言！\n\u003c/span\u003e    \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eListField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"n\"\u003echild\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eallow_empty\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"88:1-111:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# casting/views.py\n\u003c/span\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.decorators\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eapi_view\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.response\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.serializers\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eCastingRequestSerializer\u003c/span\u003e\n\n\u003cspan class=\"nd\"\u003e@api_view\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ePOST\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ecasting_view\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# ① serializer でバリデーション\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eCastingRequestSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eerrors\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e400\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n    \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ② 実処理（バリデーション済みのクリーンなデータを使う）\n\u003c/span\u003e    \u003cspan class=\"n\"\u003eresults\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\n    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003enum\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003ecasting_numbers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecasting_number\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003enum\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estatus\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eprocessed\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e# ③ レスポンスを返す\n\u003c/span\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esuccess\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edata\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"113:1-116:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e【処理の流れ（イメージ）】\nリクエスト (JSON) ➔ serializer（自動バリデーション） ➔ view（実処理・レスポンス作成） ➔ レスポンス (JSON)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"118:1-124:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"119:1-119:47\"\u003e\u003cstrong\u003e✅ メリット（推奨される理由）\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"120:1-123:121\"\u003e\n\u003cli data-sourcepos=\"120:1-120:132\"\u003e\n\u003cstrong\u003eView がシンプルに\u003c/strong\u003e: 入力チェックは serializer に集約でき、view はビジネスロジックに集中できる\u003c/li\u003e\n\u003cli data-sourcepos=\"121:1-121:116\"\u003e\n\u003cstrong\u003e再利用しやすい\u003c/strong\u003e: バリデーションの再利用がしやすい（他の view でも使い回せる）\u003c/li\u003e\n\u003cli data-sourcepos=\"122:1-122:121\"\u003e\n\u003cstrong\u003e設計が統一\u003c/strong\u003e: 他のアプリ（例: \u003ccode\u003efile_manager\u003c/code\u003e など）とも設計パターンを綺麗に統一できる\u003c/li\u003e\n\u003cli data-sourcepos=\"123:1-123:121\"\u003e\n\u003cstrong\u003eテストが書きやすい\u003c/strong\u003e: serializer 単体にデータを渡すだけでバリデーションテストが可能\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"126:1-126:153\"\u003eこのように、\u003cstrong\u003eシリアライザを使うことで、DRF の設計思想に沿ったシンプルで保守しやすいコード\u003c/strong\u003e になります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"129:1-129:51\"\u003e\n\u003cspan id=\"-バリデーションの基本4ステップ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%9F%BA%E6%9C%AC4%E3%82%B9%E3%83%86%E3%83%83%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ バリデーションの基本4ステップ\u003c/h3\u003e\n\u003cp data-sourcepos=\"131:1-131:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe668d67b-821b-47ce-b000-23094f45dd43.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=d293049c708c665e9cbe24e2bb965bd1\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe668d67b-821b-47ce-b000-23094f45dd43.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=d293049c708c665e9cbe24e2bb965bd1\" alt=\"740295a2-66ff-47e3-8754-b2c4ccb0cd37.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Fe668d67b-821b-47ce-b000-23094f45dd43.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=2813a6557fa3d538c314a05b6d365b8a 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e668d67b-821b-47ce-b000-23094f45dd43.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"133:1-133:264\"\u003eシリアライザのバリデーションは、上記の通り \u003cstrong\u003e「① 入力データを渡す ➔ ② \u003ccode\u003eis_valid()\u003c/code\u003e で検証 ➔ ③ 成功なら \u003ccode\u003evalidated_data\u003c/code\u003e / ④ 失敗なら \u003ccode\u003eerrors\u003c/code\u003e」\u003c/strong\u003e という非常にシンプルな4ステップで動きます。\u003c/p\u003e\n\u003cul data-sourcepos=\"135:1-139:0\"\u003e\n\u003cli data-sourcepos=\"135:1-135:112\"\u003e\n\u003cstrong\u003e① 入力データを渡す\u003c/strong\u003e: \u003ccode\u003eserializer = UserSerializer(data=request.data)\u003c/code\u003e で生データをセット\u003c/li\u003e\n\u003cli data-sourcepos=\"136:1-136:87\"\u003e\n\u003cstrong\u003e② 入力値を検証\u003c/strong\u003e: \u003ccode\u003eserializer.is_valid()\u003c/code\u003e を呼び出して検証を実行\u003c/li\u003e\n\u003cli data-sourcepos=\"137:1-137:133\"\u003e\n\u003cstrong\u003e③ 成功時\u003c/strong\u003e: クリーンな検証済みデータが \u003ccode\u003eserializer.validated_data\u003c/code\u003e（内部では \u003ccode\u003e_validated_data\u003c/code\u003e）に入る\u003c/li\u003e\n\u003cli data-sourcepos=\"138:1-139:0\"\u003e\n\u003cstrong\u003e④ 失敗時\u003c/strong\u003e: バリデーションエラーの詳細が \u003ccode\u003eserializer.errors\u003c/code\u003e（内部では \u003ccode\u003e_errors\u003c/code\u003e）に入る\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv data-sourcepos=\"140:1-143:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"141:1-142:229\"\u003e\u003cstrong\u003e💡 超重要ルール：\u003ccode\u003evalidated_data\u003c/code\u003e と \u003ccode\u003eerrors\u003c/code\u003e は必ず \u003ccode\u003eis_valid()\u003c/code\u003e の後に使う！\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003eis_valid()\u003c/code\u003e を実行する前に \u003ccode\u003eserializer.validated_data\u003c/code\u003e や \u003ccode\u003eserializer.errors\u003c/code\u003e を参照しようとすると、\u003ccode\u003eAssertionError\u003c/code\u003e が発生します。必ず \u003ccode\u003eis_valid()\u003c/code\u003e を実行した後に取り出してください。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"146:1-146:71\"\u003e\n\u003cspan id=\"1-is_valid-の内部処理バリデーションの全体像\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-is_valid-%E3%81%AE%E5%86%85%E9%83%A8%E5%87%A6%E7%90%86%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%85%A8%E4%BD%93%E5%83%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. is_valid() の内部処理（バリデーションの全体像）\u003c/h2\u003e\n\u003cp data-sourcepos=\"148:1-148:135\"\u003e\u003ccode\u003eserializer.is_valid()\u003c/code\u003e を実行したとき、裏側では以下のパイプラインが大まかに順番に動作しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"150:1-150:155\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Ff51c4a00-b10b-4d38-b40a-e7e4242a216d.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=5dcbf1e0b91a19c549738b2605706629\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Ff51c4a00-b10b-4d38-b40a-e7e4242a216d.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=5dcbf1e0b91a19c549738b2605706629\" alt=\"53fa751d-6574-4373-9511-9572b450e469.avif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4223670%2Ff51c4a00-b10b-4d38-b40a-e7e4242a216d.avif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=62bef968a39b94cd0bc4bc89d66ee3cd 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/f51c4a00-b10b-4d38-b40a-e7e4242a216d.avif\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003col data-sourcepos=\"152:1-159:0\"\u003e\n\u003cli data-sourcepos=\"152:1-152:87\"\u003e\n\u003cstrong\u003e基本検証\u003c/strong\u003e: 型チェック、必須確認、文字数制限（\u003ccode\u003emax_length\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"153:1-153:140\"\u003e\n\u003cstrong\u003e\u003ccode\u003evalidate_\u0026lt;field\u0026gt;()\u003c/code\u003e\u003c/strong\u003e: 単一フィールドに対する独自ルールチェック（例: \u003ccode\u003evalidate_email()\u003c/code\u003e でドメイン制限）\u003c/li\u003e\n\u003cli data-sourcepos=\"154:1-154:140\"\u003e\n\u003cstrong\u003e\u003ccode\u003eSerializer.validators\u003c/code\u003e\u003c/strong\u003e: 複数フィールドの一意性制約などのクラスバリデータ（例: \u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"155:1-155:152\"\u003e\n\u003cstrong\u003e\u003ccode\u003evalidate()\u003c/code\u003e\u003c/strong\u003e: 複数フィールドを組み合わせた相関チェック（例: 「開始日時 \u0026lt; 終了日時」「パスワード一致」）\u003c/li\u003e\n\u003cli data-sourcepos=\"156:1-159:0\"\u003e\n\u003cstrong\u003e検証結果の確定\u003c/strong\u003e:\n\u003cul data-sourcepos=\"157:4-159:0\"\u003e\n\u003cli data-sourcepos=\"157:4-157:79\"\u003e\n\u003cstrong\u003e成功時\u003c/strong\u003e: クリーンな値が \u003ccode\u003eserializer.validated_data\u003c/code\u003e に格納\u003c/li\u003e\n\u003cli data-sourcepos=\"158:4-159:0\"\u003e\n\u003cstrong\u003e失敗時\u003c/strong\u003e: エラー詳細が \u003ccode\u003eserializer.errors\u003c/code\u003e に格納（400 Bad Request）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv data-sourcepos=\"160:1-162:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"161:1-161:339\"\u003e※各バリデータが具体的にどの詳細な順番で実行されるのか（内部順序やフックの完全なライフサイクル）は、後ほど \u003cstrong\u003e第5章・第6章\u003c/strong\u003e でじっくり解説します。まずは「基本検証 ➔ 単一チェック ➔ 全体チェック」という大きな流れを押さえましょう！\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"165:1-165:82\"\u003e\n\u003cspan id=\"2-ステップ1各フィールドの基本検証field-オプション\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%B9%E3%83%86%E3%83%83%E3%83%971%E5%90%84%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%81%AE%E5%9F%BA%E6%9C%AC%E6%A4%9C%E8%A8%BCfield-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. ステップ1：各フィールドの基本検証（Field オプション）\u003c/h2\u003e\n\u003cp data-sourcepos=\"167:1-167:138\"\u003eシリアライザが最初に実行するのが、各フィールドの型定義やオプションに基づく基本チェックです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"169:1-174:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserRegisterSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eusername\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emin_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eemail\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eEmailField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequired\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eIntegerField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emin_value\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emax_value\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e120\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequired\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"176:1-179:0\"\u003e\n\u003cli data-sourcepos=\"176:1-176:113\"\u003e\n\u003cstrong\u003e型の検証\u003c/strong\u003e: \u003ccode\u003eage\u003c/code\u003e に文字列 \u003ccode\u003e\"abc\"\u003c/code\u003e が送られてきたら、自動的に整数型エラーを検知\u003c/li\u003e\n\u003cli data-sourcepos=\"177:1-179:0\"\u003e\n\u003cstrong\u003e制約の検証\u003c/strong\u003e: \u003ccode\u003emax_length\u003c/code\u003e や \u003ccode\u003emin_value\u003c/code\u003e を超える値、必須項目（\u003ccode\u003erequired=True\u003c/code\u003e）の未送信を自動でブロック\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"180:1-180:89\"\u003e\n\u003cspan id=\"3-ステップ2単一フィールドの独自ルール検証validate_\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E3%82%B9%E3%83%86%E3%83%83%E3%83%972%E5%8D%98%E4%B8%80%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%81%AE%E7%8B%AC%E8%87%AA%E3%83%AB%E3%83%BC%E3%83%AB%E6%A4%9C%E8%A8%BCvalidate_\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. ステップ2：単一フィールドの独自ルール検証（validate_）\u003c/h2\u003e\n\u003cp data-sourcepos=\"182:1-182:217\"\u003e特定のフィールドに対して「アプリ固有のビジネスルール」を適用したい場合は、\u003cstrong\u003e\u003ccode\u003evalidate_\u0026lt;フィールド名\u0026gt;(self, value)\u003c/code\u003e\u003c/strong\u003e という命名規則でメソッドを定義します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"184:1-201:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserRegisterSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eemail\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eEmailField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003evalidate_email\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003e特定のドメインのみを許可する独自バリデーション\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eallowed_domain\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eexample.com\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n        \u003cspan class=\"n\"\u003edomain\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e@\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n        \n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003edomain\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003eallowed_domain\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"c1\"\u003e# バリデーションNGの場合は ValidationError を送出\n\u003c/span\u003e            \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eValidationError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003eallowed_domain\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s\"\u003e のメールアドレスのみ登録可能です。\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \n        \u003cspan class=\"c1\"\u003e# 検証OKの場合は、必ず受け取った値（または加工した値）を return する\n\u003c/span\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elower\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"203:1-207:0\"\u003e\n\u003cli data-sourcepos=\"203:1-203:99\"\u003eメソッドの第2引数 \u003ccode\u003evalue\u003c/code\u003e に、そのフィールドの入力値が渡ってきます。\u003c/li\u003e\n\u003cli data-sourcepos=\"204:1-204:104\"\u003e不正な場合は \u003cstrong\u003e\u003ccode\u003eserializers.ValidationError(\"エラーメッセージ\")\u003c/code\u003e\u003c/strong\u003e を送出します。\u003c/li\u003e\n\u003cli data-sourcepos=\"205:1-207:0\"\u003e\n\u003cstrong\u003e超重要\u003c/strong\u003e: 検証にパスした場合は、\u003cstrong\u003e必ず値を \u003ccode\u003ereturn\u003c/code\u003e\u003c/strong\u003e してください（値を return しないと \u003ccode\u003eNone\u003c/code\u003e になってしまいます）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"208:1-208:72\"\u003e\n\u003cspan id=\"4-ステップ3複数フィールドの横断検証validate\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%82%B9%E3%83%86%E3%83%83%E3%83%973%E8%A4%87%E6%95%B0%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%81%AE%E6%A8%AA%E6%96%AD%E6%A4%9C%E8%A8%BCvalidate\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. ステップ3：複数フィールドの横断検証（validate）\u003c/h2\u003e\n\u003cp data-sourcepos=\"210:1-210:259\"\u003e「パスワードとパスワード確認の一致」や「開始日と終了日の前後関係」など、\u003cstrong\u003e2つ以上のフィールドを組み合わせた相関チェック\u003c/strong\u003e を行うには、\u003cstrong\u003e\u003ccode\u003evalidate(self, attrs)\u003c/code\u003e\u003c/strong\u003e メソッドを定義します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"212:1-232:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eEventSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003etitle\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eCharField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_length\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eDateField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eDateField\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003eシリアライザ全体の横断バリデーション\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n        \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003estart_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eend_date\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003estart_date\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eend_date\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n            \u003cspan class=\"c1\"\u003e# 全体エラー（non_field_errors）として返す場合\n\u003c/span\u003e            \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eValidationError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e開始日は終了日以前の日付を指定してください。\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n            \u003cspan class=\"c1\"\u003e# または、特定フィールドのエラーとして辞書形式で返す場合\n\u003c/span\u003e            \u003cspan class=\"c1\"\u003e# raise serializers.ValidationError({\"end_date\": \"終了日は開始日以降の日付を指定してください。\"})\n\u003c/span\u003e\n        \u003cspan class=\"c1\"\u003e# 検証OKの場合は、必ず検証済み辞書 attrs を return する\n\u003c/span\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"234:1-237:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"235:1-236:266\"\u003e\u003cstrong\u003e💡 PATCH（一部更新）時の相関バリデーションについて\u003c/strong\u003e\u003cbr\u003e\n一部更新（PATCH）では未送信のフィールドが \u003ccode\u003eattrs\u003c/code\u003e に含まれません。そのため、相関バリデーションを行う際は第六講で学んだ通り、既存インスタンス（\u003ccode\u003eself.instance\u003c/code\u003e）から値を補完して検証します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"240:1-240:91\"\u003e\n\u003cspan id=\"5-extra_kwargs-と-metavalidators-による設定ベースのバリデーション\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-extra_kwargs-%E3%81%A8-metavalidators-%E3%81%AB%E3%82%88%E3%82%8B%E8%A8%AD%E5%AE%9A%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. \u003ccode\u003eextra_kwargs\u003c/code\u003e と \u003ccode\u003eMeta.validators\u003c/code\u003e による設定ベースのバリデーション\u003c/h2\u003e\n\u003cp data-sourcepos=\"242:1-242:217\"\u003eDRF では、\u003ccode\u003evalidate_\u0026lt;field\u0026gt;()\u003c/code\u003e や \u003ccode\u003evalidate()\u003c/code\u003e のようにメソッドを定義する方法だけでなく、\u003ccode\u003eclass Meta\u003c/code\u003e 内に設定を書くことでバリデーションを追加することもできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"244:1-244:43\"\u003e代表的なのが、以下の2つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"246:1-248:0\"\u003e\n\u003cli data-sourcepos=\"246:1-246:95\"\u003e\n\u003cstrong\u003e\u003ccode\u003eextra_kwargs\u003c/code\u003e\u003c/strong\u003e：特定のフィールドに追加設定やバリデータを設定する\u003c/li\u003e\n\u003cli data-sourcepos=\"247:1-248:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003eMeta.validators\u003c/code\u003e\u003c/strong\u003e：Serializer 全体に対するバリデータを設定する\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"249:1-250:0\"\u003e\n\u003ch3 data-sourcepos=\"251:1-251:21\"\u003e\n\u003cspan id=\"1-extra_kwargs\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-extra_kwargs\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003eextra_kwargs\u003c/code\u003e\n\u003c/h3\u003e\n\u003ch4 data-sourcepos=\"253:1-253:11\"\u003e\n\u003cspan id=\"役割\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%BD%B9%E5%89%B2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e役割\u003c/h4\u003e\n\u003cp data-sourcepos=\"254:1-255:146\"\u003e\u003ccode\u003eextra_kwargs\u003c/code\u003e は、\u003ccode\u003eModelSerializer\u003c/code\u003e が自動生成するフィールドに対して、追加の設定を行うための辞書です。\u003cbr\u003e\n\u003ccode\u003erequired\u003c/code\u003e や \u003ccode\u003eread_only\u003c/code\u003e のほか、\u003cstrong\u003e特定の1フィールドに対して既存の Validator を追加したい場合\u003c/strong\u003e に利用します。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"257:1-257:28\"\u003e\n\u003cspan id=\"regexvalidator-の例\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#regexvalidator-%E3%81%AE%E4%BE%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003eRegexValidator\u003c/code\u003e の例\u003c/h4\u003e\n\u003cp data-sourcepos=\"258:1-258:132\"\u003e特定フィールドに \u003ccode\u003eRegexValidator\u003c/code\u003e を追加し、英数字とアンダースコア以外の入力を禁止する例です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"260:1-281:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003edjango.core.validators\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eRegexValidator\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e__all__\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\n        \u003cspan class=\"n\"\u003eextra_kwargs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n            \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esender\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n                \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003evalidators\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n                    \u003cspan class=\"nc\"\u003eRegexValidator\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n                        \u003cspan class=\"n\"\u003eregex\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sa\"\u003er\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e^[a-zA-Z0-9_]+$\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n                        \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eSender must be alphanumeric or contain underscores.\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n                    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n                \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"283:1-291:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eextra_kwargs\n    ↓\n特定フィールドの追加設定（sender）\n    ↓\nvalidators\n    ↓\nRegexValidator\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"293:1-294:0\"\u003e\n\u003ch3 data-sourcepos=\"295:1-295:24\"\u003e\n\u003cspan id=\"2-metavalidators\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-metavalidators\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eMeta.validators\u003c/code\u003e\n\u003c/h3\u003e\n\u003ch4 data-sourcepos=\"297:1-297:11\"\u003e\n\u003cspan id=\"役割-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%BD%B9%E5%89%B2-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e役割\u003c/h4\u003e\n\u003cp data-sourcepos=\"298:1-298:229\"\u003e\u003ccode\u003eclass Meta\u003c/code\u003e 直下の \u003ccode\u003evalidators\u003c/code\u003e は、特定のフィールド単体ではなく、\u003cstrong\u003eSerializer 全体（複数フィールドの組み合わせなど）に作用するバリデータを設定\u003c/strong\u003e するために使います。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"300:1-300:37\"\u003e\n\u003cspan id=\"uniquetogethervalidator-の例\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#uniquetogethervalidator-%E3%81%AE%E4%BE%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e の例\u003c/h4\u003e\n\u003cp data-sourcepos=\"301:1-301:168\"\u003e代表例が、複数フィールドの組み合わせの一意性（ユニーク制約）をデータベース照合で保証する \u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"303:1-320:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003erest_framework.validators\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eUniqueTogetherValidator\u003c/span\u003e\n\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003e.models\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModelSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeta\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n        \u003cspan class=\"n\"\u003efields\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e__all__\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\n        \u003cspan class=\"n\"\u003evalidators\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"nc\"\u003eUniqueTogetherValidator\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n                \u003cspan class=\"n\"\u003equeryset\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eobjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n                \u003cspan class=\"n\"\u003efields\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003esender\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003erecipient\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003econtent\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n            \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"322:1-330:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eMeta.validators\n      ↓\nSerializer全体に作用\n      ↓\nUniqueTogetherValidator\n      ↓\n複数フィールド（sender + recipient + content）の組み合わせを一意に検証\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"332:1-333:0\"\u003e\n\u003ch3 data-sourcepos=\"334:1-334:28\"\u003e\n\u003cspan id=\"3-実行タイミング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E5%AE%9F%E8%A1%8C%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 実行タイミング\u003c/h3\u003e\n\u003cp data-sourcepos=\"336:1-336:156\"\u003e\u003ccode\u003eserializer.is_valid()\u003c/code\u003e が実行されたとき、これまでに登場したバリデーションは大まかに以下の順番で呼び出されます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"338:1-346:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eextra_kwargs.validators\n      ↓\nvalidate_\u0026lt;field\u0026gt;()\n      ↓\nMeta.validators\n      ↓\nvalidate()\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"348:1-350:0\"\u003e\n\u003cli data-sourcepos=\"348:1-348:156\"\u003e\n\u003cstrong\u003e前半（フィールド単位）\u003c/strong\u003e: \u003ccode\u003eextra_kwargs\u003c/code\u003e のバリデータや \u003ccode\u003evalidate_\u0026lt;field\u0026gt;()\u003c/code\u003e が各フィールドごとに順次実行されます。\u003c/li\u003e\n\u003cli data-sourcepos=\"349:1-350:0\"\u003e\n\u003cstrong\u003e後半（Serializer全体）\u003c/strong\u003e: 全フィールドの型変換・基本検証を通過した後、\u003ccode\u003eMeta.validators\u003c/code\u003e による全体検証や自作の \u003ccode\u003evalidate()\u003c/code\u003e が実行されます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"351:1-352:0\"\u003e\n\u003ch3 data-sourcepos=\"353:1-353:26\"\u003e\n\u003cspan id=\"4-4方式の比較表\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-4%E6%96%B9%E5%BC%8F%E3%81%AE%E6%AF%94%E8%BC%83%E8%A1%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 4方式の比較表\u003c/h3\u003e\n\u003cp data-sourcepos=\"355:1-356:159\"\u003e本講で登場した \u003cstrong\u003e4つのバリデーション手法\u003c/strong\u003e の特徴と使い分けのまとめです。\u003cbr\u003e\n実務で「どこに書くべきか？」「どれを使うべきか？」と迷った際は、この表を逆引き辞書として活用してください。\u003c/p\u003e\n\u003ctable data-sourcepos=\"358:1-363:222\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"358:1-358:103\"\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"358:2-358:30\"\u003eバリデーション方法\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"358:32-358:45\"\u003e書く場所\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"358:47-358:60\"\u003e主な対象\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"358:62-358:87\"\u003e主な用途・具体例\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"358:89-358:102\"\u003e選ぶ基準\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"360:1-360:215\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"360:2-360:38\"\u003e\u003cstrong\u003e\u003ccode\u003eextra_kwargs\u003c/code\u003e の \u003ccode\u003evalidators\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"360:40-360:72\"\u003e\n\u003ccode\u003eclass Meta\u003c/code\u003e → \u003ccode\u003eextra_kwargs\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"360:74-360:91\"\u003e1フィールド\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"360:93-360:132\"\u003e\n\u003ccode\u003eRegexValidator\u003c/code\u003e で文字種を制限\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"360:134-360:214\"\u003e\u003cstrong\u003eModelSerializerの特定フィールドに既存Validatorを追加したい\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"361:1-361:215\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"361:2-361:30\"\u003e\u003cstrong\u003e\u003ccode\u003evalidate_\u0026lt;field名\u0026gt;()\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"361:32-361:67\"\u003eSerializerクラスのメソッド\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"361:69-361:86\"\u003e1フィールド\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"361:88-361:149\"\u003eメールドメイン制限、業務固有の値チェック\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"361:151-361:214\"\u003e\u003cstrong\u003e1フィールドに独自Pythonロジックを書きたい\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"362:1-362:195\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"362:2-362:24\"\u003e\u003cstrong\u003e\u003ccode\u003eMeta.validators\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"362:26-362:39\"\u003e\u003ccode\u003eclass Meta\u003c/code\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"362:41-362:61\"\u003eSerializerレベル\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"362:63-362:123\"\u003e\n\u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e による組み合わせ一意性\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"362:125-362:194\"\u003e\u003cstrong\u003e再利用可能なSerializerレベルValidatorを設定したい\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"363:1-363:222\"\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"363:2-363:19\"\u003e\u003cstrong\u003e\u003ccode\u003evalidate()\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"363:21-363:56\"\u003eSerializerクラスのメソッド\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"363:58-363:99\"\u003eSerializer全体 / 複数フィールド\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"363:101-363:148\"\u003e開始日 ≤ 終了日、sender ≠ recipient\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"363:150-363:221\"\u003e\u003cstrong\u003e複数フィールドを使った独自ロジックを書きたい\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"366:1-366:102\"\u003e\n\u003cspan id=\"6-シリアライザを自在に操る主要フックと処理ライフサイクルの全貌\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%82%92%E8%87%AA%E5%9C%A8%E3%81%AB%E6%93%8D%E3%82%8B%E4%B8%BB%E8%A6%81%E3%83%95%E3%83%83%E3%82%AF%E3%81%A8%E5%87%A6%E7%90%86%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%81%AE%E5%85%A8%E8%B2%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. シリアライザを自在に操る「主要フック」と処理ライフサイクルの全貌\u003c/h2\u003e\n\u003cp data-sourcepos=\"368:1-368:313\"\u003eシリアライザは、リクエストを受け取ってからデータベースへ保存し、レスポンスを返すまでの各ステップで、開発者がロジックを割り込ませて上書きできるように \u003cstrong\u003e「フック（Hook）」と呼ばれるメソッド群\u003c/strong\u003e が用意されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"370:1-370:246\"\u003eクライアントからのリクエストが返送されるまでの \u003cstrong\u003e完全な処理ライフサイクルと ①〜⑦ のフックが呼び出されるタイミング\u003c/strong\u003e を1枚のマップにまとめると、以下のようになります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"372:1-372:80\"\u003e\n\u003cspan id=\"-シリアライザの完全処理パイプライン全体マップ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6%E3%81%AE%E5%AE%8C%E5%85%A8%E5%87%A6%E7%90%86%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%E5%85%A8%E4%BD%93%E3%83%9E%E3%83%83%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ シリアライザの完全処理パイプライン（全体マップ）\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"374:1-451:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eリクエスト\n   ↓\nJSONなどの入力データ\n   ↓\nSerializer(data=request.data)\n   ↓\nserializer.is_valid()\n   │\n   └─ Serializer.run_validation(data)\n        │\n        ├─ ① Serializer.to_internal_value(data)\n        │      │\n        │      ├─ フィールドA\n        │      │    ↓\n        │      │  Field.run_validation()\n        │      │    ↓\n        │      │  Field.to_internal_value()\n        │      │    ↓\n        │      │  Field.validators\n        │      │  （RegexValidatorなど）\n        │      │    ↓\n        │      │  ② validate_\u0026lt;fieldA\u0026gt;()\n        │      │\n        │      ├─ フィールドB\n        │      │    ↓\n        │      │   同じ処理\n        │      │\n        │      └─ フィールドC...\n        │\n        │\n        ├─ ③ Serializer.validators\n        │      （Meta.validators に設定されたもの）\n        │      ↓\n        │   UniqueTogetherValidator など\n        │\n        └─ ④ validate(attrs)\n               ↓\n           複数フィールドをまたぐ\n           独自バリデーション\n\n               ↓\n        validated_data 完成\n               ↓\n\nserializer.save()\n   │\n   ├─ 新規作成なら\n   │      ↓\n   │   ⑤ create(validated_data)\n   │\n   └─ 更新なら\n          ↓\n       ⑤ update(instance, validated_data)\n\n               ↓\n          Model instance\n               ↓\n\nserializer.data\n   ↓\n⑥ to_representation(instance)\n   │\n   ├─ 通常フィールドを出力値へ変換\n   │\n   └─ SerializerMethodField があれば\n          ↓\n       ⑦ get_\u0026lt;field\u0026gt;(obj)\n\n   ↓\nPythonのdict/list\n   ↓\nRenderer\n   ↓\nJSON\n   ↓\nレスポンス\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"453:1-453:62\"\u003e\n\u003cspan id=\"-主要フック一覧の対応まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E4%B8%BB%E8%A6%81%E3%83%95%E3%83%83%E3%82%AF%E4%B8%80%E8%A6%A7%E3%81%AE%E5%AF%BE%E5%BF%9C%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e▼ 主要フック一覧（①〜⑦の対応まとめ）\u003c/h3\u003e\n\u003cp data-sourcepos=\"455:1-456:276\"\u003e上記のフロー図に登場する ①〜⑦ のフックメソッドの一覧です。\u003cbr\u003e\nこれらを押さえておくことで、「入力値の変換・加工」「個別チェック」「相関チェック」「独自保存」「レスポンスの動的拡張」など、実務で発生するあらゆる要件に迷わず対応できるようになります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"458:1-466:219\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"458:1-458:109\"\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"458:2-458:9\"\u003e番号\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"458:11-458:42\"\u003eメソッド名 (関数定義)\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"458:44-458:72\"\u003e呼ばれるタイミング\u003c/th\u003e\n\u003cth style=\"text-align: left\" data-sourcepos=\"458:74-458:108\"\u003e主な用途・処理フェーズ\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"460:1-460:256\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"460:2-460:10\"\u003e\u003cstrong\u003e①\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"460:12-460:48\"\u003e\u003cstrong\u003e\u003ccode\u003eto_internal_value(self, data)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"460:50-460:99\"\u003e入力データを内部値に変換するとき\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"460:101-460:255\"\u003eクライアントから渡された生の入力データ（JSONなど）を各フィールドの Python 内部型に変換する初期ゲートウェイ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"461:1-461:203\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"461:2-461:10\"\u003e\u003cstrong\u003e②\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"461:12-461:51\"\u003e\u003cstrong\u003e\u003ccode\u003evalidate_\u0026lt;field名\u0026gt;(self, value)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"461:53-461:102\"\u003e特定フィールドのバリデーション時\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"461:104-461:202\"\u003e対象フィールドの個別ビジネスルール検証（\u003ccode\u003eField.validators\u003c/code\u003e の後に実行）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"462:1-462:242\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"462:2-462:10\"\u003e\u003cstrong\u003e③\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"462:12-462:69\"\u003e\n\u003cstrong\u003e\u003ccode\u003eSerializer.validators\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e(\u003ccode\u003eclass Meta.validators\u003c/code\u003e)\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"462:71-462:132\"\u003e全フィールド確定後の全体バリデーション時\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"462:134-462:241\"\u003e\n\u003ccode\u003eUniqueTogetherValidator\u003c/code\u003e など、DB問い合わせを伴う複数フィールドの一意性チェック\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"463:1-463:188\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"463:2-463:10\"\u003e\u003cstrong\u003e④\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"463:12-463:40\"\u003e\u003cstrong\u003e\u003ccode\u003evalidate(self, attrs)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"463:42-463:91\"\u003e全体バリデーションの最終ステップ\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"463:93-463:187\"\u003e複数フィールドにまたがる自作相関チェック（開始日 \u0026lt; 終了日など）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"464:1-464:251\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"464:2-464:10\"\u003e\u003cstrong\u003e⑤\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"464:12-464:95\"\u003e\n\u003cstrong\u003e\u003ccode\u003ecreate(self, validated_data)\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\u003cstrong\u003e\u003ccode\u003eupdate(self, instance, validated_data)\u003c/code\u003e\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"464:97-464:139\"\u003e\n\u003ccode\u003eserializer.save()\u003c/code\u003e で保存するとき\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"464:141-464:250\"\u003e新規レコード挿入（POST時: \u003ccode\u003ecreate\u003c/code\u003e）または既存レコード更新（PUT/PATCH時: \u003ccode\u003eupdate\u003c/code\u003e）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"465:1-465:215\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"465:2-465:10\"\u003e\u003cstrong\u003e⑥\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"465:12-465:52\"\u003e\u003cstrong\u003e\u003ccode\u003eto_representation(self, instance)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"465:54-465:106\"\u003eDBオブジェクトをJSON等に変換するとき\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"465:108-465:214\"\u003eデータベースのモデルオブジェクトを出力用データ（Python辞書 ➔ JSON）へ変換\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"466:1-466:219\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"466:2-466:10\"\u003e\u003cstrong\u003e⑦\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"466:12-466:44\"\u003e\u003cstrong\u003e\u003ccode\u003eget_\u0026lt;field名\u0026gt;(self, obj)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"466:46-466:92\"\u003e\n\u003ccode\u003eSerializerMethodField\u003c/code\u003e の値を作るとき\u003c/td\u003e\n\u003ctd style=\"text-align: left\" data-sourcepos=\"466:94-466:218\"\u003eモデルに存在しない動的な計算値やカスタム属性をレスポンスに含める（※第六講で解説）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"469:1-469:96\"\u003e\n\u003cspan id=\"7-実務必須テクニックraise_exceptiontrue-による自動エラーレスポンス\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E5%AE%9F%E5%8B%99%E5%BF%85%E9%A0%88%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AFraise_exceptiontrue-%E3%81%AB%E3%82%88%E3%82%8B%E8%87%AA%E5%8B%95%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. 実務必須テクニック：raise_exception=True による自動エラーレスポンス\u003c/h2\u003e\n\u003cp data-sourcepos=\"471:1-471:123\"\u003e実務の View で \u003ccode\u003eserializer.is_valid()\u003c/code\u003e を書く際、以下のような \u003ccode\u003eif-else\u003c/code\u003e を毎回書くのは冗長です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"473:1-480:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 冗長な書き方\n\u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_201_CREATED\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_400_BAD_REQUEST\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"482:1-482:314\"\u003eDRF では、\u003cstrong\u003e\u003ccode\u003eraise_exception=True\u003c/code\u003e\u003c/strong\u003e を引数に渡すことで、バリデーションエラー時に自動的に例外（\u003ccode\u003eValidationError\u003c/code\u003e）を発生させ、DRF が裏側で自動的に \u003cstrong\u003e400 Bad Request（JSON形式のエラーメッセージ付き）\u003c/strong\u003e をクライアントへ返却してくれます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"484:1-489:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# 実務の標準的な書き方（スマート！）\n\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eis_valid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eraise_exception\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTP_201_CREATED\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"491:1-491:124\"\u003eこの1行の引数を指定するだけで、ネストが浅くなりコードの見通しが劇的に改善されます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"494:1-494:117\"\u003e\n\u003cspan id=\"8-発展コラムdrfの弱点validated_dataの型追跡問題とフロントエンドtypescriptの役割\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-%E7%99%BA%E5%B1%95%E3%82%B3%E3%83%A9%E3%83%A0drf%E3%81%AE%E5%BC%B1%E7%82%B9validated_data%E3%81%AE%E5%9E%8B%E8%BF%BD%E8%B7%A1%E5%95%8F%E9%A1%8C%E3%81%A8%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89typescript%E3%81%AE%E5%BD%B9%E5%89%B2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. 発展コラム：DRFの弱点「validated_dataの型追跡問題」とフロントエンドTypeScriptの役割\u003c/h2\u003e\n\u003cp data-sourcepos=\"496:1-496:321\"\u003eここまでシリアライザの強力なバリデーション機能を見てきましたが、現代の Web 開発の視点から、\u003cstrong\u003eあえて DRF の弱点（デメリット）\u003c/strong\u003e と、それを踏まえた \u003cstrong\u003e「フロントエンドとバックエンドの型戦略」\u003c/strong\u003e について整理しておきましょう。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"498:1-498:110\"\u003e\n\u003cspan id=\"1-drf-の弱点通過後はただの辞書型が追えないデータになってしまう\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-drf-%E3%81%AE%E5%BC%B1%E7%82%B9%E9%80%9A%E9%81%8E%E5%BE%8C%E3%81%AF%E3%81%9F%E3%81%A0%E3%81%AE%E8%BE%9E%E6%9B%B8%E5%9E%8B%E3%81%8C%E8%BF%BD%E3%81%88%E3%81%AA%E3%81%84%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. DRF の弱点：通過後は「ただの辞書（型が追えないデータ）」になってしまう\u003c/h3\u003e\n\u003cp data-sourcepos=\"499:1-499:240\"\u003eDRF では、どんなに厳格にフィールド定義やバリデーションを行っても、検証を通過したデータは \u003cstrong\u003e単なる Python の辞書（\u003ccode\u003edict\u003c/code\u003e 型である \u003ccode\u003evalidated_data\u003c/code\u003e）\u003c/strong\u003e に変換されてしまいます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"501:1-509:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# is_valid() を通過した後は単なる dict（辞書型）\n\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eserializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evalidated_data\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# ❌ エディタのコード補完（型ヒント）が効かない！\n# ❌ キー名をタイポ（打ち間違い）しても、実行時までエラーに気付けない！\n\u003c/span\u003e\u003cspan class=\"n\"\u003euser_id\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003euser_id\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003cspan class=\"n\"\u003etitle\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003etitle\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"511:1-511:36\"\u003e\n\u003cspan id=\"fastapi--pydantic-との比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#fastapi--pydantic-%E3%81%A8%E3%81%AE%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eFastAPI / Pydantic との比較\u003c/h4\u003e\n\u003cp data-sourcepos=\"512:1-512:231\"\u003e近年バックエンドで人気を集めている \u003cstrong\u003eFastAPI（Pydantic）\u003c/strong\u003e の場合、バリデーション通過後のデータも \u003cstrong\u003e「型が完全に定義されたクラスオブジェクト」\u003c/strong\u003e として扱われます。\u003c/p\u003e\n\u003cul data-sourcepos=\"514:1-516:0\"\u003e\n\u003cli data-sourcepos=\"514:1-514:176\"\u003e\n\u003cstrong\u003eFastAPI (Pydantic)\u003c/strong\u003e: \u003ccode\u003euser.user_id\u003c/code\u003e や \u003ccode\u003euser.title\u003c/code\u003e のようにオブジェクト属性としてアクセスでき、IDE で完全に型推論・自動補完が効く。\u003c/li\u003e\n\u003cli data-sourcepos=\"515:1-516:0\"\u003e\n\u003cstrong\u003eDRF (Serializer)\u003c/strong\u003e: \u003ccode\u003evalidated_data[\"...\"]\u003c/code\u003e という文字列キーによる辞書アクセスになり、ここで Python の静的な型追跡がプツリと途切れてしまう。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"517:1-517:152\"\u003eこれは、動的型付けが主流だった時代に設計された DRF が抱える、近代的なアーキテクチャ上の弱点と言えます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"519:1-519:89\"\u003e\n\u003cspan id=\"2-システム全体で型安全性を保つ設計typescript--drf-serializer\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%85%A8%E4%BD%93%E3%81%A7%E5%9E%8B%E5%AE%89%E5%85%A8%E6%80%A7%E3%82%92%E4%BF%9D%E3%81%A4%E8%A8%AD%E8%A8%88typescript--drf-serializer\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. システム全体で型安全性を保つ設計（TypeScript × DRF Serializer）\u003c/h3\u003e\n\u003cp data-sourcepos=\"521:1-521:200\"\u003eDRF では、\u003ccode\u003eserializer.is_valid()\u003c/code\u003e を通過した後の \u003ccode\u003evalidated_data\u003c/code\u003e が Python の \u003ccode\u003edict\u003c/code\u003e として扱われるため、バックエンド内部では静的な型追跡が弱くなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"523:1-523:216\"\u003eこの弱点をシステム全体の設計として補う方法の一つが、\u003cstrong\u003eフロントエンドで TypeScript を利用し、バックエンドの API スキーマと型定義を一致させること\u003c/strong\u003eです。\u003c/p\u003e\n\u003cp data-sourcepos=\"525:1-525:214\"\u003eフロントエンドでは TypeScript によって、API に送信するデータや受信するレスポンスの型不整合、キー名の誤り、必須項目の不足などを開発時に検出できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"527:1-527:112\"\u003e一方、バックエンドでは DRF Serializer が実際に届いたデータを実行時に検証します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"529:1-543:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eTypeScript\n↓\nフロント側でAPI型の不整合を開発時に検出\n\nDRF Serializer\n↓\n実際に届いたデータを実行時に検証\n\nvalidated_data\n↓\nPythonのdictとして短い範囲だけ利用\n\nModel / DTO\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"545:1-545:140\"\u003eつまり、\u003ccode\u003evalidated_data\u003c/code\u003e が \u003ccode\u003edict\u003c/code\u003e になることで、DRF のバックエンド内部では静的な型追跡が弱くなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"547:1-547:300\"\u003e一方で、バックエンドの API スキーマを基準としてフロントエンド側にも対応する TypeScript の型定義を用意しておけば、API の入出力における型不整合やキー名の誤り、必須項目の不足などを開発時に検出しやすくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"549:1-549:268\"\u003eさらにバックエンド内部では、\u003ccode\u003evalidated_data\u003c/code\u003e を長距離にわたって持ち回らず、Serializer 内部や保存直前などの短い範囲だけで利用し、早い段階で Model や DTO などの型を持つオブジェクトへ変換します。\u003c/p\u003e\n\u003cp data-sourcepos=\"551:1-551:18\"\u003eこのように、\u003c/p\u003e\n\u003cul data-sourcepos=\"553:1-556:0\"\u003e\n\u003cli data-sourcepos=\"553:1-553:85\"\u003e\n\u003cstrong\u003eTypeScript\u003c/strong\u003e でフロントエンド側の API 型を静的にチェックする\u003c/li\u003e\n\u003cli data-sourcepos=\"554:1-554:78\"\u003e\n\u003cstrong\u003eDRF Serializer\u003c/strong\u003e で実際に届いたデータを実行時に検証する\u003c/li\u003e\n\u003cli data-sourcepos=\"555:1-556:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003evalidated_data\u003c/code\u003e\u003c/strong\u003e を \u003ccode\u003edict\u003c/code\u003e のまま扱う範囲を最小限にする\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"557:1-557:230\"\u003eという3段構えにすることで、DRF の型追跡上の弱点をシステム全体の設計で補いながら、フロントエンドからバックエンドまで型とデータの整合性を保ちやすくなります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"560:1-560:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"562:1-584:0\"\u003e\n\u003cli data-sourcepos=\"562:1-563:227\"\u003e\n\u003cstrong\u003eなぜシリアライザを使うのか？\u003c/strong\u003e:\n\u003cul data-sourcepos=\"563:3-563:227\"\u003e\n\u003cli data-sourcepos=\"563:3-563:227\"\u003eView での \u003ccode\u003erequest.data\u003c/code\u003e 直接チェック（if文地獄・Fat View）を回避し、入力検証をシリアライザに集約することで、コードの保守性・再利用性・テスト容易性を最大化する\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"564:1-566:84\"\u003e\n\u003cstrong\u003eバリデーションの4ステップ\u003c/strong\u003e:\n\u003cul data-sourcepos=\"565:3-566:84\"\u003e\n\u003cli data-sourcepos=\"565:3-565:85\"\u003e入力 ➔ \u003ccode\u003eis_valid()\u003c/code\u003e ➔ 成功なら \u003ccode\u003evalidated_data\u003c/code\u003e / 失敗なら \u003ccode\u003eerrors\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"566:3-566:84\"\u003e※\u003ccode\u003evalidated_data\u003c/code\u003e と \u003ccode\u003eerrors\u003c/code\u003e は必ず \u003ccode\u003eis_valid()\u003c/code\u003e の後に参照する！\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"567:1-570:106\"\u003e\n\u003cstrong\u003e3段階の検証パイプライン\u003c/strong\u003e:\n\u003col data-sourcepos=\"568:3-570:106\"\u003e\n\u003cli data-sourcepos=\"568:3-568:74\"\u003eフィールド基本検証（型・文字数・必須チェック）\u003c/li\u003e\n\u003cli data-sourcepos=\"569:3-569:81\"\u003e単一フィールド検証（\u003ccode\u003evalidate_\u0026lt;field\u0026gt;()\u003c/code\u003e、必ず return する）\u003c/li\u003e\n\u003cli data-sourcepos=\"570:3-570:106\"\u003e複数フィールド横断検証（\u003ccode\u003evalidate()\u003c/code\u003e、PATCH時は既存インスタンスから補完）\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"571:1-575:90\"\u003e\n\u003cstrong\u003e四つのバリデーション手法と使い分け\u003c/strong\u003e:\n\u003cul data-sourcepos=\"572:3-575:90\"\u003e\n\u003cli data-sourcepos=\"572:3-572:118\"\u003e\n\u003ccode\u003eextra_kwargs\u003c/code\u003e の \u003ccode\u003evalidators\u003c/code\u003e: ModelSerializerの特定フィールドに既存Validatorを追加したいとき\u003c/li\u003e\n\u003cli data-sourcepos=\"573:3-573:93\"\u003e\n\u003ccode\u003evalidate_\u0026lt;field名\u0026gt;()\u003c/code\u003e: 1フィールドに独自Pythonロジックを書きたいとき\u003c/li\u003e\n\u003cli data-sourcepos=\"574:3-574:93\"\u003e\n\u003ccode\u003eMeta.validators\u003c/code\u003e: 再利用可能なSerializerレベルValidatorを設定したいとき\u003c/li\u003e\n\u003cli data-sourcepos=\"575:3-575:90\"\u003e\n\u003ccode\u003evalidate()\u003c/code\u003e: 複数フィールドを使った独自ロジックを書きたいとき\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"576:1-577:230\"\u003e\n\u003cstrong\u003eシリアライザの主要フック\u003c/strong\u003e:\n\u003cul data-sourcepos=\"577:3-577:230\"\u003e\n\u003cli data-sourcepos=\"577:3-577:230\"\u003e入力・検証（\u003ccode\u003eto_internal_value\u003c/code\u003e, \u003ccode\u003evalidate_\u0026lt;field\u0026gt;\u003c/code\u003e, \u003ccode\u003evalidate\u003c/code\u003e）から保存（\u003ccode\u003ecreate\u003c/code\u003e, \u003ccode\u003eupdate\u003c/code\u003e）、出力（\u003ccode\u003eto_representation\u003c/code\u003e, \u003ccode\u003eget_\u0026lt;field\u0026gt;\u003c/code\u003e）まで各ライフサイクルを柔軟にオーバーライド可能\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"578:1-579:89\"\u003e\n\u003cstrong\u003e実務のTips\u003c/strong\u003e:\n\u003cul data-sourcepos=\"579:3-579:89\"\u003e\n\u003cli data-sourcepos=\"579:3-579:89\"\u003e\n\u003ccode\u003eserializer.is_valid(raise_exception=True)\u003c/code\u003e でスマートに 400 エラーを返却\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"580:1-584:0\"\u003e\n\u003cstrong\u003eDRFの弱点と3段構えの型戦略\u003c/strong\u003e:\n\u003cul data-sourcepos=\"581:3-584:0\"\u003e\n\u003cli data-sourcepos=\"581:3-581:79\"\u003e\n\u003cstrong\u003eTypeScript\u003c/strong\u003e: フロントエンド側の API 型を静的にチェック\u003c/li\u003e\n\u003cli data-sourcepos=\"582:3-582:104\"\u003e\n\u003cstrong\u003eDRF Serializer\u003c/strong\u003e: 実際に届いたデータを API 境界で実行時に検証（最後の砦）\u003c/li\u003e\n\u003cli data-sourcepos=\"583:3-584:0\"\u003e\n\u003cstrong\u003e\u003ccode\u003evalidated_data\u003c/code\u003e\u003c/strong\u003e: \u003ccode\u003edict\u003c/code\u003e のまま長距離持ち回らず、早い段階で Model や DTO へ変換して影響範囲を最小限にする\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"585:1-586:257\"\u003eシリアライザのバリデーションを完全にマスターしたことで、堅牢で安全な WebAPI を構築する準備が整いました！\u003cbr\u003e\n次回は、シリアライザで処理されたデータをフロントエンドへ返す \u003cstrong\u003e\u003ca href=\"/items/5260e8be137026ca99d5\"\u003e【第八講】DRFのView徹底解剖・リクエスト処理フローとクラス階層\u003c/a\u003e\u003c/strong\u003e の世界へ進んでいきましょう！\u003c/p\u003e\n\u003chr data-sourcepos=\"588:1-589:0\"\u003e\n\u003cp data-sourcepos=\"590:1-590:238\"\u003e\u003cstrong\u003e\u003ca href=\"/items/914c08a85e63388664e8\"\u003e← 第六講：DRFの中核「シリアライザ」徹底解剖・3大分類と更新処理\u003c/a\u003e | \u003ca href=\"/items/4250ea04fb61f5bc4f0d\"\u003eまとめページに戻る\u003c/a\u003e | \u003ca href=\"/items/5260e8be137026ca99d5\"\u003e第八講へ →\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n","body":"\n\u003e **Lecture 07: Mastering Serializer Validation — is_valid Pipeline \u0026 Error Handling**\n\n[← DRF × Vue.js 実践講座：まとめページに戻る](/items/4250ea04fb61f5bc4f0d) | [← 第六講：DRFの中核「シリアライザ」徹底解剖・3大分類と更新処理](/items/914c08a85e63388664e8)\n\n## はじめに：なぜシリアライザのバリデーションが重要なのか？\n\n第六講では、DRF の最重要コンポーネントであるシリアライザの全体像と3大分類（Serializer / ModelSerializer / ListSerializer）、そして通信全体のデータの流れ（Parser 〜 Serializer 〜 Renderer）を学びました。\n\nWebAPI を運用する上で、最も恐ろしいのは **「不正なデータや悪意あるデータがデータベースに入り込んでしまうこと」** です。  \nフロントエンド（Vue.js）側でもフォームの入力チェックは行いますが、API はブラウザ以外のツール（Postman や curl、悪意のあるスクリプト）からも直接呼び出されるため、**バックエンドでの徹底的なバリデーション（入力値検証）が最後の砦** となります。\n\n本講では、DRF のバリデーションの基本サイクルから、裏側で動く検証パイプライン、単一・複数フィールドの独自バリデーション、そして実務で頻出するエラー制御テクニックまでを完全攻略します！\n\n### ▼ そもそもシリアライザを使わないとどうなる？（手動検証 vs Serializer集約）\n\n「そもそも、View の中で `request.data` を取り出して `if` 文でチェックすれば、わざわざシリアライザなんて作らなくても動くのでは？」と思ったことはないでしょうか？\n\nたしかに Python コードとしては動作しますが、実務では **「非推奨に近い書き方」** とされています。  \n具体例として、「複数のキャスト番号（配列）を受け取る API」を例に、シリアライザを使わない場合と使う場合を比較してみましょう。\n\n#### 1. 非推奨に近い書き方（シリアライザを使わない）\nView の中で `request.data` を直接触り、手動でチェックしている状態です。\n\n```python\n# casting/views.py\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\n\n@api_view([\"POST\"])\ndef casting_view(request):\n    # ① request.data を直接取り出す\n    casting_numbers = request.data.get(\"casting_numbers\")\n\n    # ② 手動でバリデーション（泥沼の if 文地獄...）\n    if casting_numbers is None:\n        return Response({\"success\": False, \"error\": \"casting_numbers が必須です\"}, status=400)\n\n    if not isinstance(casting_numbers, list):\n        return Response({\"success\": False, \"error\": \"casting_numbers は配列で指定してください\"}, status=400)\n\n    if len(casting_numbers) == 0:\n        return Response({\"success\": False, \"error\": \"casting_numbers は1件以上指定してください\"}, status=400)\n\n    for num in casting_numbers:\n        if not isinstance(num, str):\n            return Response({\"success\": False, \"error\": \"各要素は文字列で指定してください\"}, status=400)\n\n    # ③ 実処理\n    results = []\n    for num in casting_numbers:\n        results.append({\"casting_number\": num, \"status\": \"processed\"})\n\n    # ④ レスポンスを返す（DRFがJSONに変換して返す）\n    return Response({\"success\": True, \"data\": results}, status=200)\n```\n\n```text\n【処理の流れ（イメージ）】\nリクエスト (JSON) ➔ view（手動バリデーション：必須・型・空配列・要素型チェック ＋ 実処理） ➔ レスポンス (JSON)\n```\n\n:::note alert\n**❌ デメリット（非推奨に近い理由）**\n* **View の責務が肥大化**: View が入力チェックまで担当してしまい、ファットビュー（Fat View）になる\n* **メンテナンスが大変**: チェック項目が増えるほど View が長くなり、可読性が著しく低下する\n* **設計の不統一**: 他の API とバリデーションの書き方やエラー形式が統一されず、バラバラになる\n* **テストが書きにくい**: View を経由しないとバリデーション部分を個別にテストできない\n:::\n\n---\n\n#### 2. 推奨される書き方（シリアライザを使う）\n**「入力バリデーションはシリアライザに集約し、View は本来の処理に集中する」** のが DRF の美しい設計思想です。\n\n```python\n# casting/serializers.py\nfrom rest_framework import serializers\n\nclass CastingRequestSerializer(serializers.Serializer):\n    # 必須・配列・空不可・各要素は文字列をたった数行で宣言！\n    casting_numbers = serializers.ListField(\n        child=serializers.CharField(),\n        allow_empty=False\n    )\n```\n\n```python\n# casting/views.py\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\nfrom .serializers import CastingRequestSerializer\n\n@api_view([\"POST\"])\ndef casting_view(request):\n    # ① serializer でバリデーション\n    serializer = CastingRequestSerializer(data=request.data)\n\n    if not serializer.is_valid():\n        return Response({\"success\": False, \"errors\": serializer.errors}, status=400)\n\n    casting_numbers = serializer.validated_data[\"casting_numbers\"]\n\n    # ② 実処理（バリデーション済みのクリーンなデータを使う）\n    results = []\n    for num in casting_numbers:\n        results.append({\"casting_number\": num, \"status\": \"processed\"})\n\n    # ③ レスポンスを返す\n    return Response({\"success\": True, \"data\": results}, status=200)\n```\n\n```text\n【処理の流れ（イメージ）】\nリクエスト (JSON) ➔ serializer（自動バリデーション） ➔ view（実処理・レスポンス作成） ➔ レスポンス (JSON)\n```\n\n:::note info\n**✅ メリット（推奨される理由）**\n* **View がシンプルに**: 入力チェックは serializer に集約でき、view はビジネスロジックに集中できる\n* **再利用しやすい**: バリデーションの再利用がしやすい（他の view でも使い回せる）\n* **設計が統一**: 他のアプリ（例: `file_manager` など）とも設計パターンを綺麗に統一できる\n* **テストが書きやすい**: serializer 単体にデータを渡すだけでバリデーションテストが可能\n:::\n\nこのように、**シリアライザを使うことで、DRF の設計思想に沿ったシンプルで保守しやすいコード** になります。\n\n\n### ▼ バリデーションの基本4ステップ\n\n![740295a2-66ff-47e3-8754-b2c4ccb0cd37.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/e668d67b-821b-47ce-b000-23094f45dd43.avif)\n\nシリアライザのバリデーションは、上記の通り **「① 入力データを渡す ➔ ② `is_valid()` で検証 ➔ ③ 成功なら `validated_data` / ④ 失敗なら `errors`」** という非常にシンプルな4ステップで動きます。\n\n* **① 入力データを渡す**: `serializer = UserSerializer(data=request.data)` で生データをセット\n* **② 入力値を検証**: `serializer.is_valid()` を呼び出して検証を実行\n* **③ 成功時**: クリーンな検証済みデータが `serializer.validated_data`（内部では `_validated_data`）に入る\n* **④ 失敗時**: バリデーションエラーの詳細が `serializer.errors`（内部では `_errors`）に入る\n\n:::note tip\n**💡 超重要ルール：`validated_data` と `errors` は必ず `is_valid()` の後に使う！**  \n`is_valid()` を実行する前に `serializer.validated_data` や `serializer.errors` を参照しようとすると、`AssertionError` が発生します。必ず `is_valid()` を実行した後に取り出してください。\n:::\n\n\n## 1. is_valid() の内部処理（バリデーションの全体像）\n\n`serializer.is_valid()` を実行したとき、裏側では以下のパイプラインが大まかに順番に動作しています。\n\n![53fa751d-6574-4373-9511-9572b450e469.avif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4223670/f51c4a00-b10b-4d38-b40a-e7e4242a216d.avif)\n\n1. **基本検証**: 型チェック、必須確認、文字数制限（`max_length`）\n2. **`validate_\u003cfield\u003e()`**: 単一フィールドに対する独自ルールチェック（例: `validate_email()` でドメイン制限）\n3. **`Serializer.validators`**: 複数フィールドの一意性制約などのクラスバリデータ（例: `UniqueTogetherValidator`）\n4. **`validate()`**: 複数フィールドを組み合わせた相関チェック（例: 「開始日時 \u003c 終了日時」「パスワード一致」）\n5. **検証結果の確定**:\n   - **成功時**: クリーンな値が `serializer.validated_data` に格納\n   - **失敗時**: エラー詳細が `serializer.errors` に格納（400 Bad Request）\n\n:::note info\n※各バリデータが具体的にどの詳細な順番で実行されるのか（内部順序やフックの完全なライフサイクル）は、後ほど **第5章・第6章** でじっくり解説します。まずは「基本検証 ➔ 単一チェック ➔ 全体チェック」という大きな流れを押さえましょう！\n:::\n\n\n## 2. ステップ1：各フィールドの基本検証（Field オプション）\n\nシリアライザが最初に実行するのが、各フィールドの型定義やオプションに基づく基本チェックです。\n\n```python\nclass UserRegisterSerializer(serializers.Serializer):\n    username = serializers.CharField(max_length=20, min_length=3)\n    email = serializers.EmailField(required=True)\n    age = serializers.IntegerField(min_value=0, max_value=120, required=False)\n```\n\n* **型の検証**: `age` に文字列 `\"abc\"` が送られてきたら、自動的に整数型エラーを検知\n* **制約の検証**: `max_length` や `min_value` を超える値、必須項目（`required=True`）の未送信を自動でブロック\n\n\n## 3. ステップ2：単一フィールドの独自ルール検証（validate_\u003cfield\u003e）\n\n特定のフィールドに対して「アプリ固有のビジネスルール」を適用したい場合は、**`validate_\u003cフィールド名\u003e(self, value)`** という命名規則でメソッドを定義します。\n\n```python\nfrom rest_framework import serializers\n\nclass UserRegisterSerializer(serializers.Serializer):\n    email = serializers.EmailField()\n\n    def validate_email(self, value):\n        \"\"\"特定のドメインのみを許可する独自バリデーション\"\"\"\n        allowed_domain = \"example.com\"\n        domain = value.split(\"@\")[-1]\n        \n        if domain != allowed_domain:\n            # バリデーションNGの場合は ValidationError を送出\n            raise serializers.ValidationError(f\"{allowed_domain} のメールアドレスのみ登録可能です。\")\n        \n        # 検証OKの場合は、必ず受け取った値（または加工した値）を return する\n        return value.lower()\n```\n\n* メソッドの第2引数 `value` に、そのフィールドの入力値が渡ってきます。\n* 不正な場合は **`serializers.ValidationError(\"エラーメッセージ\")`** を送出します。\n* **超重要**: 検証にパスした場合は、**必ず値を `return`** してください（値を return しないと `None` になってしまいます）。\n\n\n## 4. ステップ3：複数フィールドの横断検証（validate）\n\n「パスワードとパスワード確認の一致」や「開始日と終了日の前後関係」など、**2つ以上のフィールドを組み合わせた相関チェック** を行うには、**`validate(self, attrs)`** メソッドを定義します。\n\n```python\nclass EventSerializer(serializers.Serializer):\n    title = serializers.CharField(max_length=100)\n    start_date = serializers.DateField()\n    end_date = serializers.DateField()\n\n    def validate(self, attrs):\n        \"\"\"シリアライザ全体の横断バリデーション\"\"\"\n        start_date = attrs.get(\"start_date\")\n        end_date = attrs.get(\"end_date\")\n\n        if start_date and end_date and start_date \u003e end_date:\n            # 全体エラー（non_field_errors）として返す場合\n            raise serializers.ValidationError(\"開始日は終了日以前の日付を指定してください。\")\n\n            # または、特定フィールドのエラーとして辞書形式で返す場合\n            # raise serializers.ValidationError({\"end_date\": \"終了日は開始日以降の日付を指定してください。\"})\n\n        # 検証OKの場合は、必ず検証済み辞書 attrs を return する\n        return attrs\n```\n\n:::note info\n**💡 PATCH（一部更新）時の相関バリデーションについて**  \n一部更新（PATCH）では未送信のフィールドが `attrs` に含まれません。そのため、相関バリデーションを行う際は第六講で学んだ通り、既存インスタンス（`self.instance`）から値を補完して検証します。\n:::\n\n\n## 5. `extra_kwargs` と `Meta.validators` による設定ベースのバリデーション\n\nDRF では、`validate_\u003cfield\u003e()` や `validate()` のようにメソッドを定義する方法だけでなく、`class Meta` 内に設定を書くことでバリデーションを追加することもできます。\n\n代表的なのが、以下の2つです。\n\n* **`extra_kwargs`**：特定のフィールドに追加設定やバリデータを設定する\n* **`Meta.validators`**：Serializer 全体に対するバリデータを設定する\n\n---\n\n### 1. `extra_kwargs`\n\n#### 役割\n`extra_kwargs` は、`ModelSerializer` が自動生成するフィールドに対して、追加の設定を行うための辞書です。  \n`required` や `read_only` のほか、**特定の1フィールドに対して既存の Validator を追加したい場合** に利用します。\n\n#### `RegexValidator` の例\n特定フィールドに `RegexValidator` を追加し、英数字とアンダースコア以外の入力を禁止する例です。\n\n```python\nfrom django.core.validators import RegexValidator\nfrom rest_framework import serializers\nfrom .models import Message\n\n\nclass MessageSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Message\n        fields = \"__all__\"\n\n        extra_kwargs = {\n            \"sender\": {\n                \"validators\": [\n                    RegexValidator(\n                        regex=r\"^[a-zA-Z0-9_]+$\",\n                        message=\"Sender must be alphanumeric or contain underscores.\"\n                    )\n                ]\n            }\n        }\n```\n\n```text\nextra_kwargs\n    ↓\n特定フィールドの追加設定（sender）\n    ↓\nvalidators\n    ↓\nRegexValidator\n```\n\n---\n\n### 2. `Meta.validators`\n\n#### 役割\n`class Meta` 直下の `validators` は、特定のフィールド単体ではなく、**Serializer 全体（複数フィールドの組み合わせなど）に作用するバリデータを設定** するために使います。\n\n#### `UniqueTogetherValidator` の例\n代表例が、複数フィールドの組み合わせの一意性（ユニーク制約）をデータベース照合で保証する `UniqueTogetherValidator` です。\n\n```python\nfrom rest_framework import serializers\nfrom rest_framework.validators import UniqueTogetherValidator\nfrom .models import Message\n\n\nclass MessageSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Message\n        fields = \"__all__\"\n\n        validators = [\n            UniqueTogetherValidator(\n                queryset=Message.objects.all(),\n                fields=[\"sender\", \"recipient\", \"content\"]\n            )\n        ]\n```\n\n```text\nMeta.validators\n      ↓\nSerializer全体に作用\n      ↓\nUniqueTogetherValidator\n      ↓\n複数フィールド（sender + recipient + content）の組み合わせを一意に検証\n```\n\n---\n\n### 3. 実行タイミング\n\n`serializer.is_valid()` が実行されたとき、これまでに登場したバリデーションは大まかに以下の順番で呼び出されます。\n\n```text\nextra_kwargs.validators\n      ↓\nvalidate_\u003cfield\u003e()\n      ↓\nMeta.validators\n      ↓\nvalidate()\n```\n\n* **前半（フィールド単位）**: `extra_kwargs` のバリデータや `validate_\u003cfield\u003e()` が各フィールドごとに順次実行されます。\n* **後半（Serializer全体）**: 全フィールドの型変換・基本検証を通過した後、`Meta.validators` による全体検証や自作の `validate()` が実行されます。\n\n---\n\n### 4. 4方式の比較表\n\n本講で登場した **4つのバリデーション手法** の特徴と使い分けのまとめです。  \n実務で「どこに書くべきか？」「どれを使うべきか？」と迷った際は、この表を逆引き辞書として活用してください。\n\n| バリデーション方法 | 書く場所 | 主な対象 | 主な用途・具体例 | 選ぶ基準 |\n| :--- | :--- | :--- | :--- | :--- |\n| **`extra_kwargs` の `validators`** | `class Meta` → `extra_kwargs` | 1フィールド | `RegexValidator` で文字種を制限 | **ModelSerializerの特定フィールドに既存Validatorを追加したい** |\n| **`validate_\u003cfield名\u003e()`** | Serializerクラスのメソッド | 1フィールド | メールドメイン制限、業務固有の値チェック | **1フィールドに独自Pythonロジックを書きたい** |\n| **`Meta.validators`** | `class Meta` | Serializerレベル | `UniqueTogetherValidator` による組み合わせ一意性 | **再利用可能なSerializerレベルValidatorを設定したい** |\n| **`validate()`** | Serializerクラスのメソッド | Serializer全体 / 複数フィールド | 開始日 ≤ 終了日、sender ≠ recipient | **複数フィールドを使った独自ロジックを書きたい** |\n\n\n## 6. シリアライザを自在に操る「主要フック」と処理ライフサイクルの全貌\n\nシリアライザは、リクエストを受け取ってからデータベースへ保存し、レスポンスを返すまでの各ステップで、開発者がロジックを割り込ませて上書きできるように **「フック（Hook）」と呼ばれるメソッド群** が用意されています。\n\nクライアントからのリクエストが返送されるまでの **完全な処理ライフサイクルと ①〜⑦ のフックが呼び出されるタイミング** を1枚のマップにまとめると、以下のようになります。\n\n### ▼ シリアライザの完全処理パイプライン（全体マップ）\n\n```text\nリクエスト\n   ↓\nJSONなどの入力データ\n   ↓\nSerializer(data=request.data)\n   ↓\nserializer.is_valid()\n   │\n   └─ Serializer.run_validation(data)\n        │\n        ├─ ① Serializer.to_internal_value(data)\n        │      │\n        │      ├─ フィールドA\n        │      │    ↓\n        │      │  Field.run_validation()\n        │      │    ↓\n        │      │  Field.to_internal_value()\n        │      │    ↓\n        │      │  Field.validators\n        │      │  （RegexValidatorなど）\n        │      │    ↓\n        │      │  ② validate_\u003cfieldA\u003e()\n        │      │\n        │      ├─ フィールドB\n        │      │    ↓\n        │      │   同じ処理\n        │      │\n        │      └─ フィールドC...\n        │\n        │\n        ├─ ③ Serializer.validators\n        │      （Meta.validators に設定されたもの）\n        │      ↓\n        │   UniqueTogetherValidator など\n        │\n        └─ ④ validate(attrs)\n               ↓\n           複数フィールドをまたぐ\n           独自バリデーション\n\n               ↓\n        validated_data 完成\n               ↓\n\nserializer.save()\n   │\n   ├─ 新規作成なら\n   │      ↓\n   │   ⑤ create(validated_data)\n   │\n   └─ 更新なら\n          ↓\n       ⑤ update(instance, validated_data)\n\n               ↓\n          Model instance\n               ↓\n\nserializer.data\n   ↓\n⑥ to_representation(instance)\n   │\n   ├─ 通常フィールドを出力値へ変換\n   │\n   └─ SerializerMethodField があれば\n          ↓\n       ⑦ get_\u003cfield\u003e(obj)\n\n   ↓\nPythonのdict/list\n   ↓\nRenderer\n   ↓\nJSON\n   ↓\nレスポンス\n```\n\n### ▼ 主要フック一覧（①〜⑦の対応まとめ）\n\n上記のフロー図に登場する ①〜⑦ のフックメソッドの一覧です。  \nこれらを押さえておくことで、「入力値の変換・加工」「個別チェック」「相関チェック」「独自保存」「レスポンスの動的拡張」など、実務で発生するあらゆる要件に迷わず対応できるようになります。\n\n| 番号 | メソッド名 (関数定義) | 呼ばれるタイミング | 主な用途・処理フェーズ |\n| :---: | :--- | :--- | :--- |\n| **①** | **`to_internal_value(self, data)`** | 入力データを内部値に変換するとき | クライアントから渡された生の入力データ（JSONなど）を各フィールドの Python 内部型に変換する初期ゲートウェイ |\n| **②** | **`validate_\u003cfield名\u003e(self, value)`** | 特定フィールドのバリデーション時 | 対象フィールドの個別ビジネスルール検証（`Field.validators` の後に実行） |\n| **③** | **`Serializer.validators`**\u003cbr\u003e(`class Meta.validators`) | 全フィールド確定後の全体バリデーション時 | `UniqueTogetherValidator` など、DB問い合わせを伴う複数フィールドの一意性チェック |\n| **④** | **`validate(self, attrs)`** | 全体バリデーションの最終ステップ | 複数フィールドにまたがる自作相関チェック（開始日 \u003c 終了日など） |\n| **⑤** | **`create(self, validated_data)`**\u003cbr\u003e**`update(self, instance, validated_data)`** | `serializer.save()` で保存するとき | 新規レコード挿入（POST時: `create`）または既存レコード更新（PUT/PATCH時: `update`） |\n| **⑥** | **`to_representation(self, instance)`** | DBオブジェクトをJSON等に変換するとき | データベースのモデルオブジェクトを出力用データ（Python辞書 ➔ JSON）へ変換 |\n| **⑦** | **`get_\u003cfield名\u003e(self, obj)`** | `SerializerMethodField` の値を作るとき | モデルに存在しない動的な計算値やカスタム属性をレスポンスに含める（※第六講で解説） |\n\n\n## 7. 実務必須テクニック：raise_exception=True による自動エラーレスポンス\n\n実務の View で `serializer.is_valid()` を書く際、以下のような `if-else` を毎回書くのは冗長です。\n\n```python\n# 冗長な書き方\nif serializer.is_valid():\n    serializer.save()\n    return Response(serializer.data, status=status.HTTP_201_CREATED)\nelse:\n    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)\n```\n\nDRF では、**`raise_exception=True`** を引数に渡すことで、バリデーションエラー時に自動的に例外（`ValidationError`）を発生させ、DRF が裏側で自動的に **400 Bad Request（JSON形式のエラーメッセージ付き）** をクライアントへ返却してくれます。\n\n```python\n# 実務の標準的な書き方（スマート！）\nserializer.is_valid(raise_exception=True)\nserializer.save()\nreturn Response(serializer.data, status=status.HTTP_201_CREATED)\n```\n\nこの1行の引数を指定するだけで、ネストが浅くなりコードの見通しが劇的に改善されます。\n\n\n## 8. 発展コラム：DRFの弱点「validated_dataの型追跡問題」とフロントエンドTypeScriptの役割\n\nここまでシリアライザの強力なバリデーション機能を見てきましたが、現代の Web 開発の視点から、**あえて DRF の弱点（デメリット）** と、それを踏まえた **「フロントエンドとバックエンドの型戦略」** について整理しておきましょう。\n\n### 1. DRF の弱点：通過後は「ただの辞書（型が追えないデータ）」になってしまう\nDRF では、どんなに厳格にフィールド定義やバリデーションを行っても、検証を通過したデータは **単なる Python の辞書（`dict` 型である `validated_data`）** に変換されてしまいます。\n\n```python\n# is_valid() を通過した後は単なる dict（辞書型）\ndata = serializer.validated_data\n\n# ❌ エディタのコード補完（型ヒント）が効かない！\n# ❌ キー名をタイポ（打ち間違い）しても、実行時までエラーに気付けない！\nuser_id = data[\"user_id\"]\ntitle = data.get(\"title\")\n```\n\n#### FastAPI / Pydantic との比較\n近年バックエンドで人気を集めている **FastAPI（Pydantic）** の場合、バリデーション通過後のデータも **「型が完全に定義されたクラスオブジェクト」** として扱われます。\n\n* **FastAPI (Pydantic)**: `user.user_id` や `user.title` のようにオブジェクト属性としてアクセスでき、IDE で完全に型推論・自動補完が効く。\n* **DRF (Serializer)**: `validated_data[\"...\"]` という文字列キーによる辞書アクセスになり、ここで Python の静的な型追跡がプツリと途切れてしまう。\n\nこれは、動的型付けが主流だった時代に設計された DRF が抱える、近代的なアーキテクチャ上の弱点と言えます。\n\n### 2. システム全体で型安全性を保つ設計（TypeScript × DRF Serializer）\n\nDRF では、`serializer.is_valid()` を通過した後の `validated_data` が Python の `dict` として扱われるため、バックエンド内部では静的な型追跡が弱くなります。\n\nこの弱点をシステム全体の設計として補う方法の一つが、**フロントエンドで TypeScript を利用し、バックエンドの API スキーマと型定義を一致させること**です。\n\nフロントエンドでは TypeScript によって、API に送信するデータや受信するレスポンスの型不整合、キー名の誤り、必須項目の不足などを開発時に検出できます。\n\n一方、バックエンドでは DRF Serializer が実際に届いたデータを実行時に検証します。\n\n```text\nTypeScript\n↓\nフロント側でAPI型の不整合を開発時に検出\n\nDRF Serializer\n↓\n実際に届いたデータを実行時に検証\n\nvalidated_data\n↓\nPythonのdictとして短い範囲だけ利用\n\nModel / DTO\n```\n\nつまり、`validated_data` が `dict` になることで、DRF のバックエンド内部では静的な型追跡が弱くなります。\n\n一方で、バックエンドの API スキーマを基準としてフロントエンド側にも対応する TypeScript の型定義を用意しておけば、API の入出力における型不整合やキー名の誤り、必須項目の不足などを開発時に検出しやすくなります。\n\nさらにバックエンド内部では、`validated_data` を長距離にわたって持ち回らず、Serializer 内部や保存直前などの短い範囲だけで利用し、早い段階で Model や DTO などの型を持つオブジェクトへ変換します。\n\nこのように、\n\n* **TypeScript** でフロントエンド側の API 型を静的にチェックする\n* **DRF Serializer** で実際に届いたデータを実行時に検証する\n* **`validated_data`** を `dict` のまま扱う範囲を最小限にする\n\nという3段構えにすることで、DRF の型追跡上の弱点をシステム全体の設計で補いながら、フロントエンドからバックエンドまで型とデータの整合性を保ちやすくなります。\n\n\n## まとめ\n\n* **なぜシリアライザを使うのか？**:\n  * View での `request.data` 直接チェック（if文地獄・Fat View）を回避し、入力検証をシリアライザに集約することで、コードの保守性・再利用性・テスト容易性を最大化する\n* **バリデーションの4ステップ**:\n  * 入力 ➔ `is_valid()` ➔ 成功なら `validated_data` / 失敗なら `errors`\n  * ※`validated_data` と `errors` は必ず `is_valid()` の後に参照する！\n* **3段階の検証パイプライン**:\n  1. フィールド基本検証（型・文字数・必須チェック）\n  2. 単一フィールド検証（`validate_\u003cfield\u003e()`、必ず return する）\n  3. 複数フィールド横断検証（`validate()`、PATCH時は既存インスタンスから補完）\n* **四つのバリデーション手法と使い分け**:\n  * `extra_kwargs` の `validators`: ModelSerializerの特定フィールドに既存Validatorを追加したいとき\n  * `validate_\u003cfield名\u003e()`: 1フィールドに独自Pythonロジックを書きたいとき\n  * `Meta.validators`: 再利用可能なSerializerレベルValidatorを設定したいとき\n  * `validate()`: 複数フィールドを使った独自ロジックを書きたいとき\n* **シリアライザの主要フック**:\n  * 入力・検証（`to_internal_value`, `validate_\u003cfield\u003e`, `validate`）から保存（`create`, `update`）、出力（`to_representation`, `get_\u003cfield\u003e`）まで各ライフサイクルを柔軟にオーバーライド可能\n* **実務のTips**:\n  * `serializer.is_valid(raise_exception=True)` でスマートに 400 エラーを返却\n* **DRFの弱点と3段構えの型戦略**:\n  * **TypeScript**: フロントエンド側の API 型を静的にチェック\n  * **DRF Serializer**: 実際に届いたデータを API 境界で実行時に検証（最後の砦）\n  * **`validated_data`**: `dict` のまま長距離持ち回らず、早い段階で Model や DTO へ変換して影響範囲を最小限にする\n\nシリアライザのバリデーションを完全にマスターしたことで、堅牢で安全な WebAPI を構築する準備が整いました！  \n次回は、シリアライザで処理されたデータをフロントエンドへ返す **[【第八講】DRFのView徹底解剖・リクエスト処理フローとクラス階層](/items/5260e8be137026ca99d5)** の世界へ進んでいきましょう！\n\n---\n\n**[← 第六講：DRFの中核「シリアライザ」徹底解剖・3大分類と更新処理](/items/914c08a85e63388664e8) | [まとめページに戻る](/items/4250ea04fb61f5bc4f0d) | [第八講へ →](/items/5260e8be137026ca99d5)**\n","coediting":false,"comments_count":0,"created_at":"2026-09-19T12:38:15+09:00","group":null,"id":"04b2c3524ec379961d82","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Django","versions":[]},{"name":"DRF","versions":[]},{"name":"Python","versions":[]},{"name":"rest","versions":[]},{"name":"Vue.js","versions":[]}],"title":"DRF × Vue.js 実践講座【第七講】シリアライザのバリデーション完全攻略・is_validの仕組みとエラー制御","updated_at":"2026-09-20T01:24:53+09:00","url":"https://qiita.com/mashikokoutaro1019/items/04b2c3524ec379961d82","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":4,"github_login_name":"kotaromashiko","id":"mashikokoutaro1019","items_count":34,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4223670,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocLhGA1m69Wj3mwBbWNN_K3bGO3DrJ7lGj36By-be2U_y9c9Mw=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:29\"\u003eEDIT UPDATE処理を通して\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-5:21\"\u003eROUTE MODEL BINDING\u003cbr\u003e\nルートモデルバインディングってすごい\u003cbr\u003e\nお話をします。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"7:1-7:27\"\u003e\n\u003cspan id=\"route-model-bindingとは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#route-model-binding%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eROUTE MODEL BINDINGとは\u003c/h1\u003e\n\u003cp data-sourcepos=\"8:1-8:68\"\u003eURLのIDからMODELを自動取得してくれるLaravelの仕組み\u003c/p\u003e\n\u003ch1 data-sourcepos=\"10:1-10:42\"\u003e\n\u003cspan id=\"route-model-bindingのここがすごい\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#route-model-binding%E3%81%AE%E3%81%93%E3%81%93%E3%81%8C%E3%81%99%E3%81%94%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eROUTE MODEL BINDINGのここがすごい\u003c/h1\u003e\n\u003ch3 data-sourcepos=\"11:1-11:54\"\u003e\n\u003cspan id=\"1コントローラーがシンプルに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E3%81%8C%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1:コントローラーがシンプルに！！！\u003c/h3\u003e\n\u003ch3 data-sourcepos=\"12:1-12:33\"\u003e\n\u003cspan id=\"2安全性が高い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2%E5%AE%89%E5%85%A8%E6%80%A7%E3%81%8C%E9%AB%98%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2:安全性が高い！！！\u003c/h3\u003e\n\u003cp data-sourcepos=\"13:1-14:35\"\u003e見つからない時に404エラーを返してくれるので\u003cbr\u003e\nエラー処理書かなくてもOK\u003c/p\u003e\n\u003ch3 data-sourcepos=\"15:1-15:33\"\u003e\n\u003cspan id=\"3可読性が高い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3%E5%8F%AF%E8%AA%AD%E6%80%A7%E3%81%8C%E9%AB%98%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3:可読性が高い！！！\u003c/h3\u003e\n\u003cp data-sourcepos=\"16:1-17:33\"\u003e「Product $product」と書くだけで「Productモデルが渡ってくる！」\u003cbr\u003e\nと一目でわかる！！！！\u003c/p\u003e\n\u003ch3 data-sourcepos=\"18:1-18:48\"\u003e\n\u003cspan id=\"4公式が推奨している\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4%E5%85%AC%E5%BC%8F%E3%81%8C%E6%8E%A8%E5%A5%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4:公式が推奨している！！！！！\u003c/h3\u003e\n\u003cp data-sourcepos=\"19:1-19:54\"\u003e公式ドキュメント盛りだくさん！！！！\u003c/p\u003e\n\u003ch1 data-sourcepos=\"21:1-21:26\"\u003e\n\u003cspan id=\"実際に書いてみた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AB%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際に書いてみた\u003c/h1\u003e\n\u003cp data-sourcepos=\"23:1-26:36\"\u003eここはPHPのフレームワークであるLaravelと、\u2028\u003cbr\u003e\nフロントエンド開発ツールのInertia.jsを組み合わせて、\u003cbr\u003e\n商品の編集画面（ページ）を表示する処理をするところ\u003cbr\u003e\nフロントはVueで書いてます\u003c/p\u003e\n\u003cp data-sourcepos=\"28:1-28:71\"\u003e（index初めその他のコントローラーについては省略）\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"29:1-44:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// ProductsController\u003c/span\u003e\n\u003cspan class=\"kn\"\u003enamespace\u003c/span\u003e \u003cspan class=\"nn\"\u003eApp\\Http\\Controllers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eApp\\Http\\Requests\\ProductRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eApp\\Models\\Product\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kn\"\u003euse\u003c/span\u003e \u003cspan class=\"nc\"\u003eInertia\\Inertia\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// ここは商品管理画面の表示をするページ用のコントローラー\u003c/span\u003e\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eshow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eProduct\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eInertia\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003erender\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'Products/Show'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'product'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"45:1-45:70\"\u003e具体的には、以下の3つの処理を同時に行っている。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"47:1-47:80\"\u003e\n\u003cspan id=\"1データの自動取得ルートモデルバインディング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E8%87%AA%E5%8B%95%E5%8F%96%E5%BE%97%E3%83%AB%E3%83%BC%E3%83%88%E3%83%A2%E3%83%87%E3%83%AB%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1:データの自動取得（ルートモデルバインディング）\u2028\u2028\u003c/h2\u003e\n\u003cp data-sourcepos=\"48:1-50:55\"\u003e引数の \u003ccode\u003eProduct $product\u003c/code\u003e によって、\u2028\u003cbr\u003e\nURLに含まれるID（例: /products/5/edit の 5）に対応する商品データを、\u2028\u003cbr\u003e\nデータベースから自動的に1件取得\u2028\u2028\u2028\u003c/p\u003e\n\u003ch2 data-sourcepos=\"52:1-52:47\"\u003e\n\u003cspan id=\"2画面コンポーネントの指定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2%E7%94%BB%E9%9D%A2%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E6%8C%87%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2:画面（コンポーネント）の指定\u003c/h2\u003e\n\u003cp data-sourcepos=\"53:1-56:48\"\u003e\u003ccode\u003eInertia::render('Products/Edit', ...)\u003c/code\u003eで\u003cbr\u003e\nフロントエンド（Vue.jsやReactなど）の\u003cbr\u003e\nresources/js/Pages/Products/Edit にある\u003cbr\u003e\n画面ファイルを表示するように指示\u003c/p\u003e\n\u003ch2 data-sourcepos=\"58:1-58:66\"\u003e\n\u003cspan id=\"3フロントエンドへデータを渡す第2引数を設定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%B8%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B8%A1%E3%81%99%E7%AC%AC2%E5%BC%95%E6%95%B0%E3%82%92%E8%A8%AD%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3:フロントエンドへデータを渡す第2引数を設定\u003c/h2\u003e\n\u003cp data-sourcepos=\"59:1-61:56\"\u003e\u003ccode\u003e'product' =\u0026gt; $product\u003c/code\u003e と指定することで、\u2028\u003cbr\u003e\n自動取得した商品データをフロントエンドの画面側に\u003cbr\u003e\n「props（プロパティ）」として渡している\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-64:109\"\u003eこれをしているから\u003cbr\u003e\n\u003cstrong\u003e編集画面を開いたときに最初からその商品の名前や価格が入力欄に表示される\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 data-sourcepos=\"66:1-66:38\"\u003e\n\u003cspan id=\"なぜ書かなくていいのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E6%9B%B8%E3%81%8B%E3%81%AA%E3%81%8F%E3%81%A6%E3%81%84%E3%81%84%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ書かなくていいのか？\u003c/h1\u003e\n\u003cp data-sourcepos=\"68:1-70:81\"\u003e\u003cstrong\u003eLaravelのルートモデルバインディング（Route Model Binding）\u003cbr\u003e\nという強力な機能が、\u003cbr\u003e\n裏側で自動的に \u003ccode\u003efind()\u003c/code\u003e 相当の処理を行ってくれているから\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"72:1-72:173\"\u003eURLからの推測:この edit メソッドを呼び出すルーティング（URLの設定）は\u2028\u003ccode\u003eRoute::get('/products/{product}/edit', ...)\u003c/code\u003e のようになっている\u003c/p\u003e\n\u003cp data-sourcepos=\"74:1-74:65\"\u003eURLの `{product} の部分に、商品のID（例: 5）が入り\u003c/p\u003e\n\u003cp data-sourcepos=\"76:1-78:29\"\u003e型指定による自動化:引数で \u003ccode\u003eProduct $product\u003c/code\u003e と型を指定しているため\u003cbr\u003e\nLaravelが「URLの変数 {product} は、Product モデルのID（5）のことだな」\u003cbr\u003e\nと自動で解釈(えらい)\u003c/p\u003e\n\u003cp data-sourcepos=\"80:1-82:101\"\u003e自動でデータベース検索:Laravelが裏側で自動的に \u003ccode\u003eProduct::findOrFail(5)\u003c/code\u003e を実行\u003cbr\u003e\n見つかった商品データを \u003ccode\u003e$product\u003c/code\u003e に入れた状態でこの edit メソッドを呼び出し\u003cbr\u003e\n\u003cstrong\u003e開発者がわざわざ \u003ccode\u003eProduct::find($id)\u003c/code\u003e を手動で書く必要がなくなっている\u003c/strong\u003e\u2028\u003c/p\u003e\n\u003ch2 data-sourcepos=\"84:1-84:55\"\u003e\n\u003cspan id=\"もし手動で書く場合-コードが長くなる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%82%E3%81%97%E6%89%8B%E5%8B%95%E3%81%A7%E6%9B%B8%E3%81%8F%E5%A0%B4%E5%90%88-%E3%82%B3%E3%83%BC%E3%83%89%E3%81%8C%E9%95%B7%E3%81%8F%E3%81%AA%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eもし手動で書く場合 コードが長くなる\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"86:1-98:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// ルートモデルバインディングを使わない場合（IDを直接受け取る）\u003c/span\u003e\n\n    \u003cspan class=\"n\"\u003ephp\u003c/span\u003e\u003cspan class=\"err\"\u003e\u2028\u003c/span\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eedit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$id\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// 手動でデータベースから探す必要がある\u003c/span\u003e\n    \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eProduct\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003efindOrFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$id\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \n\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eInertia\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003erender\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'Products/Edit'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n        \u003cspan class=\"s1\"\u003e'product'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"100:1-103:37\"\u003e手動の場合、\u003cbr\u003e\n\u003cstrong\u003e1:IDを受け取る\u003cbr\u003e\n2:自分でMODELを検索する必要がある\u003cbr\u003e\n3:コードが長くなりやすい\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"105:1-108:70\"\u003eこれがROUTEMODELBINDINGだと\u003cbr\u003e\n自動化されているおかげで、コードがシンプルで読みやすくなる\u003cbr\u003e\nもしURLで指定されたIDの商品がデータベースに存在しない場合は、\u003cbr\u003e\n\u003cstrong\u003e自動的に404エラー（Not Found）画面を返してくれる\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"110:1-111:104\"\u003eまた、この仕組み（ルートモデルバインディング）を動かすには\u003cbr\u003e\nroutes/web.php（ルーティング）の設定が正しく連動している必要があるから注意\u003c/p\u003e\n\u003ch1 data-sourcepos=\"113:1-113:43\"\u003e\n\u003cspan id=\"実際に書いてみたedit-update\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AB%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%81%9Fedit-update\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際に書いてみた（EDIT UPDATE）\u003c/h1\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"114:1-139:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// editでProductモデルを受け取りますという型と\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// ↓それを入れる箱である変数 $productを一行目で定義\u003c/span\u003e\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eedit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eProduct\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// ↓まさかのここでもう既にMODELが渡されている！！！！！！！！！！！    \u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nc\"\u003eInertia\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"nf\"\u003erender\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'Products/Edit'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'product'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\n        \n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eProductRequest\u003c/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eProduct\u003c/span\u003e \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// 1. バリデーション済みの安全なデータのみを取得\u003c/span\u003e\n    \u003cspan class=\"nv\"\u003e$validated\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003evalidated\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e// 2. 安全なデータのみでモデルを更新\u003c/span\u003e\n    \u003cspan class=\"nv\"\u003e$product\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$validated\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e// 一覧画面へリダイレクト\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003eredirect\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003eroute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'products.index'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"nf\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'success'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'商品の内容を更新しました'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"141:1-145:69\"\u003eLaravelの仕様上、第一引数はフォームリクエストとして動作\u003cbr\u003e\n（\u003ccode\u003eProductRequest $request\u003c/code\u003eのとこ）\u003cbr\u003e\nコントローラ側で「どのデータを安全に扱うか」の意図が曖昧になりがち\u2028\u2028\u003cbr\u003e\nセキュリティ（意図しないデータの更新）\u003ccode\u003e$request-\u0026gt;validated()\u003c/code\u003e は、\u2028\u003cbr\u003e\nProductRequest の rules() で定義した項目だけを抽出\u2028\u2028\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"147:1-179:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// ProductRequest 一部抜粋\u003c/span\u003e\n\n\n\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProductRequest\u003c/span\u003e \u003cspan class=\"kd\"\u003eextends\u003c/span\u003e \u003cspan class=\"nc\"\u003eFormRequest\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e//（中略）\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003erules\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"kt\"\u003earray\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'name'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'required|max:100'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'price'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'required|integer'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'stock'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'required|integer'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'description'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'max:200'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e];\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"n\"\u003emessages\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"kt\"\u003earray\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'name.required'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'商品名を入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'name.max'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'100文字以内で入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'price.required'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'価格を入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'price.integer'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'半角数字で入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'stock.required'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'在庫数を入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'stock.integer'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'半角数字で入力してください。'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"s1\"\u003e'description.max'\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e'200文字以内で入力してください'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e];\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"180:1-183:105\"\u003eリクエストデータの受け取り方によっては\u2028\u003cbr\u003e\nIDが変更されたり管理者フラグなどが書き換えられるリスク（マスアサインメント脆弱性）\u003cbr\u003e\nが残るので注意\u003cbr\u003e\n→リクエストデータをそのままデータベースに流し込んでいるだけになる\u2028\u2028\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"185:1-189:54\"\u003e\n\u003cp data-sourcepos=\"185:2-189:54\"\u003eMass Assignment（大量代入）\u003cbr\u003e\nWebアプリケーションが外部から受け取ったデータを、\u003cbr\u003e\nオブジェクトの属性に一括で代入する機能\u003cbr\u003e\n便利な反面、意図しない属性まで書き換えられる可能性があり、\u003cbr\u003e\nセキュリティ上の重大なリスクとなる。\u003c/p\u003e\n\u003c/blockquote\u003e\n","body":"EDIT UPDATE処理を通して\n\nROUTE MODEL BINDING\nルートモデルバインディングってすごい\nお話をします。\n\n# ROUTE MODEL BINDINGとは\nURLのIDからMODELを自動取得してくれるLaravelの仕組み\n\n# ROUTE MODEL BINDINGのここがすごい\n### 1:コントローラーがシンプルに！！！\n### 2:安全性が高い！！！\n見つからない時に404エラーを返してくれるので\nエラー処理書かなくてもOK\n### 3:可読性が高い！！！\n「Product $product」と書くだけで「Productモデルが渡ってくる！」\nと一目でわかる！！！！\n### 4:公式が推奨している！！！！！\n公式ドキュメント盛りだくさん！！！！\n\n# 実際に書いてみた\n\nここはPHPのフレームワークであるLaravelと、\u2028\nフロントエンド開発ツールのInertia.jsを組み合わせて、\n商品の編集画面（ページ）を表示する処理をするところ\nフロントはVueで書いてます\n\n（index初めその他のコントローラーについては省略）\n~~~php\n// ProductsController\nnamespace App\\Http\\Controllers;\n\nuse App\\Http\\Requests\\ProductRequest;\nuse App\\Models\\Product;\nuse Inertia\\Inertia;\n\n// ここは商品管理画面の表示をするページ用のコントローラー\n    public function show(Product $product)\n    {\n        return Inertia::render('Products/Show', [\n            'product' =\u003e $product,\n        ]);\n    }\n~~~\n具体的には、以下の3つの処理を同時に行っている。\n  \n## 1:データの自動取得（ルートモデルバインディング）\u2028\u2028\n引数の `Product $product` によって、\u2028\nURLに含まれるID（例: /products/5/edit の 5）に対応する商品データを、\u2028\nデータベースから自動的に1件取得\u2028\u2028\u2028\n\n## 2:画面（コンポーネント）の指定\n`Inertia::render('Products/Edit', ...)`で\nフロントエンド（Vue.jsやReactなど）の\nresources/js/Pages/Products/Edit にある\n画面ファイルを表示するように指示\n\n## 3:フロントエンドへデータを渡す第2引数を設定\n`'product' =\u003e $product` と指定することで、\u2028\n自動取得した商品データをフロントエンドの画面側に\n「props（プロパティ）」として渡している\n\nこれをしているから\n**編集画面を開いたときに最初からその商品の名前や価格が入力欄に表示される**\n\n# なぜ書かなくていいのか？\n\n**Laravelのルートモデルバインディング（Route Model Binding）\nという強力な機能が、\n裏側で自動的に `find()` 相当の処理を行ってくれているから**\n\nURLからの推測:この edit メソッドを呼び出すルーティング（URLの設定）は\u2028`Route::get('/products/{product}/edit', ...)` のようになっている\n\nURLの `{product} の部分に、商品のID（例: 5）が入り\n\n型指定による自動化:引数で `Product $product` と型を指定しているため\nLaravelが「URLの変数 {product} は、Product モデルのID（5）のことだな」\nと自動で解釈(えらい)\n\n自動でデータベース検索:Laravelが裏側で自動的に `Product::findOrFail(5)` を実行\n見つかった商品データを `$product` に入れた状態でこの edit メソッドを呼び出し\n**開発者がわざわざ `Product::find($id)` を手動で書く必要がなくなっている**\u2028\n\n## もし手動で書く場合 コードが長くなる\n\n~~~php\n// ルートモデルバインディングを使わない場合（IDを直接受け取る）\n\n    php\u2028public function edit($id)\n    {\n    // 手動でデータベースから探す必要がある\n    $product = Product::findOrFail($id); \n\n    return Inertia::render('Products/Edit', [\n        'product' =\u003e $product,\n    ]);\n    }\n~~~\n\n手動の場合、\n**1:IDを受け取る\n2:自分でMODELを検索する必要がある\n3:コードが長くなりやすい**\n\nこれがROUTEMODELBINDINGだと\n自動化されているおかげで、コードがシンプルで読みやすくなる\nもしURLで指定されたIDの商品がデータベースに存在しない場合は、\n**自動的に404エラー（Not Found）画面を返してくれる**\n\nまた、この仕組み（ルートモデルバインディング）を動かすには\nroutes/web.php（ルーティング）の設定が正しく連動している必要があるから注意\n\n# 実際に書いてみた（EDIT UPDATE）\n~~~php\n// editでProductモデルを受け取りますという型と\n// ↓それを入れる箱である変数 $productを一行目で定義\n    public function edit(Product $product)\n    {\n// ↓まさかのここでもう既にMODELが渡されている！！！！！！！！！！！    \n        return Inertia::render('Products/Edit', [\n            'product' =\u003e $product,\n        ]);\n\n        \n    }\n\n    public function update(ProductRequest $request, Product $product)\n    {\n    // 1. バリデーション済みの安全なデータのみを取得\n    $validated = $request-\u003evalidated();\n\n    // 2. 安全なデータのみでモデルを更新\n    $product-\u003eupdate($validated);\n\n    // 一覧画面へリダイレクト\n    return redirect()-\u003eroute('products.index')\n        -\u003ewith('success', '商品の内容を更新しました');\n    }\n~~~\n\nLaravelの仕様上、第一引数はフォームリクエストとして動作 \n（`ProductRequest $request`のとこ）\nコントローラ側で「どのデータを安全に扱うか」の意図が曖昧になりがち\u2028\u2028\nセキュリティ（意図しないデータの更新）`$request-\u003evalidated()` は、\u2028\nProductRequest の rules() で定義した項目だけを抽出\u2028\u2028\n\n~~~php\n// ProductRequest 一部抜粋\n\n\nclass ProductRequest extends FormRequest\n{\n\n//（中略）\n\n    public function rules(): array\n    {\n        return [\n            'name' =\u003e 'required|max:100',\n            'price' =\u003e 'required|integer',\n            'stock' =\u003e 'required|integer',\n            'description' =\u003e 'max:200',\n        ];\n    }\n\n    public function messages(): array\n    {\n        return [\n            'name.required' =\u003e '商品名を入力してください',\n            'name.max' =\u003e '100文字以内で入力してください',\n            'price.required' =\u003e '価格を入力してください',\n            'price.integer' =\u003e '半角数字で入力してください',\n            'stock.required' =\u003e '在庫数を入力してください',\n            'stock.integer' =\u003e '半角数字で入力してください。',\n            'description.max' =\u003e '200文字以内で入力してください',\n        ];\n    }\n}\n~~~\nリクエストデータの受け取り方によっては\u2028\nIDが変更されたり管理者フラグなどが書き換えられるリスク（マスアサインメント脆弱性）\nが残るので注意\n→リクエストデータをそのままデータベースに流し込んでいるだけになる\u2028\u2028\n\n\u003eMass Assignment（大量代入）\nWebアプリケーションが外部から受け取ったデータを、\nオブジェクトの属性に一括で代入する機能\n便利な反面、意図しない属性まで書き換えられる可能性があり、\nセキュリティ上の重大なリスクとなる。\n","coediting":false,"comments_count":0,"created_at":"2026-09-18T15:44:53+09:00","group":null,"id":"e733eb3f0133dd6a3f4f","likes_count":0,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"Laravel","versions":[]},{"name":"Vue.js","versions":[]},{"name":"inertia","versions":[]},{"name":"PHP","versions":[]}],"title":"【Laravel】ROUTE MODEL BINDINGがすごい","updated_at":"2026-09-18T16:22:42+09:00","url":"https://qiita.com/metappi/items/e733eb3f0133dd6a3f4f","user":{"description":"プログラミングスクールRUNTEQ卒業後、\r\n受託企業に就職しました。\r\nPHP Laravel Vueをメインに学習中。\r\n楽しみながら学んでいきたいと考えております。\r\n夢は所さんになることです。\r\nまだまだエンジニアのタマゴなので間違い、ご意見等ございましたら\r\nぜひ頂戴したいと思っております。よろしくお願いいたします。","facebook_id":"","followees_count":21,"followers_count":32,"github_login_name":"Zundabyon","id":"metappi","items_count":79,"linkedin_id":"","location":"Japan","name":"(めたっぴ72期A) 小笠原","organization":"","permanent_id":4155281,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4155281/profile-images/1789107820","team_only":false,"twitter_screen_name":"zundabyon","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:8\"\u003e\n\u003cspan id=\"tldr\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tldr\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTL;DR\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:92\"\u003eAIに質問すると、以前より簡単に知識を得られるようになりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:152\"\u003e一方で、AIとの会話を読んだ直後は理解した気になっていても、数日後には内容を思い出せないことがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:145\"\u003eそこで、AIとの会話や資料から暗記カードを作成し、ブラウザ上で復習できる \u003ccode\u003eflashcard-viewer\u003c/code\u003e を作りました。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:72\"\u003e今回作ったものは、以下のようなシンプルなPWAです。\u003c/p\u003e\n\u003cul data-sourcepos=\"11:1-19:0\"\u003e\n\u003cli data-sourcepos=\"11:1-11:61\"\u003eAIとの会話や資料から暗記カード教材を作成\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-12:65\"\u003e生成されたZIPファイルをブラウザから取り込み\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:41\"\u003e一問一答と穴埋め形式に対応\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-14:65\"\u003e「もう一度」「微妙」「覚えた」で進捗を管理\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-15:68\"\u003eカードの検索、セクション・進捗による絞り込み\u003c/li\u003e\n\u003cli data-sourcepos=\"16:1-16:44\"\u003eカード本文の修正と自分用メモ\u003c/li\u003e\n\u003cli data-sourcepos=\"17:1-17:47\"\u003e教材・進捗・メモを端末内に保存\u003c/li\u003e\n\u003cli data-sourcepos=\"18:1-19:0\"\u003e一度読み込めばオフラインでも利用可能\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"20:1-20:164\"\u003e「AIで知識を得る」だけでなく、「あとから思い出す」ための導線を自分用に作った、というのが今回の開発の目的です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"22:1-38:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e会話・資料\n    │\n    ▼\nいつものAI\n    │ 暗記カード教材を生成\n    ▼\nflashcards.zip\n    │\n    ▼\nFlashcard Viewer\n    │\n    ├─ 教材\n    ├─ 学習進捗\n    └─ 自分用メモ\n       ※ブラウザ内に保存\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"39:1-39:33\"\u003e\n\u003cspan id=\"アプリとソースコード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A8%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eアプリとソースコード\u003c/h2\u003e\n\u003cul data-sourcepos=\"41:1-43:0\"\u003e\n\u003cli data-sourcepos=\"41:1-41:70\"\u003eアプリ：\u003ca href=\"https://flashcard-viewer.atoook.com/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eFlashcard Viewer\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-43:0\"\u003eソースコード：\u003ca href=\"https://github.com/atoook/Flashcard-Viewer\" rel=\"nofollow noopener\" target=\"_blank\"\u003eGitHubリポジトリ\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"44:1-44:9\"\u003e\n\u003cspan id=\"背景\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%83%8C%E6%99%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e背景\u003c/h2\u003e\n\u003cp data-sourcepos=\"46:1-46:80\"\u003e最近は、分からないことがあればすぐにAIへ質問できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"48:1-48:153\"\u003e技術の調査、書籍の要約、英語表現の確認、資格試験の学習など、インプットのスピードはかなり上がりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"50:1-50:161\"\u003eただ、AIとの会話を読んでいるときは理解できていても、後から自分で説明しようとすると出てこないことがあります。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"52:1-52:47\"\u003e\n\u003cp data-sourcepos=\"52:3-52:47\"\u003e「わかった」と「覚えた」は違う\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"54:1-54:71\"\u003eAIとの会話は、その場での理解にはとても便利です。\u003c/p\u003e\n\u003cp data-sourcepos=\"56:1-56:120\"\u003e一方で、知識を定着させるには、時間を置いて自分の記憶から思い出すことも必要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"58:1-58:155\"\u003eそこで、AIとの会話をそのまま読み返すのではなく、復習しやすいカードに変換する仕組みを作ることにしました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"60:1-60:82\"\u003e\n\u003cspan id=\"ankiのような既存サービスがある中でなぜ自分で作るのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#anki%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA%E6%97%A2%E5%AD%98%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%8C%E3%81%82%E3%82%8B%E4%B8%AD%E3%81%A7%E3%81%AA%E3%81%9C%E8%87%AA%E5%88%86%E3%81%A7%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eAnkiのような既存サービスがある中で、なぜ自分で作るのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"62:1-62:112\"\u003e暗記カードのサービスとしては、Ankiのようなすでに完成度の高いものがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"64:1-64:91\"\u003eそのため、最初からAnkiの代替を作ろうとしたわけではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-66:153\"\u003e今回、自分で作ってみようと思った理由は、自分の学習方法に必要な範囲だけを小さくまとめたかったからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"68:1-68:63\"\u003e特に欲しかったのは、次のような流れでした。\u003c/p\u003e\n\u003col data-sourcepos=\"70:1-74:0\"\u003e\n\u003cli data-sourcepos=\"70:1-70:35\"\u003eAIとの会話や資料を渡す\u003c/li\u003e\n\u003cli data-sourcepos=\"71:1-71:50\"\u003eAIに暗記カード教材を作ってもらう\u003c/li\u003e\n\u003cli data-sourcepos=\"72:1-72:39\"\u003eその教材をすぐに復習する\u003c/li\u003e\n\u003cli data-sourcepos=\"73:1-74:0\"\u003e間違えたカードだけ後から見直す\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"75:1-75:123\"\u003eまた、個人利用なので、アカウント登録やサーバーへの教材保存も必要ありませんでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"77:1-77:84\"\u003e今回の目的は、機能の多い学習サービスを作ることではなく、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"79:1-79:76\"\u003e\n\u003cp data-sourcepos=\"79:3-79:76\"\u003e自分が普段使っているAIと、自分用の復習画面をつなぐ\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"81:1-81:15\"\u003eことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"83:1-83:195\"\u003e既存サービスを使うのが間違いということではなく、自分の学習フローに合わせて、必要最小限の道具を作ることにも意味があると考えました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"85:1-85:24\"\u003e\n\u003cspan id=\"教材を作る流れ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%95%99%E6%9D%90%E3%82%92%E4%BD%9C%E3%82%8B%E6%B5%81%E3%82%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e教材を作る流れ\u003c/h2\u003e\n\u003cp data-sourcepos=\"87:1-87:101\"\u003eトップ画面の「教材を作る」から、AIに渡すプロンプトをコピーできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"89:1-89:92\"\u003e任意で「何を教材にするか」を入力し、AIとの会話に貼り付けます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"91:1-94:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e例：\nこの会話で質問した英語表現を教材にしてください\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"96:1-96:113\"\u003eAIには、会話や添付資料をもとに、以下の形式のZIPファイルを作成してもらいます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"98:1-102:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e教材名.flashcards.zip\n├── manifest.json\n└── cards.json\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"104:1-104:135\"\u003e生成されたZIPをViewerに取り込むと、教材名やカード枚数を確認して、そのまま学習を開始できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"106:1-106:197\"\u003eAIがファイルを正しく生成できなかった場合は、Viewerに表示されたエラー内容と正式なスキーマを含む「修正依頼プロンプト」をコピーできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"108:1-108:167\"\u003eそのプロンプトとエラーになったZIPをAIに渡すことで、教材の内容を変えずに形式だけ修正してもらえるようにしています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"110:1-110:36\"\u003e\n\u003cspan id=\"対応しているカード形式\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%82%AB%E3%83%BC%E3%83%89%E5%BD%A2%E5%BC%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e対応しているカード形式\u003c/h2\u003e\n\u003cp data-sourcepos=\"112:1-112:58\"\u003e現在は、シンプルな2種類に絞っています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"114:1-114:16\"\u003e\n\u003cspan id=\"一問一答\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%B8%80%E5%95%8F%E4%B8%80%E7%AD%94\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e一問一答\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"116:1-119:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e問題：Spring Beanのデフォルトスコープは？\n答え：singleton\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"121:1-121:13\"\u003e\n\u003cspan id=\"穴埋め\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%A9%B4%E5%9F%8B%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e穴埋め\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"123:1-125:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e水の化学式は ？？？ です。\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"127:1-127:66\"\u003e答えを見ると、隠れていた部分が表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"129:1-129:207\"\u003e最初から画像や複雑なレイアウトなどに対応するのではなく、まずは「問いを見て、思い出して、答えを確認する」という学習の中心部分に絞りました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"131:1-131:33\"\u003e\n\u003cspan id=\"学習画面でできること\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AD%A6%E7%BF%92%E7%94%BB%E9%9D%A2%E3%81%A7%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e学習画面でできること\u003c/h2\u003e\n\u003cp data-sourcepos=\"133:1-133:75\"\u003e学習画面では、カードごとに覚え具合を記録できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"135:1-138:0\"\u003e\n\u003cli data-sourcepos=\"135:1-135:14\"\u003eもう一度\u003c/li\u003e\n\u003cli data-sourcepos=\"136:1-136:8\"\u003e微妙\u003c/li\u003e\n\u003cli data-sourcepos=\"137:1-138:0\"\u003e覚えた\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"139:1-139:114\"\u003e進捗はカード単位で保存されるため、「もう一度」だけを表示することもできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"141:1-141:93\"\u003eまた、カードが増えてきたときのために、以下にも対応しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"143:1-149:0\"\u003e\n\u003cli data-sourcepos=\"143:1-143:14\"\u003e用語検索\u003c/li\u003e\n\u003cli data-sourcepos=\"144:1-144:38\"\u003eセクションによる絞り込み\u003c/li\u003e\n\u003cli data-sourcepos=\"145:1-145:29\"\u003e進捗による絞り込み\u003c/li\u003e\n\u003cli data-sourcepos=\"146:1-146:41\"\u003e前回見ていたカードから再開\u003c/li\u003e\n\u003cli data-sourcepos=\"147:1-147:26\"\u003e問題・答えの修正\u003c/li\u003e\n\u003cli data-sourcepos=\"148:1-149:0\"\u003eカードごとの自分用メモ\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"150:1-150:104\"\u003eAIが作った問題が少し分かりにくいときは、Viewer内で自分用に修正できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"152:1-152:174\"\u003e「この説明を読んだときに自分が引っかかったポイント」などをメモしておくと、後から見返したときにも使いやすくなります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"154:1-154:21\"\u003e\n\u003cspan id=\"技術スタック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e技術スタック\u003c/h2\u003e\n\u003cul data-sourcepos=\"156:1-163:0\"\u003e\n\u003cli data-sourcepos=\"156:1-156:7\"\u003eVue 3\u003c/li\u003e\n\u003cli data-sourcepos=\"157:1-157:12\"\u003eTypeScript\u003c/li\u003e\n\u003cli data-sourcepos=\"158:1-158:6\"\u003eVite\u003c/li\u003e\n\u003cli data-sourcepos=\"159:1-159:14\"\u003eTailwind CSS\u003c/li\u003e\n\u003cli data-sourcepos=\"160:1-160:11\"\u003eIndexedDB\u003c/li\u003e\n\u003cli data-sourcepos=\"161:1-161:22\"\u003ePWA / Service Worker\u003c/li\u003e\n\u003cli data-sourcepos=\"162:1-163:0\"\u003eCloudflare Pages\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"164:1-164:48\"\u003eバックエンドは用意していません。\u003c/p\u003e\n\u003cp data-sourcepos=\"166:1-166:108\"\u003e教材、学習進捗、カードの修正内容、メモはブラウザ内のIndexedDBに保存します。\u003c/p\u003e\n\u003cp data-sourcepos=\"168:1-168:118\"\u003eそのため、Flashcard Viewer自体が教材や進捗を外部サーバーへ送信することはありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"170:1-170:119\"\u003eただし、教材作成時に会話や資料を送る先は、利用するAIサービスの仕様に依存します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"172:1-172:42\"\u003e\n\u003cspan id=\"教材フォーマットを分離した\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%95%99%E6%9D%90%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%82%92%E5%88%86%E9%9B%A2%E3%81%97%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e教材フォーマットを分離した\u003c/h2\u003e\n\u003cp data-sourcepos=\"174:1-174:182\"\u003e今回の実装では、Viewerの中に教材生成ロジックを直接組み込むのではなく、\u003ccode\u003eFlashcard Package v1\u003c/code\u003e という教材フォーマットを定義しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"176:1-180:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eFlashcard Package\n├── manifest.json\n└── cards.json\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"182:1-182:90\"\u003eViewerはこの形式のZIPを読み込み、内容を検証してから保存します。\u003c/p\u003e\n\u003cp data-sourcepos=\"184:1-184:189\"\u003e教材IDとカードIDは安定した識別子として扱っているため、同じカードIDを維持したまま教材を更新すれば、既存の学習進捗も引き継げます。\u003c/p\u003e\n\u003cp data-sourcepos=\"186:1-186:173\"\u003eこのように、教材を作る側と教材を見る側を分けたことで、将来的には別のツールや別のAIから教材を生成することもできます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"188:1-188:39\"\u003e\n\u003cspan id=\"シンプルさを優先した理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%95%E3%82%92%E5%84%AA%E5%85%88%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eシンプルさを優先した理由\u003c/h2\u003e\n\u003cp data-sourcepos=\"190:1-190:87\"\u003e今回のアプリでは、最初から次のような機能は入れていません。\u003c/p\u003e\n\u003cul data-sourcepos=\"192:1-197:0\"\u003e\n\u003cli data-sourcepos=\"192:1-192:29\"\u003eユーザーアカウント\u003c/li\u003e\n\u003cli data-sourcepos=\"193:1-193:20\"\u003eクラウド同期\u003c/li\u003e\n\u003cli data-sourcepos=\"194:1-194:35\"\u003e複雑な学習アルゴリズム\u003c/li\u003e\n\u003cli data-sourcepos=\"195:1-195:23\"\u003eソーシャル機能\u003c/li\u003e\n\u003cli data-sourcepos=\"196:1-197:0\"\u003e教材の共有機能\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"198:1-198:57\"\u003eこれらが不要ということではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"200:1-200:102\"\u003eまずは、自分が実際に使う学習サイクルを成立させることを優先しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"202:1-212:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eAIでインプットする\n    ↓\nカードに変換する\n    ↓\n思い出す\n    ↓\n進捗を記録する\n    ↓\n忘れた頃にもう一度見る\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"214:1-214:90\"\u003eこの流れが使いやすければ、あとから必要な機能を追加できます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"216:1-216:33\"\u003e\n\u003cspan id=\"作ってみて感じたこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%A6%E6%84%9F%E3%81%98%E3%81%9F%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e作ってみて感じたこと\u003c/h2\u003e\n\u003cp data-sourcepos=\"218:1-218:74\"\u003eAIによって、教材を作るコストはかなり下がりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"220:1-220:150\"\u003e以前なら、会話や資料を読んで、自分で重要なポイントを抜き出し、問題と答えを整理する必要がありました。\u003c/p\u003e\n\u003cp data-sourcepos=\"222:1-222:101\"\u003e今は、AIに一定のルールを与えることで、その作業の多くを任せられます。\u003c/p\u003e\n\u003cp data-sourcepos=\"224:1-224:84\"\u003eただし、教材を作ることと、知識を身につけることは別です。\u003c/p\u003e\n\u003cp data-sourcepos=\"226:1-226:146\"\u003eAIがきれいな要約やカードを作ってくれても、それを自分で思い出す時間がなければ、知識は定着しません。\u003c/p\u003e\n\u003cp data-sourcepos=\"228:1-228:30\"\u003e今回の開発を通じて、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"230:1-230:115\"\u003e\n\u003cp data-sourcepos=\"230:3-230:115\"\u003eAIはインプットを助けてくれるが、思い出す部分まで自動化してくれるわけではない\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"232:1-232:45\"\u003eということを改めて感じました。\u003c/p\u003e\n\u003cp data-sourcepos=\"234:1-234:152\"\u003eそのため、AIとの会話をカードに変換し、定期的に思い出せる場所を用意することに意味があると思っています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"236:1-236:33\"\u003e\n\u003cspan id=\"今後やってみたいこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BB%8A%E5%BE%8C%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E3%81%84%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e今後やってみたいこと\u003c/h2\u003e\n\u003cp data-sourcepos=\"238:1-238:63\"\u003e今後は、以下のような機能も検討しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"240:1-246:0\"\u003e\n\u003cli data-sourcepos=\"240:1-240:35\"\u003e復習タイミングの最適化\u003c/li\u003e\n\u003cli data-sourcepos=\"241:1-241:32\"\u003eカードの並び順の改善\u003c/li\u003e\n\u003cli data-sourcepos=\"242:1-242:31\"\u003eMarkdownや画像への対応\u003c/li\u003e\n\u003cli data-sourcepos=\"243:1-243:32\"\u003e教材パッケージの共有\u003c/li\u003e\n\u003cli data-sourcepos=\"244:1-244:40\"\u003eAI以外からの教材インポート\u003c/li\u003e\n\u003cli data-sourcepos=\"245:1-246:0\"\u003e複数端末でのデータ移行の改善\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"247:1-247:144\"\u003eただ、機能を増やしすぎると、最初の目的である「気軽に復習する」から離れてしまう可能性もあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"249:1-249:99\"\u003eまずは、自分が継続して使えるシンプルさを維持したいと考えています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"251:1-251:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"253:1-253:83\"\u003eAIによって、知識を得ること自体はとても簡単になりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"255:1-255:111\"\u003e一方で、得た知識を後から思い出せる状態にするには、復習の仕組みが必要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"257:1-257:160\"\u003e今回作った \u003ccode\u003eflashcard-viewer\u003c/code\u003e は、AIとの会話や資料を、自分用の暗記カードに変換して復習するための小さなアプリです。\u003c/p\u003e\n\u003cp data-sourcepos=\"259:1-259:166\"\u003eAnkiのような完成度の高いサービスがある中で自分で作った理由は、すべての人に向けた機能を揃えるためではありません。\u003c/p\u003e\n\u003cul data-sourcepos=\"261:1-265:0\"\u003e\n\u003cli data-sourcepos=\"261:1-261:44\"\u003e自分の学習フローに合わせたい\u003c/li\u003e\n\u003cli data-sourcepos=\"262:1-262:43\"\u003eAIとの会話から教材を作りたい\u003c/li\u003e\n\u003cli data-sourcepos=\"263:1-263:53\"\u003e教材や進捗を端末内だけで管理したい\u003c/li\u003e\n\u003cli data-sourcepos=\"264:1-265:0\"\u003e必要な機能だけのシンプルな画面で使いたい\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"266:1-266:42\"\u003eという目的があったからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"268:1-268:240\"\u003e今回は自分専用のアプリとして作りましたが、教材フォーマットとViewerを分離しているため、汎用化すれば、同じような学習スタイルの人にも使ってもらえる可能性があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"270:1-270:75\"\u003e「わかった」で終わらせず、「思い出せる」に変える。\u003c/p\u003e\n\u003cp data-sourcepos=\"272:1-272:99\"\u003eそのための小さな道具として、これからも改善していきたいと思います。\u003c/p\u003e\n","body":"## TL;DR\n\nAIに質問すると、以前より簡単に知識を得られるようになりました。\n\n一方で、AIとの会話を読んだ直後は理解した気になっていても、数日後には内容を思い出せないことがあります。\n\nそこで、AIとの会話や資料から暗記カードを作成し、ブラウザ上で復習できる `flashcard-viewer` を作りました。\n\n今回作ったものは、以下のようなシンプルなPWAです。\n\n- AIとの会話や資料から暗記カード教材を作成\n- 生成されたZIPファイルをブラウザから取り込み\n- 一問一答と穴埋め形式に対応\n- 「もう一度」「微妙」「覚えた」で進捗を管理\n- カードの検索、セクション・進捗による絞り込み\n- カード本文の修正と自分用メモ\n- 教材・進捗・メモを端末内に保存\n- 一度読み込めばオフラインでも利用可能\n\n「AIで知識を得る」だけでなく、「あとから思い出す」ための導線を自分用に作った、というのが今回の開発の目的です。\n\n```text\n会話・資料\n    │\n    ▼\nいつものAI\n    │ 暗記カード教材を生成\n    ▼\nflashcards.zip\n    │\n    ▼\nFlashcard Viewer\n    │\n    ├─ 教材\n    ├─ 学習進捗\n    └─ 自分用メモ\n       ※ブラウザ内に保存\n```\n## アプリとソースコード\n\n- アプリ：[Flashcard Viewer](https://flashcard-viewer.atoook.com/)\n- ソースコード：[GitHubリポジトリ](https://github.com/atoook/Flashcard-Viewer)\n\n## 背景\n\n最近は、分からないことがあればすぐにAIへ質問できます。\n\n技術の調査、書籍の要約、英語表現の確認、資格試験の学習など、インプットのスピードはかなり上がりました。\n\nただ、AIとの会話を読んでいるときは理解できていても、後から自分で説明しようとすると出てこないことがあります。\n\n\u003e 「わかった」と「覚えた」は違う\n\nAIとの会話は、その場での理解にはとても便利です。\n\n一方で、知識を定着させるには、時間を置いて自分の記憶から思い出すことも必要です。\n\nそこで、AIとの会話をそのまま読み返すのではなく、復習しやすいカードに変換する仕組みを作ることにしました。\n\n## Ankiのような既存サービスがある中で、なぜ自分で作るのか\n\n暗記カードのサービスとしては、Ankiのようなすでに完成度の高いものがあります。\n\nそのため、最初からAnkiの代替を作ろうとしたわけではありません。\n\n今回、自分で作ってみようと思った理由は、自分の学習方法に必要な範囲だけを小さくまとめたかったからです。\n\n特に欲しかったのは、次のような流れでした。\n\n1. AIとの会話や資料を渡す\n2. AIに暗記カード教材を作ってもらう\n3. その教材をすぐに復習する\n4. 間違えたカードだけ後から見直す\n\nまた、個人利用なので、アカウント登録やサーバーへの教材保存も必要ありませんでした。\n\n今回の目的は、機能の多い学習サービスを作ることではなく、\n\n\u003e 自分が普段使っているAIと、自分用の復習画面をつなぐ\n\nことです。\n\n既存サービスを使うのが間違いということではなく、自分の学習フローに合わせて、必要最小限の道具を作ることにも意味があると考えました。\n\n## 教材を作る流れ\n\nトップ画面の「教材を作る」から、AIに渡すプロンプトをコピーできます。\n\n任意で「何を教材にするか」を入力し、AIとの会話に貼り付けます。\n\n```text\n例：\nこの会話で質問した英語表現を教材にしてください\n```\n\nAIには、会話や添付資料をもとに、以下の形式のZIPファイルを作成してもらいます。\n\n```text\n教材名.flashcards.zip\n├── manifest.json\n└── cards.json\n```\n\n生成されたZIPをViewerに取り込むと、教材名やカード枚数を確認して、そのまま学習を開始できます。\n\nAIがファイルを正しく生成できなかった場合は、Viewerに表示されたエラー内容と正式なスキーマを含む「修正依頼プロンプト」をコピーできます。\n\nそのプロンプトとエラーになったZIPをAIに渡すことで、教材の内容を変えずに形式だけ修正してもらえるようにしています。\n\n## 対応しているカード形式\n\n現在は、シンプルな2種類に絞っています。\n\n### 一問一答\n\n```text\n問題：Spring Beanのデフォルトスコープは？\n答え：singleton\n```\n\n### 穴埋め\n\n```text\n水の化学式は ？？？ です。\n```\n\n答えを見ると、隠れていた部分が表示されます。\n\n最初から画像や複雑なレイアウトなどに対応するのではなく、まずは「問いを見て、思い出して、答えを確認する」という学習の中心部分に絞りました。\n\n## 学習画面でできること\n\n学習画面では、カードごとに覚え具合を記録できます。\n\n- もう一度\n- 微妙\n- 覚えた\n\n進捗はカード単位で保存されるため、「もう一度」だけを表示することもできます。\n\nまた、カードが増えてきたときのために、以下にも対応しています。\n\n- 用語検索\n- セクションによる絞り込み\n- 進捗による絞り込み\n- 前回見ていたカードから再開\n- 問題・答えの修正\n- カードごとの自分用メモ\n\nAIが作った問題が少し分かりにくいときは、Viewer内で自分用に修正できます。\n\n「この説明を読んだときに自分が引っかかったポイント」などをメモしておくと、後から見返したときにも使いやすくなります。\n\n## 技術スタック\n\n- Vue 3\n- TypeScript\n- Vite\n- Tailwind CSS\n- IndexedDB\n- PWA / Service Worker\n- Cloudflare Pages\n\nバックエンドは用意していません。\n\n教材、学習進捗、カードの修正内容、メモはブラウザ内のIndexedDBに保存します。\n\nそのため、Flashcard Viewer自体が教材や進捗を外部サーバーへ送信することはありません。\n\nただし、教材作成時に会話や資料を送る先は、利用するAIサービスの仕様に依存します。\n\n## 教材フォーマットを分離した\n\n今回の実装では、Viewerの中に教材生成ロジックを直接組み込むのではなく、`Flashcard Package v1` という教材フォーマットを定義しました。\n\n```text\nFlashcard Package\n├── manifest.json\n└── cards.json\n```\n\nViewerはこの形式のZIPを読み込み、内容を検証してから保存します。\n\n教材IDとカードIDは安定した識別子として扱っているため、同じカードIDを維持したまま教材を更新すれば、既存の学習進捗も引き継げます。\n\nこのように、教材を作る側と教材を見る側を分けたことで、将来的には別のツールや別のAIから教材を生成することもできます。\n\n## シンプルさを優先した理由\n\n今回のアプリでは、最初から次のような機能は入れていません。\n\n- ユーザーアカウント\n- クラウド同期\n- 複雑な学習アルゴリズム\n- ソーシャル機能\n- 教材の共有機能\n\nこれらが不要ということではありません。\n\nまずは、自分が実際に使う学習サイクルを成立させることを優先しました。\n\n```text\nAIでインプットする\n    ↓\nカードに変換する\n    ↓\n思い出す\n    ↓\n進捗を記録する\n    ↓\n忘れた頃にもう一度見る\n```\n\nこの流れが使いやすければ、あとから必要な機能を追加できます。\n\n## 作ってみて感じたこと\n\nAIによって、教材を作るコストはかなり下がりました。\n\n以前なら、会話や資料を読んで、自分で重要なポイントを抜き出し、問題と答えを整理する必要がありました。\n\n今は、AIに一定のルールを与えることで、その作業の多くを任せられます。\n\nただし、教材を作ることと、知識を身につけることは別です。\n\nAIがきれいな要約やカードを作ってくれても、それを自分で思い出す時間がなければ、知識は定着しません。\n\n今回の開発を通じて、\n\n\u003e AIはインプットを助けてくれるが、思い出す部分まで自動化してくれるわけではない\n\nということを改めて感じました。\n\nそのため、AIとの会話をカードに変換し、定期的に思い出せる場所を用意することに意味があると思っています。\n\n## 今後やってみたいこと\n\n今後は、以下のような機能も検討しています。\n\n- 復習タイミングの最適化\n- カードの並び順の改善\n- Markdownや画像への対応\n- 教材パッケージの共有\n- AI以外からの教材インポート\n- 複数端末でのデータ移行の改善\n\nただ、機能を増やしすぎると、最初の目的である「気軽に復習する」から離れてしまう可能性もあります。\n\nまずは、自分が継続して使えるシンプルさを維持したいと考えています。\n\n## まとめ\n\nAIによって、知識を得ること自体はとても簡単になりました。\n\n一方で、得た知識を後から思い出せる状態にするには、復習の仕組みが必要です。\n\n今回作った `flashcard-viewer` は、AIとの会話や資料を、自分用の暗記カードに変換して復習するための小さなアプリです。\n\nAnkiのような完成度の高いサービスがある中で自分で作った理由は、すべての人に向けた機能を揃えるためではありません。\n\n- 自分の学習フローに合わせたい\n- AIとの会話から教材を作りたい\n- 教材や進捗を端末内だけで管理したい\n- 必要な機能だけのシンプルな画面で使いたい\n\nという目的があったからです。\n\n今回は自分専用のアプリとして作りましたが、教材フォーマットとViewerを分離しているため、汎用化すれば、同じような学習スタイルの人にも使ってもらえる可能性があります。\n\n「わかった」で終わらせず、「思い出せる」に変える。\n\nそのための小さな道具として、これからも改善していきたいと思います。\n","coediting":false,"comments_count":0,"created_at":"2026-09-18T08:43:23+09:00","group":null,"id":"df0ff03ee1f703a36870","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"AI","versions":[]},{"name":"学習","versions":[]},{"name":"個人開発","versions":[]},{"name":"Vue.js","versions":[]},{"name":"PWA","versions":[]}],"title":"AI時代のインプットを「わかった」で終わらせないために、flashcard-viewerを作った","updated_at":"2026-09-18T08:43:23+09:00","url":"https://qiita.com/atok/items/df0ff03ee1f703a36870","user":{"description":"辛ラーメンが好きです。","facebook_id":"","followees_count":0,"followers_count":2,"github_login_name":null,"id":"atok","items_count":11,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":150636,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/150636/e847a89753d82abfa18483415d96690888314146/large.png?1697883424","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null}]