Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

झຯ͸ϒϩά! w ٕज़ϒϩάΛॻ͘ͷ͕झຯ w ि̍ຊϧʔϧ w ӈਤ͸ϒϩάΧςΰϦͷ8PSEDMPVE w ,PUMJO H31$ 4QSJOH#PPUํ໘ͷΤϯ τϦ͕ଟ͍ͷͰڵຯ͋Δํ͸ߪಡΛ ฏ೔Πϯϓοτि຤Ξ΢τϓοτͿΖ͙ IUUQCMPHTPVTIJNF

Slide 4

Slide 4 text

"HFOEB '3&4)ͱαʔόαΠυͱ,PUMJO w ಋೖϚΠΫϩαʔϏεɺαʔόͷ࢓૊ΈͳͲ ,PUMJOΒ͍͠ίʔυΛٻΊͯ w ίʔυྫͱར༻৔໘Λ߹Θͤͯ঺հ %#ΞΫηεϑϨʔϜϫʔΫʹ͍ͭͯ ςετɺ"1*υΩϡϝϯςʔγϣϯʹ͍ͭͯ ·ͱΊ

Slide 5

Slide 5 text

ੜ์ૹಈըϓϥοτϑΥʔϜ .JDSPTFSWJDFT"SDIJUFDUVSF'VMM%PDLFSOJ[FE"NB[PO8FC4FSWJDFT

Slide 6

Slide 6 text

ϦϦʔεଓ͖·͢

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

ϚΠΫϩαʔϏεͱH31$ w ̍ͭͷϚΠΫϩαʔϏεʹ̍ͭͷϨϙδτϦɺ̍ͭͷϦιʔεʢ%#ʣ͕ίϯηϓτ w ̍ͭͷϚΠΫϩαʔϏεʹ(MPCBMͱ*OUFSOBMͷH31$4FSWFSΛىಈ͢Δ w 1PSUͰH31$4FSWFSΛ෼͚ͯ1VCMJDʗ1SJWBUFͷΞΫηεΛ੍ݶ͢Δ

Slide 9

Slide 9 text

4QSJOH#PPU w ओʹ࠾༻͍ͯ͠ΔϑϨʔϜϫʔΫ w ։ൃதͷϚΠΫϩαʔϏεʹ.Λಋೖ w HSQDKBWBͱͷ૬ੑ͸໰୊ͳ͍ w ,PUMJOTVQQPSU w ίʔυΛγϯϓϧʹݟ௨͠Α͘*EJPNBUJD,PUMJO $PEFΛॻ͘͜ͱΛ৺͕͚Δ w 3FJpFEUZQFQBSBNFUFST 5ZQFBMJBT 3PVUFS 'VODUJPO%4- IUUQTEPDTTQSJOHJPTQSJOHEPDTDVSSFOUTQSJOH GSBNFXPSLSFGFSFODFLPUMJOIUNM

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

1 object Validator { 2 inline fun 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 ܕΛݶఆ͢Δ͜ͱͳ͍൚༻ؔ਺΁վળͰ͖ͨ

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

5ZQF"MJBT 1 interface AppClient { 2 fun verifyReceipt(req: Req): Res 3 } 4 5 @Service 6 class AppService( 7 private val appStoreCli: AppClient, 8 private val andAppCli: AppClient) w "QQ4UPSFͳͷ͔(PPHMF1MBZͳͷ͔ܕ͔Β$MJFOU͕൑அ͠ʹ͍͘

Slide 14

Slide 14 text

5ZQF"MJBT 1 typealias AppStoreCli = AppClient 2 typealias AndAppCli = AppClient 3 4 @Service 5 class AppService( 6 private val appStoreCli: AppStoreCli, 7 private val andAppCli: AndAppCli) w ܕʹผ໊Λ͚ͭͯݟ௨͕͠ྑ͘ͳͬͨ

Slide 15

Slide 15 text

5ZQF"MJBT w ؔ਺Ϧςϥϧʹ΋UZQFBMJBTͰผ໊͕͚ͭΒΕΔ w 3PVUFS'VODUJPO%4-ͷ3PVUFTܕ΋ؔ਺ϦςϥϧͷUZQFBMJBT typealias Routes = RouterFunctionDsl.() -> Unit

Slide 16

Slide 16 text

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Λ࡞Γؔ਺ࢀরͰҾ਺ͱͯ͠౉͢ͷ͕Φεεϝ

Slide 17

Slide 17 text

3PVUFS'VODUJPO%4- 1 @Component 2 class TaskHandler(private val taskRepository: TaskRepository) { 3 4 fun getTask(req: ServerRequest): Mono = 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 = 11 Validator.validBody(req).let { 12 taskRepository.createTask(it.title, it.description).let { 13 ServerResponse.ok().json().body(Mono.just(it)) 14 } 15 } 16 } (ServerRequest) -> Mono w UBTL"1*ͷ5BTL)BOEMFSΫϥε w SFR4FSWFS3FRVFTU ΛҾ਺ʹ.POP4FSWFS3FTQPOTFΛฦؔ͢਺͕ฒͿ

Slide 18

Slide 18 text

3PVUFS'VODUJPO%4- w ೝূॲཧ΍ϩάग़ྗɺྫ֎ΤϥʔͷΩϟονͳͲ"01͸ʁ w 'JMUFS'VODUJPOΛ࡞Ε͹ϦΫΤετલޙͰॲཧΛڬΊΔ w "1*ͷςετ w 8FC5FTU$MJFOUΛ͔͍ͭϧʔςΟϯάఆٛΛόΠϯσΟϯάͨ͠ $MJFOUΛͭ͘Γ࣮ࢪ͢Δ w ैདྷͷˏ$POUSPMMFSΛ͚ͭͨϧʔςΟϯάఆٛͱڞଘͰ͖Δ w 4QSJOH͸)UUQ4FSWFSͷؔ৺ࣄͷ3FRVFTU3FTQPOTFɺ*OUFSDFQUPS ΍&YDFQUJPO'JMUFSͳͲΛ৽͍͠ܗͰαϙʔτ

Slide 19

Slide 19 text

%PNB w %#ΞΫηεϑϨʔϜϫʔΫ w ,PUMJOΛ࣮ݧతʹαϙʔτʢެࣜࢀরʣ w ,PUMJO %PNBͰಈ࡞֬ೝʢʣ w 42-ςϯϓϨʔτͱΤϯςΟςΟΫϥεɾ%BPͷҾ਺ΛϚοϐϯά w ΤϯςΟςΟΛEBUBDMBTTͰఆٛͰ͖Δ

Slide 20

Slide 20 text

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ΫϥεͷϚοϐϯά

Slide 21

Slide 21 text

%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Ͱ͋Δ͔໌֬ʹ൑ผͰ͖Δ

Slide 22

Slide 22 text

%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 Λ࢖͍ߋ৽ΧϥϜΛ໌֬ʹ

Slide 23

Slide 23 text

%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ͱ֦ுؔ਺ΛೖΕΔػӡ

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

%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ʹ͸͋Δ

Slide 26

Slide 26 text

6OJU5FTUJOH w +6OJU w LPUMJOUFTUΛ࢖͍͍͚ͨͲ*%&ͱ૬ੑ͕ѱ͍ w *%&ͰಛఆͷςετΛ࣮ߦͰ͖ͳ͍ w ϞοΫςετ͸.PDLJUP 1PXFS.PDLͰ w 1SJWBUFؔ਺ͷϞοΫɺH31$4FSWFS$MJFOUͳͲ༷ʑͳ૚ͷςετ ͕Մೳ

Slide 27

Slide 27 text

6OJU5FTUJOH w ৄࡉ͸ϒϩάʹ·ͱΊͯ͋Γ·͢ w ʮ,PUMJOͰ.PDLςετͷ·ͱΊʯ w IUUQCMPHTPVTIJNFFOUSZ

Slide 28

Slide 28 text

"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Λ͔ͭ͏

Slide 29

Slide 29 text

"1*%PDVNFOUBUJPO w ৄࡉ͸ϒϩάʹ·ͱΊͯ͋Γ·͢ w ʮ4QSJOH#PPUͱBQJ%PDΛ࿈ܞͤͯ͞Έ ͨ w IUUQCMPHTPVTIJNFFOUSZ

Slide 30

Slide 30 text

·ͱΊ w 4QSJOH#PPU,PUMJOH31$ߏ੒ɺ໰୊ͳ͘Քಇ w FRESH!ϓϩδΣΫτ͸௅ઓΛଓ͚·͢ w ,PUMJOΒ͍͠ίʔυΛٻΊͯ։ൃ͢Δͱָ͍͠ w 3FJpFEUZQFQBSBNFUFS֦ுؔ਺ͳͲ w 5FTU"QJ%PDํ໘ͷ७ਖ਼,PUMJO-JCSBSZͷొ৔΍੒ख़ʹظ଴ w ϒϩάͰ,PUMJO H31$ؔ࿈ͷΞ΢τϓοτΛܧଓ͠·͢

Slide 31

Slide 31 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ