Believe you can

If you can dream it, you can do it.

SpringBoot+JPAからTiDBを使ってみる

ZOZO Advent Calendar 2022 カレンダー Vol.3 の 15 日目の記事です。

前回はTiDBとMySQLの機能比較を行いました。

chichi1091.hatenablog.jp

今回はSpringBootとJPAを利用してTiDBに接続するための方法や注意点を整理していきたいと思います。

環境

以下の環境で動作確認を行いました。

  • Java 17
  • Kotlin 1.7.20
  • Spring Boot 3.0.0
  • Spring-Data-JPA
  • TiDB 6.1

TiDBをDockerで起動する

こちらを参照にさせていただきdocker-compose.yamlを作成しました。 v5.4.0を利用していますが、M1 Macで動かす場合はv6.1.0にする必要がありましたのでご注意ください。
それぞれのコンテナの役割は次の図が参考になると思います。

github.com

起動をしたらデータベースとユーザを作成していきます。さすがMySQL互換のTiDB、コマンドはMySQLのコマンドと同じ。なのでMySQLユーザであればおなじみのコマンドになります。

$ mysql -h 127.0.0.1 -P 4000 -u root

mysql> create database tidb_sample CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 0 rows affected (0.10 sec)

mysql> create user tidb@'%' IDENTIFIED by 'tidbpassword';
Query OK, 0 rows affected (0.02 sec)

mysql> GRANT ALL PRIVILEGES ON * . * TO tidb@'%';
Query OK, 0 rows affected (0.02 sec)

これでTiDBの準備は完了です。MySQL Workbenchなどで接続をしてみてください。

SpringBoot

TiDBはMySQL互換なので、HibernateMySQL dialectで接続することができるのですが、PingCAP社が作成したTiDB dialectを利用するとTiDB独自のWindow関数などの利用が行えるようになります。TiDB dialectが組み込まれたHibernateはまだspring-boot-starter-data-jpaに入っていませんので、6.0.0.Beta2以降のHibernate-Coreに差し替える必要があります。

サンプルが提供されているので簡単に行えると考えていましたが、SpringBoot3以外でHibernate-Coreの差し替えを行うと次の例外が起こりSpringBootが起動しません。。サンプルの通りTiDB dialectを利用するにはSpringBoot3を利用したほうがよさそうです。

java.lang.NoClassDefFoundError: javax/persistence/EntityManagerFactory
    at org.springframework.data.jpa.util.BeanDefinitionUtils.<clinit>(BeanDefinitionUtils.java:57) ~[spring-data-jpa-2.7.3.jar:2.7.3]
    at org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor.postProcessBeanFactory(EntityManagerBeanDefinitionRegistrarPostProcessor.java:72) ~[spring-data-jpa-2.7.3.jar:2.7.3]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:325) ~[spring-context-5.3.23.jar:5.3.23]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:191) ~[spring-context-5.3.23.jar:5.3.23]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:746) ~[spring-context-5.3.23.jar:5.3.23]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:564) ~[spring-context-5.3.23.jar:5.3.23]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.4.jar:2.7.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-2.7.4.jar:2.7.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.4.jar:2.7.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.4.jar:2.7.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.7.4.jar:2.7.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-2.7.4.jar:2.7.4]
    at com.example.TidbSpringbootJpaApplicationKt.main(TidbSpringbootJpaApplication.kt:14) ~[main/:na]

buid.gradleでは次のようにすることで差し替えることができます。

dependencies {
    implementation ("org.springframework.boot:spring-boot-starter-parent:3.0.0")
    implementation ("org.springframework.boot:spring-boot-starter-web:3.0.0")
    implementation ("org.springframework.boot:spring-boot-starter-data-jpa:3.0.0") {
        exclude group: "org.hibernate", module: "hibernate-core"
    }
    implementation ("org.hibernate.orm:hibernate-core:6.1.5.Final")
}

接続先(application.yml)

datasource にはDockerで定義した接続先を指定するだけで、MySQLのときと特に変わりはありません。JPAdatabase-platformHibernateの差し替えで利用できるようになったTiDB dialectを指定するぐらいで特段注意する点はないかと思います。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:4000/tidb_sample
    username: tidb
    password: tidbpassword
    type: com.zaxxer.hikari.HikariDataSource
  jpa:
    database-platform: org.hibernate.dialect.TiDBDialect

JPA

テーブルは簡単なバグトラッキングシステムをイメージした以下の3テーブルを操作することにします。

  • Bugs:バグを管理するテーブル
  • Comments:バグのコメントを管理するテーブル
  • Accounts:アカウントを管理するテーブル

ざっくりとしたER図はこちら。

エンティティ

TiDBで auto_increment の利用ができないため自動採番を行いたい場合はシーケンスを利用する必要がありますので、 @GeneratedValue@SequenceGenerator でシーケンスの利用を指定しています。

@Entity
@Table(name = "bugs")
data class Bugs(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="bug_id")
    @SequenceGenerator(name="bug_id", sequenceName="bug_id_seq", allocationSize=1)
    @Column(name = "bug_id")
    val bugId: Int?,
    @Column(name = "date_reported")
    val dateReported: Date,
    @Column(name = "summary")
    val summary: String,
    @Column(name = "description")
    val description: String,
    @OneToOne
    @JoinColumn(name = "reportedBy", referencedColumnName = "account_id")
    val reportedBy: Accounts,
    @OneToOne
    @JoinColumn(name = "assignedTo", referencedColumnName = "account_id")
    val assignedTo: Accounts,
    @Enumerated(EnumType.STRING)
    val status: Status,
)

@Entity
@Table(name = "comments")
data class Comments(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="comment_id")
    @SequenceGenerator(name="comment_id", sequenceName="comment_id_seq", allocationSize=1)
    @Column(name = "comment_id")
    val commentId: Int?,
    @ManyToOne
    @JoinColumn(name = "bug_id", referencedColumnName = "bug_id")
    val bug: Bugs,
    @OneToOne
    @JoinColumn(name = "author", referencedColumnName = "account_id")
    val author: Accounts,
    val commentDate: Date,
    val comment: String,
)

@Entity
@Table(name = "accounts")
data class Accounts(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="account_id")
    @SequenceGenerator(name="account_id", sequenceName="account_id_seq", allocationSize=1)
    @Column(name = "account_id")
    val accountId: Int?,
    @Column(name = "name")
    val name: String,
    @Column(name = "email")
    val email: String,
)

これでHibernateddl-auto でテーブルを作成すると次のリソースが作成されました。

sequenceName で指定したテーブルが作られています。どうもTiDBではシーケンスはテーブルリソースとして作成され採番機能を実現しているようです。試しに account_id_seq にselectを行うと [42S02][1051] Unknown table '' とエラーが出てしまいます。 select nextval(account_id_seq); といったシーケンスを取得する関数を利用することで次の値を取得することができます。この辺はMariaDBに寄せてる感じです。

まとめ

使ってみた結果、MySQLに接続しているのとほぼ変わりなくTiDBを利用することができました。他のORマッパーでも利用することができると思いますが、TiDB dialectが吸収している差異を自力で解決する必要があるため、できるだけJPAを使うのがよさそうに思います(JPAに好き嫌いがあるとは思いますが)。
後は性能や運用面、費用で問題がなければ実運用でも十分採用することができるのではないかと感じました。2回に渡ってTiDBを調べてみましたが参考になれば幸いです。