FRESH!プロジェクト/Server Side Kotlin 活用事例 / CA.kt #3

5b8405524ccf31982734d2d3eac6af89?s=47 soushi
October 18, 2017

FRESH!プロジェクト/Server Side Kotlin 活用事例 / CA.kt #3

5b8405524ccf31982734d2d3eac6af89?s=128

soushi

October 18, 2017
Tweet

Transcript

  1. '3&4)ϓϩδΣΫτʗ 4FSWFS4JEF,PUMJO׆༻ࣄྫ $"LU ໺ᖒ૱࢙!TPVTIJ@OP[BXB

  2. BCPVUNF w αʔόαΠυΤϯδχΞ w ʙ'3&4)ॴଐLUྺ @soushi_nozawa @soushin blog.soushi.me

  3. झຯ͸ϒϩά! w ٕज़ϒϩάΛॻ͘ͷ͕झຯ w ि̍ຊϧʔϧ w ӈਤ͸ϒϩάΧςΰϦͷ8PSEDMPVE w ,PUMJO H31$

    4QSJOH#PPUํ໘ͷΤϯ τϦ͕ଟ͍ͷͰڵຯ͋Δํ͸ߪಡΛ ฏ೔Πϯϓοτि຤Ξ΢τϓοτͿΖ͙ IUUQCMPHTPVTIJNF
  4. "HFOEB  '3&4)ͱαʔόαΠυͱ,PUMJO w ಋೖϚΠΫϩαʔϏεɺαʔόͷ࢓૊ΈͳͲ  ,PUMJOΒ͍͠ίʔυΛٻΊͯ w ίʔυྫͱར༻৔໘Λ߹Θͤͯ঺հ 

    %#ΞΫηεϑϨʔϜϫʔΫʹ͍ͭͯ  ςετɺ"1*υΩϡϝϯςʔγϣϯʹ͍ͭͯ  ·ͱΊ
  5. ੜ์ૹಈըϓϥοτϑΥʔϜ .JDSPTFSWJDFT"SDIJUFDUVSF'VMM%PDLFSOJ[FE"NB[PO8FC4FSWJDFT

  6. ϦϦʔεଓ͖·͢

  7. '3&4)ͱαʔόαΠυͱ,PUMJO w ʙ͜Ε·Ͱ༷ʑͳϚΠΫϩαʔϏεʹ,PUMJOΛಋೖ w ܾࡁूܭɺΞφϦςΟΫεɺϓογϡ௨஌ɺϩάऩूɺ"1*ήʔτ ΢ΣΠɺγεςϜج൫ͳͲ w ϲ݄ʹ̍ͭͷϖʔεͰ৽͍͠ϚΠΫϩαʔϏεΛϦϦʔε w εΫϥοϓˍϏϧυͰߦਐ͢Δ'3&4)։ൃମ੍΋ޙԡ͠

    w ༷ʑͳγεςϜཁ݅ʹॊೈʹରԠͰ͖͍ͯΔ
  8. ϚΠΫϩαʔϏεͱH31$ w ̍ͭͷϚΠΫϩαʔϏεʹ̍ͭͷϨϙδτϦɺ̍ͭͷϦιʔεʢ%#ʣ͕ίϯηϓτ w ̍ͭͷϚΠΫϩαʔϏεʹ(MPCBMͱ*OUFSOBMͷH31$4FSWFSΛىಈ͢Δ w 1PSUͰH31$4FSWFSΛ෼͚ͯ1VCMJDʗ1SJWBUFͷΞΫηεΛ੍ݶ͢Δ

  9. 4QSJOH#PPU w ओʹ࠾༻͍ͯ͠ΔϑϨʔϜϫʔΫ w ։ൃதͷϚΠΫϩαʔϏεʹ.Λಋೖ w HSQDKBWBͱͷ૬ੑ͸໰୊ͳ͍ w ,PUMJOTVQQPSU w

    ίʔυΛγϯϓϧʹݟ௨͠Α͘*EJPNBUJD,PUMJO $PEFΛॻ͘͜ͱΛ৺͕͚Δ w 3FJpFEUZQFQBSBNFUFST 5ZQFBMJBT 3PVUFS 'VODUJPO%4- IUUQTEPDTTQSJOHJPTQSJOHEPDTDVSSFOUTQSJOH GSBNFXPSLSFGFSFODFLPUMJOIUNM
  10. 3FJpFEUZQFQBSBNFUFST 1 data class RequestCreateTask(val title: String, 2 val description:

    String) 3 4 object Validator { 5 fun validBody(req: ServerRequest): RequestCreateTask = 6 try { 7 req.bodyToMono(RequestCreateTask::class.java).block() 8 ?: throw BadRequestException("invalid body") 9 } catch (e: DecodingException) { 10 throw BadRequestException("decoding error") 11 } 12 } w 3FRVFTU$SFBUF5BTL͔͠ػೳ͠ͳ͍7BMJEBUPS
  11. 1 object Validator { 2 inline fun <reified T> validBody(req:

    ServerRequest): T = 3 try { 4 req.bodyToMono(T::class.java).block() 5 ?: throw BadRequestException("invalid body") 6 } catch (e: DecodingException) { 7 throw BadRequestException("decoding error") 8 } 9 } 3FJpFEUZQFQBSBNFUFST w SFJpFEΛ͔ͭ͏͜ͱͰίʔυதͷܕύϥϝʔλ5ʹΞΫηεͰ͖Δ w ܕΛݶఆ͢Δ͜ͱͳ͍൚༻ؔ਺΁վળͰ͖ͨ
  12. 3FJpFEUZQFQBSBNFUFST w 4PNF$MBTTDMBTTKBWBͷΑ͏ͳ$MBTTࢀর͸සग़͢Δ w SFJpFEΛ׆༻͢Ε͹ίʔυΛվળͰ͖Δ w ,PUMJOͰ͸ɺ3FJpFE ֦ுؔ਺ͱ্खʹ෇͖߹͏ w +BDLTPONPEVMFLPUMJO

    w SFJpFEΛ͔͍ͭ,PUMJOΛαϙʔτ֦ͨ͠ுؔ਺Λఏڙ // java Some result = objectMapper.readValue(json, Some.class); // kotlin val some: Some = objectMapper.readValue(json)
  13. 5ZQF"MJBT 1 interface AppClient<in Req, out Res> { 2 fun

    verifyReceipt(req: Req): Res 3 } 4 5 @Service 6 class AppService( 7 private val appStoreCli: AppClient<AppStoreReq, AppStoreRes>, 8 private val andAppCli: AppClient<AndAppReq, AndAppRes>) w "QQ4UPSFͳͷ͔(PPHMF1MBZͳͷ͔ܕ͔Β$MJFOU͕൑அ͠ʹ͍͘
  14. 5ZQF"MJBT 1 typealias AppStoreCli = AppClient<AppStoreReq, AppStoreRes> 2 typealias AndAppCli

    = AppClient<AndAppReq, AndAppRes> 3 4 @Service 5 class AppService( 6 private val appStoreCli: AppStoreCli, 7 private val andAppCli: AndAppCli) w ܕʹผ໊Λ͚ͭͯݟ௨͕͠ྑ͘ͳͬͨ
  15. 5ZQF"MJBT w ؔ਺Ϧςϥϧʹ΋UZQFBMJBTͰผ໊͕͚ͭΒΕΔ w 3PVUFS'VODUJPO%4-ͷ3PVUFTܕ΋ؔ਺ϦςϥϧͷUZQFBMJBT typealias Routes = RouterFunctionDsl.() ->

    Unit
  16. 3PVUFS'VODUJPO%4- 1 @Configuration 2 class Routes(private val taskHandler: TaskHandler, 3

    private val authFilter: AuthFilter) { 4 @Bean 5 fun apis() = router { 6 (accept(MediaType.APPLICATION_JSON) and "/tasks").nest { 7 GET("/{id}", taskHandler::getTask) 8 POST("/", taskHandler::postTask) 9 } 10 }.filter(authFilter) 11 } w 4QSJOH͔Βͷ৽ػೳͰ"1*ϧʔςΟϯάͷ%4- w "1*ͷϝΠϯॲཧ͸)BOEMFSΛ࡞Γؔ਺ࢀরͰҾ਺ͱͯ͠౉͢ͷ͕Φεεϝ
  17. 3PVUFS'VODUJPO%4- 1 @Component 2 class TaskHandler(private val taskRepository: TaskRepository) {

    3 4 fun getTask(req: ServerRequest): Mono<ServerResponse> = 5 (req.pathVariable("id").toLongOrNull() 6 ?: throw WebAppException.BadRequestException("required id")).let { 7 ServerResponse.ok().json().body(Mono.just(taskRepository.getTaskById(it))) 8 } 9 10 fun postTask(req: ServerRequest): Mono<ServerResponse> = 11 Validator.validBody<RequestCreateTask>(req).let { 12 taskRepository.createTask(it.title, it.description).let { 13 ServerResponse.ok().json().body(Mono.just(it)) 14 } 15 } 16 } (ServerRequest) -> Mono<ServerResponse> w UBTL"1*ͷ5BTL)BOEMFSΫϥε w SFR4FSWFS3FRVFTU ΛҾ਺ʹ.POP4FSWFS3FTQPOTFΛฦؔ͢਺͕ฒͿ
  18. 3PVUFS'VODUJPO%4- w ೝূॲཧ΍ϩάग़ྗɺྫ֎ΤϥʔͷΩϟονͳͲ"01͸ʁ w 'JMUFS'VODUJPOΛ࡞Ε͹ϦΫΤετલޙͰॲཧΛڬΊΔ w "1*ͷςετ w 8FC5FTU$MJFOUΛ͔͍ͭϧʔςΟϯάఆٛΛόΠϯσΟϯάͨ͠ $MJFOUΛͭ͘Γ࣮ࢪ͢Δ

    w ैདྷͷˏ$POUSPMMFSΛ͚ͭͨϧʔςΟϯάఆٛͱڞଘͰ͖Δ w 4QSJOH͸)UUQ4FSWFSͷؔ৺ࣄͷ3FRVFTU3FTQPOTFɺ*OUFSDFQUPS ΍&YDFQUJPO'JMUFSͳͲΛ৽͍͠ܗͰαϙʔτ
  19. %PNB w %#ΞΫηεϑϨʔϜϫʔΫ w ,PUMJOΛ࣮ݧతʹαϙʔτʢެࣜࢀরʣ w ,PUMJO %PNBͰಈ࡞֬ೝʢʣ w 42-ςϯϓϨʔτͱΤϯςΟςΟΫϥεɾ%BPͷҾ਺ΛϚοϐϯά

    w ΤϯςΟςΟΛEBUBDMBTTͰఆٛͰ͖Δ
  20. 1 // src/main/java/app/dao/TaskDao.java 2 3 @ConfigAutowireable 4 @Dao 5 public

    interface TaskDao { 6 @Select 7 Task selectById(Long id); 8 } %PNB 1 // src/main/resources/META-INF/app/dao/TaskDao/selectById.sql 2 3 SELECT * FROM task WHERE task_id = /*id*/0 w 42-ςϯϓϨʔτͱ%BPΫϥεͷϚοϐϯά
  21. %PNB 1 @Entity(immutable = true) 2 @Table(name = "task") 3

    data class Task( 4 @Id 5 @GeneratedValue(strategy = GenerationType.IDENTITY) 6 @Column(name = "task_id") 7 val id: Int? = null, 8 @Column(name = "title") 9 val title: String, 10 @Column(name = "finished_at") 11 val finishedAt: LocalDateTime?, 12 @Column(name = "created_at") 13 val createdAt: LocalDateTime, 14 @Column(name = "updated_at") 15 val updatedAt: LocalDateTime 16 ) w ΤϯςΟςΟ͸EBUBDMBTTͰఆٛ͢Δ w ΧϥϜ͕OVMMBCMFͰ͋Δ͔໌֬ʹ൑ผͰ͖Δ
  22. %PNB 1 @Repository 2 class TaskRepository(val dao: TaskDao) { 3

    4 fun updateById(id: Long, title: String) { 5 val entity = dao.selectById(id) ?: 6 throw RepositoryException.NotFoundException("not found") 7 val now = LocalDateTime.now() 8 dao.update(entity.copy(title = title, updatedAt = now)).let { 9 if (it == 0) 10 throw RepositoryException.UpdateNotFoundException("not found") 11 } 12 } 13 } w EBUBDMBTTͷDPQZ Λ࢖͍ߋ৽ΧϥϜΛ໌֬ʹ
  23. %PNB 1 class TaskFactory(private val config: Config) { 2 3

    fun selectByTitle(title: String) { 4 val builder = SelectBuilder.newInstance(config) 5 .sql("SELECT * FROM task") 6 .sql("WHERE") 7 .sql("title =").param(String::class.java, "My Task") 8 builder.getEntityResultList(Task::class.java) 9 } 10 } w %BPςετέʔεʹԠͯ͡σʔλऔಘͷݕূΛ͢ΔͨΊͷ'BDUPSZΫϥε w %BPςετͰؾܰʹ42-Λྲྀ͍ͨ͠ͱ͖͸4FMFDU#VJMEFS͕ศར w SFJpFEͱ֦ுؔ਺ΛೖΕΔػӡ
  24. %PNB 1 // SelectBuilderExt.kt 2 3 inline fun <reified T>

    SelectBuilder.param(param: T) = 4 param(T::class.java, param) 5 6 fun <Res : Any> SelectBuilder.getEntityResultList(rClass: KClass<Res>) = 7 getEntitySingleResult(rClass.java) w QBSBNؔ਺͸3FJpFEΛ͔͍ͭ42-ΫΤϦύϥϝʔλͷҾ਺͕͚̍ͭͩʹ͢Δ w HFU&OUJUZ3FTVMU-JTUؔ਺͸,PUMJOΫϥεΛҾ਺ʹ͢Δ
  25. %PNB 1 class TaskFactory(private val config: Config) { 2 3

    fun selectByTitle(title: String) { 4 val builder = SelectBuilder.newInstance(config) 5 .sql("SELECT * FROM task") 6 .sql("WHERE") 7 .sql("title =").param("My Task") 8 builder.getEntityResultList(Task::class) 9 } 10 } w QBSBN HFU&OUJUZ3FTVMU-JTU ͕εοΩϦ w ֦ுؔ਺Λ૊Έ߹ΘͤͯίʔυΛվળ͢Δָ͕͠͞,PUMJOʹ͸͋Δ
  26. 6OJU5FTUJOH w +6OJU w LPUMJOUFTUΛ࢖͍͍͚ͨͲ*%&ͱ૬ੑ͕ѱ͍ w *%&ͰಛఆͷςετΛ࣮ߦͰ͖ͳ͍ w ϞοΫςετ͸.PDLJUP 1PXFS.PDLͰ

    w 1SJWBUFؔ਺ͷϞοΫɺH31$4FSWFS$MJFOUͳͲ༷ʑͳ૚ͷςετ ͕Մೳ
  27. 6OJU5FTUJOH w ৄࡉ͸ϒϩάʹ·ͱΊͯ͋Γ·͢ w ʮ,PUMJOͰ.PDLςετͷ·ͱΊʯ w IUUQCMPHTPVTIJNFFOUSZ 

  28. "1*%PDVNFOUBUJPO w 4QSJOH'PY 4XBHHFS ɺ4QSJOH3FTU%PDT͸࢖͑ͳ͍ w 4QSJOH#PPU͸αϙʔτͰ͖͍ͯͳ͍ w BQJ%PDͰ"1*࢓༷υΩϡϝϯτΛੜ੒ w

    IUUQBQJEPDKTDPN w *OMJOF%PDVNFOUBUJPOGPS3&45GVMXFC"1*T w ͲΜͳݴޠͰ΋ಋೖ͠΍͍͢ w H31$͸QSPUPHFOEPDΛ͔ͭ͏
  29. "1*%PDVNFOUBUJPO w ৄࡉ͸ϒϩάʹ·ͱΊͯ͋Γ·͢ w ʮ4QSJOH#PPUͱBQJ%PDΛ࿈ܞͤͯ͞Έ ͨ w IUUQCMPHTPVTIJNFFOUSZ 

  30. ·ͱΊ w 4QSJOH#PPU ,PUMJO H31$ߏ੒ɺ໰୊ͳ͘Քಇ w FRESH!ϓϩδΣΫτ͸௅ઓΛଓ͚·͢ w ,PUMJOΒ͍͠ίʔυΛٻΊͯ։ൃ͢Δͱָ͍͠ w

    3FJpFEUZQFQBSBNFUFS ֦ுؔ਺ͳͲ w 5FTU"QJ%PDํ໘ͷ७ਖ਼,PUMJO-JCSBSZͷొ৔΍੒ख़ʹظ଴ w ϒϩάͰ,PUMJO H31$ؔ࿈ͷΞ΢τϓοτΛܧଓ͠·͢
  31. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ