テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その4

はじめに Part1 でValueObject・Policy・Entityを作り、Part2 でServiceをDI+Mockテストで固めた。 Part3 でPrismaを繋ぎ、GET /api/books と POST /api/books を動かした。 Part4では残りのエンドポイントを実装する。やることは3つ。 カスタム例外クラスの導入 PATCH /api/books/:id/start と PATCH /api/books/:id/complete の実装 DELETE /api/books/:id の実装 そして「エラー種別ごとにHTTPステータスを整理する」という設計判断を掘り下げる。 また、Part3でPrisma v7特有のセットアップをしたが、v6以前と何が変わったのかをここで整理しておく。 0. Prisma v7で何が変わったか Part3でPrismaをセットアップしたとき、v6以前と比べていくつか「見慣れない書き方」が必要だった。 v7は破壊的変更が多く、ネット上のv6時代の記事を参考にすると詰まる箇所がある。 ここで整理しておく。 参考: Upgrade to Prisma ORM 7 | Prisma Documentation generator の変更 // ❌ v6以前 generator client { provider = "prisma-client-js" } // ✅ v7 generator client { provider = "prisma-client" output = "../src/generated/prisma" } v7では prisma-client-js が廃止され prisma-client に変わった。 Rustベースのエンジンを廃止しTypeScriptネイティブになったことに伴う変更だ。 また output が必須になり、node_modules への自動生成はなくなった。 ...

March 4, 2026 · 6 min

テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その5

はじめに Part4 でカスタム例外クラスと残りのエンドポイントを実装し、APIが完成した。 Part5では src/app/page.tsx に簡易UIを実装する。バックエンドのロジックやテストには一切触れない。 フロントエンドからAPIを叩いて動くものを作るだけだ。 実装方針 page.tsx をClient Componentにする。 Server Componentでfetchする方法もあるが、ボタン操作のたびにstateを更新して再描画する必要がある。 今回のUIは「ボタンを押す → APIを叩く → 一覧を再取得して画面を更新する」という流れが中心なので、 useState + useEffect で管理するClient Componentのほうがシンプルだ。 "use client"; ファイル先頭にこの1行を追加する。 実装するUI 本の追加フォーム(id / title / isbn) 本棚(Unread / Reading / Completed のグループ表示) 各本へのアクションボタン Unread → 「読み始める」ボタン Reading → 評価入力(1〜5)+「読了にする」ボタン 全ステータス → 削除ボタン 型定義 APIレスポンスの型を定義する。 type BookStatus = "Unread" | "Reading" | "Completed"; type Book = { id: string; title: string; isbn: string; status: BookStatus; rating: number | null; }; バックエンドの ReadingStatus は as const で定義した文字列リテラルなので、そのまま使える。 データ取得 async function fetchBooks() { try { const res = await fetch("/api/books"); if (!res.ok) throw new Error("取得失敗"); const data: Book[] = await res.json(); setBooks(data); } catch (e) { setError(e instanceof Error ? e.message : "不明なエラー"); } finally { setLoading(false); } } useEffect(() => { fetchBooks(); }, []); fetchBooks は追加・ステータス変更・削除の後にも呼ぶ。 サーバーの状態を正として再取得するシンプルな方針だ。 楽観的更新(Optimistic Update)は今回やらない。 ...

March 4, 2026 · 2 min

AVL木と赤黒木をPythonで実装して比較する

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてLinuxスケジューラの話が出てきた。CFSもEEVDFも赤黒木を使っている。赤黒木とは何かを調べていくうちにAVL木との比較が面白かったので、両方Pythonで実装してベンチマークを取った。 実装コードはClaude (Anthropic) により生成。リポジトリは以下。 https://github.com/wasuken/avl-rb-tree 自己平衡二分探索木とは まず前提として、ただの二分探索木には問題がある。 挿入順: 1, 2, 3, 4, 5 1 \ 2 \ 3 \ 4 ← 一直線になる 挿入順によっては木が一直線になり、検索がO(n)に劣化する。これを解決するのが自己平衡二分探索木で、挿入・削除のたびに自動でバランスを取り直す。AVL木と赤黒木はどちらもこのカテゴリに属する。 AVL木 1962年にソ連の数学者Adelson-VelskyとLandis(AVLの名前の由来)が考案した世界初の自己平衡二分探索木。 バランスの管理方法 各ノードに高さ(height)を持たせ、左右の高さの差(バランス係数)が常に1以内になるよう管理する。 def _balance_factor(self, node): return self._height(node.left) - self._height(node.right) 差が2以上になったら回転で修正する。 回転 回転は「親子関係を1段入れ替えるだけ」の操作。二分探索木の順序を壊さずに形だけ変える。 rotate_right(y): y x / \ / \ x C → A y / \ / \ A t2 t2 C t2 は回転で行き場を失う孫ノード。二分探索木の順序的に x < t2 < y が保証されているので、yの左に付け替えるだけでよい。 バランス崩れのパターンは4つ(LL, RR, LR, RL)で、それぞれ1〜2回の回転で解消できる。 挿入・削除 挿入・削除のたびに _rebalance が呼ばれ、高さの更新とバランスチェックが走る。 ...

March 1, 2026 · 2 min

LinuxのCFSとEEVDFを整理する - スケジューラはなぜ赤黒木を使うのか

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてスケジューラ周りの理解が曖昧だったので、生成AIや公式ドキュメントを使って整理した。 CFS (Completely Fair Scheduler) とは Linux 2.6.23から導入されたプロセススケジューラ。「全プロセスに公平にCPU時間を与える」という思想で設計されている。 vruntime(仮想実行時間) vruntimeは「実際の実行時間をNICE値で補正した値」で、CFSの核心となる指標。 vruntime += 実際のCPU時間 × (1024 / プロセスの重み) NICE値が低い(優先度高)→ 重みが大きい → vruntimeの増加が遅い → より長くCPUを使える NICE値が高い(優先度低)→ 重みが小さい → vruntimeの増加が速い → すぐ交代させられる CFSは「vruntimeが最も小さいプロセスを次に実行する」というルールで動く。後ろ向きの指標(過去の使用量の累積)であることがEEVDFとの本質的な差になる。 NICE値と重み NICE値は -20(最高優先度)〜 +19(最低優先度)の範囲で、内部的に重みに変換される。 NICE 0 → weight 1024 NICE -1 → weight 1277(約1.25倍) NICE +1 → weight 820(約0.8倍) NICE -20 → weight 88761 NICE +19 → weight 15 1段階変わるごとに約10%のCPU時間が変化する設計になっている。 タイムスライスとスケジューリングレイテンシ スケジューリングレイテンシは「全プロセスが最低1回実行されるべき目標周期」。デフォルト約6〜24ms(プロセス数による)。 タイムスライスはその比例配分: タイムスライス = スケジューリングレイテンシ × (タスクの重み / キュー内の全タスクの重みの合計) 具体例: ...

March 1, 2026 · 2 min

Linuxの起動フローを整理する - UEFI/BIOSからinitまで

はじめに [https://info.nikkeibp.co.jp/media/LIN/atcl/books/070900046/:embed:cite] 上記の技術書を読んでいて、ブートローダとLinuxの初期スタート時の役割とか順番がいまいち掴めなかったので生成AIや他の記事など別軸から調べ直してまとめた。 起動フロー全体像 UEFI/BIOS ↓ POST(ハードウェア初期化)、ブートデバイス選択 ブートローダー(GRUB等) ↓ /boot/vmlinuz(カーネルイメージ)をメモリに展開 ↓ /boot/initramfs をメモリに展開 カーネル起動 ↓ initramfsを一時的な / としてマウント ↓ ドライバ読み込み、本物のrootデバイスを認識 ↓ 本物のroot FSをマウント(switch_root) /sbin/init(systemd)に移譲 各フェーズの詳細 1. UEFI/BIOS 起動の最初はUEFI(または旧来のBIOS)が担う。 POST(Power-On Self Test): メモリ、CPU、周辺デバイスの初期化 ブートデバイスの選択(NVMe, SSD, PXEなど) UEFIの場合はEFIパーティション(ESP)から .efi ファイルを直接実行できる UEFIとBIOSの大きな違いとして、UEFIはGPTディスクのネイティブサポートや、セキュアブートの仕組みを持つ。 2. ブートローダー(GRUB2等) UEFI/BIOSからブートローダーに制御が渡る。 代表的なものはGRUB2で、設定ファイルは /boot/grub/grub.cfg にある。 ブートローダーの役割はシンプルで、以下の2点だけ: カーネルイメージ(vmlinuz)をメモリに展開する initramfs(initramfs-*.img)をメモリに展開する # /boot 以下の典型的な構成 $ ls /boot/ grub/ initramfs-6.1.0-28-amd64.img vmlinuz-6.1.0-28-amd64 ブートローダー自身はルートFSのマウントをしない。あくまでカーネルとinitramfsをメモリに置いて制御を渡すだけ。 3. カーネル起動とinitramfs ここが一番誤解されやすいフェーズ。 カーネルが起動すると、まず**initramfs(Initial RAM Filesystem)**を一時的なルート(/)としてマウントする。 なぜinitramfsが必要か? カーネル本体はコンパクトに保つ設計になっており、NVMeやLVMやLUKS(暗号化)といった本物のディスクにアクセスするためのドライバを、起動時に動的にロードする必要がある。 initramfsはそのためのミニマルな環境を提供する。 initramfs の中身(概略) /init → 起動スクリプト /lib/modules → カーネルモジュール(ドライバ) /bin, /sbin → busybox等の最低限のコマンド群 処理の流れ: ...

February 28, 2026 · 1 min

歴史地図アプリを雑にk3sへデプロイした

歴史地図アプリの構成 React + TypeScript + Vite + MapLibre GL のSPA。歴史的国境データ(GeoJSON)を表示するアプリ。 データはpublic/data/以下にGeoJSONを置く構成で、.gitignoreに含めているためリポジトリには入っていない。 ちなみに、生成するスクリプトはあるが、GEMINIを利用しないといけない。 しかし、APIキーのレート制限が入ってしまったので、ローカルで生成済みのデータを持ち込むことにした。 インフラ構成 自宅のProxmox上にLXCコンテナとしてk3sクラスタを構築している。マスター1台+ノード1台の最小構成。 外部公開はNginx Proxy Manager(NPM)でポートフォワーディングしており、DuckDNSのドメインにSSL終端している。 インターネット ↓ Nginx Proxy Manager(SSL終端) ↓ k3s NodePort ↓ Pod 問題:データファイルをどう持ち込むか public/data/がgitignoreされているため、コンテナ内でgit cloneしてもデータが存在しない。 選択肢はいくつかあったが、今回はk3sのhostPathボリュームでマウントする方針にした。 だるいファイル転送 データファイルをk3sノードに転送するのが一番面倒だった。 Proxmoxのファイルアップロード → UIの制限でNG ngrok経由 → Tailscale環境のためlocalhostの名前解決失敗 結局TailscaleのIPでProxmoxホストに転送 → pct pushでLXCコンテナへ # ProxmoxホストからLXCへ pct push <CTID> /path/to/data.tar.gz /tmp/data.tar.gz # k3sマスターで解凍 mkdir -p /opt/history-map-data tar -xzf /tmp/data.tar.gz -C /opt/history-map-data 融通の効かないViteとふわふわClaude君の罠 npm run previewはデフォルトで許可ホストを制限する。Nginx Proxy Manager経由でアクセスするとBlocked requestが出る。 環境変数で全許可とかできたらよかったけど、結論だけ言うとできなかった。少なくともClaude君の指示では何をどうしても駄目だったので、最終的にvite.config.tsをデプロイ時に動的に書き換えることで回避した。 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF 最終的なYAML apiVersion: apps/v1 kind: Deployment metadata: name: history-map spec: replicas: 1 selector: matchLabels: app: history-map template: metadata: labels: app: history-map spec: nodeName: k3s-master containers: - name: history-map image: node:20-alpine workingDir: /app command: ["sh", "-c"] args: - | apk add --no-cache git git clone https://github.com/wasuken/history-map-app.git /app --depth=1 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF mkdir -p /app/public/data cp -r /data/historical /app/public/data/historical cp -r /data/modern /app/public/data/modern cp /data/translation-cache.json /app/public/data/translation-cache.json npm install npx vite build npm run preview -- --host 0.0.0.0 --port 3000 ports: - containerPort: 3000 volumeMounts: - name: map-data mountPath: /data volumes: - name: map-data hostPath: path: /opt/history-map-data type: Directory --- apiVersion: v1 kind: Service metadata: name: history-map-service spec: selector: app: history-map ports: - port: 80 targetPort: 3000 nodePort: 30080 type: NodePort nodeName: k3s-masterを指定しているのはhostPathがPodの動くノード上に存在する必要があるため。ProxmoxのNPM(Nginx Proxy Manager)から このNodePortに向けてプロキシを設定している。 ...

February 25, 2026 · 2 min

WSL2でExpo + E2Eテスト(MaestroとDetox)を試みて完全に詰んだ話

TL;DR WSL2環境でExpo(React Native)のE2EテストをMaestroとDetoxで試みたが、どちらもWSL2とWindowsエミュレータの構造的な問題で動かなかった。 かなり過言ではあるが、あえて感情的になるならば、Mobile開発においてMac以外は人権がない。というかあまりにもMac環境以外がだるすぎる。 環境 OS: Windows + WSL2(Ubuntu) Expo SDK 54 / React Native 0.81.5 New Architecture有効 Androidエミュレータ: Windows側で動作(Medium Phone API 36) ADB: Windows側のものをWSL2から参照 Maestroを試みる インストール curl -Ls "https://get.maestro.mobile.dev" | bash export PATH="$HOME/.maestro/bin:$PATH" ここで最初の罠。maestro --helpを叩くとAI系の全く別のCLIツールが応答した。同名の別アプリが先にPATHに入っていたため。$HOME/.maestro/binをPATHの先頭に置くことで解決。 フローの準備 # .maestro/add_and_complete_task.yml appId: com.example.myapp --- - launchApp - tapOn: text: "追加" - inputText: "テストタスク" - tapOn: text: "追加する" - assertVisible: text: "NOW" 実行して即死 You have 0 devices connected, which is not enough to run 1 shards. エミュレータはWindows側で動いており、adb devicesにはemulator-5554が見えている。しかしMaestroはWSL2側でデバイスを探すため認識できない。 --udid=emulator-5554を指定しても: Device emulator-5554 was requested, but it is not connected. maestro start-device --platform=androidを試みると: ...

February 23, 2026 · 2 min

react-native-pdf 6.7.7のiOS表示問題をpatch-packageで解決する

はじめに 業務でreact-native-pdfを使用した際、AndroidではPDFが正常に表示されるのにiOSでは表示されないという問題に遭遇しました。 この記事では、GitHubのissueで共有された解決策であるpatch-packageを使ったパッチ適用方法について解説します。 問題の概要 環境 { "react-native-pdf": "^6.7.7", "react-native": "0.80.1", "react-native-blob-util": "^0.22.2" } 症状 Android: PDF表示が正常に動作 iOS: PDFが表示されない この問題は、React Native 0.80以降でreact-native-pdfを使用した際に発生することが確認されています。 参考: pdf is not displayed,Android is working fine, but there are problems with iOS #966 解決策: patch-packageを使う GitHubのissueで@anhnguyen123さんが共有してくれたパッチファイルを適用することで、この問題を解決できます。 1. patch-packageのインストール まず、patch-packageとpostinstall-postinstallをdevDependenciesとしてインストールします。 # npmの場合 npm install --save-dev patch-package # yarnの場合 yarn add --dev patch-package postinstall-postinstall 参考: patch-package - npm 2. package.jsonにpostinstallスクリプトを追加 package.jsonのscriptsセクションに、postinstallスクリプトを追加します。 { "scripts": { "postinstall": "patch-package" } } このスクリプトにより、npm installまたはyarn installを実行するたびに、自動的にパッチが適用されます。 3. パッチファイルの配置 GitHubのissueからパッチファイルreact-native-pdf+6.7.7.patchをダウンロードし、プロジェクトルートにpatchesディレクトリを作成してそこに配置します。 ...

February 17, 2026 · 2 min

歴史地図アプリに日本語検索を実装: GeoJSONデータの効率的な翻訳手法

はじめに 歴史的国境を可視化する地図アプリを作っていたら、「日本語で国名検索ができない」という問題に直面した。外部のGeoJSONデータは英語のみで、日本語プロパティがない。 そこで、Gemini APIを使って効率的にデータを翻訳し、日本語検索を実装した手法を紹介する。 問題: 外部GeoJSONデータには日本語がない 使用したデータソース: 現代国境: Natural Earth (約200カ国) 歴史的国境: aourednik/historical-basemaps (18ファイル、紀元前2000年〜1920年) { "type": "Feature", "properties": { "NAME": "France", "NAME_JA": null // ← 日本語プロパティがない! }, "geometry": { ... } } このままでは「フランス」で検索できない。 解決策: 翻訳キャッシュを使った効率的なデータ拡張 アプローチ1: 愚直な方法 (非効率) 各ファイルごとに全データをLLMに投げる: // ❌ 非効率: 同じ国名を何度も翻訳 for (const file of geoJsonFiles) { const data = await fetch(file); const translated = await translateAll(data); // Franceを18回翻訳... await save(translated); } 問題点: 同じ国名が複数ファイルに登場 → 重複翻訳 トークン消費が膨大 処理時間が長い アプローチ2: 翻訳キャッシュ方式 (効率的) ✅ 全ファイル共通の翻訳キャッシュを使い回す: // ✅ 効率的: 一度翻訳した国名は二度と翻訳しない const translationCache = {}; // { "France": "フランス", ... } for (const file of geoJsonFiles) { const data = await fetch(file); // 未翻訳の国名のみ抽出 const newNames = extractUntranslatedNames(data, translationCache); // 新規の国名だけ翻訳 if (newNames.length > 0) { const translations = await translate(newNames); Object.assign(translationCache, translations); } // キャッシュを使って適用 applyTranslations(data, translationCache); await save(data); } 実装: Node.jsスクリプト 完全なコード この手法をNode.jsスクリプトとして実装した。Gemini 2.5 Flash Liteを使用している。この程度の翻訳ならこれで十分。 ...

February 15, 2026 · 4 min

PostgreSQLのCTEが現場で少ない理由を実務経験から考える

はじめに バッチ処理で大量のデータ変換を行う際、PostgreSQLのCTE(Common Table Expression、WITH句)を多用していた時期がありました。複雑な変換処理を段階的に分割できて、コードの見通しも良くなる便利な機能です。 しかし、実際の現場でCTEを使っているコードは意外と少ない。サブクエリや一時テーブルが使われているケースの方が圧倒的に多い印象です。 この記事では、実務でCTEを使って感じた強み・弱みと、「なぜ現場ではCTEが少ないのか」を考察します。特にPostgreSQL 12で大きく改善された最適化の仕組みについても解説します。 CTEの基本おさらい CTEはWITH句を使って一時的な結果セットを定義し、メインクエリから参照できる機能です。 WITH regional_sales AS ( SELECT region, SUM(amount) AS total_sales FROM orders GROUP BY region ) SELECT region, total_sales FROM regional_sales WHERE total_sales > 10000; サブクエリと似ていますが、名前をつけて再利用できる点が特徴です。変数のように扱えて、複雑なクエリを段階的に構築できます。 CTEの強みと弱み 強み 1. 可読性・保守性の向上 ネストしたサブクエリ地獄を回避できます。処理を論理的なステップに分割して、各ステップに名前をつけられるため、コードレビューやメンテナンスが格段に楽になります。 2. 再帰クエリが書ける WITH RECURSIVEを使えば、階層構造(組織図、カテゴリツリー)を扱えます。これはCTE独自の強みで、サブクエリでは実現できません。 3. 複数箇所から参照できる 同じCTEを複数回参照できます(ただし最適化の観点で注意が必要、後述)。サブクエリだと同じ処理を重複して書く必要があります。 4. デバッグしやすい 各CTEを個別に実行して中間結果を確認できます。サブクエリだと抜き出して実行するのが面倒です。 5. 変換処理の分離 SELECT句での複雑な計算を先にCTEで処理しておけます。WHERE句で使いたいけど計算が複雑な場合に便利です。 弱み 1. 親クエリのパラメータを参照できない サブクエリなら外側の列を参照できる(相関サブクエリ)のに対し、CTEは独立しているため参照できません。 -- サブクエリなら可能 SELECT * FROM orders o WHERE amount > (SELECT AVG(amount) FROM orders WHERE region = o.region); -- CTEでは不可能(外側のo.regionを参照できない) 2. 大量データ・長時間処理には不向き メモリ上に保持されるため、巨大データだと辛い。一時テーブルならインデックスを作成したり統計情報を活用できます。 実際、バッチ処理で数百万行のデータを扱う際、CTEよりも一時テーブルの方がパフォーマンスが良いケースが多かったです。 3. PostgreSQL 11以前は「最適化バリア」になる これが最大の問題でした。次のセクションで詳しく解説します。 ...

February 12, 2026 · 3 min