Skip to content

Latest commit

ย 

History

History
439 lines (347 loc) ยท 19.3 KB

File metadata and controls

439 lines (347 loc) ยท 19.3 KB

StationAPI ใ‚ขใƒผใ‚ญใƒ†ใ‚ฏใƒใƒฃใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ

ๆœ€็ต‚ๆ›ดๆ–ฐ: 2026ๅนด8ๆœˆ22ๆ—ฅ

็›ฎๆฌก


ๆฆ‚่ฆ

ๆ—ฅๆœฌใฎ้‰„้“้ง…ใƒปใƒใ‚นๅœใฎๆƒ…ๅ ฑใ‚’่ฟ”ใ™ GraphQL API ใงใ™ใ€‚Cloudflare Workers ไธŠใงๅ‹•ใใ€ ใƒ‡ใƒผใ‚ฟใฏ WASM ใซๅŸ‹ใ‚่พผใ‚“ใง้…ใ‚Šใพใ™ใ€‚ใ‚ตใƒผใƒใƒผใƒ—ใƒญใ‚ปใ‚นใ‚‚ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใ‚‚ๆŒใกใพใ›ใ‚“ใ€‚

ไปฅๅ‰ใฏ gRPC-Web ใ‚’่ฟ”ใ™ใ‚ชใƒณใƒ—ใƒฌใฎใ‚ตใƒผใƒใƒผใงใ€PostgreSQL ใ‚’่ชญใฟใ€ใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆใฏ BFF (TrainLCD/BFF) ใŒ GraphQL ใธๅค‰ๆ›ใ—ใŸใ‚‚ใฎใ‚’ไฝฟใฃใฆใ„ใพใ—ใŸใ€‚gRPC-Web ใงใ‚ใ‚‹ ๅฟ…็„ถๆ€งใŒ็„กใ‹ใฃใŸใŸใ‚ GraphQL ใ‚’็›ดๆŽฅ่ฟ”ใ™ๅฝขใซใ—ใ€BFF ใ”ใจๅปƒๆญขใ—ใฆใ„ใพใ™ใ€‚

ๆŠ€่ก“ใ‚นใ‚ฟใƒƒใ‚ฏ

็”จ้€” ๆŽก็”จใ—ใฆใ„ใ‚‹ใ‚‚ใฎ
ๅฎŸ่กŒ็’ฐๅขƒ Cloudflare Workers (wasm32-unknown-unknown)
API GraphQL (async-graphql 7)
ใƒ‡ใƒผใ‚ฟ ใƒ“ใƒซใƒ‰ๆ™‚ใซ WASM ใธๅŸ‹ใ‚่พผใ‚€ CSV (generated/*.csv)
ใƒ‡ใƒผใ‚ฟ็”Ÿๆˆ preprocessor crate (็ด” Rust)
ใƒ‡ใƒ—ใƒญใ‚ค wrangler

ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใ‚’ไฝฟใ‚ใชใ„ใŸใ‚ใ€sqlx ใ‚‚ๆŽฅ็ถšใƒ—ใƒผใƒซใ‚‚ใ‚ใ‚Šใพใ›ใ‚“ใ€‚ๆคœ็ดขใฏ ่ตทๅ‹•ๆ™‚ใซ็ต„ใฟ็ซ‹ใฆใŸใ‚คใƒณใƒกใƒขใƒช็ดขๅผ•ใซๅฏพใ™ใ‚‹่ตฐๆŸปใง่กŒใ„ใพใ™ใ€‚


ๅ…จไฝ“ๆง‹ๆˆ

  data/*.csv          GTFS (ZIP)        ODPT (JSON)
  ้‰„้“ใฎๆญฃใƒ‡ใƒผใ‚ฟ        ใƒใ‚น 5 ใƒ•ใ‚ฃใƒผใƒ‰      ๆฑๆ€ฅใƒใ‚น
      โ”‚                    โ”‚                 โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚   preprocessor     โ”‚  ็ด” Rustใ€‚ๅ„้ง…ๅœ่ปŠใฎ็ณป็ตฑ็”Ÿๆˆใจ
                 โ”‚   (ใƒ“ใƒซใƒ‰ๆ™‚ใƒ„ใƒผใƒซ)  โ”‚  GTFS ็ตฑๅˆใ‚’่กŒใ†
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚
                    generated/*.csv     7 ใƒ†ใƒผใƒ–ใƒซ
                           โ”‚
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚  build.rs          โ”‚  CSV ใ‚’ OUT_DIR ใธ้…็ฝฎใ—ใ€
                 โ”‚                    โ”‚  sst ใฏๅ›บๅฎš้•ทใƒใ‚คใƒŠใƒชใธๅค‰ๆ›
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚  worker-build      โ”‚
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚
                        WASM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Cloudflare Workers
                                          โ”‚
                                     GraphQL (POST /)
                                          โ”‚
                                       TrainLCD

ใƒ‡ใƒผใ‚ฟใฏ WASM ใซๅŸ‹ใ‚่พผใพใ‚Œใ‚‹ใŸใ‚ใ€ใƒ‡ใƒผใ‚ฟๆ›ดๆ–ฐใฎใŸใณใซๅ†ใƒ‡ใƒ—ใƒญใ‚คใŒ่ฆใ‚Šใพใ™ใ€‚ ่ตทๅ‹•ๆ™‚ๅ–ใ‚Š่พผใฟใง่‡ชๅ‹•ๅๆ˜ ใ•ใ‚Œใ‚‹ไฝœใ‚Šใงใฏใ‚ใ‚Šใพใ›ใ‚“ใ€‚


ใƒฌใ‚คใƒคใƒผๆง‹้€ 

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Presentation (src/graphql/)                   โ”‚  async-graphql ใฎใƒชใ‚พใƒซใƒใจๅž‹
โ”‚   Query / ๅž‹ / enum / ใ‚นใ‚ซใƒฉใƒผ                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Model (stationapi/src/model.rs)               โ”‚  API ใŒ่ฟ”ใ™ๅ€คใฎ่กจ็พ
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ UseCase (stationapi/src/use_case/)            โ”‚  ๅ•ใ„ๅˆใ‚ใ›ใฎ็ต„ใฟ็ซ‹ใฆใ€
โ”‚   QueryInteractor / DTO ๅค‰ๆ›                   โ”‚  IPAใƒปTTS ใฎ็”Ÿๆˆ
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Domain (stationapi/src/domain/)               โ”‚  ใ‚จใƒณใƒ†ใ‚ฃใƒ†ใ‚ฃใ€็ตŒ่ทฏๆŽข็ดขใ€
โ”‚   entity / repository ใƒˆใƒฌใ‚คใƒˆ / ้€Ÿๅบฆ่กจ        โ”‚  ๅˆฐ้”ๆ™‚้–“ๆŽจๅฎšใ€ๆญฃ่ฆๅŒ–
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Index (src/index.rs, src/repository.rs)       โ”‚  ๅŸ‹ใ‚่พผใฟใƒ‡ใƒผใ‚ฟใฎ็ดขๅผ•ใจ
โ”‚                                               โ”‚  repository ใƒˆใƒฌใ‚คใƒˆใฎๅฎŸ่ฃ…
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

stationapi crate ใฏ Domain / UseCase / Model ใ ใ‘ใ‚’ๆŒใคใƒฉใ‚คใƒ–ใƒฉใƒชใงใ€ Worker ใจ preprocessor ใฎๅŒๆ–นใ‹ใ‚‰ๅ‚็…งใ•ใ‚Œใพใ™ใ€‚wasm32 ใงใƒ“ใƒซใƒ‰ใงใใ‚‹ๅฟ…่ฆใŒ ใ‚ใ‚‹ใŸใ‚ใ€I/O ใ‚’ไผดใ†ไพๅญ˜ใฏๅ…ฅใ‚Œใพใ›ใ‚“ใ€‚

Domain ๅฑค (stationapi/src/domain/)

ใ‚จใƒณใƒ†ใ‚ฃใƒ†ใ‚ฃใ€ใƒชใƒใ‚ธใƒˆใƒชใฎๆŠฝ่ฑกใ€ใŠใ‚ˆใณ็ด”็ฒ‹ใช่จˆ็ฎ— (haversineใ€็ตŒ่ทฏใฎๆŽข็ดขใ€ ๅˆฐ้”ๆ™‚้–“ใฎๆŽจๅฎšใ€้€Ÿๅบฆ่กจใ€ใƒญใƒผใƒžๅญ—ใƒปIPA ๅค‰ๆ›ใ€ๆคœ็ดข็”จใฎๆญฃ่ฆๅŒ–) ใ‚’ๆŒใกใพใ™ใ€‚

UseCase ๅฑค (stationapi/src/use_case/)

QueryInteractor ใŒ repository ใƒˆใƒฌใ‚คใƒˆ่ถŠใ—ใซใƒ‡ใƒผใ‚ฟใ‚’้›†ใ‚ใ€้ง…ใธ่ทฏ็ทšใƒปไบ‹ๆฅญ่€…ใƒป ๅˆ—่ปŠ็จฎๅˆฅใ‚’ไป˜ไธŽใ—ใพใ™ใ€‚N+1 ใ‚’้ฟใ‘ใ‚‹ใŸใ‚ใ€้–ข้€ฃใƒ‡ใƒผใ‚ฟใฏๅธธใซไธ€ๆ‹ฌใงๅ–ใ‚Šใพใ™ใ€‚

DTO (use_case/dto/) ใŒใƒ‰ใƒกใ‚คใƒณใ‚จใƒณใƒ†ใ‚ฃใƒ†ใ‚ฃใ‚’ Model ใธๅค‰ๆ›ใ—ใพใ™ใ€‚IPA ใจ TTS ใ‚ปใ‚ฐใƒกใƒณใƒˆใฎ็”Ÿๆˆใฏใ“ใ“ใซใ‚ใ‚Šใพใ™ใ€‚

Model ๅฑค (stationapi/src/model.rs)

API ใŒ่ฟ”ใ™ๅ€คใฎ่กจ็พใงใ™ใ€‚ใ‚‚ใจใฏ .proto ใ‹ใ‚‰ prost ใŒ็”Ÿๆˆใ—ใฆใ„ใŸๅž‹ใงใ€ gRPC ใ‚’ใ‚„ใ‚ใŸใ‚ใจใ‚‚ใ€ไธŠ่จ˜ใฎ IPAใƒปTTS ็”ŸๆˆใŒใ“ใ“ใธใฎๅค‰ๆ›ใซใถใ‚‰ไธ‹ใŒใฃใฆใ„ใ‚‹ใŸใ‚ ใƒ‰ใƒกใ‚คใƒณใ‚จใƒณใƒ†ใ‚ฃใƒ†ใ‚ฃใจ GraphQL ๅž‹ใฎ้–“ใซๆฎ‹ใ—ใฆใ‚ใ‚Šใพใ™ใ€‚

Presentation ๅฑค (src/graphql/)

async-graphql ใฎ Query ใƒชใ‚พใƒซใƒใจๅž‹ๅฎš็พฉใงใ™ใ€‚Model ใ‹ใ‚‰ GraphQL ๅž‹ใธๅค‰ๆ›ใ—ใพใ™ใ€‚

Index ๅฑค (src/index.rs, src/repository.rs)

ๅŸ‹ใ‚่พผใฟ CSV ใ‚’ isolate ่ตทๅ‹•ๆ™‚ใซไธ€ๅบฆใ ใ‘ใƒ‘ใƒผใ‚นใ—ใ€OnceLock ใซไฟๆŒใ—ใพใ™ใ€‚ src/repository.rs ใŒ 4 ใคใฎ repository ใƒˆใƒฌใ‚คใƒˆใ‚’ๅฎŸ่ฃ…ใ—ใ€UseCase ๅฑคใ‹ใ‚‰ใฏ ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚น็‰ˆใจๅŒใ˜ใ‚คใƒณใ‚ฟใƒผใƒ•ใ‚งใƒผใ‚นใง่ฆ‹ใˆใพใ™ใ€‚


ใƒ‡ใƒผใ‚ฟใƒ‘ใ‚คใƒ—ใƒฉใ‚คใƒณ

data/*.csv ใ‚’ใใฎใพใพ Worker ใธๆธกใ™ใจๆœฌ็•ชใจๆŒ™ๅ‹•ใŒๅค‰ใ‚ใ‚Šใพใ™ใ€‚

  • ๅˆ—่ปŠ็จฎๅˆฅใ‚’ๆŒใŸใชใ„่ทฏ็ทšใซใฏๅ„้ง…ๅœ่ปŠใฎ็ณป็ตฑใ‚’่ฃœใ†ๅฟ…่ฆใŒใ‚ใ‚‹ (็ด„2,400่กŒใ€ ๆœ‰ๅŠนใช้ง…ใฎ็ด„21%ใŒๅฝฑ้Ÿฟใ‚’ๅ—ใ‘ใ‚‹)ใ€‚ใ“ใฎ่กŒใฏ data/*.csv ใซๅญ˜ๅœจใ—ใชใ„
  • ใƒใ‚นๅœใƒปใƒใ‚น่ทฏ็ทšใƒปใƒใ‚น็ณป็ตฑใฏ GTFS ใจ ODPT ใฎ JSON ใ‹ใ‚‰่ตทใ“ใ™ๅฟ…่ฆใŒใ‚ใ‚‹

ใ“ใ‚Œใ‚’่กŒใ†ใฎใŒ preprocessor crate ใงใ™ใ€‚

make data     # cargo run --profile tool -p stationapi-preprocessor

ๅ‡ฆ็†ใฎๆตใ‚Œ:

  1. data/*.csv ใ‚’่ชญใ‚€ (# ๅง‹ใพใ‚Šใฎๅˆ—ใฏๅ–ใ‚Š่พผใพใชใ„)
  2. ๅ„้ง…ๅœ่ปŠใฎ็ณป็ตฑใ‚’็”Ÿๆˆใ™ใ‚‹ (generate_virtual_local_rail_services)
  3. GTFS ใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ๅ–ๅพ—ใƒปๅฑ•้–‹ใ—ใฆ่ชญใ‚€ (้ƒฝๅ–ถใƒป่ฅฟๆญฆใƒปไบฌ็Ž‹ใƒปๆฑๆ€ฅใ‚ณใƒŸใƒฅใƒ‹ใƒ†ใ‚ฃ 3 ๅŒบ)
  4. ๆฑๆ€ฅใƒใ‚นใฎ ODPT JSON ใ‚’่ชญใ‚€ (7 ๆ—ฅ้–“ใ‚ญใƒฃใƒƒใ‚ทใƒฅ)
  5. ใƒใ‚นใ‚’ lines / stations / types / station_station_types ใธ็ตฑๅˆใ™ใ‚‹
  6. generated/*.csv ใ‚’ๆ›ธใๅ‡บใ™

station_station_types.id ใฏๅœ่ปŠ้ †ๅบใใฎใ‚‚ใฎใจใ—ใฆๅ‚็…งใ•ใ‚Œใ‚‹ใŸใ‚ใ€ ่กŒใฎไธฆใณใซๆ„ๅ‘ณใŒใ‚ใ‚Šใพใ™ใ€‚ๆ›ธใๅ‡บใ—ใฏๅฟ…ใš id ๆ˜‡้ †ใง่กŒใ„ใพใ™ใ€‚

ใƒใ‚นใฎใ‚ณใƒผใƒ‰็”Ÿๆˆ

ใƒใ‚น็”ฑๆฅใฎใƒฌใ‚ณใƒผใƒ‰ใฏใ€้‰„้“ใจ่ก็ชใ—ใชใ„ๅ€คๅŸŸใธ FNV-1a ใงๆฑบๅฎš็š„ใซๅ‰ฒใ‚Šๅฝ“ใฆใพใ™ใ€‚ ๅฎŸ่กŒใฎใŸใณใซๅŒใ˜ๅ€คใซใชใ‚‹ๅฟ…่ฆใŒใ‚ใ‚‹ใŸใ‚ใ€DefaultHasher ใฏไฝฟใ„ใพใ›ใ‚“ใ€‚

ๅฏพ่ฑก ๅ€คๅŸŸ ๅ…ฅๅŠ›
line_cd 100,000,000 + route_id
station_cd 200,000,000 + (stop_id, route_id)
station_g_cd 200,000,000 + stop_id (ใพใจใ‚ๅพŒใฎไปฃ่กจ)
type_cd 100,000,000 + (route_id, shape_id)
line_group_cd 100,000,000 + (route_id, shape_id)

็’ฐๅขƒๅค‰ๆ•ฐ

ๅค‰ๆ•ฐ ๅŠนๆžœ
ODPT_ACCESS_TOKEN ้ƒฝๅ–ถใƒใ‚นไปฅๅค–ใฎใƒ•ใ‚ฃใƒผใƒ‰ใซๅฟ…่ฆใ€‚็„กใ„ๅ ดๅˆใฏ่ญฆๅ‘Šใฎใ†ใˆ่ชญใฟ้ฃ›ใฐใ™
DISABLE_BUS_FEATURE true ใงใƒใ‚นใ‚’ๅ–ใ‚Š่พผใพใชใ„ (้‰„้“ใฎใฟ)

ใ‚คใƒณใƒกใƒขใƒช็ดขๅผ•

PostgreSQL ใฎใ‚ฏใ‚จใƒชใฏไปฅไธ‹ใฎใ‚ˆใ†ใซ็ฝฎใๆ›ใˆใฆใ„ใพใ™ใ€‚

PostgreSQL Worker
point(lat,lon) <-> point() haversine ใฎๅ…จไปถ่ตฐๆŸป (select_nth_unstable_by ใงไธŠไฝใฎใฟ็ขบๅฎš)
pg_trgm ใฎ GIN ใ‚คใƒณใƒ‡ใƒƒใ‚ฏใ‚น contains()
station_station_types ใฎ JOIN HashMap ใซใ‚ˆใ‚‹็ดขๅผ•

pg_trgm ใฏ LIKE '%...%' ใ‚’้ซ˜้€ŸๅŒ–ใ™ใ‚‹ใ‚คใƒณใƒ‡ใƒƒใ‚ฏใ‚นใงใ‚ใฃใฆ้กžไผผๅบฆๆคœ็ดขใงใฏ ใชใ„ใŸใ‚ใ€contains() ใง่ซ–็†็š„ใซ็ญ‰ไพกใช็ตๆžœใซใชใ‚Šใพใ™ใ€‚ๆญฃ่ฆๅŒ–ใฏ domain ๅฑคใฎ normalize_for_search ใ‚’ใใฎใพใพๅ‘ผใณใพใ™ใ€‚

39,204 ไปถ (ใƒใ‚น่พผใฟ) ใฎๅ…จไปถ่ตฐๆŸปใงใ‚‚ๅฎŸๆธฌ 10ms ๅฐใซๅŽใพใ‚Šใพใ™ใ€‚

station_station_types.csv ใฏ 65,281 ่กŒใ‚ใ‚Šใ€่ตทๅ‹•ๆ™‚ใฎ CSV ใƒ‘ใƒผใ‚นใŒ ใ‚ณใƒผใƒซใƒ‰ใ‚นใ‚ฟใƒผใƒˆใฎๅคงๅŠใ‚’ๅ ใ‚ใฆใ„ใพใ—ใŸใ€‚ๅ…จๅˆ—ใŒๆ•ดๆ•ฐใชใฎใงใ€build.rs ใŒ 1 ่กŒ = i32 x 4 ใฎๅ›บๅฎš้•ทใƒใ‚คใƒŠใƒช (sst.bin) ใธไบ‹ๅ‰ๅค‰ๆ›ใ—ใฆใ„ใพใ™ใ€‚


GraphQL ใจใ‚นใ‚ญใƒผใƒžไธ€่‡ดใฎๆ‹…ไฟ

ใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใฏใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆไบ’ๆ›ใฎใŸใ‚ใ€ใ‚ตใƒ–ใƒ‰ใƒกใ‚คใƒณ็›ดไธ‹ใงใ‚ฏใ‚จใƒชใ‚’ๅ—ใ‘ใพใ™ใ€‚

ใƒ‘ใ‚น ๅ†…ๅฎน
POST / ใ‚ฏใ‚จใƒชๅฎŸ่กŒ
GET / GraphiQL
GET /__schema SDL (CI ใŒๅ–ๅพ—ใ—ใฆ็ชใๅˆใ‚ใ›ใ‚‹)
GET /__health ็ดขๅผ•ใฎไปถๆ•ฐ
GET /__ping ใƒ‡ใƒผใ‚ฟใซ่งฆใ‚‰ใชใ„็–Ž้€š็ขบ่ช

async-graphql ใฏใ‚ณใƒผใƒ‰ใƒ•ใ‚กใƒผใ‚นใƒˆใชใฎใงใ€Rust ใฎๅž‹ใ‚’ๅค‰ใˆใ‚‹ใจ SDL ใŒๅค‰ใ‚ใ‚Šใพใ™ใ€‚ ใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆใŒๅฃŠใ‚Œใ‚‹ๅค‰ๆ›ดใซๆฐ—ไป˜ใ‘ใ‚‹ใ‚ˆใ†ใ€schema/public.graphql ใ‚’ๆญฃใจใ—ใฆ scripts/compare_schema.py ใŒ็ชใๅˆใ‚ใ›ใ€CI ใงๅทฎๅˆ†ใŒใ‚ใ‚Œใฐๅคฑๆ•—ใ•ใ›ใพใ™ใ€‚ ๅž‹ใจใƒ•ใ‚ฃใƒผใƒซใƒ‰ใฏ้›†ๅˆใจใ—ใฆใ€enum ใฏ้ †ๅบ่พผใฟใงๆฏ”่ผƒใ—ใพใ™ใ€‚

ๆ„ๅ›ณ็š„ใซใ‚นใ‚ญใƒผใƒžใ‚’ๅค‰ใˆใ‚‹ใจใใฏใ“ใฎใƒ•ใ‚กใ‚คใƒซใ‚‚ๆ›ดๆ–ฐใ—ใพใ™ใ€‚ใใฎๅทฎๅˆ†ใŒ ใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆใธใฎๅฝฑ้Ÿฟ็ฏ„ๅ›ฒใใฎใ‚‚ใฎใซใชใ‚Šใพใ™ใ€‚

ๅฎŸ่ฃ…ไธŠใฎๆณจๆ„:

  • async-graphql ใฏ enum ๅ€คใ‚’ๆ—ขๅฎšใง SCREAMING_SNAKE_CASE ใซใ™ใ‚‹ใ€‚ๅ…ฌ้–‹ใ‚นใ‚ญใƒผใƒžใฏ PascalCase ใชใฎใง rename_items ใงๆƒใˆใฆใ„ใ‚‹
  • PascalCase ๅค‰ๆ›ใงใฏ JR ใŒ Jr ใซใชใ‚‹ใŸใ‚ใ€ใ“ใฎๅ€คใ ใ‘ name ใ‚’ๆ˜Ž็คบใ—ใฆใ„ใ‚‹
  • Station / StationNested ใฎใ‚ˆใ†ใซๅŒไธ€ๆง‹้€ ใงๅๅ‰ใŒ้•ใ†ๅž‹ใฏใ€SDL ใ‚’ๅˆใ‚ใ›ใ‚‹ ใŸใ‚ใƒžใ‚ฏใƒญใงไธกๆ–นๅฎš็พฉใ—ใฆใ„ใ‚‹ใ€‚Nested ๅž‹ใฏไบ’ใ„ใ‚’ๅ‚็…งใ™ใ‚‹ใฎใง Box ใง ้–“ๆŽฅๅŒ–ใ—ใชใ„ใจ็„ก้™ใ‚ตใ‚คใ‚บใซใชใ‚‹

ๅ‘ฝๅ่ฆๅ‰‡

ๅŒใ˜ใ€Œ้ง…ใ€ใ‚’ๆŒ‡ใ™ๅž‹ใŒๅฑคใ”ใจใซ 3 ใคใ‚ใ‚Šใพใ™ใ€‚

็จฎๅˆฅ ๅ ดๆ‰€ ็›ฎ็š„ ็‰นๅพด
Record src/index.rs ๅŸ‹ใ‚่พผใฟ CSV ใฎ 1 ่กŒ ๆคœ็ดขใซ่ฆใ‚‹ๅˆ—ใ ใ‘ใ‚’ๆŒใค่ปฝ้‡ใชๆง‹้€ ไฝ“
Entity stationapi/src/domain/entity/ ใƒ‰ใƒกใ‚คใƒณใƒขใƒ‡ใƒซ ใƒใ‚นใƒˆๆง‹้€ ใ€ๅคš่จ€่ชžๅฏพๅฟœใ€็ด„66ใƒ•ใ‚ฃใƒผใƒซใƒ‰
Model stationapi/src/model.rs API ใŒ่ฟ”ใ™ๅ€ค ๅˆ—ๆŒ™ๅž‹ใฏ i32 ใฎใพใพๆŒใค

Record ๆง‹้€ ไฝ“

// src/index.rs
pub struct StationRecord {
    pub station_cd: i32,
    pub station_g_cd: i32,
    pub name: String,
    // ๆคœ็ดขใซไฝฟใ†ๅˆ—ใ ใ‘ใ€‚ๅฟœ็ญ”็”จใฎ Station ใฏๅฟ…่ฆใซใชใฃใฆใ‹ใ‚‰็ต„ใฟ็ซ‹ใฆใ‚‹
}

ๅ…จไปถ่ตฐๆŸปใ‚’ๆฏŽใƒชใ‚ฏใ‚จใ‚นใƒˆ่กŒใ†ใŸใ‚ใ€Station ใ‚จใƒณใƒ†ใ‚ฃใƒ†ใ‚ฃ (66 ใƒ•ใ‚ฃใƒผใƒซใƒ‰) ใ‚’ ็ดขๅผ•ใซๆŒใŸใ›ใšใ€ๅฟœ็ญ”็”Ÿๆˆๆ™‚ใซใ ใ‘็ต„ใฟ็ซ‹ใฆใพใ™ใ€‚ใƒญใƒผใƒžๅญ—ๅใฎๅฐๆ–‡ๅญ—็‰ˆใฎใ‚ˆใ†ใซใ€ ๆฏ”่ผƒใฎใŸใณใซ่จˆ็ฎ—ใ™ใ‚‹ใจ้ซ˜ใใคใใ‚‚ใฎใฏ็ดขๅผ•ๆ™‚ใซๆŒใฃใฆใŠใใพใ™ใ€‚

Entity ๆง‹้€ ไฝ“

// stationapi/src/domain/entity/station.rs
pub struct Station {
    pub station_cd: u32,
    pub line: Option<Box<Line>>,
    pub lines: Vec<Line>,
    pub station_numbers: Vec<StationNumber>,
    // ...
}
  • ใƒ“ใ‚ธใƒใ‚นใ‚ปใƒžใƒณใƒ†ใ‚ฃใ‚ฏใ‚นใ‚’ๅๆ˜ ใ—ใŸๅž‹ (StopCondition ๅˆ—ๆŒ™ๅž‹ใชใฉ)
  • ๅคš่จ€่ชžๅ: station_name_r (ใƒญใƒผใƒžๅญ—)ใ€station_name_zhใ€station_name_ko

ๅค‰ๆ›ใƒ•ใƒญใƒผ

generated/*.csv
    โ†“  ่ตทๅ‹•ๆ™‚ใซไธ€ๅบฆใ ใ‘ใƒ‘ใƒผใ‚น
Record (StationRecord)
    โ†“  to_entity(): ่ทฏ็ทšใฎๅฑžๆ€งใ‚’ๅŸ‹ใ‚ใ‚‹
Entity (Station)
    โ†“  UseCase ๅฑคใงใƒใ‚นใƒˆใƒ‡ใƒผใ‚ฟใ‚’ไป˜ไธŽ
Enriched Entity
    โ†“  DTO ๅค‰ๆ›: IPA / TTS ใ‚ปใ‚ฐใƒกใƒณใƒˆใ‚’็”Ÿๆˆ
Model (model::Station)
    โ†“  From ๅค‰ๆ›
GraphQL ๅž‹

ใƒ‡ใƒผใ‚ฟใƒ•ใƒญใƒผ

ๅ…ธๅž‹็š„ใชใƒชใ‚ฏใ‚จใ‚นใƒˆใƒ•ใƒญใƒผ

[Client]
    โ”‚
    โ–ผ POST / (GraphQL)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Presentation (src/graphql/query.rs)           โ”‚
โ”‚  โ””โ”€ Query::station()                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚
    โ–ผ QueryUseCase ใƒกใ‚ฝใƒƒใƒ‰ๅ‘ผใณๅ‡บใ—
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ UseCase (use_case/interactor/query.rs)        โ”‚
โ”‚  โ”œโ”€ QueryInteractor::get_station_by_id()      โ”‚
โ”‚  โ””โ”€ update_station_vec_with_attributes()      โ”‚
โ”‚      โ”œโ”€ ้ง…ใ‚ฐใƒซใƒผใƒ—ไธ€ๆ‹ฌๅ–ๅพ—                      โ”‚
โ”‚      โ”œโ”€ ่ทฏ็ทšไธ€ๆ‹ฌๅ–ๅพ—                            โ”‚
โ”‚      โ”œโ”€ ไบ‹ๆฅญ่€…ไธ€ๆ‹ฌๅ–ๅพ—                          โ”‚
โ”‚      โ””โ”€ ๅˆ—่ปŠ็จฎๅˆฅไธ€ๆ‹ฌๅ–ๅพ—                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚
    โ–ผ repository ใƒˆใƒฌใ‚คใƒˆ็ตŒ็”ฑ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Index (src/repository.rs, src/index.rs)       โ”‚
โ”‚  โ””โ”€ MemStationRepository::find_by_id()        โ”‚
โ”‚      โ””โ”€ HashMap ๅ‚็…ง / ๅ…จไปถ่ตฐๆŸป                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚
    โ–ผ Record โ†’ Entity ๅค‰ๆ›
    โ–ผ Entity โ†’ Model ๅค‰ๆ› (use_case/dto/)
    โ–ผ Model โ†’ GraphQL ๅž‹
[Client]

ไธ€ๆ‹ฌๅ–ๅพ—ใฏ N+1 ใ‚’้ฟใ‘ใ‚‹ใŸใ‚ใฎใ‚‚ใฎใงใ€ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นๆ™‚ไปฃใ‹ใ‚‰ๅค‰ใˆใฆใ„ใพใ›ใ‚“ใ€‚ ใ‚คใƒณใƒกใƒขใƒชใงใ‚‚ใ€้ง…ใ”ใจใซ็ดขๅผ•ใ‚’ๅผ•ใ็›ดใ™ใ‚ˆใ‚Šไธ€ๅบฆใซ้›†ใ‚ใŸใปใ†ใŒ็ด ็›ดใงใ™ใ€‚

ใ‚จใƒฉใƒผไผๆ’ญ

DomainError
    โ†“ ? ๆผ”็ฎ—ๅญ
UseCaseError
    โ†“ From ใƒˆใƒฌใ‚คใƒˆ
async_graphql::Error
    โ†“
GraphQL ใฎ errors ใƒ•ใ‚ฃใƒผใƒซใƒ‰

ๆœชๅฎŸ่ฃ…ใฎ repository ใƒกใ‚ฝใƒƒใƒ‰ใฏ DomainError ใ‚’่ฟ”ใ™่จญ่จˆใซใ—ใฆใ‚ใ‚Šใพใ™ใ€‚ ้ป™ใฃใฆ็ฉบใ‚’่ฟ”ใ™ใจๆญฃๅธธๅฟœ็ญ”ใซ่ฆ‹ใˆใฆๅฎŸ่ฃ…ๆผใ‚Œใซๆฐ—ไป˜ใ‘ใชใ„ใŸใ‚ใงใ™ (็งป่กŒๆ™‚ใ€ ๅฎŸ้š›ใซใ“ใฎ่จญ่จˆใฎใŠใ‹ใ’ใง 1 ไปถใฎๆผใ‚ŒใŒ 500 ๅฟœ็ญ”ใจใ—ใฆๆคœๅ‡บใงใใพใ—ใŸ)ใ€‚


ใƒ‡ใ‚ฃใƒฌใ‚ฏใƒˆใƒชๆง‹้€ 

.
โ”œโ”€โ”€ Cargo.toml            # stationapi-worker (wasm32 ๅฐ‚็”จ) + workspace
โ”œโ”€โ”€ wrangler.jsonc        # staging / production ใฎ่จญๅฎš
โ”œโ”€โ”€ build.rs              # CSV ใ‚’ OUT_DIR ใธ้…็ฝฎใ€sst.bin ใ‚’็”Ÿๆˆ
โ”œโ”€โ”€ src/                  # Worker ๆœฌไฝ“
โ”‚   โ”œโ”€โ”€ lib.rs            # ใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆ
โ”‚   โ”œโ”€โ”€ index.rs          # ๅŸ‹ใ‚่พผใฟใƒ‡ใƒผใ‚ฟใฎใƒ‘ใƒผใ‚นใจ็ดขๅผ•
โ”‚   โ”œโ”€โ”€ repository.rs     # repository ใƒˆใƒฌใ‚คใƒˆใฎๅฎŸ่ฃ…
โ”‚   โ””โ”€โ”€ graphql/          # GraphQL ใฎๅž‹ใƒปใƒชใ‚พใƒซใƒ
โ”‚       โ”œโ”€โ”€ query.rs      # 18 ใ‚ฏใ‚จใƒช
โ”‚       โ”œโ”€โ”€ types.rs      # ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆๅž‹
โ”‚       โ”œโ”€โ”€ enums.rs      # ๅˆ—ๆŒ™ๅž‹
โ”‚       โ””โ”€โ”€ scalar.rs     # UInt32 ใ‚นใ‚ซใƒฉใƒผ
โ”‚
โ”œโ”€โ”€ schema/
โ”‚   โ””โ”€โ”€ public.graphql    # ๅ…ฌ้–‹ใ‚นใ‚ญใƒผใƒžใฎๆญฃ (CI ใŒ็ชใๅˆใ‚ใ›ใ‚‹)
โ”‚
โ”œโ”€โ”€ stationapi/           # ใƒ‰ใƒกใ‚คใƒณใจใƒฆใƒผใ‚นใ‚ฑใƒผใ‚น (Worker ใจ preprocessor ใŒๅ…ฑๆœ‰)
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ domain/
โ”‚       โ”‚   โ”œโ”€โ”€ entity/           # Station / Line / TrainType / Company ...
โ”‚       โ”‚   โ”œโ”€โ”€ repository/       # ๆŠฝ่ฑกใ‚คใƒณใ‚ฟใƒผใƒ•ใ‚งใƒผใ‚น
โ”‚       โ”‚   โ”œโ”€โ”€ arrival_estimation.rs
โ”‚       โ”‚   โ”œโ”€โ”€ segment_speed_table.rs
โ”‚       โ”‚   โ”œโ”€โ”€ speed_table.rs
โ”‚       โ”‚   โ”œโ”€โ”€ ipa.rs
โ”‚       โ”‚   โ”œโ”€โ”€ romaji.rs
โ”‚       โ”‚   โ””โ”€โ”€ normalize.rs
โ”‚       โ”œโ”€โ”€ use_case/
โ”‚       โ”‚   โ”œโ”€โ”€ interactor/query.rs   # QueryInteractor
โ”‚       โ”‚   โ”œโ”€โ”€ traits/query.rs       # QueryUseCase ใƒˆใƒฌใ‚คใƒˆ
โ”‚       โ”‚   โ””โ”€โ”€ dto/                  # Entity โ†’ Model ๅค‰ๆ›
โ”‚       โ””โ”€โ”€ model.rs                  # API ใŒ่ฟ”ใ™ๅ€คใฎ่กจ็พ
โ”‚
โ”œโ”€โ”€ preprocessor/         # generated/*.csv ใ‚’ไฝœใ‚‹ใƒ“ใƒซใƒ‰ๆ™‚ใƒ„ใƒผใƒซ
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ rail.rs       # data/*.csv ใฎ่ชญใฟ่พผใฟใจๅ„้ง…ๅœ่ปŠใฎ็ณป็ตฑ็”Ÿๆˆ
โ”‚       โ”œโ”€โ”€ gtfs/         # GTFS / ODPT ใฎๅ–ๅพ—ใƒป่งฃ้‡ˆใƒป็ตฑๅˆ
โ”‚       โ”œโ”€โ”€ codes.rs      # ใƒใ‚น็”จใ‚ณใƒผใƒ‰ใฎ็”Ÿๆˆ
โ”‚       โ”œโ”€โ”€ table.rs      # ๅ‡บๅŠ›ใƒ†ใƒผใƒ–ใƒซใฎ่กจ็พ
โ”‚       โ””โ”€โ”€ emit.rs       # CSV ๆ›ธใๅ‡บใ—
โ”‚
โ”œโ”€โ”€ data_validator/       # data/*.csv ใฎๆ•ดๅˆๆ€งๆคœๆŸป
โ”œโ”€โ”€ data/                 # ้‰„้“ใฎๆญฃใƒ‡ใƒผใ‚ฟ (CSV) ใจ GTFS ใฎๅฑ•้–‹ๅ…ˆ
โ”œโ”€โ”€ generated/            # preprocessor ใฎๅ‡บๅŠ› (git ็ฎก็†ๅค–)
โ”œโ”€โ”€ scripts/              # ใƒ‡ใƒผใ‚ฟๆ•ดๅ‚™ใƒปใ‚นใ‚ญใƒผใƒžๆฏ”่ผƒใฎใ‚นใ‚ฏใƒชใƒ—ใƒˆ
โ””โ”€โ”€ tools/                # IPA ใ‚ซใƒใƒฌใƒƒใ‚ธ็›ฃๆŸป

้‹็”จ

็’ฐๅขƒใฎไฝฟใ„ๅˆ†ใ‘

ไป–ใฎ Worker ใจๆƒใˆใฆใ€env ็œ็•ฅๆ™‚ใ‚’ staging ใซใ—ใฆใ‚ใ‚Šใพใ™ใ€‚

make deploy             # wrangler deploy --env=""         -> stationapi-stg
make deploy-production  # wrangler deploy --env production -> stationapi

wrangler 4 ใฏ่ค‡ๆ•ฐ็’ฐๅขƒใŒใ‚ใ‚‹็Šถๆ…‹ใง --env ใ‚’็œ็•ฅใ™ใ‚‹ใจ่ญฆๅ‘Šใ™ใ‚‹ใŸใ‚ใ€ staging ใ‚’ๆŒ‡ใ™ๅ ดๅˆใ‚‚ --env="" ใ‚’ๆ˜Ž็คบใ—ใพใ™ใ€‚

ๆณจๆ„็‚น

  • ใƒ‡ใƒผใ‚ฟๆ›ดๆ–ฐใฎใŸใณใซๅ†ใƒ‡ใƒ—ใƒญใ‚คใŒ่ฆใ‚‹ใ€‚ WASM ใซๅŸ‹ใ‚่พผใ‚€ใŸใ‚
  • custom domain ใฏไบŒ้‡ใซ็™ป้Œฒใงใใชใ„ใ€‚ ใƒ‰ใƒกใ‚คใƒณใ‚’็งปใ™้š›ใฏใ€ๅ…ˆใซๅ…ƒใฎ Worker ใ‹ใ‚‰ๅค–ใ—ใฆใƒ‡ใƒ—ใƒญใ‚คใ™ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚‹
  • generated/ ใฏ git ็ฎก็†ๅค–ใ€‚ ใ‚ฏใƒญใƒผใƒณ็›ดๅพŒใซใฏ็„กใ„ใฎใงใ€make data ใง ไฝœใ‚‹ใ€‚็„กใ„ใพใพ worker-build ใ™ใ‚‹ใจ data/*.csv ใซใƒ•ใ‚ฉใƒผใƒซใƒใƒƒใ‚ฏใ—ใ€ ๅ„้ง…ๅœ่ปŠใฎ็ณป็ตฑใจใƒใ‚นใŒๆฌ ใ‘ใŸ็Šถๆ…‹ใงใƒ“ใƒซใƒ‰ใ•ใ‚Œใ‚‹ (่ญฆๅ‘Šใฏๅ‡บใ‚‹)

้–ข้€ฃใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ