JJUG CCC 2023 Spring コンファレンスC 10:25 - 10:45
で使用したスライドです。 (仮)は永遠に(仮)です 白いページは時間の都合で入りきれず、喋らなかった補足の部分です
SpringBoot+Kotlinで使うExposed(仮)タイトルから(仮)を取り忘れた・・・!
View Slide
自己紹介名前:なかやまひろ(Twitterは@setys0でスナネコアイコン)お仕事:東京都内の企業でWebサービス開発 宮仕えに飽きるとフリーランスJava暦:お仕事だと10年ちょっと(たぶん) 一番最初に触ったのは学生時代にアプレット・・・最近の趣味:AIを使ってコードや絵を生成して遊んでます
今日のスライドはこちら本スライドは右のQRコードから取得できます。https://speakerdeck.com/wenas/jjug-ccc-2023-exposedまた、ちょっと試してみたい方向けに、DDLとクエリのサンプルのプロジェクトを用意してみました。
最初におことわり本日お話しするExposedはKotlinのSQLライブラリです。解説やサンプルコードはSpringを利用していますが、ExposedをJavaで利用することはできません。(たぶん・・・)本セッションはKotlinに触れたことがなくても大丈夫なように、Exposedの紹介だけでなく採用した経緯やその効果についてもお話ししたいと思います。
今日お話しすることExposedの紹介Exposedを採用した経緯と効果Springに導入して動かすまでまとめ
今日お話ししないことExposedの細かい機能の紹介(20分じゃ足りないので・・・!)Spring+Kotlin+Exposedで構築したアプリケーションのうち、Exposedじゃない部分の話(APIとかデプロイとか・・・)Java要素・・・
Exposedとはマスコットのイカ!
ExposedとはExposedはKotlinで実装されたSQLライブラリです。ライセンスはApacheLicense 2.0で、4/17時点でGitHubの⭐は7033。開発はKotlin、Intellij IDEAを開発しているJetBrains。OSSではありますが大きな開発元が背後についているので安心感があります。
Exposedが提供する機能Exposedには2つのAPIがあります。・DSL(Domain Specific Language)SQLに近い書き方ができるタイプセーフなAPI。今日はこちらについて紹介します。・DAO (Data Access Object)一般的なORMフレームワークと同等の機能を持つ軽量API。
ExposedのDSL APIとはテーブルに対応するオブジェクトと、そのオブジェクトからクエリを実行するタイプセーフなAPIです。クエリではSQLのうち以下の機能をAPIで記述できます。次のページでそれぞれのサンプルを簡単に紹介します。Basic CRUD ,Where expression, Conditional where, Count,Order-by ,Group-by, Limit, Join, Union, Alias, Schema,Sequence, Batch Insert, Insert From Select
テーブルオブジェクト// 製造者(maker)テーブルの外部参照をもつ製品(product)テーブルobject Products : IntIdTable("product", "product_id") {val name = varchar("name", 128) // 商品名(予約語はEscape)val makerId = reference("maker_id", Makers) // 外部参照val price = decimal("price", 20, 5) // 価格val summary = varchar("summary", 1024).nullable() // Null許容列val registerDate = date("register_date") // 登録日val createdAt = timestamp("created_at")}// ほとんどDDL!
テーブルオブジェクトから生成するクエリ(メーカーごとの価格の合計を抽出する)Products.join(Makers, JoinType.INNER).slice(Makers.id, Makers.name, Products.price.sum()).select {Products.registerDate greaterEqLocalDate.now().minusWeeks(1L)}.groupBy { Makers.id }.map {// 結果を設定する}// 赤字の部分がDSLでクエリを組み立てている部分です
● insertAndGetIdでInsert時にシーケンスで採番されたPKを戻り値で取れるのが使いやすい● insertIgnoreにすることでキー重複が発生した場合に例外が発生しなくなるのがいい● JOINがサクサク書ける。いわゆるフラグ類を列ではなくFKだけ持つテーブルのレコードの有無で表現したが、クエリで表現するときに全然苦にならない。個人的に好きなところ
Exposedを採用した経緯
開発したシステムの説明サーバーサイドの言語をKotlinにしたいという思いがありました。理由はKotlinのMultiplatformやWASMがよさそうに見えてて、Kotlinにしておけばフロントとメンバーを兼任できるかもと考えたためです。構築するシステムはフロントエンド(スマホアプリ)向けのAPI、バッチ処理、運用管理向けアプリというよくある構成です。それぞれは別のプロセスですが、データベースは共有しています。
ざっくりシステム構成
各サービスについて軽く説明● バッチはDB内のデータの集計と、外部から受領したデータの登録を行います。登録したデータは主にAPIが参照します。● APIはユーザーによるデータの参照、更新を行います。リソースのIDを指定した処理がほとんどです。● 管理アプリは登録されているデータを横断的に取得します。それぞれのサービスごとに開発チームが分かれています。また、データベースは共有していますが、プロセス間で直接通信することはほぼありません。
フレームワークについて言語はkotlinですが、フレームワークは統一していません。モバイルアプリ向けのAPIはKtor、バッチと管理アプリはSpring Boot(Kotlin)を利用することにしました。そのため、KtorとSpringで共通に使えるSQLフレームワークを検討した結果、Exposedを採用することにしました。Ktorは起動が爆速(1秒くらい)なところがとても良い・・・。
Exposedをどのように導入したかExposedのDSL(SQLに近い記述ができるAPI)で利用するテーブルObject(アクセスするテーブルの列、型を定義)を作成し、各プロセスで共有しました。共有するのはテーブルObjectのみとし、クエリの共有は行わないことにしました。(それぞれのサービスで実装)クエリを共有しない理由はプロセスによって必要なデータが異なり、共有するメリットがほとんどないことです。
テーブルに対応するオブジェクトのみ共有
なぜExposedを選択したのか?Kotlinで使えるSQLフレームワークは他にもあります。例えばJPAやMyBatisもKotlinで利用できます。それらと比較した時、なぜExposedを選んだのか?
選定時に重視した点SQLフレームワーク選定をするにあたり、以下の点を重視しました。● タイプセーフに書ける● 実装時に選択肢が少ないもの(どちらかというとおまけ)
タイプセーフに書ける前提としてSQLをできるだけ活用するという思いがありました。例えば「ひと月の商品別の売上額」という集計が必要な場合、アプリの処理ではなくSQLで実装したい。この時に必要となるsumやgroup byの構文を文字列で実装するのはつらいので、流れるようなメソッドチェーンでクエリを実行したい。また、結果も列がnullableであればアプリでもNull安全になるようにしたい。ExposedのDSLはこの要求を満たしてくれます。
実装する選択肢が少ないものを選ぶフレームワークで複数の実装手段があるのは避けたいと考えていました。ExposedにはDSLとDAOの2つしかなく、SQLに近い実装をするのであればDSLに絞れるのが良いです。
以前、そこそこ大規模なプロジェクトでSpring Data JPAを使っていた時、チームごとに実装がバラバラな問題が発生した。個人的にはそれは仕方ないことだと思うが、「統一されていないことを許せない」勢力が出てきて揉めたのが辛い・・・。そして最終的にNativeQueryに全部書き直すということに・・・。選択肢が少ないものがよかった理由補足
● SQL推しなんですけど文字列では書きたくない。IDEだとコード補完とかありますが・・・。● MyBatisとかであるSQLの内のパラメーターを置換する仕組みが好きじゃない。● なんだかんだでクエリ組み立てる時のIFは発生すると思っている。それをどこで吸収するかだが、やはりアプリの処理でやりたい。(MyBatisのifは好きじゃない・・・)MyBatisの悪口っぽくなってる・・・。選定理由補足
実際、導入してどうだったのか選定理由を満たしているのは導入前からわかっていましたが、それ以上の効果もありました。DB共有の場合、とあるプロセスによるテーブルの修正が別サービスの障害となるケースがあります。今回はDLLとテーブルオブジェクトを同一リポジトリで管理し、常にDBとExposedが同期する状態を維持したことで、Exposedがインターフェースの役割を果たしました。
インターフェース?例えば、あるサービスがテーブルの列をNullableに変更します。テーブルオブジェクトも合わせて修正することで、同じ列を見ているサービスが変更を検知しやすくなりました。テーブル共有には不安がありましたが、このおかげで大きな問題は着ることなく開発ができました。// この列を利用している箇所でコンパイルエラーになり、変更に気付けるval name = varchar("name", 128).nullable()
DB共有という不安定になりがちな仕組みがExposedで安定したインターフェースとしての役割
● サービス間で更新の競合がなかった(というよりないように設計した)○ 例えば同じキーのレコードを複数のサービスから更新する場合、更新する列が一致しないのであればテーブルを分けた○ Updateを極力排除し、Insert+更新イベントという設計にした● 発表のスライドにあったnullable話は、バッチ取得する外部データ周りで多く発生した○ 外部IFが適当なところが多くて、実際アクセスしたらnullということが何回か・・・うまくいった理由補足
Springに導入して動かすまで
Exposedを動かす手順1. Dependencyの追加2. application.propertiesの修正3. ExposedConfigクラスの作成4. テーブルに対応するオブジェクトを作成5. @Transactionalなメソッドでクエリ実行4と5はExposedの説明時に記載したので省略します
Dependencyの追加(build.gradle.ktの場合)repositories {mavenCentral()}dependencies {// Exposedが提供しているexposed-spring-boot-starterimplementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.41.1")// 日付関連をJavaのDate-Time APIで扱うimplementation("org.jetbrains.exposed:exposed-java-time:0.41.1")}
application.propertiesの修正# データベースの接続spring.datasource.url=jdbc:mysql://localhost:3306/pfmspring.datasource.username=ktoruserspring.datasource.password=ktorpassspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver# DSLからDDLを生成して実行する(デフォルトはfalse)spring.exposed.generate-ddl = false# 実行されたSQLを出力する(デフォルトはfalse)spring.exposed.show-sql=true
ExposedConfigクラスの作成// SpringBoot 3.XではGitHubに記載されている通りに書いでもダメ・・・// ExposedAutoConfigurationを読み込んで、// DataSourceTransactionManagerAutoConfigurationを無効にする@Import(value = {ExposedAutoConfiguration::class})@EnableAutoConfiguration(exclude ={DataSourceTransactionManagerAutoConfiguration::class})@Configurationpublic open class ExposedConfig
補足:logbackの設定
実行時例外実行時にエラーが発生した場合、ExposedSQLExceptionがスローされます。実際に発生したSQLExceptionは、ExposedSQLExceptionから取得できます。データソース、トランザクション管理はSpring側で行っているため、そこで発生した例外はExposedを使わない場合と変わりません(例えばDataSourceの設定や誤っている等々)
まとめ
Exposedを使ってみてどうだったか小さなところで不満はあるものの、全体としては非常に良かった。SQLフレームワークで気になるところのパフォーマンスでも問題は発生していません。DSLではSQLに近い構文で書け、それがそのままSQLになります。よくORMで発生する「実行されたSQLが辛い」という問題も発生せず、開発効率もよかったです。(少なくとも発行されたSQLでトラブルはなし)N+1問題とかも自分でそう作らない限りは起きません。
導入してよかった(補足)ExposedのDSLは主にバッチでいい感じだった。ただ、そもそもKotlinでバッチ書く需要がそもそもあるのだろうかという点は少し気になった。あとはテストでDBのデータを取得してAssertするとき、DSLを気兼ねなく使えるのがよかった。気兼ねなくというのは、ORM使ってるとAssert用のデータ取得するときにORM使ったコードを用いるかで大体悩む・・・。導入してよかった(補足)
辛かったところほとんどないです。強いてあげるなら0.39.1から0.41.1にバージョンアップした時に非互換があったところです。あとはそもそもSQLが苦手だとDSL APIはちょっと辛いです。困った時は公式ドキュメントとStackOverFlow(先人が同じとこでハマってる)
日本での採用事例もありますExposed kotlinで検索すると結構日本語の記事がヒットします。ちなみに私がExposedと出会ったのは「KtorとNuxt.jsで作るWebアプリケーション入門」という本でした。KotlinでAPI作ってみたい人におすすめです。
最後に自分はKotlinに入門したばかりの初心者です(1年未満)。でもSpringと組み合わせることで、あまりハマることなくKotlinかけてます。Springだと「いざとなったらJavaで」という保険にもなります。ExposedもサーバーサイドKotlinも楽しいのでぜひ!今ならAIパワーで入門も楽・・・だと思います。
おまけ
将来的なロードマップロードマップには以下の3つが記載されています。● R2DBC● RepositoryパターンのDAO● Kotlin Nativeの対応これ以外にも各データベース固有の型の対応は随時行われています。
ChatGPTでコードは生成できますか全然いけます!GitHubのサンプルプロジェクトでは、ChatGPTにテーブルとDSLを作ってもらってます。ただ、GPTの生成コードのバージョンが古く、Importでエラーになる部分があることと、最新のAPIに対応していない点があります。とはいえ、複雑なクエリも結構生成してくれるので、試しに使ってみるのもいいかも。
興味があるのでJavaで使いたい!ごめんなさい、たぶん無理です!JavaでSQLチックに書きたい場合、Jooqというライブラリもあるのでこちらを試してみるのもいいかもしれません。商用で使うと有償になりますが・・・。ちなみにChatGPTは「できます!」と言ってサンプルコード出力しましたが動きませんでした orz